mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
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:
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)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user