mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Export collect commands with aux by default again.
Match live collect: include discover-backed aux CLIs in plan/export (and unbound sections); primary-only via include_aux=false. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f274d8ea72
commit
9342c0c7ce
4 changed files with 60 additions and 35 deletions
|
|
@ -1029,18 +1029,43 @@ def _resolve_export_profile(profile_id: str):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _append_aux_commands(
|
||||||
|
section: dict[str, Any],
|
||||||
|
profile: Any,
|
||||||
|
*,
|
||||||
|
params: dict[str, str],
|
||||||
|
add_cmd,
|
||||||
|
) -> None:
|
||||||
|
"""Append resolved aux CLIs onto a plan section (dedupe via add_cmd)."""
|
||||||
|
for aux in list(getattr(profile, "aux_commands", None) or []):
|
||||||
|
try:
|
||||||
|
ra = resolve_aux_command(aux, params=dict(params or {}))
|
||||||
|
except ValueError as exc:
|
||||||
|
section["notes"].append(f"aux {getattr(aux, 'key', '')}: {exc}")
|
||||||
|
continue
|
||||||
|
if add_cmd(ra.command):
|
||||||
|
section["commands"].append(
|
||||||
|
{
|
||||||
|
"command": ra.command,
|
||||||
|
"role": "aux",
|
||||||
|
"aux_key": ra.key,
|
||||||
|
"params": dict(params or {}),
|
||||||
|
"profile_id": ra.profile_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def plan_task_collect_commands(
|
def plan_task_collect_commands(
|
||||||
db: Session,
|
db: Session,
|
||||||
task_id: str,
|
task_id: str,
|
||||||
*,
|
*,
|
||||||
enabled_only: bool = True,
|
enabled_only: bool = True,
|
||||||
include_aux: bool = False,
|
include_aux: bool = True,
|
||||||
) -> 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).
|
||||||
|
|
||||||
By default only each monitoring item's primary command is listed.
|
Includes each item's primary command plus aux enrich CLIs by default
|
||||||
Aux CLIs (parser enrich helpers such as FIB/config under BGP summary)
|
(same as live collect). Pass include_aux=False for primary-only lists.
|
||||||
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:
|
||||||
|
|
@ -1131,6 +1156,8 @@ def plan_task_collect_commands(
|
||||||
"profile_id": profile.profile_id,
|
"profile_id": profile.profile_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if include_aux:
|
||||||
|
_append_aux_commands(section, profile, params={}, add_cmd=_add_cmd)
|
||||||
sections.append(section)
|
sections.append(section)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -1148,6 +1175,8 @@ def plan_task_collect_commands(
|
||||||
"profile_id": profile.profile_id,
|
"profile_id": profile.profile_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if include_aux:
|
||||||
|
_append_aux_commands(section, profile, params={}, add_cmd=_add_cmd)
|
||||||
sections.append(section)
|
sections.append(section)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -1168,24 +1197,13 @@ def plan_task_collect_commands(
|
||||||
"profile_id": pid,
|
"profile_id": pid,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if not include_aux:
|
if include_aux:
|
||||||
continue
|
_append_aux_commands(
|
||||||
for aux in list(getattr(hit.profile if hit else profile, "aux_commands", None) or []):
|
section,
|
||||||
try:
|
hit.profile if hit else profile,
|
||||||
ra = resolve_aux_command(aux, params=dict(params or {}))
|
params=dict(params or {}),
|
||||||
except ValueError as exc:
|
add_cmd=_add_cmd,
|
||||||
section["notes"].append(f"aux {getattr(aux, 'key', '')}: {exc}")
|
)
|
||||||
continue
|
|
||||||
if _add_cmd(ra.command):
|
|
||||||
section["commands"].append(
|
|
||||||
{
|
|
||||||
"command": ra.command,
|
|
||||||
"role": "aux",
|
|
||||||
"aux_key": ra.key,
|
|
||||||
"params": dict(params or {}),
|
|
||||||
"profile_id": ra.profile_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# No concrete primary cmds → still show template as comment
|
# No concrete primary cmds → still show template as comment
|
||||||
if not any(c.get("role") == "primary" for c in section["commands"]):
|
if not any(c.get("role") == "primary" for c in section["commands"]):
|
||||||
|
|
@ -1224,7 +1242,7 @@ def export_task_commands_text(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
*,
|
*,
|
||||||
enabled_only: bool = True,
|
enabled_only: bool = True,
|
||||||
include_aux: bool = False,
|
include_aux: bool = True,
|
||||||
) -> 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(False),
|
include_aux: bool = Query(True),
|
||||||
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(False),
|
include_aux: bool = Query(True),
|
||||||
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."""
|
||||||
|
|
|
||||||
|
|
@ -82,24 +82,23 @@ class BizStateExportCommandsTests(unittest.TestCase):
|
||||||
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 and "summary" 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)
|
# Default export includes discover-backed aux (config_vrf), not FIB
|
||||||
|
self.assertTrue(any("running-config vrf" in c for c in cmds))
|
||||||
self.assertFalse(any("forwarding route" in c for c in cmds))
|
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(
|
primary_only = plan_task_collect_commands(
|
||||||
self.db, self.task.id, include_aux=True
|
self.db, self.task.id, include_aux=False
|
||||||
)
|
)
|
||||||
# BGP VRF summary aux is only config_vrf (FIB is a separate monitor item)
|
self.assertFalse(any("running-config vrf" in c for c in primary_only["commands"]))
|
||||||
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("# aux:", text)
|
||||||
|
self.assertIn("running-config vrf", 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:
|
||||||
|
|
@ -119,16 +118,23 @@ class BizStateExportCommandsTests(unittest.TestCase):
|
||||||
sec = plan["items"][0]
|
sec = plan["items"][0]
|
||||||
notes = " ".join(sec.get("notes") or [])
|
notes = " ".join(sec.get("notes") or [])
|
||||||
self.assertIn("requires parameter bindings", notes)
|
self.assertIn("requires parameter bindings", notes)
|
||||||
self.assertEqual(plan["command_count"], 0)
|
# Primary is template-only; aux (config_vrf) still counts as executable
|
||||||
|
self.assertEqual(plan["command_count"], 1)
|
||||||
|
self.assertTrue(any("running-config vrf" in c for c in plan["commands"]))
|
||||||
tmpl_cmds = [c for c in sec.get("commands") or [] if c.get("role") == "template"]
|
tmpl_cmds = [c for c in sec.get("commands") or [] if c.get("role") == "template"]
|
||||||
self.assertEqual(len(tmpl_cmds), 1)
|
self.assertEqual(len(tmpl_cmds), 1)
|
||||||
self.assertIn("<vrf>", tmpl_cmds[0]["command"])
|
self.assertIn("<vrf>", tmpl_cmds[0]["command"])
|
||||||
|
aux_cmds = [c for c in sec.get("commands") or [] if c.get("role") == "aux"]
|
||||||
|
self.assertEqual(len(aux_cmds), 1)
|
||||||
|
|
||||||
text = export_task_commands_text(self.db, self.task.id)
|
text = export_task_commands_text(self.db, self.task.id)
|
||||||
self.assertIn("# show bgp vpnv4 unicast vrf <vrf> summary", text)
|
self.assertIn("# show bgp vpnv4 unicast vrf <vrf> summary", text)
|
||||||
|
self.assertIn("# aux:", text)
|
||||||
|
self.assertIn("running-config vrf", text)
|
||||||
# Template must not appear in the flat executable list
|
# Template must not appear in the flat executable list
|
||||||
flat = text.split("# ---- flat unique commands ----", 1)[-1]
|
flat = text.split("# ---- flat unique commands ----", 1)[-1]
|
||||||
self.assertNotIn("<vrf>", flat)
|
self.assertNotIn("<vrf>", flat)
|
||||||
|
self.assertIn("running-config vrf", flat)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -1931,7 +1931,8 @@ export const bizStateDownloadTaskCommands = async (
|
||||||
opts?: { includeAux?: boolean; enabledOnly?: boolean },
|
opts?: { includeAux?: boolean; enabledOnly?: boolean },
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const q = new URLSearchParams();
|
const q = new URLSearchParams();
|
||||||
if (opts?.includeAux === false) q.set("include_aux", "false");
|
// Default includes aux (matches live collect); pass includeAux:false to omit.
|
||||||
|
q.set("include_aux", opts?.includeAux === false ? "false" : "true");
|
||||||
if (opts?.enabledOnly === false) q.set("enabled_only", "false");
|
if (opts?.enabledOnly === false) q.set("enabled_only", "false");
|
||||||
const qs = q.toString();
|
const qs = q.toString();
|
||||||
const path =
|
const path =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue