mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
fix(tools): merge same-device entries before device command execution
A batch carrying the same device twice lost all but the last entry's commands: the per-device command map kept only the last entry while result processing iterated every original entry, so each row reported the last entry's output under its own command list.c3c78f99aconsolidated the merge into the MCP template renderer, but that only runs when a template is passed — the MCP direct-commands path and gns3-copilot's direct _run path were left uncovered. Normalize once in each tool's _run (single point covering all entry paths, restoring thec42b3a59bcoverage) and keep the renderer merge as boundary normalization. Blocked-command info now also accumulates across same-device entries instead of last-winning in the per-device map.
This commit is contained in:
5 files changed
+300
-5
No files matched your search
@@ -47,7 +47,6 @@ from nornir.core.task import AggregatedResult, Result, Task
|
||||
from nornir_netmiko.tasks import netmiko_send_config
|
||||
|
||||
from gns3server.agent.gns3_copilot.gns3_client import get_gns3_server_host
|
||||
|
||||
# Import custom Netmiko device types for GNS3 emulation
|
||||
# This registers gns3_huawei_telnet_ce and other custom device types
|
||||
# NOTE: Must be imported BEFORE any Nornir operations to ensure device types are registered
|
||||
@@ -55,6 +54,9 @@ from gns3server.agent.gns3_copilot.utils import get_device_ports_from_topology
|
||||
from gns3server.agent.gns3_copilot.utils.command_filter import (
|
||||
filter_forbidden_commands,
|
||||
)
|
||||
from gns3server.agent.gns3_copilot.utils.device_configs import (
|
||||
merge_duplicate_device_configs,
|
||||
)
|
||||
|
||||
# Explicitly register custom device types to ensure they are available
|
||||
# This is a safety measure in case the auto-registration on import doesn't work
|
||||
@@ -196,6 +198,9 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
if len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
return device_configs_list
|
||||
|
||||
# Merge same-device entries before anything keys by device_name
|
||||
device_configs_list = merge_duplicate_device_configs(device_configs_list, commands_field="config_commands")
|
||||
|
||||
# Filter forbidden commands and store blocked commands info
|
||||
device_configs_list, blocked_commands_map = self._filter_forbidden_commands_from_device_configs(
|
||||
device_configs_list
|
||||
|
||||
@@ -47,7 +47,6 @@ from nornir.core.task import AggregatedResult, Result, Task
|
||||
from nornir_netmiko.tasks import netmiko_multiline
|
||||
|
||||
from gns3server.agent.gns3_copilot.gns3_client import get_gns3_server_host
|
||||
|
||||
# Import custom Netmiko device types for GNS3 emulation
|
||||
# This registers gns3_huawei_telnet_ce and other custom device types
|
||||
# NOTE: Must be imported BEFORE any Nornir operations to ensure device types are registered
|
||||
@@ -55,6 +54,9 @@ from gns3server.agent.gns3_copilot.utils import get_device_ports_from_topology
|
||||
from gns3server.agent.gns3_copilot.utils.command_filter import (
|
||||
filter_forbidden_commands,
|
||||
)
|
||||
from gns3server.agent.gns3_copilot.utils.device_configs import (
|
||||
merge_duplicate_device_configs,
|
||||
)
|
||||
|
||||
# Explicitly register custom device types to ensure they are available
|
||||
# This is a safety measure in case the auto-registration on import doesn't work
|
||||
@@ -199,6 +201,9 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
if len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
return device_configs_list
|
||||
|
||||
# Merge same-device entries before anything keys by device_name
|
||||
device_configs_list = merge_duplicate_device_configs(device_configs_list, commands_field="commands")
|
||||
|
||||
# Filter forbidden commands and store blocked commands info
|
||||
device_configs_list, blocked_commands_map = self._filter_forbidden_commands_from_device_configs(
|
||||
device_configs_list
|
||||
|
||||
@@ -47,11 +47,13 @@ from nornir.core.task import AggregatedResult, Result, Task
|
||||
from nornir_netmiko.tasks import netmiko_multiline
|
||||
|
||||
from gns3server.agent.gns3_copilot.gns3_client import get_gns3_server_host
|
||||
|
||||
# Import custom Netmiko device types for GNS3 emulation
|
||||
# This registers gns3_vpcs_telnet and other custom device types
|
||||
# NOTE: Must be imported BEFORE any Nornir operations to ensure device types are registered
|
||||
from gns3server.agent.gns3_copilot.utils import get_device_ports_from_topology
|
||||
from gns3server.agent.gns3_copilot.utils.device_configs import (
|
||||
merge_duplicate_device_configs,
|
||||
)
|
||||
|
||||
# Explicitly register VPCS device type to ensure it is available
|
||||
try:
|
||||
@@ -179,6 +181,9 @@ class VPCSCommands(BaseTool):
|
||||
if len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
return device_configs_list
|
||||
|
||||
# Merge same-device entries before anything keys by device_name
|
||||
device_configs_list = merge_duplicate_device_configs(device_configs_list, commands_field="commands")
|
||||
|
||||
# Create a mapping of device names to their commands
|
||||
device_configs_map = self._configs_map(device_configs_list)
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# GNS3-Copilot - AI-powered Network Lab Assistant for GNS3
|
||||
#
|
||||
# This file is part of GNS3-Copilot project.
|
||||
#
|
||||
# GNS3-Copilot is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the
|
||||
# Free Software Foundation, either version 3 of the License, or (at your
|
||||
# option) any later version.
|
||||
#
|
||||
# GNS3-Copilot is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
# Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with GNS3-Copilot. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Copyright (C) 2025 Yue Guobin (岳国宾)
|
||||
# Author: Yue Guobin (岳国宾)
|
||||
#
|
||||
# Project Home: https://github.com/yueguobin/gns3-copilot
|
||||
#
|
||||
"""
|
||||
Device config entry normalization shared by the tools_v2 executors.
|
||||
|
||||
Callers (LLM agents via MCP, or gns3-copilot directly) may send the same
|
||||
device several times in one batch. Every downstream stage keys by
|
||||
device_name, so duplicates must merge before execution, not after.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def merge_duplicate_device_configs(
|
||||
device_configs: list[dict[str, Any]],
|
||||
commands_field: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Merge entries that share a device_name, concatenating their command lists.
|
||||
|
||||
Without this merge the per-device command map keeps only the last
|
||||
entry's commands while result processing iterates every original entry —
|
||||
duplicate devices would silently drop all but the last command batch and
|
||||
every row would report that same output.
|
||||
|
||||
Args:
|
||||
device_configs: Device config entries; each holds device_name and a
|
||||
command list under commands_field.
|
||||
commands_field: Field carrying the commands ("commands" or
|
||||
"config_commands" depending on the tool).
|
||||
|
||||
Returns:
|
||||
One entry per device_name, in first-appearance order. The first
|
||||
entry's extra fields are preserved; later duplicates only contribute
|
||||
their commands.
|
||||
"""
|
||||
merged: dict[str, dict[str, Any]] = {}
|
||||
for config in device_configs:
|
||||
name = config.get("device_name")
|
||||
if not name:
|
||||
continue
|
||||
commands = list(config.get(commands_field, []))
|
||||
if name in merged:
|
||||
merged[name][commands_field].extend(commands)
|
||||
else:
|
||||
entry = dict(config)
|
||||
entry[commands_field] = commands
|
||||
merged[name] = entry
|
||||
return list(merged.values())
|
||||
Reference in new issue
Block a user