marker: key _marker_filter_bridges by (name, link_id) so multi-link nodes toggle every copy

The _marker_filter_bridges dict was keyed by marker name alone, so when one
node hosted the same filter name on several links (IUOL-BRIDGE per node with
many bays/units, or a multi-interface router), successive apply calls
overwrote earlier entries. pause_marker_definition then toggled only the last
recorded bridge/location — other copies stayed active and kept emitting.

Key by (name, link_id) so each copy is independent, and iterate all matching
entries in _ubridge_set_marker_filter_state (both generic bridge and IOU
iol_bridge override). Toggle route existence checks also iterate matching
names. Tests updated.
This commit is contained in:
YueGuobin 2026-08-02 23:48:24 +08:00
parent 34b644a548
commit ff907da5f6
No known key found for this signature in database
9 changed files with 25 additions and 28 deletions

View File

@ -269,7 +269,7 @@ async def toggle_cloud_marker(
Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2).
""" """
if marker_name not in node._marker_filter_bridges: if not any(n == marker_name for (n, lid) in node._marker_filter_bridges):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Marker '{marker_name}' is not installed on this node", detail=f"Marker '{marker_name}' is not installed on this node",

View File

@ -423,7 +423,7 @@ async def toggle_docker_marker(
Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2).
""" """
if marker_name not in node._marker_filter_bridges: if not any(n == marker_name for (n, lid) in node._marker_filter_bridges):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Marker '{marker_name}' is not installed on this node", detail=f"Marker '{marker_name}' is not installed on this node",

View File

@ -382,7 +382,7 @@ async def toggle_dynamips_marker(
Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2).
""" """
if marker_name not in node._marker_filter_bridges: if not any(n == marker_name for (n, lid) in node._marker_filter_bridges):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Marker '{marker_name}' is not installed on this node", detail=f"Marker '{marker_name}' is not installed on this node",

View File

@ -361,7 +361,7 @@ async def toggle_iou_marker(
Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2).
""" """
if marker_name not in node._marker_filter_bridges: if not any(n == marker_name for (n, lid) in node._marker_filter_bridges):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Marker '{marker_name}' is not installed on this node", detail=f"Marker '{marker_name}' is not installed on this node",

View File

@ -453,7 +453,7 @@ async def toggle_qemu_marker(
Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2).
""" """
if marker_name not in node._marker_filter_bridges: if not any(n == marker_name for (n, lid) in node._marker_filter_bridges):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Marker '{marker_name}' is not installed on this node", detail=f"Marker '{marker_name}' is not installed on this node",

View File

@ -360,7 +360,7 @@ async def toggle_vpcs_marker(
Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2).
""" """
if marker_name not in node._marker_filter_bridges: if not any(n == marker_name for (n, lid) in node._marker_filter_bridges):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Marker '{marker_name}' is not installed on this node", detail=f"Marker '{marker_name}' is not installed on this node",

View File

@ -1181,29 +1181,29 @@ class BaseNode:
) )
# Remember which bridge hosts this filter so an instant on/off toggle # Remember which bridge hosts this filter so an instant on/off toggle
# (no NIO rebuild) can resolve it by name alone. # (no NIO rebuild) can resolve it by name alone.
self._marker_filter_bridges[name] = bridge_name # keyed (name, link_id) so a node that hosts markers for several links
# (e.g. IOU with one IOL-BRIDGE and many bays/units) records each
# copy independently — toggle below iterates all matching entries.
self._marker_filter_bridges[name, link_id] = bridge_name
async def _ubridge_set_marker_filter_state(self, name, enabled): async def _ubridge_set_marker_filter_state(self, name, enabled):
""" """
Toggle an installed marker filter on/off with a single uBridge command Toggle an installed marker filter on/off with a single uBridge command
(``bridge enable_packet_filter on|off``) no NIO reset/reapply, so the (``bridge enable_packet_filter on|off``) no NIO reset/reapply, so the
pcap identity and emitted counter are preserved (ubridge contract §3.2). pcap identity and emitted counter are preserved (ubridge contract §3.2).
The bridge is resolved from the namebridge map populated at apply time; The bridge is resolved from the (name, link_id)bridge map populated at
IOU overrides this for its ``iol_bridge`` command shape. apply time; entries are iterated so a node that hosts the same marker name
on several links (e.g. IOU with one IOL-BRIDGE per node) toggles every
copy. IOU overrides this for its ``iol_bridge`` command shape.
:param name: marker filter name :param name: marker filter name
:param enabled: True = on (signal+pcap), False = off (paused tap) :param enabled: True = on (signal+pcap), False = off (paused tap)
""" """
bridge_name = self._marker_filter_bridges.get(name)
if not bridge_name:
# Marker not installed on this uBridge (node not started, or not yet
# applied). The controller-layer `enabled` is still authoritative and
# is honoured when the node starts and applies the marker, so a
# toggle here is a no-op rather than an error.
return
state = "on" if enabled else "off" state = "on" if enabled else "off"
await self._ubridge_send(f"bridge enable_packet_filter {bridge_name} {name} {state}") for (n, lid), bridge_name in list(self._marker_filter_bridges.items()):
if n == name:
await self._ubridge_send(f"bridge enable_packet_filter {bridge_name} {name} {state}")
async def _ubridge_marker_pause(self): async def _ubridge_marker_pause(self):
""" """

