diff --git a/gns3server/api/routes/compute/cloud_nodes.py b/gns3server/api/routes/compute/cloud_nodes.py index b6efc06ca..96fa825fd 100644 --- a/gns3server/api/routes/compute/cloud_nodes.py +++ b/gns3server/api/routes/compute/cloud_nodes.py @@ -269,7 +269,7 @@ async def toggle_cloud_marker( 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( status_code=status.HTTP_404_NOT_FOUND, detail=f"Marker '{marker_name}' is not installed on this node", diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py index e40daeb72..96bda4b68 100644 --- a/gns3server/api/routes/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -423,7 +423,7 @@ async def toggle_docker_marker( 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( status_code=status.HTTP_404_NOT_FOUND, detail=f"Marker '{marker_name}' is not installed on this node", diff --git a/gns3server/api/routes/compute/dynamips_nodes.py b/gns3server/api/routes/compute/dynamips_nodes.py index 38c7a5624..8ffdf4756 100644 --- a/gns3server/api/routes/compute/dynamips_nodes.py +++ b/gns3server/api/routes/compute/dynamips_nodes.py @@ -382,7 +382,7 @@ async def toggle_dynamips_marker( 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( status_code=status.HTTP_404_NOT_FOUND, detail=f"Marker '{marker_name}' is not installed on this node", diff --git a/gns3server/api/routes/compute/iou_nodes.py b/gns3server/api/routes/compute/iou_nodes.py index 289993633..728ef3ad3 100644 --- a/gns3server/api/routes/compute/iou_nodes.py +++ b/gns3server/api/routes/compute/iou_nodes.py @@ -361,7 +361,7 @@ async def toggle_iou_marker( 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( status_code=status.HTTP_404_NOT_FOUND, detail=f"Marker '{marker_name}' is not installed on this node", diff --git a/gns3server/api/routes/compute/qemu_nodes.py b/gns3server/api/routes/compute/qemu_nodes.py index 6ce9252d4..217531ff4 100644 --- a/gns3server/api/routes/compute/qemu_nodes.py +++ b/gns3server/api/routes/compute/qemu_nodes.py @@ -453,7 +453,7 @@ async def toggle_qemu_marker( 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( status_code=status.HTTP_404_NOT_FOUND, detail=f"Marker '{marker_name}' is not installed on this node", diff --git a/gns3server/api/routes/compute/vpcs_nodes.py b/gns3server/api/routes/compute/vpcs_nodes.py index 2b796f979..ca2c2175b 100644 --- a/gns3server/api/routes/compute/vpcs_nodes.py +++ b/gns3server/api/routes/compute/vpcs_nodes.py @@ -360,7 +360,7 @@ async def toggle_vpcs_marker( 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( status_code=status.HTTP_404_NOT_FOUND, detail=f"Marker '{marker_name}' is not installed on this node", diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 54092bcda..7362aa68e 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -1181,29 +1181,29 @@ class BaseNode: ) # Remember which bridge hosts this filter so an instant on/off toggle # (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): """ Toggle an installed marker filter on/off with a single uBridge command (``bridge enable_packet_filter … on|off``) — no NIO reset/reapply, so the pcap identity and emitted counter are preserved (ubridge contract §3.2). - The bridge is resolved from the name→bridge map populated at apply time; - IOU overrides this for its ``iol_bridge`` command shape. + The bridge is resolved from the (name, link_id)→bridge map populated at + 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 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" - 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): """ diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index ef9cd28d8..c24340171 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -1330,18 +1330,15 @@ class IOUVM(BaseNode): str(self.project.id), self._id, name, link_id, tag ) # 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): - """IOU override: toggle via ``iol_bridge enable_packet_filter 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" - 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): """ diff --git a/tests/compute/test_base_node.py b/tests/compute/test_base_node.py index cee8644f3..764af1e6e 100644 --- a/tests/compute/test_base_node.py +++ b/tests/compute/test_base_node.py @@ -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._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) 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._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) 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() await node._ubridge_apply_markers("VPCS-10", nio) 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"