mirror of
https://github.com/hansjone/netx.git
synced 2026-10-11 05:43:20 +08:00
Show aux commands in biz-state UI; keep only config_vrf under BGP VRF summary.
Add an Aux column for each monitor item. Drop ip_route/ipv6_route as summary enrich aux so FIB stays a standalone collect profile. Export defaults to primary commands only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
67930bb348
commit
089c88d35e
9 changed files with 116 additions and 29 deletions
|
|
@ -807,7 +807,7 @@ def _zte_status_profiles() -> list[ParseProfile]:
|
||||||
textfsm_command="show bgp vpnv4 unicast summary",
|
textfsm_command="show bgp vpnv4 unicast summary",
|
||||||
description=(
|
description=(
|
||||||
"Per-VRF BGP VPNv4 peer summary. Bind one or more VRFs; "
|
"Per-VRF BGP VPNv4 peer summary. Bind one or more VRFs; "
|
||||||
"aux: IPv4 FIB + config_vrf."
|
"aux: config_vrf (RD/AF enrich). IPv4 FIB is a separate monitor item."
|
||||||
),
|
),
|
||||||
placeholders=[_VRF_PLACEHOLDER_IPV4],
|
placeholders=[_VRF_PLACEHOLDER_IPV4],
|
||||||
fields=list(_BGP_PEER_FIELDS),
|
fields=list(_BGP_PEER_FIELDS),
|
||||||
|
|
@ -817,7 +817,6 @@ def _zte_status_profiles() -> list[ParseProfile]:
|
||||||
kind="collect",
|
kind="collect",
|
||||||
aux_commands=[
|
aux_commands=[
|
||||||
AuxCommand(key="config_vrf", profile_id="zte.config_vrf"),
|
AuxCommand(key="config_vrf", profile_id="zte.config_vrf"),
|
||||||
AuxCommand(key="ip_route", profile_id="zte.ip_route_vrf"),
|
|
||||||
],
|
],
|
||||||
enrich_joins=[
|
enrich_joins=[
|
||||||
EnrichJoin(
|
EnrichJoin(
|
||||||
|
|
@ -839,7 +838,7 @@ def _zte_status_profiles() -> list[ParseProfile]:
|
||||||
textfsm_command="show bgp vpnv6 unicast summary",
|
textfsm_command="show bgp vpnv6 unicast summary",
|
||||||
description=(
|
description=(
|
||||||
"Per-VRF BGP VPNv6 peer summary. Bind one or more VRFs; "
|
"Per-VRF BGP VPNv6 peer summary. Bind one or more VRFs; "
|
||||||
"aux: IPv6 FIB + config_vrf."
|
"aux: config_vrf (RD/AF enrich). IPv6 FIB is a separate monitor item."
|
||||||
),
|
),
|
||||||
placeholders=[_VRF_PLACEHOLDER_IPV6],
|
placeholders=[_VRF_PLACEHOLDER_IPV6],
|
||||||
fields=list(_BGP_PEER_FIELDS),
|
fields=list(_BGP_PEER_FIELDS),
|
||||||
|
|
@ -849,7 +848,6 @@ def _zte_status_profiles() -> list[ParseProfile]:
|
||||||
kind="collect",
|
kind="collect",
|
||||||
aux_commands=[
|
aux_commands=[
|
||||||
AuxCommand(key="config_vrf", profile_id="zte.config_vrf"),
|
AuxCommand(key="config_vrf", profile_id="zte.config_vrf"),
|
||||||
AuxCommand(key="ipv6_route", profile_id="zte.ipv6_route_vrf"),
|
|
||||||
],
|
],
|
||||||
enrich_joins=[
|
enrich_joins=[
|
||||||
EnrichJoin(
|
EnrichJoin(
|
||||||
|
|
@ -1548,6 +1546,17 @@ def metric_field_map() -> dict[str, list[FieldDef]]:
|
||||||
|
|
||||||
def profile_to_public_dict(p: ParseProfile, *, overrides: dict[str, Any] | None = None) -> dict[str, Any]:
|
def profile_to_public_dict(p: ParseProfile, *, overrides: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
ov = overrides or {}
|
ov = overrides or {}
|
||||||
|
aux_out: list[dict[str, Any]] = []
|
||||||
|
for a in p.aux_commands or []:
|
||||||
|
ap = get_profile(a.profile_id)
|
||||||
|
aux_out.append(
|
||||||
|
{
|
||||||
|
"key": a.key,
|
||||||
|
"profile_id": a.profile_id,
|
||||||
|
"title": (ap.title if ap else "") or a.profile_id,
|
||||||
|
"command_template": (ap.command_template if ap else "") or "",
|
||||||
|
}
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"profile_id": p.profile_id,
|
"profile_id": p.profile_id,
|
||||||
"vendor_key": p.vendor_key,
|
"vendor_key": p.vendor_key,
|
||||||
|
|
@ -1591,13 +1600,7 @@ def profile_to_public_dict(p: ParseProfile, *, overrides: dict[str, Any] | None
|
||||||
"kind": p.kind,
|
"kind": p.kind,
|
||||||
"match": p.match,
|
"match": p.match,
|
||||||
"textfsm_command": p.textfsm_command or p.command_template,
|
"textfsm_command": p.textfsm_command or p.command_template,
|
||||||
"aux_commands": [
|
"aux_commands": aux_out,
|
||||||
{
|
|
||||||
"key": a.key,
|
|
||||||
"profile_id": a.profile_id,
|
|
||||||
}
|
|
||||||
for a in (p.aux_commands or [])
|
|
||||||
],
|
|
||||||
"enrich_joins": [
|
"enrich_joins": [
|
||||||
{
|
{
|
||||||
"from_aux": j.from_aux,
|
"from_aux": j.from_aux,
|
||||||
|
|
|
||||||
|
|
@ -1034,12 +1034,13 @@ def plan_task_collect_commands(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
*,
|
*,
|
||||||
enabled_only: bool = True,
|
enabled_only: bool = True,
|
||||||
include_aux: bool = True,
|
include_aux: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Plan concrete collect CLIs for a task (no device login).
|
"""Plan concrete collect CLIs for a task (no device login).
|
||||||
|
|
||||||
expand_all items (unbound optional discover) are listed with a note; aux
|
By default only each monitoring item's primary command is listed.
|
||||||
commands are resolved from primary params when include_aux is True.
|
Aux CLIs (parser enrich helpers such as FIB/config under BGP summary)
|
||||||
|
are omitted unless include_aux=True — they are not separate UI items.
|
||||||
"""
|
"""
|
||||||
task = db.get(BizStateTask, task_id)
|
task = db.get(BizStateTask, task_id)
|
||||||
if not task:
|
if not task:
|
||||||
|
|
@ -1223,7 +1224,7 @@ def export_task_commands_text(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
*,
|
*,
|
||||||
enabled_only: bool = True,
|
enabled_only: bool = True,
|
||||||
include_aux: bool = True,
|
include_aux: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Plain-text export of planned collect commands (one CLI per line + section headers)."""
|
"""Plain-text export of planned collect commands (one CLI per line + section headers)."""
|
||||||
plan = plan_task_collect_commands(
|
plan = plan_task_collect_commands(
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ def api_get_task(task_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||||
def api_plan_task_commands(
|
def api_plan_task_commands(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
enabled_only: bool = Query(True),
|
enabled_only: bool = Query(True),
|
||||||
include_aux: bool = Query(True),
|
include_aux: bool = Query(False),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""List planned collect CLIs for a task (bindings expanded; no device login)."""
|
"""List planned collect CLIs for a task (bindings expanded; no device login)."""
|
||||||
|
|
@ -190,7 +190,7 @@ def api_plan_task_commands(
|
||||||
def api_export_task_commands(
|
def api_export_task_commands(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
enabled_only: bool = Query(True),
|
enabled_only: bool = Query(True),
|
||||||
include_aux: bool = Query(True),
|
include_aux: bool = Query(False),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
"""Download planned collect commands as a .txt file."""
|
"""Download planned collect commands as a .txt file."""
|
||||||
|
|
|
||||||
|
|
@ -76,19 +76,30 @@ class BizStateExportCommandsTests(unittest.TestCase):
|
||||||
)
|
)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
plan = plan_task_collect_commands(self.db, self.task.id, include_aux=True)
|
plan = plan_task_collect_commands(self.db, self.task.id)
|
||||||
self.assertEqual(plan["ne_name"], "PE1")
|
self.assertEqual(plan["ne_name"], "PE1")
|
||||||
self.assertGreaterEqual(plan["command_count"], 2)
|
self.assertGreaterEqual(plan["command_count"], 2)
|
||||||
cmds = plan["commands"]
|
cmds = plan["commands"]
|
||||||
self.assertTrue(any("lldp" in c.lower() for c in cmds))
|
self.assertTrue(any("lldp" in c.lower() for c in cmds))
|
||||||
self.assertTrue(any("CUST_A" in c for c in cmds))
|
self.assertTrue(any("CUST_A" in c and "summary" in c for c in cmds))
|
||||||
|
# Default export is primary-only (no FIB/config aux under BGP summary)
|
||||||
|
self.assertFalse(any("forwarding route" in c for c in cmds))
|
||||||
|
self.assertFalse(any("running-config vrf" in c for c in cmds))
|
||||||
# Disabled item excluded by default
|
# Disabled item excluded by default
|
||||||
self.assertFalse(any("isis" in c.lower() for c in cmds))
|
self.assertFalse(any("isis" in c.lower() for c in cmds))
|
||||||
|
|
||||||
|
with_aux = plan_task_collect_commands(
|
||||||
|
self.db, self.task.id, include_aux=True
|
||||||
|
)
|
||||||
|
# BGP VRF summary aux is only config_vrf (FIB is a separate monitor item)
|
||||||
|
self.assertTrue(any("running-config vrf" in c for c in with_aux["commands"]))
|
||||||
|
self.assertFalse(any("forwarding route" in c for c in with_aux["commands"]))
|
||||||
|
|
||||||
text = export_task_commands_text(self.db, self.task.id)
|
text = export_task_commands_text(self.db, self.task.id)
|
||||||
self.assertIn("task_id=t_export", text)
|
self.assertIn("task_id=t_export", text)
|
||||||
self.assertIn("show lldp neighbor brief", text)
|
self.assertIn("show lldp neighbor brief", text)
|
||||||
self.assertIn("CUST_A", text)
|
self.assertIn("CUST_A", text)
|
||||||
|
self.assertNotIn("# aux:", text)
|
||||||
self.assertIn("# ---- flat unique commands ----", text)
|
self.assertIn("# ---- flat unique commands ----", text)
|
||||||
|
|
||||||
def test_unbound_required_placeholder_noted(self) -> None:
|
def test_unbound_required_placeholder_noted(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -578,20 +578,20 @@ class ZteExtendedParserTests(unittest.TestCase):
|
||||||
|
|
||||||
v4 = get_profile("zte.bgp_vpnv4_vrf_summary")
|
v4 = get_profile("zte.bgp_vpnv4_vrf_summary")
|
||||||
assert v4 is not None
|
assert v4 is not None
|
||||||
self.assertTrue(any(a.key == "ip_route" for a in v4.aux_commands))
|
self.assertEqual([a.key for a in v4.aux_commands], ["config_vrf"])
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
expand_from_bindings(profile=v4, bindings=[])
|
expand_from_bindings(profile=v4, bindings=[])
|
||||||
bound = expand_from_bindings(profile=v4, bindings=[{"vrf": "CUST_A"}])
|
bound = expand_from_bindings(profile=v4, bindings=[{"vrf": "CUST_A"}])
|
||||||
self.assertEqual(bound[0][0], "show bgp vpnv4 unicast vrf CUST_A summary | one-line")
|
self.assertEqual(bound[0][0], "show bgp vpnv4 unicast vrf CUST_A summary | one-line")
|
||||||
ra_ip = resolve_aux_command(
|
ra_cfg = resolve_aux_command(
|
||||||
next(a for a in v4.aux_commands if a.key == "ip_route"),
|
next(a for a in v4.aux_commands if a.key == "config_vrf"),
|
||||||
params={"vrf": "CUST_A"},
|
params={"vrf": "CUST_A"},
|
||||||
)
|
)
|
||||||
self.assertEqual(ra_ip.command, "show ip forwarding route vrf CUST_A | one-line")
|
self.assertEqual(ra_cfg.command, "show running-config vrf | one-line")
|
||||||
|
|
||||||
v6 = get_profile("zte.bgp_vpnv6_vrf_summary")
|
v6 = get_profile("zte.bgp_vpnv6_vrf_summary")
|
||||||
assert v6 is not None
|
assert v6 is not None
|
||||||
self.assertTrue(any(a.key == "ipv6_route" for a in v6.aux_commands))
|
self.assertEqual([a.key for a in v6.aux_commands], ["config_vrf"])
|
||||||
|
|
||||||
records = [
|
records = [
|
||||||
{"vrf_name": "CUST_A", "address_families": "ipv4,ipv6", "rd": "100:1"},
|
{"vrf_name": "CUST_A", "address_families": "ipv4,ipv6", "rd": "100:1"},
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,7 @@ const en = {
|
||||||
profiles: "Monitor items",
|
profiles: "Monitor items",
|
||||||
enable: "On",
|
enable: "On",
|
||||||
command: "Command template",
|
command: "Command template",
|
||||||
|
colAux: "Aux commands",
|
||||||
params: "Bindings",
|
params: "Bindings",
|
||||||
discoverVrf: "Discover VRFs",
|
discoverVrf: "Discover VRFs",
|
||||||
discoverBind: "Discover / Bind",
|
discoverBind: "Discover / Bind",
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,7 @@ const zh = {
|
||||||
profiles: "监控项",
|
profiles: "监控项",
|
||||||
enable: "启用",
|
enable: "启用",
|
||||||
command: "命令模板",
|
command: "命令模板",
|
||||||
|
colAux: "辅命令",
|
||||||
params: "参数关联",
|
params: "参数关联",
|
||||||
discoverVrf: "发现 VRF",
|
discoverVrf: "发现 VRF",
|
||||||
discoverBind: "发现 / 关联",
|
discoverBind: "发现 / 关联",
|
||||||
|
|
|
||||||
|
|
@ -11331,7 +11331,7 @@ html.login-page--paused .login-page__flare {
|
||||||
|
|
||||||
.bs-profiles-table {
|
.bs-profiles-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 960px;
|
min-width: 1180px;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
|
|
@ -11352,21 +11352,26 @@ html.login-page--paused .login-page__flare {
|
||||||
|
|
||||||
.bs-profiles-table th:nth-child(2),
|
.bs-profiles-table th:nth-child(2),
|
||||||
.bs-profiles-table td:nth-child(2) {
|
.bs-profiles-table td:nth-child(2) {
|
||||||
width: 220px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-profiles-table th:nth-child(3),
|
.bs-profiles-table th:nth-child(3),
|
||||||
.bs-profiles-table td:nth-child(3) {
|
.bs-profiles-table td:nth-child(3) {
|
||||||
width: 240px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-profiles-table th:nth-child(4),
|
.bs-profiles-table th:nth-child(4),
|
||||||
.bs-profiles-table td:nth-child(4) {
|
.bs-profiles-table td:nth-child(4) {
|
||||||
width: 108px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-profiles-table th:nth-child(5),
|
.bs-profiles-table th:nth-child(5),
|
||||||
.bs-profiles-table td:nth-child(5) {
|
.bs-profiles-table td:nth-child(5) {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-table th:nth-child(6),
|
||||||
|
.bs-profiles-table td:nth-child(6) {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -11379,6 +11384,40 @@ html.login-page--paused .login-page__flare {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bs-profiles-table .bs-aux-cell {
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-table .bs-aux-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-table .bs-aux-list li {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-table .bs-aux-key {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-profiles-table .bs-aux-cmd {
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.bs-profiles-table .bs-params-cell {
|
.bs-profiles-table .bs-params-cell {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,12 @@ type Profile = {
|
||||||
metric_id: string;
|
metric_id: string;
|
||||||
kind?: string;
|
kind?: string;
|
||||||
placeholders?: Placeholder[];
|
placeholders?: Placeholder[];
|
||||||
|
aux_commands?: Array<{
|
||||||
|
key: string;
|
||||||
|
profile_id: string;
|
||||||
|
title?: string;
|
||||||
|
command_template?: string;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BatchRow = {
|
type BatchRow = {
|
||||||
|
|
@ -1429,6 +1435,7 @@ export function BizStatePage() {
|
||||||
<th>{t("bizState.params")}</th>
|
<th>{t("bizState.params")}</th>
|
||||||
<th>{t("bizState.colActions")}</th>
|
<th>{t("bizState.colActions")}</th>
|
||||||
<th>{t("bizState.command")}</th>
|
<th>{t("bizState.command")}</th>
|
||||||
|
<th>{t("bizState.colAux")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -1443,6 +1450,7 @@ export function BizStatePage() {
|
||||||
}[];
|
}[];
|
||||||
const needsBind = (prof.placeholders || []).length > 0;
|
const needsBind = (prof.placeholders || []).length > 0;
|
||||||
const phNames = (prof.placeholders || []).map((p) => p.name);
|
const phNames = (prof.placeholders || []).map((p) => p.name);
|
||||||
|
const auxList = prof.aux_commands || [];
|
||||||
let bindLines: string[] = [];
|
let bindLines: string[] = [];
|
||||||
if (needsBind && binds.length) {
|
if (needsBind && binds.length) {
|
||||||
if (phNames.length >= 2) {
|
if (phNames.length >= 2) {
|
||||||
|
|
@ -1533,7 +1541,30 @@ export function BizStatePage() {
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code className="bs-cmd-cell">{prof.command_template}</code>
|
<code className="bs-cmd-cell" title={prof.command_template}>
|
||||||
|
{prof.command_template}
|
||||||
|
</code>
|
||||||
|
</td>
|
||||||
|
<td className="bs-aux-cell">
|
||||||
|
{auxList.length ? (
|
||||||
|
<ul className="bs-aux-list">
|
||||||
|
{auxList.map((a) => {
|
||||||
|
const label = a.title || a.key || a.profile_id;
|
||||||
|
const tmpl = String(a.command_template || "").trim();
|
||||||
|
return (
|
||||||
|
<li key={`${a.key}:${a.profile_id}`} title={tmpl || a.profile_id}>
|
||||||
|
<span className="bs-aux-key">{a.key}</span>
|
||||||
|
<span className="muted"> · {label}</span>
|
||||||
|
{tmpl ? (
|
||||||
|
<code className="bs-aux-cmd">{tmpl}</code>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<span className="muted">—</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue