mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
List aux under every export item; dedupe only in flat summary.
Shared aux CLIs (e.g. running-config bgp) now repeat per monitoring section; the trailing unique list still collapses duplicates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9342c0c7ce
commit
c20a731f72
2 changed files with 108 additions and 38 deletions
|
|
@ -1034,25 +1034,49 @@ def _append_aux_commands(
|
||||||
profile: Any,
|
profile: Any,
|
||||||
*,
|
*,
|
||||||
params: dict[str, str],
|
params: dict[str, str],
|
||||||
add_cmd,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Append resolved aux CLIs onto a plan section (dedupe via add_cmd)."""
|
"""Append resolved aux CLIs onto a plan section (section-local dedupe only)."""
|
||||||
|
existing = {
|
||||||
|
str(c.get("command") or "").strip()
|
||||||
|
for c in section.get("commands") or []
|
||||||
|
if str(c.get("role") or "") == "aux"
|
||||||
|
}
|
||||||
for aux in list(getattr(profile, "aux_commands", None) or []):
|
for aux in list(getattr(profile, "aux_commands", None) or []):
|
||||||
try:
|
try:
|
||||||
ra = resolve_aux_command(aux, params=dict(params or {}))
|
ra = resolve_aux_command(aux, params=dict(params or {}))
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
section["notes"].append(f"aux {getattr(aux, 'key', '')}: {exc}")
|
section["notes"].append(f"aux {getattr(aux, 'key', '')}: {exc}")
|
||||||
continue
|
continue
|
||||||
if add_cmd(ra.command):
|
cmd = normalize_command(ra.command)
|
||||||
section["commands"].append(
|
if not cmd or cmd in existing:
|
||||||
{
|
continue
|
||||||
"command": ra.command,
|
existing.add(cmd)
|
||||||
"role": "aux",
|
section["commands"].append(
|
||||||
"aux_key": ra.key,
|
{
|
||||||
"params": dict(params or {}),
|
"command": cmd,
|
||||||
"profile_id": ra.profile_id,
|
"role": "aux",
|
||||||
}
|
"aux_key": ra.key,
|
||||||
)
|
"params": dict(params or {}),
|
||||||
|
"profile_id": ra.profile_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _flat_unique_commands(sections: list[dict[str, Any]]) -> list[str]:
|
||||||
|
"""Dedupe executable CLIs across items (templates excluded)."""
|
||||||
|
flat: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for sec in sections:
|
||||||
|
for c in sec.get("commands") or []:
|
||||||
|
role = str(c.get("role") or "primary")
|
||||||
|
if role == "template":
|
||||||
|
continue
|
||||||
|
cmd = normalize_command(c.get("command"))
|
||||||
|
if not cmd or cmd in seen:
|
||||||
|
continue
|
||||||
|
seen.add(cmd)
|
||||||
|
flat.append(cmd)
|
||||||
|
return flat
|
||||||
|
|
||||||
|
|
||||||
def plan_task_collect_commands(
|
def plan_task_collect_commands(
|
||||||
|
|
@ -1064,8 +1088,9 @@ def plan_task_collect_commands(
|
||||||
) -> 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).
|
||||||
|
|
||||||
Includes each item's primary command plus aux enrich CLIs by default
|
Each monitoring item lists its own primary + aux CLIs (aux repeated per
|
||||||
(same as live collect). Pass include_aux=False for primary-only lists.
|
item when shared). The top-level ``commands`` list is the deduped union
|
||||||
|
for scripting. Pass include_aux=False for primary-only lists.
|
||||||
"""
|
"""
|
||||||
task = db.get(BizStateTask, task_id)
|
task = db.get(BizStateTask, task_id)
|
||||||
if not task:
|
if not task:
|
||||||
|
|
@ -1077,16 +1102,6 @@ def plan_task_collect_commands(
|
||||||
items = q.order_by(BizStateTaskItem.sort_order.asc()).all()
|
items = q.order_by(BizStateTaskItem.sort_order.asc()).all()
|
||||||
|
|
||||||
sections: list[dict[str, Any]] = []
|
sections: list[dict[str, Any]] = []
|
||||||
flat: list[str] = []
|
|
||||||
seen: set[str] = set()
|
|
||||||
|
|
||||||
def _add_cmd(cmd: str) -> bool:
|
|
||||||
c = normalize_command(cmd)
|
|
||||||
if not c or c in seen:
|
|
||||||
return False
|
|
||||||
seen.add(c)
|
|
||||||
flat.append(c)
|
|
||||||
return True
|
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
title = str(item.title or "").strip()
|
title = str(item.title or "").strip()
|
||||||
|
|
@ -1103,11 +1118,11 @@ def plan_task_collect_commands(
|
||||||
|
|
||||||
if kind == "custom_raw":
|
if kind == "custom_raw":
|
||||||
cmd = normalize_command(item.command_override)
|
cmd = normalize_command(item.command_override)
|
||||||
if cmd and _add_cmd(cmd):
|
if cmd:
|
||||||
section["commands"].append(
|
section["commands"].append(
|
||||||
{"command": cmd, "role": "primary", "params": {}, "profile_id": ""}
|
{"command": cmd, "role": "primary", "params": {}, "profile_id": ""}
|
||||||
)
|
)
|
||||||
elif not cmd:
|
else:
|
||||||
section["notes"].append("empty custom command")
|
section["notes"].append("empty custom command")
|
||||||
sections.append(section)
|
sections.append(section)
|
||||||
continue
|
continue
|
||||||
|
|
@ -1157,7 +1172,7 @@ def plan_task_collect_commands(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if include_aux:
|
if include_aux:
|
||||||
_append_aux_commands(section, profile, params={}, add_cmd=_add_cmd)
|
_append_aux_commands(section, profile, params={})
|
||||||
sections.append(section)
|
sections.append(section)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -1176,7 +1191,7 @@ def plan_task_collect_commands(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if include_aux:
|
if include_aux:
|
||||||
_append_aux_commands(section, profile, params={}, add_cmd=_add_cmd)
|
_append_aux_commands(section, profile, params={})
|
||||||
sections.append(section)
|
sections.append(section)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -1188,21 +1203,19 @@ def plan_task_collect_commands(
|
||||||
pid = str(
|
pid = str(
|
||||||
(hit.profile.profile_id if hit else profile.profile_id) or ""
|
(hit.profile.profile_id if hit else profile.profile_id) or ""
|
||||||
).strip()
|
).strip()
|
||||||
if _add_cmd(cmd):
|
section["commands"].append(
|
||||||
section["commands"].append(
|
{
|
||||||
{
|
"command": cmd,
|
||||||
"command": cmd,
|
"role": "primary",
|
||||||
"role": "primary",
|
"params": dict(params or {}),
|
||||||
"params": dict(params or {}),
|
"profile_id": pid,
|
||||||
"profile_id": pid,
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
if include_aux:
|
if include_aux:
|
||||||
_append_aux_commands(
|
_append_aux_commands(
|
||||||
section,
|
section,
|
||||||
hit.profile if hit else profile,
|
hit.profile if hit else profile,
|
||||||
params=dict(params or {}),
|
params=dict(params or {}),
|
||||||
add_cmd=_add_cmd,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# No concrete primary cmds → still show template as comment
|
# No concrete primary cmds → still show template as comment
|
||||||
|
|
@ -1225,6 +1238,7 @@ def plan_task_collect_commands(
|
||||||
|
|
||||||
sections.append(section)
|
sections.append(section)
|
||||||
|
|
||||||
|
flat = _flat_unique_commands(sections)
|
||||||
return {
|
return {
|
||||||
"task_id": task.id,
|
"task_id": task.id,
|
||||||
"ne_name": task.ne_name or "",
|
"ne_name": task.ne_name or "",
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,62 @@ class BizStateExportCommandsTests(unittest.TestCase):
|
||||||
self.assertIn("running-config vrf", text)
|
self.assertIn("running-config vrf", text)
|
||||||
self.assertIn("# ---- flat unique commands ----", text)
|
self.assertIn("# ---- flat unique commands ----", text)
|
||||||
|
|
||||||
|
def test_shared_aux_repeated_per_item_deduped_in_flat(self) -> None:
|
||||||
|
"""Each item shows its aux; flat summary dedupes shared CLIs."""
|
||||||
|
in_item = BizStateTaskItem(
|
||||||
|
id="i_in",
|
||||||
|
task_id=self.task.id,
|
||||||
|
source_profile_id="zte.bgp_vpnv4_neighbor_in",
|
||||||
|
kind="catalog",
|
||||||
|
enabled=True,
|
||||||
|
title="Neighbor In",
|
||||||
|
sort_order=1,
|
||||||
|
)
|
||||||
|
out_item = BizStateTaskItem(
|
||||||
|
id="i_out",
|
||||||
|
task_id=self.task.id,
|
||||||
|
source_profile_id="zte.bgp_vpnv4_neighbor_out",
|
||||||
|
kind="catalog",
|
||||||
|
enabled=True,
|
||||||
|
title="Neighbor Out",
|
||||||
|
sort_order=2,
|
||||||
|
)
|
||||||
|
self.db.add_all([in_item, out_item])
|
||||||
|
self.db.add_all(
|
||||||
|
[
|
||||||
|
BizStateTaskItemBinding(
|
||||||
|
id="b_in",
|
||||||
|
item_id=in_item.id,
|
||||||
|
placeholder="neighbor",
|
||||||
|
value="10.0.0.1",
|
||||||
|
),
|
||||||
|
BizStateTaskItemBinding(
|
||||||
|
id="b_out",
|
||||||
|
item_id=out_item.id,
|
||||||
|
placeholder="neighbor",
|
||||||
|
value="10.0.0.1",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
plan = plan_task_collect_commands(self.db, self.task.id)
|
||||||
|
aux_cli = "show running-config bgp | one-line"
|
||||||
|
# Both item sections must list the aux
|
||||||
|
for sec in plan["items"]:
|
||||||
|
aux = [c for c in sec["commands"] if c.get("role") == "aux"]
|
||||||
|
self.assertEqual(len(aux), 1, sec["title"])
|
||||||
|
self.assertEqual(aux[0]["command"], aux_cli)
|
||||||
|
|
||||||
|
# Flat list has the shared aux only once
|
||||||
|
self.assertEqual(sum(1 for c in plan["commands"] if c == aux_cli), 1)
|
||||||
|
|
||||||
|
text = export_task_commands_text(self.db, self.task.id)
|
||||||
|
# Two section aux markers + one flat occurrence
|
||||||
|
self.assertEqual(text.count("# aux:"), 2)
|
||||||
|
flat = text.split("# ---- flat unique commands ----", 1)[-1]
|
||||||
|
self.assertEqual(flat.count(aux_cli), 1)
|
||||||
|
|
||||||
def test_unbound_required_placeholder_noted(self) -> None:
|
def test_unbound_required_placeholder_noted(self) -> None:
|
||||||
item = BizStateTaskItem(
|
item = BizStateTaskItem(
|
||||||
id="i_exp",
|
id="i_exp",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue