mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
fix: resolve the code-review findings on the kernel datapath
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.
This commit is contained in:
21 files changed
+654
-274
No files matched your search
@@ -200,7 +200,11 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
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")
|
||||
try:
|
||||
device_configs_list = merge_duplicate_device_configs(device_configs_list, commands_field="config_commands")
|
||||
except ValueError as e:
|
||||
logger.error("Invalid device_configs input: %s", e)
|
||||
return [{"status": "failed", "error": str(e)}]
|
||||
|
||||
# Filter forbidden commands and store blocked commands info
|
||||
device_configs_list, blocked_commands_map = self._filter_forbidden_commands_from_device_configs(
|
||||
|
||||
@@ -203,7 +203,11 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
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")
|
||||
try:
|
||||
device_configs_list = merge_duplicate_device_configs(device_configs_list, commands_field="commands")
|
||||
except ValueError as e:
|
||||
logger.error("Invalid device_configs input: %s", e)
|
||||
return [{"status": "failed", "error": str(e)}]
|
||||
|
||||
# Filter forbidden commands and store blocked commands info
|
||||
device_configs_list, blocked_commands_map = self._filter_forbidden_commands_from_device_configs(
|
||||
|
||||
@@ -183,7 +183,11 @@ class VPCSCommands(BaseTool):
|
||||
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")
|
||||
try:
|
||||
device_configs_list = merge_duplicate_device_configs(device_configs_list, commands_field="commands")
|
||||
except ValueError as e:
|
||||
logger.error("Invalid device_configs input: %s", e)
|
||||
return [{"status": "failed", "error": str(e)}]
|
||||
|
||||
# Create a mapping of device names to their commands
|
||||
device_configs_map = self._configs_map(device_configs_list)
|
||||
|
||||
@@ -34,7 +34,7 @@ from typing import Any
|
||||
|
||||
|
||||
def merge_duplicate_device_configs(
|
||||
device_configs: list[dict[str, Any]],
|
||||
device_configs: list[Any],
|
||||
commands_field: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
@@ -46,8 +46,9 @@ def merge_duplicate_device_configs(
|
||||
every row would report that same output.
|
||||
|
||||
Args:
|
||||
device_configs: Device config entries; each holds device_name and a
|
||||
command list under commands_field.
|
||||
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).
|
||||
|
||||
@@ -55,12 +56,18 @@ def merge_duplicate_device_configs(
|
||||
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 config in device_configs:
|
||||
name = config.get("device_name")
|
||||
if not name:
|
||||
continue
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user