feat: add tag-keyed aggregate replay over paused markers' pcaps

Markers on different links sharing a tag form one distributed capture
session. Once every marker under the tag is paused (409 otherwise), three
read-only endpoints replay it:

- GET .../markers/tags/{tag}/replay/range merges the per-marker pcaps by
  scanning 16-byte record headers only (no tshark) into a timestamp-
  ordered frame list (per-second buckets above a 5000-frame cap)
- GET .../replay/frames?ts=&window_ms= returns frames in [T, T+window];
  an empty window is a normal empty array
- GET .../replay/frame/detail lazily decodes one frame the user opened:
  raw bytes for the hex view read straight from the pcap, protocol tree
  from 'tshark -T pdml' mapped isomorphically to JSON (every attribute
  survives, values stay strings). tshark reads a /tmp scratch copy with a
  scratch HOME — hardened profiles deny it the project directory.

Sort key is (ts, source file, frame number): ts is not unique across a
merge. The ts parameter round-trips as the exact string from the frame
list. Round-trip tests pin the PDML→JSON fidelity (element count and
attribute coverage).
This commit is contained in:
YueGuobin committed 2026-09-05 21:54:52 +08:00
1 parent 5c285f631b
commit a8fa529252
7 files changed
+1309 -1

No files matched your search

@@ -44,6 +44,8 @@ from gns3server.controller.link import _UNSET
from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError
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 import marker_replay
from gns3server.controller.marker_replay import TsharkError, TsharkMissingError
from gns3server.utils.asyncio import aiozipstream
from gns3server.utils.path import is_safe_path
from gns3server.db.repositories.templates import TemplatesRepository
@@ -219,6 +221,87 @@ def get_project_markers(project: Project = Depends(dep_project)) -> dict:
return project.markers
@router.get(
"/{project_id}/markers/tags/{tag}/replay/range",
dependencies=[Depends(has_privilege("Project.Audit"))],
)
def replay_tag_range(tag: int, project: Project = Depends(dep_project)) -> dict:
"""
Aggregate replay timeline for a tag: merges the pcap of every marker
carrying ``tag`` into one timestamp-ordered view (design reference:
``marker_replay`` module docstring).
The tag gate applies: every marker under the tag must be paused
(``enabled: false``) — 409 otherwise. The response carries the timeline
bounds, per-source stats, and the full merged frame list while under the
frame cap (5000); above it the list is replaced by per-second buckets.
Required privilege: Project.Audit
"""
return marker_replay.build_timeline(project, tag)
@router.get(
"/{project_id}/markers/tags/{tag}/replay/frames",
dependencies=[Depends(has_privilege("Project.Audit"))],
)
def replay_tag_frames(
tag: int,
ts: str,
window_ms: int = 100,
limit: int = 1000,
project: Project = Depends(dep_project),
) -> dict:
"""
Frames with ts in ``[ts, ts + window_ms]`` merged across every source of
the tag. A time with no frames is a normal successful answer:
``{"frames": []}``. The tag gate applies (409 while any marker captures).
``ts`` must be the exact string returned by the range response — never
re-serialize it through a float.
Required privilege: Project.Audit
"""
return marker_replay.query_frames(project, tag, ts, window_ms=window_ms, limit=limit)
@router.get(
"/{project_id}/markers/tags/{tag}/replay/frame/detail",
dependencies=[Depends(has_privilege("Project.Audit"))],
)
async def replay_tag_frame_detail(
tag: int,
ts: str,
node_id: str,
link_id: str,
marker: str,
project: Project = Depends(dep_project),
) -> dict:
"""
Decode exactly one frame (lazy — invoked when the user opens a frame,
never by the timeline itself): raw bytes for the hex view read straight
from the pcap, protocol tree from ``tshark -T pdml`` mapped isomorphically
to JSON (every PDML attribute survives, values stay strings).
``ts`` must be the exact string from the frame list; ``node_id`` +
``link_id`` + ``marker`` identify the source pcap. The tag gate applies.
Required privilege: Project.Audit
"""
try:
return await marker_replay.decode_frame(project, tag, ts, node_id, link_id, marker)
except TsharkMissingError:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="tshark is not installed on this server — frame detail is unavailable",
)
except TsharkError as e:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))
# ---------------------------------------------------------------------------
# Project-level marker definitions (global rules inherited by every link)
# ---------------------------------------------------------------------------