mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-12 21:45:41 +03:00
Compute: - Track the anchor that owns uBridge's single AF_PACKET capture slot: a second kernel port's capture is refused with a clear error (its flag rolled back), a failed start no longer wedges the port, and only the owning port's stop issues the process-wide `capture stop_kernel` (docker/qemu/iou/dynamips/switch, via the mixin's slot helpers). - Unwind failed starts instead of leaking uBridge and the anchor TAPs: QEMU's stop body runs under try/finally, QEMU and IOU start the datapath inside the launch try, Dynamips undoes a half-attached port (the retry used to be short-circuited into a silent black hole), and Docker deletes the veth pair when the namespace move fails. - Restore the relay filter guard on IOU (kernel-only filters raise NodeError, an uncompilable BPF line degrades to a warning), validate the eBPF/quota/window/bpf filter shapes before touching the bridge (short or non-numeric values surfaced as 500s after partial state), and refuse a second NIO on an occupied Docker/QEMU port (the guard IOU and Dynamips already had). - Probe fixes: `_probe_bridge_tap` survives a failing uBridge start, and the probe caches key on ctime too, so a chmod/setcap remedy re-probes. Controller: - Parse GNS3_UNIX_SOCKET_NIO like the compute does (line-anchored, truthy value) and mirror the compute's class selection in kernel_anchor_type. - Read partial capability reports defensively; make BridgeNIO.bridge required-but-nullable so an omitted key is a 422 rather than a KeyError. - Strip the packet_loss correlation slot even when it is zero: the relay filter takes exactly one argument and rejected `packet_loss 10 0`. Ethernet switch: skip ports whose absorbed anchor does not exist yet (the join is deferred by design) and roll the ports_mapping back when the VLAN reconcile fails, so a retry still diffs against the old mapping. Agent: reject device config entries without a device_name instead of silently dropping them (a malformed batch used to execute partially while reporting success). Also split the netem builder to clear C901 and drop the two per-file ruff ignores the PR had added, keeping the remove-only policy.
79 lines
3.0 KiB
Python
79 lines
3.0 KiB
Python
# 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[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 (parsed tool input, validated
|
|
here); 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.
|
|
|
|
Raises:
|
|
ValueError: An entry is not an object or has no usable device_name.
|
|
Such an entry cannot be addressed; dropping it silently would
|
|
let a malformed batch execute partially while every returned
|
|
row reports success.
|
|
"""
|
|
merged: dict[str, dict[str, Any]] = {}
|
|
for index, config in enumerate(device_configs):
|
|
if not isinstance(config, dict) or not config.get("device_name"):
|
|
raise ValueError(f"device_configs[{index}] must be an object with a non-empty 'device_name' field")
|
|
name = config["device_name"]
|
|
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())
|