From 06e1511773b408ca6841a48069fd9447e0a7987b Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 14 Jun 2026 00:27:13 +0800 Subject: [PATCH] feat: Jinja2 template support in device_config_send - Add optional 'template' param with Jinja2 syntax - Each device entry can use 'vars' dict instead of 'config_commands' - Template rendered per device, merged with existing commands - Rendering errors returned inline for AI self-correction --- gns3server/api/routes/mcp/__init__.py | 6 ++++ gns3server/api/routes/mcp/device_config.py | 34 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/gns3server/api/routes/mcp/__init__.py b/gns3server/api/routes/mcp/__init__.py index 0709de355..045808fbe 100644 --- a/gns3server/api/routes/mcp/__init__.py +++ b/gns3server/api/routes/mcp/__init__.py @@ -1296,9 +1296,15 @@ async def device_config_send( device_configs: Annotated[list, Field( description="List of device configs. Each entry: {\"device_name\": \"R1\", \"config_commands\": [\"int lo0\", \"ip add 1.1.1.1 255.255.255.255\"]}" )], + template: Annotated[str | None, Field(description="Optional Jinja2 template. Use with vars in each device to reduce token usage for batch config. Example: \"interface lo{{ n }}\\nip address {{ ip }} 255.255.255.255\"")] = None, ) -> list[dict[str, Any]]: """Send configuration commands to network devices via console (telnet/SSH). + Two modes: + 1. Direct commands: each device has config_commands=[...] + 2. Jinja2 template: provide template + vars per device — template is rendered for each + Example: device_configs=[{\"device_name\": \"R1\", \"vars\": {\"n\": 0, \"ip\": \"1.1.1.1\"}}] + Devices must be started first (use node_start or node_start_all). Device type is auto-detected from the 'device_type:' tag on each node. Common device types: cisco_ios_telnet, cisco_xr_telnet, huawei_telnet, gns3_huawei_telnet_ce diff --git a/gns3server/api/routes/mcp/device_config.py b/gns3server/api/routes/mcp/device_config.py index 6d886a81c..c64430070 100644 --- a/gns3server/api/routes/mcp/device_config.py +++ b/gns3server/api/routes/mcp/device_config.py @@ -33,18 +33,52 @@ import json import logging from typing import Any +from jinja2 import Template as JinjaTemplate, TemplateError as JinjaError + log = logging.getLogger(__name__) +def _render_template(template: str, device_configs: list[dict]) -> list[dict]: + """Render a Jinja2 template for each device's vars into config_commands. + + Each device in device_configs can have: + - "vars": dict of template variables (rendered into config_commands) + - "config_commands": merged after rendering if already present + """ + jinja = JinjaTemplate(template) + results = [] + for dev in device_configs: + rendered = dev.copy() + vars_data = rendered.pop("vars", {}) + if vars_data: + try: + output = jinja.render(**vars_data) + lines = [l for l in output.splitlines() if l.strip()] + existing = rendered.get("config_commands", []) + rendered["config_commands"] = existing + lines + except JinjaError as e: + error_msg = f"Template rendering failed for '{dev.get('device_name')}': {e}" + log.error(error_msg) + return [{"error": error_msg}] + results.append(rendered) + return results + + # ── Tool handlers ────────────────────────────────────────────────────────── def device_config_send_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> list[dict[str, Any]]: """Send configuration commands to network devices via console.""" project_id = params.get("project_id") device_configs = params.get("device_configs") + template = params.get("template") if not project_id or not device_configs: return [{"error": "project_id and device_configs are required"}] + if template: + device_configs = _render_template(template, device_configs) + if len(device_configs) == 1 and "error" in device_configs[0]: + return device_configs + from gns3server.agent.gns3_copilot.tools_v2.config_tools_nornir import ExecuteMultipleDeviceConfigCommands tool = ExecuteMultipleDeviceConfigCommands()