mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-25 03:12:23 +02:00
fix: reconcile marker filters in _ubridge_apply_markers (delete/update)
The batch marker-def fan-out (PR #2848) routed create/update/delete marker_definition through memory_only + a batch PUT /nios/batch that re-applies markers via _ubridge_apply_markers. But _ubridge_apply_markers was strictly add-only: it skipped any (name, link_id) already in _marker_filter_bridges, and reset_packet_filters preserves mark filters (contract). So: * delete_marker_definition left the deleted marker's filter alive in uBridge (still matching / signalling / writing pcap) until node restart. * update_marker_definition (bpf/tag/direction change) never reached uBridge — the live filter kept the old expression until node restart. Make _ubridge_apply_markers a real reconcile against the desired nio.markers: - installed but no longer desired → delete_packet_filter + unlink pcap + unregister - desired with changed filter field → rebuild (delete + re-add) - desired with only enabled changed → instant toggle (pcap preserved) - desired and unchanged → skip - desired and new → add Track installed specs in a parallel _marker_specs dict so changes can be detected. Both base_node and the IOU iol_bridge override are updated. Added tests for the delete-removed and rebuild-changed-bpf paths.
This commit is contained in:
parent
f045cde0da
commit
958c45b6fc
@ -103,6 +103,10 @@ class BaseNode:
|
|||||||
# marker filter name -> uBridge bridge_name (recorded at apply time so
|
# marker filter name -> uBridge bridge_name (recorded at apply time so
|
||||||
# _ubridge_set_marker_filter_state can toggle on/off without an NIO rebuild).
|
# _ubridge_set_marker_filter_state can toggle on/off without an NIO rebuild).
|
||||||
self._marker_filter_bridges = {}
|
self._marker_filter_bridges = {}
|
||||||
|
# Parallel store of the installed marker spec (bpf/tag/direction/enabled/...)
|
||||||
|
# keyed by (name, link_id) so _ubridge_apply_markers can reconcile: detect
|
||||||
|
# deletions and field changes instead of being add-only.
|
||||||
|
self._marker_specs = {}
|
||||||
|
|
||||||
if self._console is not None:
|
if self._console is not None:
|
||||||
# use a previously allocated console port
|
# use a previously allocated console port
|
||||||
@ -994,6 +998,7 @@ class BaseNode:
|
|||||||
# gone too — clear the map so the next apply re-installs them all rather
|
# gone too — clear the map so the next apply re-installs them all rather
|
||||||
# than skipping them as "already installed".
|
# than skipping them as "already installed".
|
||||||
self._marker_filter_bridges.clear()
|
self._marker_filter_bridges.clear()
|
||||||
|
self._marker_specs.clear()
|
||||||
|
|
||||||
async def add_ubridge_udp_connection(self, bridge_name, source_nio, destination_nio):
|
async def add_ubridge_udp_connection(self, bridge_name, source_nio, destination_nio):
|
||||||
"""
|
"""
|
||||||
@ -1176,6 +1181,7 @@ class BaseNode:
|
|||||||
if nio is not None and getattr(nio, "markers", None):
|
if nio is not None and getattr(nio, "markers", None):
|
||||||
nio.markers.pop(name, None)
|
nio.markers.pop(name, None)
|
||||||
bridge_name = self._marker_filter_bridges.pop((name, link_id), None)
|
bridge_name = self._marker_filter_bridges.pop((name, link_id), None)
|
||||||
|
self._marker_specs.pop((name, link_id), None)
|
||||||
if bridge_name is not None:
|
if bridge_name is not None:
|
||||||
await self._ubridge_delete_marker_filter(bridge_name, name)
|
await self._ubridge_delete_marker_filter(bridge_name, name)
|
||||||
try:
|
try:
|
||||||
@ -1223,34 +1229,71 @@ class BaseNode:
|
|||||||
|
|
||||||
async def _ubridge_apply_markers(self, bridge_name, nio):
|
async def _ubridge_apply_markers(self, bridge_name, nio):
|
||||||
"""
|
"""
|
||||||
Install the traffic-insight markers carried by *nio* onto bridge
|
Reconcile the traffic-insight markers carried by *nio* onto bridge
|
||||||
*bridge_name* that aren't already there. uBridge's ``reset_packet_filters``
|
*bridge_name* with what is already installed there.
|
||||||
preserves mark filters (contract), so on an NIO update we add only the new
|
|
||||||
ones — re-adding an existing marker would either duplicate it or
|
uBridge's ``reset_packet_filters`` preserves mark filters (contract), so
|
||||||
close/reopen its pcap. Called from ``add_ubridge_udp_connection`` (fresh
|
a plain re-add would duplicate them; instead this diffs the desired
|
||||||
bridge, empty map → installs all) and ``update_ubridge_udp_connection``
|
``nio.markers`` against the installed ``_marker_specs``:
|
||||||
(incremental).
|
|
||||||
|
* installed but no longer desired → delete filter + unlink pcap
|
||||||
|
* desired with changed bpf/tag/direction/data_link_type → rebuild
|
||||||
|
(delete + add; the marker's own pcap reopens for the new BPF)
|
||||||
|
* desired with only ``enabled`` changed → instant on/off toggle
|
||||||
|
(sibling and own pcap stay open)
|
||||||
|
* desired and unchanged → skip
|
||||||
|
* desired and new → add
|
||||||
|
|
||||||
|
Called from ``add_ubridge_udp_connection`` (fresh bridge, empty maps →
|
||||||
|
installs all) and ``update_ubridge_udp_connection`` / the batch NIO
|
||||||
|
update path (incremental reconcile).
|
||||||
"""
|
"""
|
||||||
from gns3server.compute.marker.marker_manager import MarkerManager
|
from gns3server.compute.marker.marker_manager import MarkerManager
|
||||||
|
|
||||||
markers = nio.markers if hasattr(nio, 'markers') else {}
|
markers = nio.markers if hasattr(nio, 'markers') else {}
|
||||||
if not markers:
|
|
||||||
return
|
|
||||||
|
|
||||||
manager = MarkerManager.instance()
|
manager = MarkerManager.instance()
|
||||||
markers_dir = self.project.markers_working_directory()
|
markers_dir = self.project.markers_working_directory()
|
||||||
for name, spec in markers.items():
|
desired = {(name, spec.get("link_id", "")): spec for name, spec in markers.items()}
|
||||||
link_id = spec.get("link_id", "")
|
|
||||||
# Incremental: skip markers already on this bridge. uBridge keeps mark
|
# 1. Remove installed markers that are no longer desired (marker/def delete).
|
||||||
# filters across reset_packet_filters, so re-adding would duplicate (or
|
for key in list(self._marker_filter_bridges):
|
||||||
# reopen the pcap). A fresh bridge has an empty map → installs all.
|
if key not in desired:
|
||||||
if (name, link_id) in self._marker_filter_bridges:
|
mname, link_id = key
|
||||||
continue
|
installed_bridge = self._marker_filter_bridges.pop(key)
|
||||||
|
self._marker_specs.pop(key, None)
|
||||||
|
await self._ubridge_delete_marker_filter(installed_bridge, mname)
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(markers_dir, f"{self._id}_{link_id}_{mname}.pcap"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except OSError as e:
|
||||||
|
log.warning("Could not remove marker pcap for '%s' on link %s: %s", mname, link_id, e)
|
||||||
|
manager.unregister(self._id, mname)
|
||||||
|
|
||||||
|
# 2. Add newly-desired markers; rebuild ones whose filter fields changed.
|
||||||
|
rebuild_fields = ("bpf", "tag", "direction", "data_link_type")
|
||||||
|
for (name, link_id), spec in desired.items():
|
||||||
bpf = spec.get("bpf", "")
|
bpf = spec.get("bpf", "")
|
||||||
tag = spec.get("tag")
|
tag = spec.get("tag")
|
||||||
pcap_path = os.path.join(
|
enabled = spec.get("enabled", True)
|
||||||
markers_dir, f"{self._id}_{link_id}_{name}.pcap"
|
if (name, link_id) in self._marker_filter_bridges:
|
||||||
)
|
installed_spec = self._marker_specs.get((name, link_id))
|
||||||
|
if installed_spec is None:
|
||||||
|
# Installed but no recorded spec (legacy / pre-reconcile state):
|
||||||
|
# cannot diff, skip to avoid a duplicate add.
|
||||||
|
continue
|
||||||
|
if any(installed_spec.get(f) != spec.get(f) for f in rebuild_fields):
|
||||||
|
# A filter field changed → rebuild (delete + re-add).
|
||||||
|
installed_bridge = self._marker_filter_bridges.get((name, link_id))
|
||||||
|
await self._ubridge_delete_marker_filter(installed_bridge, name)
|
||||||
|
elif installed_spec.get("enabled", True) != enabled:
|
||||||
|
# Only the on/off state changed → instant toggle, pcap preserved.
|
||||||
|
await self._ubridge_set_marker_filter_state(name, enabled)
|
||||||
|
self._marker_specs[(name, link_id)] = spec
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue # unchanged
|
||||||
|
pcap_path = os.path.join(markers_dir, f"{self._id}_{link_id}_{name}.pcap")
|
||||||
try:
|
try:
|
||||||
await self._ubridge_add_marker_filter(bridge_name, name, bpf, pcap_path, tag, link_id,
|
await self._ubridge_add_marker_filter(bridge_name, name, bpf, pcap_path, tag, link_id,
|
||||||
direction=spec.get("direction"),
|
direction=spec.get("direction"),
|
||||||
@ -1268,22 +1311,19 @@ class BaseNode:
|
|||||||
# A disabled marker is installed but turned off (a paused tap), not
|
# A disabled marker is installed but turned off (a paused tap), not
|
||||||
# dropped — so the UI can flip it back on instantly with
|
# dropped — so the UI can flip it back on instantly with
|
||||||
# enable_packet_filter, no NIO rebuild (ubridge contract §3.2).
|
# enable_packet_filter, no NIO rebuild (ubridge contract §3.2).
|
||||||
if not spec.get("enabled", True):
|
if not enabled:
|
||||||
try:
|
try:
|
||||||
await self._ubridge_send(f"bridge enable_packet_filter {bridge_name} {name} off")
|
await self._ubridge_send(f"bridge enable_packet_filter {bridge_name} {name} off")
|
||||||
except UbridgeError as e:
|
except UbridgeError as e:
|
||||||
# Old ubridge without enable_packet_filter: leave it installed
|
# Old ubridge without enable_packet_filter: leave it installed
|
||||||
# (on) rather than fail the whole link/marker apply.
|
# (on) rather than fail the whole link/marker apply.
|
||||||
log.warning(f"Could not turn marker '{name}' off on {bridge_name}: {e}")
|
log.warning(f"Could not turn marker '{name}' off on {bridge_name}: {e}")
|
||||||
manager.register(
|
manager.register(str(self.project.id), self._id, name, link_id, tag)
|
||||||
str(self.project.id), self._id, name, link_id, tag
|
|
||||||
)
|
|
||||||
# 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, and keep the spec so
|
||||||
# keyed (name, link_id) so a node that hosts markers for several links
|
# the next reconcile can detect changes.
|
||||||
# (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
|
self._marker_filter_bridges[name, link_id] = bridge_name
|
||||||
|
self._marker_specs[name, link_id] = spec
|
||||||
|
|
||||||
async def _ubridge_set_marker_filter_state(self, name, enabled):
|
async def _ubridge_set_marker_filter_state(self, name, enabled):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1266,7 +1266,9 @@ class IOUVM(BaseNode):
|
|||||||
|
|
||||||
async def _ubridge_apply_markers(self, adapter_number, port_number, nio):
|
async def _ubridge_apply_markers(self, adapter_number, port_number, nio):
|
||||||
"""
|
"""
|
||||||
(Re-)apply traffic-insight markers to the IOL bridge.
|
Reconcile traffic-insight markers on the IOL bridge (diff desired
|
||||||
|
``nio.markers`` against installed ``_marker_specs``): delete removed,
|
||||||
|
rebuild changed, toggle on/off-only changes, add new, skip unchanged.
|
||||||
|
|
||||||
IOU uses ``iol_bridge`` (not ``bridge``) and the ``add_packet_filter``
|
IOU uses ``iol_bridge`` (not ``bridge``) and the ``add_packet_filter``
|
||||||
command carries extra ``{bay} {unit}`` positional arguments between the
|
command carries extra ``{bay} {unit}`` positional arguments between the
|
||||||
@ -1280,41 +1282,55 @@ class IOUVM(BaseNode):
|
|||||||
from gns3server.compute.marker.marker_manager import MarkerManager
|
from gns3server.compute.marker.marker_manager import MarkerManager
|
||||||
|
|
||||||
markers = nio.markers if hasattr(nio, 'markers') else {}
|
markers = nio.markers if hasattr(nio, 'markers') else {}
|
||||||
if not markers:
|
|
||||||
return
|
|
||||||
|
|
||||||
manager = MarkerManager.instance()
|
manager = MarkerManager.instance()
|
||||||
markers_dir = self.project.markers_working_directory()
|
markers_dir = self.project.markers_working_directory()
|
||||||
bridge_name = f"IOL-BRIDGE-{self.application_id + 512}"
|
bridge_name = f"IOL-BRIDGE-{self.application_id + 512}"
|
||||||
location = "{bridge_name} {bay} {unit}".format(
|
location = "{bridge_name} {bay} {unit}".format(
|
||||||
bridge_name=bridge_name, bay=adapter_number, unit=port_number
|
bridge_name=bridge_name, bay=adapter_number, unit=port_number
|
||||||
)
|
)
|
||||||
for name, spec in markers.items():
|
desired = {(name, spec.get("link_id", "")): spec for name, spec in markers.items()}
|
||||||
link_id = spec.get("link_id", "")
|
|
||||||
# Incremental: skip markers already installed on this port. A NIO
|
# 1. Remove installed markers that are no longer desired.
|
||||||
# update carries EVERY marker on the port (e.g. an inherited
|
for key in list(self._marker_filter_bridges):
|
||||||
# global-* copy plus a newly added private one); uBridge's
|
if key not in desired:
|
||||||
# add_packet_filter rejects a duplicate filter name (packet_filter.c),
|
mname, link_id = key
|
||||||
# so we must not re-add one already here — mirrors the generic
|
installed_location = self._marker_filter_bridges.pop(key)
|
||||||
# _ubridge_apply_markers guard. A fresh bridge has an empty map
|
self._marker_specs.pop(key, None)
|
||||||
# (cleared on _stop_ubridge) so all are installed.
|
await self._ubridge_delete_marker_filter(installed_location, mname)
|
||||||
if (name, link_id) in self._marker_filter_bridges:
|
try:
|
||||||
continue
|
os.remove(os.path.join(markers_dir, f"{self._id}_{link_id}_{mname}.pcap"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except OSError as e:
|
||||||
|
log.warning("Could not remove marker pcap for '%s' on link %s: %s", mname, link_id, e)
|
||||||
|
manager.unregister(self._id, mname)
|
||||||
|
|
||||||
|
# 2. Add / reconcile desired markers.
|
||||||
|
rebuild_fields = ("bpf", "tag", "direction", "data_link_type")
|
||||||
|
for (name, link_id), spec in desired.items():
|
||||||
bpf = spec.get("bpf", "")
|
bpf = spec.get("bpf", "")
|
||||||
tag = spec.get("tag")
|
tag = spec.get("tag")
|
||||||
pcap_path = os.path.join(
|
enabled = spec.get("enabled", True)
|
||||||
markers_dir, f"{self._id}_{link_id}_{name}.pcap"
|
if (name, link_id) in self._marker_filter_bridges:
|
||||||
)
|
installed_spec = self._marker_specs.get((name, link_id))
|
||||||
# Build the iol_bridge marker filter command:
|
if installed_spec is None:
|
||||||
# iol_bridge add_packet_filter {br} {bay} {unit} {name} mark "{bpf}" [tag {id}] pcap "{path}"
|
continue # installed (legacy, no spec) — skip to avoid dup
|
||||||
|
if any(installed_spec.get(f) != spec.get(f) for f in rebuild_fields):
|
||||||
|
installed_location = self._marker_filter_bridges.get((name, link_id))
|
||||||
|
await self._ubridge_delete_marker_filter(installed_location, name)
|
||||||
|
elif installed_spec.get("enabled", True) != enabled:
|
||||||
|
await self._ubridge_set_marker_filter_state(name, enabled)
|
||||||
|
self._marker_specs[(name, link_id)] = spec
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
pcap_path = os.path.join(markers_dir, f"{self._id}_{link_id}_{name}.pcap")
|
||||||
|
# iol_bridge add_packet_filter {br} {bay} {unit} {name} mark "{bpf}" [tag {id}] pcap "{path}"
|
||||||
cmd = 'iol_bridge add_packet_filter {loc} {name} mark "{bpf}"'.format(
|
cmd = 'iol_bridge add_packet_filter {loc} {name} mark "{bpf}"'.format(
|
||||||
loc=location, name=name, bpf=bpf
|
loc=location, name=name, bpf=bpf
|
||||||
)
|
)
|
||||||
if tag is not None:
|
if tag is not None:
|
||||||
cmd += f" tag {tag}"
|
cmd += f" tag {tag}"
|
||||||
# IOU uses one per-node IOL-BRIDGE for every link, so bridge+filter
|
|
||||||
# are identical across this node's links — `link` is the only way the
|
|
||||||
# controller can tell their signals apart (contract §3.2).
|
|
||||||
if link_id:
|
if link_id:
|
||||||
cmd += f" link {link_id}"
|
cmd += f" link {link_id}"
|
||||||
direction = spec.get("direction")
|
direction = spec.get("direction")
|
||||||
@ -1333,16 +1349,14 @@ class IOUVM(BaseNode):
|
|||||||
self.project.emit("log.warning", {"message": message})
|
self.project.emit("log.warning", {"message": message})
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
if not spec.get("enabled", True):
|
if not enabled:
|
||||||
try:
|
try:
|
||||||
await self._ubridge_send(f"iol_bridge enable_packet_filter {location} {name} off")
|
await self._ubridge_send(f"iol_bridge enable_packet_filter {location} {name} off")
|
||||||
except UbridgeError as e:
|
except UbridgeError as e:
|
||||||
log.warning(f"Could not turn marker '{name}' off on {location}: {e}")
|
log.warning(f"Could not turn marker '{name}' off on {location}: {e}")
|
||||||
manager.register(
|
manager.register(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.
|
|
||||||
self._marker_filter_bridges[name, link_id] = location
|
self._marker_filter_bridges[name, link_id] = location
|
||||||
|
self._marker_specs[name, link_id] = spec
|
||||||
|
|
||||||
async def _ubridge_set_marker_filter_state(self, name, enabled):
|
async def _ubridge_set_marker_filter_state(self, name, enabled):
|
||||||
"""IOU override: toggle every (name, link_id) entry via ``iol_bridge``."""
|
"""IOU override: toggle every (name, link_id) entry via ``iol_bridge``."""
|
||||||
|
|||||||
@ -367,6 +367,49 @@ async def test_apply_markers_skips_already_installed(compute_project, manager):
|
|||||||
assert not any("add_packet_filter" in c for c in cmds) # skipped, not re-added
|
assert not any("add_packet_filter" in c for c in cmds) # skipped, not re-added
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_apply_markers_deletes_removed(compute_project, manager):
|
||||||
|
# Reconcile: a marker no longer in nio.markers is deleted from uBridge
|
||||||
|
# (filter + registry), not left as an orphan still matching packets.
|
||||||
|
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
|
||||||
|
node._ubridge_send = AsyncioMagicMock()
|
||||||
|
node._ubridge_hypervisor = MagicMock()
|
||||||
|
node._ubridge_hypervisor.is_running.return_value = True
|
||||||
|
node._marker_filter_bridges["m", "L1"] = "VPCS-10"
|
||||||
|
node._marker_specs["m", "L1"] = {"bpf": "icmp", "enabled": True}
|
||||||
|
nio = NIOUDP(1234, "127.0.0.1", 4321)
|
||||||
|
nio.markers = {} # marker removed
|
||||||
|
with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm:
|
||||||
|
mm.instance.return_value.unregister = MagicMock()
|
||||||
|
await node._ubridge_apply_markers("VPCS-10", nio)
|
||||||
|
cmds = [c.args[0] for c in node._ubridge_send.call_args_list]
|
||||||
|
assert any("delete_packet_filter VPCS-10 m" in c for c in cmds)
|
||||||
|
assert ("m", "L1") not in node._marker_filter_bridges
|
||||||
|
assert ("m", "L1") not in node._marker_specs
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_apply_markers_rebuilds_changed_bpf(compute_project, manager):
|
||||||
|
# Reconcile: a marker whose bpf changed is rebuilt (delete + re-add), so
|
||||||
|
# uBridge ends up with the new expression — not the stale original.
|
||||||
|
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
|
||||||
|
node._ubridge_send = AsyncioMagicMock()
|
||||||
|
node._ubridge_hypervisor = MagicMock()
|
||||||
|
node._ubridge_hypervisor.is_running.return_value = True
|
||||||
|
node._marker_filter_bridges["m", "L1"] = "VPCS-10"
|
||||||
|
node._marker_specs["m", "L1"] = {"bpf": "icmp", "tag": None, "direction": None,
|
||||||
|
"data_link_type": None, "enabled": True}
|
||||||
|
nio = NIOUDP(1234, "127.0.0.1", 4321)
|
||||||
|
nio.markers = {"m": {"bpf": "tcp", "tag": None, "link_id": "L1",
|
||||||
|
"direction": None, "enabled": True}}
|
||||||
|
with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm:
|
||||||
|
mm.instance.return_value.register = MagicMock()
|
||||||
|
await node._ubridge_apply_markers("VPCS-10", nio)
|
||||||
|
cmds = [c.args[0] for c in node._ubridge_send.call_args_list]
|
||||||
|
assert any("delete_packet_filter VPCS-10 m" in c for c in cmds) # old removed
|
||||||
|
assert any("add_packet_filter VPCS-10 m mark" in c and "tcp" in c for c in cmds) # new added
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_stop_ubridge_clears_marker_bridges(compute_project, manager):
|
async def test_stop_ubridge_clears_marker_bridges(compute_project, manager):
|
||||||
# uBridge stopping drops every marker filter — the map must clear so the next
|
# uBridge stopping drops every marker filter — the map must clear so the next
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user