View File

@ -1330,18 +1330,15 @@ class IOUVM(BaseNode):
str(self.project.id), self._id, name, link_id, tag str(self.project.id), self._id, name, link_id, tag
) )
# Record name -> location (bridge bay unit) for instant toggle. # Record name -> location (bridge bay unit) for instant toggle.
self._marker_filter_bridges[name] = location self._marker_filter_bridges[name, link_id] = location
async def _ubridge_set_marker_filter_state(self, name, enabled): async def _ubridge_set_marker_filter_state(self, name, enabled):
"""IOU override: toggle via ``iol_bridge enable_packet_filter <bridge> <bay> <unit> <name> on|off``.""" """IOU override: toggle every (name, link_id) entry via ``iol_bridge``."""
location = self._marker_filter_bridges.get(name)
if not location:
# Marker not installed on this uBridge (node not started, or not yet
# applied); controller-layer `enabled` is authoritative. No-op.
return
state = "on" if enabled else "off" state = "on" if enabled else "off"
await self._ubridge_send(f"iol_bridge enable_packet_filter {location} {name} {state}") for (n, lid), location in list(self._marker_filter_bridges.items()):
if n == name:
await self._ubridge_send(f"iol_bridge enable_packet_filter {location} {name} {state}")
async def adapter_remove_nio_binding(self, adapter_number, port_number): async def adapter_remove_nio_binding(self, adapter_number, port_number):
""" """

View File

@ -180,7 +180,7 @@ async def test_set_marker_filter_state_off(compute_project, manager):
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager) node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock() node._ubridge_send = AsyncioMagicMock()
node._marker_filter_bridges["m"] = "VPCS-10" node._marker_filter_bridges["m", "L"] = "VPCS-10"
await node._ubridge_set_marker_filter_state("m", False) await node._ubridge_set_marker_filter_state("m", False)
node._ubridge_send.assert_called_with("bridge enable_packet_filter VPCS-10 m off") node._ubridge_send.assert_called_with("bridge enable_packet_filter VPCS-10 m off")
@ -190,7 +190,7 @@ async def test_set_marker_filter_state_on(compute_project, manager):
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager) node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock() node._ubridge_send = AsyncioMagicMock()
node._marker_filter_bridges["m"] = "VPCS-10" node._marker_filter_bridges["m", "L"] = "VPCS-10"
await node._ubridge_set_marker_filter_state("m", True) await node._ubridge_set_marker_filter_state("m", True)
node._ubridge_send.assert_called_with("bridge enable_packet_filter VPCS-10 m on") node._ubridge_send.assert_called_with("bridge enable_packet_filter VPCS-10 m on")
@ -226,4 +226,4 @@ async def test_apply_markers_turns_disabled_filter_off(compute_project, manager)
mm.instance.return_value.register = MagicMock() mm.instance.return_value.register = MagicMock()
await node._ubridge_apply_markers("VPCS-10", nio) await node._ubridge_apply_markers("VPCS-10", nio)
node._ubridge_send.assert_any_call("bridge enable_packet_filter VPCS-10 m off") node._ubridge_send.assert_any_call("bridge enable_packet_filter VPCS-10 m off")
assert node._marker_filter_bridges["m"] == "VPCS-10" assert node._marker_filter_bridges["m", "L1"] == "VPCS-10"