mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-27 18:52:23 +02:00
marker: support clearing direction via explicit null / "both"
direction was settable but not clearable: once a marker or project-level definition had direction=tx/rx, no update path could return it to "both directions", and the definition-sync fan-out silently kept stale values on every inherited copy. Introduce a _UNSET sentinel (link.py) distinct from None so updaters can tell "caller omitted direction" (preserve) from "caller passed None" (clear). Thread it through UDPLink.update_marker and Project.update_marker_definition; the two REST routes use Pydantic v2 model_fields_set to translate an explicit JSON null into the sentinel. MCP follows with a "both" token: link_marker / marker_definition handlers map direction="both" to a null in the REST body (tri-state: omit=preserve, tx/rx=set, both=clear), and the tool descriptions/docstrings document it. Backward compatible: omitting direction or passing tx/rx behaves exactly as before; only an explicit null / "both" clears.
This commit is contained in:
parent
71fa778d50
commit
37cb9f0a9c
@ -32,7 +32,7 @@ from uuid import UUID, uuid4
|
|||||||
from gns3server.controller import Controller
|
from gns3server.controller import Controller
|
||||||
from gns3server.controller.controller_error import ControllerError
|
from gns3server.controller.controller_error import ControllerError
|
||||||
from gns3server.db.repositories.rbac import RbacRepository
|
from gns3server.db.repositories.rbac import RbacRepository
|
||||||
from gns3server.controller.link import Link
|
from gns3server.controller.link import Link, _UNSET
|
||||||
from gns3server.utils.http_client import HTTPClient
|
from gns3server.utils.http_client import HTTPClient
|
||||||
from gns3server.utils.port_allocator import link_id_to_port
|
from gns3server.utils.port_allocator import link_id_to_port
|
||||||
from gns3server.utils.websocket_to_websocket import websocket_proxy
|
from gns3server.utils.websocket_to_websocket import websocket_proxy
|
||||||
@ -510,7 +510,7 @@ async def update_marker(
|
|||||||
name=marker_name,
|
name=marker_name,
|
||||||
bpf=marker_data.bpf if marker_data.bpf else None,
|
bpf=marker_data.bpf if marker_data.bpf else None,
|
||||||
tag=marker_data.tag,
|
tag=marker_data.tag,
|
||||||
direction=marker_data.direction,
|
direction=marker_data.direction if "direction" in marker_data.model_fields_set else _UNSET,
|
||||||
color=marker_data.color,
|
color=marker_data.color,
|
||||||
enabled=marker_data.enabled,
|
enabled=marker_data.enabled,
|
||||||
highlight_duration=marker_data.highlight_duration,
|
highlight_duration=marker_data.highlight_duration,
|
||||||
|
|||||||
@ -40,6 +40,7 @@ from uuid import UUID
|
|||||||
from gns3server import schemas
|
from gns3server import schemas
|
||||||
from gns3server.controller import Controller
|
from gns3server.controller import Controller
|
||||||
from gns3server.controller.project import Project
|
from gns3server.controller.project import Project
|
||||||
|
from gns3server.controller.link import _UNSET
|
||||||
from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError
|
from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError
|
||||||
from gns3server.controller.import_project import import_project as import_controller_project
|
from gns3server.controller.import_project import import_project as import_controller_project
|
||||||
from gns3server.controller.export_project import export_project as export_controller_project
|
from gns3server.controller.export_project import export_project as export_controller_project
|
||||||
@ -293,7 +294,7 @@ async def update_marker_definition(
|
|||||||
name=def_name,
|
name=def_name,
|
||||||
bpf=def_data.bpf if def_data.bpf else None,
|
bpf=def_data.bpf if def_data.bpf else None,
|
||||||
tag=def_data.tag,
|
tag=def_data.tag,
|
||||||
direction=def_data.direction,
|
direction=def_data.direction if "direction" in def_data.model_fields_set else _UNSET,
|
||||||
color=def_data.color,
|
color=def_data.color,
|
||||||
highlight_duration=def_data.highlight_duration,
|
highlight_duration=def_data.highlight_duration,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1015,7 +1015,7 @@ async def link_marker(
|
|||||||
name: Annotated[str | None, Field(description="Custom marker name for create action (auto-generated if omitted)")] = None,
|
name: Annotated[str | None, Field(description="Custom marker name for create action (auto-generated if omitted)")] = None,
|
||||||
tag: Annotated[int | None, Field(description="Numeric tag for packet correlation")] = None,
|
tag: Annotated[int | None, Field(description="Numeric tag for packet correlation")] = None,
|
||||||
enabled: Annotated[bool | None, Field(description="Enable or disable the marker (for update action)")] = None,
|
enabled: Annotated[bool | None, Field(description="Enable or disable the marker (for update action)")] = None,
|
||||||
direction: Annotated[str | None, Field(description="Direction filter: 'tx' for capture node sending only, 'rx' for capture node receiving only (omit for both)")] = None,
|
direction: Annotated[str | None, Field(description="Direction filter: 'tx' (capture node sending only), 'rx' (receiving only), or 'both' (no filter — on update this clears a previously set direction). Omit to leave unchanged on update.")] = None,
|
||||||
capture_node_id: Annotated[str | None, Field(description="UUID of the endpoint whose uBridge hosts the marker (the observer; tx/rx are from its perspective). Must be a link endpoint and marker-capable. Omit to auto-pick.")] = None,
|
capture_node_id: Annotated[str | None, Field(description="UUID of the endpoint whose uBridge hosts the marker (the observer; tx/rx are from its perspective). Must be a link endpoint and marker-capable. Omit to auto-pick.")] = None,
|
||||||
color: Annotated[str | None, Field(description="Hex color for UI highlight, e.g. '#ff5722'")] = None,
|
color: Annotated[str | None, Field(description="Hex color for UI highlight, e.g. '#ff5722'")] = None,
|
||||||
highlight_duration: Annotated[int | None, Field(description="UI highlight duration in milliseconds")] = None,
|
highlight_duration: Annotated[int | None, Field(description="UI highlight duration in milliseconds")] = None,
|
||||||
@ -1026,7 +1026,7 @@ async def link_marker(
|
|||||||
Set action='create' to add a marker, 'update' to modify it, 'delete' to remove.
|
Set action='create' to add a marker, 'update' to modify it, 'delete' to remove.
|
||||||
|
|
||||||
Create requires: project_id, link_id, action='create', bpf
|
Create requires: project_id, link_id, action='create', bpf
|
||||||
Update requires: project_id, link_id, action='update', marker_name, and at least one of (bpf, tag, enabled, color, highlight_duration)
|
Update requires: project_id, link_id, action='update', marker_name, and at least one of (bpf, tag, enabled, direction, color, highlight_duration)
|
||||||
Delete requires: project_id, link_id, action='delete', marker_name
|
Delete requires: project_id, link_id, action='delete', marker_name
|
||||||
|
|
||||||
To read current markers, use link_get — the response includes a 'markers' dict.
|
To read current markers, use link_get — the response includes a 'markers' dict.
|
||||||
@ -1052,7 +1052,7 @@ async def marker_definition(
|
|||||||
tag: Annotated[int | None, Field(description="Numeric tag for packet correlation")] = None,
|
tag: Annotated[int | None, Field(description="Numeric tag for packet correlation")] = None,
|
||||||
color: Annotated[str | None, Field(description="Hex color for UI highlight, e.g. '#ff5722'")] = None,
|
color: Annotated[str | None, Field(description="Hex color for UI highlight, e.g. '#ff5722'")] = None,
|
||||||
highlight_duration: Annotated[int | None, Field(description="UI highlight duration in milliseconds")] = None,
|
highlight_duration: Annotated[int | None, Field(description="UI highlight duration in milliseconds")] = None,
|
||||||
direction: Annotated[str | None, Field(description="Direction filter: 'tx' for capture node sending only, 'rx' for capture node receiving only (omit for both)")] = None,
|
direction: Annotated[str | None, Field(description="Direction filter: 'tx' (capture node sending only), 'rx' (receiving only), or 'both' (no filter — on update this clears a previously set direction). Omit to leave unchanged on update.")] = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Manage project-level marker definitions — traffic-insight rules that apply to ALL links.
|
"""Manage project-level marker definitions — traffic-insight rules that apply to ALL links.
|
||||||
|
|
||||||
@ -1061,7 +1061,7 @@ async def marker_definition(
|
|||||||
On delete, 'global-{name}' is removed from every link.
|
On delete, 'global-{name}' is removed from every link.
|
||||||
|
|
||||||
Create requires: project_id, action='create', bpf
|
Create requires: project_id, action='create', bpf
|
||||||
Update requires: project_id, action='update', def_name, and at least one of (bpf, tag, color, highlight_duration)
|
Update requires: project_id, action='update', def_name, and at least one of (bpf, tag, direction, color, highlight_duration)
|
||||||
Delete requires: project_id, action='delete', def_name
|
Delete requires: project_id, action='delete', def_name
|
||||||
List requires: project_id, action='list'
|
List requires: project_id, action='list'
|
||||||
|
|
||||||
|
|||||||
@ -395,9 +395,12 @@ def link_marker_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic
|
|||||||
if not bpf:
|
if not bpf:
|
||||||
return {"error": "bpf is required for create action"}
|
return {"error": "bpf is required for create action"}
|
||||||
body: dict[str, Any] = {"bpf": bpf}
|
body: dict[str, Any] = {"bpf": bpf}
|
||||||
for opt in ("name", "tag", "direction", "capture_node_id", "color", "highlight_duration"):
|
for opt in ("name", "tag", "capture_node_id", "color", "highlight_duration"):
|
||||||
if params.get(opt) is not None:
|
if params.get(opt) is not None:
|
||||||
body[opt] = params[opt]
|
body[opt] = params[opt]
|
||||||
|
# direction: "tx"/"rx" set a one-way filter; "both"/omitted = no filter.
|
||||||
|
if params.get("direction") in ("tx", "rx"):
|
||||||
|
body["direction"] = params["direction"]
|
||||||
return conn.http_call("post", base, json_data=body).json()
|
return conn.http_call("post", base, json_data=body).json()
|
||||||
|
|
||||||
marker_name = params.get("marker_name")
|
marker_name = params.get("marker_name")
|
||||||
@ -408,11 +411,17 @@ def link_marker_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic
|
|||||||
|
|
||||||
if action == "update":
|
if action == "update":
|
||||||
body = {}
|
body = {}
|
||||||
for opt in ("bpf", "tag", "direction", "enabled", "color", "highlight_duration"):
|
for opt in ("bpf", "tag", "enabled", "color", "highlight_duration"):
|
||||||
if params.get(opt) is not None:
|
if params.get(opt) is not None:
|
||||||
body[opt] = params[opt]
|
body[opt] = params[opt]
|
||||||
|
# direction tri-state: omitted=preserve, "tx"/"rx"=set, "both"=clear (→ null).
|
||||||
|
direction = params.get("direction")
|
||||||
|
if direction == "both":
|
||||||
|
body["direction"] = None
|
||||||
|
elif direction in ("tx", "rx"):
|
||||||
|
body["direction"] = direction
|
||||||
if not body:
|
if not body:
|
||||||
return {"error": "At least one update field is required (bpf, tag, enabled, color, highlight_duration)"}
|
return {"error": "At least one update field is required (bpf, tag, enabled, direction, color, highlight_duration)"}
|
||||||
return conn.http_call("put", url, json_data=body).json()
|
return conn.http_call("put", url, json_data=body).json()
|
||||||
|
|
||||||
# action == "delete"
|
# action == "delete"
|
||||||
@ -448,9 +457,12 @@ def marker_definition_handler(params: dict[str, Any], gns3_ctx: dict[str, Any])
|
|||||||
if not bpf:
|
if not bpf:
|
||||||
return {"error": "bpf is required for create action"}
|
return {"error": "bpf is required for create action"}
|
||||||
body: dict[str, Any] = {"bpf": bpf}
|
body: dict[str, Any] = {"bpf": bpf}
|
||||||
for opt in ("name", "tag", "direction", "color", "highlight_duration"):
|
for opt in ("name", "tag", "color", "highlight_duration"):
|
||||||
if params.get(opt) is not None:
|
if params.get(opt) is not None:
|
||||||
body[opt] = params[opt]
|
body[opt] = params[opt]
|
||||||
|
# direction: "tx"/"rx" set a one-way filter; "both"/omitted = no filter.
|
||||||
|
if params.get("direction") in ("tx", "rx"):
|
||||||
|
body["direction"] = params["direction"]
|
||||||
return conn.http_call("post", base, json_data=body).json()
|
return conn.http_call("post", base, json_data=body).json()
|
||||||
|
|
||||||
def_name = params.get("def_name")
|
def_name = params.get("def_name")
|
||||||
@ -461,11 +473,17 @@ def marker_definition_handler(params: dict[str, Any], gns3_ctx: dict[str, Any])
|
|||||||
|
|
||||||
if action == "update":
|
if action == "update":
|
||||||
body = {}
|
body = {}
|
||||||
for opt in ("bpf", "tag", "direction", "color", "highlight_duration"):
|
for opt in ("bpf", "tag", "color", "highlight_duration"):
|
||||||
if params.get(opt) is not None:
|
if params.get(opt) is not None:
|
||||||
body[opt] = params[opt]
|
body[opt] = params[opt]
|
||||||
|
# direction tri-state: omitted=preserve, "tx"/"rx"=set, "both"=clear (→ null).
|
||||||
|
direction = params.get("direction")
|
||||||
|
if direction == "both":
|
||||||
|
body["direction"] = None
|
||||||
|
elif direction in ("tx", "rx"):
|
||||||
|
body["direction"] = direction
|
||||||
if not body:
|
if not body:
|
||||||
return {"error": "At least one update field is required (bpf, tag, color, highlight_duration)"}
|
return {"error": "At least one update field is required (bpf, tag, direction, color, highlight_duration)"}
|
||||||
return conn.http_call("put", url, json_data=body).json()
|
return conn.http_call("put", url, json_data=body).json()
|
||||||
|
|
||||||
# action == "delete"
|
# action == "delete"
|
||||||
|
|||||||
@ -30,6 +30,13 @@ import logging
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Sentinel for "argument not passed". Distinct from None so marker/definition
|
||||||
|
# updaters can tell "caller omitted direction" (keep current value) from
|
||||||
|
# "caller passed direction=None" (clear it back to both directions). See
|
||||||
|
# UDPLink.update_marker and Project.update_marker_definition.
|
||||||
|
_UNSET = object()
|
||||||
|
|
||||||
|
|
||||||
FILTERS = [
|
FILTERS = [
|
||||||
{
|
{
|
||||||
"type": "frequency_drop",
|
"type": "frequency_drop",
|
||||||
@ -346,7 +353,7 @@ class Link:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=None):
|
async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=_UNSET):
|
||||||
"""
|
"""
|
||||||
Update an existing marker's BPF, tag, or enabled flag.
|
Update an existing marker's BPF, tag, or enabled flag.
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,7 @@ from .snapshot import Snapshot
|
|||||||
from .drawing import Drawing
|
from .drawing import Drawing
|
||||||
from .topology import project_to_topology, load_topology
|
from .topology import project_to_topology, load_topology
|
||||||
from .udp_link import UDPLink
|
from .udp_link import UDPLink
|
||||||
|
from .link import _UNSET
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
from ..utils.path import check_path_allowed, get_default_project_directory
|
from ..utils.path import check_path_allowed, get_default_project_directory
|
||||||
from ..utils.application_id import get_next_application_id
|
from ..utils.application_id import get_next_application_id
|
||||||
@ -947,7 +948,7 @@ class Project:
|
|||||||
self.dump()
|
self.dump()
|
||||||
self.emit_notification("project.updated", self.asdict())
|
self.emit_notification("project.updated", self.asdict())
|
||||||
|
|
||||||
async def update_marker_definition(self, name, bpf=None, tag=None, direction=None, color=None, highlight_duration=None):
|
async def update_marker_definition(self, name, bpf=None, tag=None, direction=_UNSET, color=None, highlight_duration=None):
|
||||||
"""
|
"""
|
||||||
Update a marker definition and sync every inherited copy on every link.
|
Update a marker definition and sync every inherited copy on every link.
|
||||||
"""
|
"""
|
||||||
@ -966,8 +967,8 @@ class Project:
|
|||||||
d["color"] = color
|
d["color"] = color
|
||||||
if highlight_duration is not None:
|
if highlight_duration is not None:
|
||||||
d["highlight_duration"] = highlight_duration
|
d["highlight_duration"] = highlight_duration
|
||||||
if direction is not None:
|
if direction is not _UNSET:
|
||||||
d["direction"] = direction
|
d["direction"] = direction # None = clear back to both directions
|
||||||
|
|
||||||
# Sync: update every inherited copy across all links.
|
# Sync: update every inherited copy across all links.
|
||||||
for link in list(self._links.values()):
|
for link in list(self._links.values()):
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from .controller_error import ControllerError, ControllerNotFoundError
|
from .controller_error import ControllerError, ControllerNotFoundError
|
||||||
from .link import Link
|
from .link import Link, _UNSET
|
||||||
from .node_types import BUILTIN_NODE_TYPES
|
from .node_types import BUILTIN_NODE_TYPES
|
||||||
from gns3server.utils.packet_filter_validation import validate_bpf_syntax, FilterValidationError
|
from gns3server.utils.packet_filter_validation import validate_bpf_syntax, FilterValidationError
|
||||||
|
|
||||||
@ -430,7 +430,7 @@ class UDPLink(Link):
|
|||||||
self._project.emit_notification("link.updated", self.asdict())
|
self._project.emit_notification("link.updated", self.asdict())
|
||||||
self._project.dump()
|
self._project.dump()
|
||||||
|
|
||||||
async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=None, color=None, highlight_duration=None, inherited=False):
|
async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=_UNSET, color=None, highlight_duration=None, inherited=False):
|
||||||
"""
|
"""
|
||||||
Update an existing marker's BPF/tag/enabled/color. Any change pushes via
|
Update an existing marker's BPF/tag/enabled/color. Any change pushes via
|
||||||
``self.update()``; uBridge picks up the new params on the next NIO
|
``self.update()``; uBridge picks up the new params on the next NIO
|
||||||
@ -470,8 +470,8 @@ class UDPLink(Link):
|
|||||||
marker_info["color"] = color
|
marker_info["color"] = color
|
||||||
if highlight_duration is not None:
|
if highlight_duration is not None:
|
||||||
marker_info["highlight_duration"] = highlight_duration
|
marker_info["highlight_duration"] = highlight_duration
|
||||||
if direction is not None:
|
if direction is not _UNSET:
|
||||||
marker_info["direction"] = direction
|
marker_info["direction"] = direction # None = clear back to both directions
|
||||||
|
|
||||||
if self._created:
|
if self._created:
|
||||||
await self.update()
|
await self.update()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user