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:
oliver 2026-09-22 14:22:46 +08:00
parent f274d8ea72
commit 9342c0c7ce
4 changed files with 60 additions and 35 deletions

View file

@ -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(

View file

@ -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."""

View 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__":

View file

@ -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 =