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:
YueGuobin committed 2026-08-02 11:37:49 +08:00
1 parent 71fa778d50
commit 37cb9f0a9c
7 files changed
+48 -21

No files matched your search

+8 -1
View File
@@ -30,6 +30,13 @@ import logging
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 = [
{
"type": "frequency_drop",
@@ -346,7 +353,7 @@ class Link:
"""
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.
+4 -3
View File
@@ -37,6 +37,7 @@ from .snapshot import Snapshot
from .drawing import Drawing
from .topology import project_to_topology, load_topology
from .udp_link import UDPLink
from .link import _UNSET
from ..config import Config
from ..utils.path import check_path_allowed, get_default_project_directory
from ..utils.application_id import get_next_application_id
@@ -947,7 +948,7 @@ class Project:
self.dump()
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.
"""
@@ -966,8 +967,8 @@ class Project:
d["color"] = color
if highlight_duration is not None:
d["highlight_duration"] = highlight_duration
if direction is not None:
d["direction"] = direction
if direction is not _UNSET:
d["direction"] = direction # None = clear back to both directions
# Sync: update every inherited copy across all links.
for link in list(self._links.values()):
+4 -4
View File
@@ -17,7 +17,7 @@
from .controller_error import ControllerError, ControllerNotFoundError
from .link import Link
from .link import Link, _UNSET
from .node_types import BUILTIN_NODE_TYPES
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.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
``self.update()``; uBridge picks up the new params on the next NIO
@@ -470,8 +470,8 @@ class UDPLink(Link):
marker_info["color"] = color
if highlight_duration is not None:
marker_info["highlight_duration"] = highlight_duration
if direction is not None:
marker_info["direction"] = direction
if direction is not _UNSET:
marker_info["direction"] = direction # None = clear back to both directions
if self._created:
await self.update()