Add default_username/default_password as controller-only node properties
(the netmiko_device_type pattern): they are not sent to the compute,
persist with the project topology and can be updated or cleared per
node. Creating a node from a template seeds them from the template
appliance metadata, and the metadata itself is dropped there so it
never leaks into the node properties.
Previously apply_defs_to_new_link ran during finalize (after
link._created=True), issuing one PUT /nio round-trip per link end for
each inherited marker — 5000+ HTTP round-trips even for a single def.
Move it into _prepare_link_from_topology (memory_only) so the inherited
markers are already in _link_data when _prepare() constructs the NIO
specs, and create_batch_nios carries them in the single batch dispatch.
Finalize no longer calls apply_defs_to_new_link. Interactive link
creation (dragging a cable in the UI) still goes through the per-link
create() → apply_defs_to_new_link path — a single link is fast.
per-def operations are project-level — create, update AND delete all
modify the marker policy on every link — so all three must batch, not
just create.
Extend memory_only to update_marker and stop_marker (merge/delete into
_markers + refresh _link_data, no per-link HTTP), and route
update_marker_definition and delete_marker_definition through the same
two-phase path as create: memory-only per link, then one
PUT /projects/{id}/nios/batch per compute.
Trade-off: a full reapply resets every marker's pcap on the link (the
old precise update_marker/stop_marker preserved sibling pcaps). For
project-level policy changes this is acceptable — real-time insight
matters more than pcap continuity, and batching turns 5000+ round-trips
into one per compute.
When a marker definition is created (or re-applied on data_link_type
change), the fan-out used to call inherit_marker -> update() on every
link, issuing one PUT /nio per link end (5000+ round-trips on a 2500-link
project). On started nodes each round-trip also reconfigured uBridge.
Two-phase fan-out:
- inherit_marker/start_marker gain memory_only: writes the marker into
link._markers and refreshes _link_data without any HTTP/emit/dump.
- _apply_def_to_all_links applies memory-only to every link, then
_batch_update_link_nios groups the updated NIO specs by compute and
sends a single PUT /projects/{id}/nios/batch per compute.
compute: new PUT /projects/{id}/nios/batch endpoint with _get_existing_nio
+ _update_nio_binding dispatch (mirrors create_batch_nios), re-applies
filters+markers to uBridge on started nodes.
Precise per-marker operations (update_marker bpf change, stop_marker on
def delete) are untouched — they deliberately avoid a full reapply to
preserve sibling marker pcaps.
Distinguish the two marker-policy fan-out paths in the log so we can tell
which is slow:
- apply_defs_to_new_link (project-open finalize): logs def count + link
count + elapsed, only when marker_definitions is non-empty.
- _marker_apply_concurrently (interactive create/update/delete def): logs
link count + elapsed per fan-out.
3 is too conservative for modern hardware; 10 provides a moderate boost
without the risk of overwhelming the host (start is the heaviest
operation: ubridge process + docker start + network config for each
node).
- start_all, stop_all: emit 'starting/stopping N nodes...' and complete
lines with project name+id, matching the open-node/link log style.
- close: emit 'project closing.../closed' bracketing the whole teardown.
- Revert start_all concurrency 20->3 and reset_console 20->3: start
(ubridge process + docker start + network) is heavy enough that high
concurrency risks overwhelming the server. stop remains 100 (kill is
light) and suspend remains 50 (pause is light).
When opening a project, nodes are created with Pool(concurrency=100).
Each _create_node did an unlocked check-then-act: 'if compute not in
_project_created_on_compute' -> await POST /projects -> add(compute).
The await let dozens of concurrent node creations race past the check
before any registered, each firing a redundant POST /projects at the
same compute. The compute-side sync handler then ran in a thread pool
and instantiated the Project N times (the repeated 'Project ... created'
INFO logs, ~16x).
Guard the check+POST+register with a project-level asyncio.Lock. The
first creation holds it for one POST; the rest acquire, see the compute
already registered, and return immediately — negligible serialization.
Also annotate the node/link progress logs with project name+id and add
completion lines, so the log clearly shows which project is loading and
when each phase finishes.
A large topology takes ~50s to open (dominated by docker daemon node
creation). Without any start marker the user sees no feedback that work
is in progress. Add two INFO lines: 'Loading N nodes...' before the node
pool and 'Creating N links...' before the bulk link prepare/dispatch.
- Drop the diagnostic stage-timing logs added during link-create perf
work (nodes / preallocate / prepare / dispatch) now that bottlenecks
are resolved and verified.
- Lower the per-NIO 'added to adapter' log in docker_vm from INFO to
DEBUG — at 5000+ NIOs per project open it floods the log at INFO.
Stage timing showed prepare taking 96s for 1275 links (vs 0.31s for the
batch dispatch itself). Root cause: _prepare_link_from_topology called
add_link (dump=True default) and update_link_style/update_show_filters_icon
(each unconditionally dump the full topology). 1275 links x serialize-
and-write-the-whole-topology = the entire 96s.
- add_link(..., dump=False): the project is dumped once at the end of open
- set link._link_style / _show_filters_icon directly instead of the
update_* helpers, which also avoids spurious 'link.updated' notifications
before the link is finalised
The final self.dump() at the end of project.open already persists everything.
Project open used to create each link by issuing two NIO POSTs from the
controller to the compute — ~5000 HTTP round-trips for a 2500-link
topology, all funnelling through the single shared controller/compute
event loop and capping throughput near 12 links/s.
Replace it with a bulk path:
- UDPLink split into _prepare() (local: ports, peer addrs, link_data)
and _commit_nios() (dispatch). create() = prepare + commit (interactive).
- Link.add_node gains batch=True: attach both nodes without triggering
per-link NIO HTTP.
- compute: new POST /projects/{id}/nios/batch endpoint with a unified
_add_nio_binding dispatch across node types (docker/qemu/iou/vpcs/
builtin differ in signature).
- project.open: prepare all links locally, group NIO entries by compute,
send each compute a single /nios/batch, then finalise (wire node/port
refs, mark created, notify, apply marker defs) in parallel.
Cuts controller->compute HTTP from O(links) to O(computes). Test added
for the batch endpoint.
Every per-link marker operation (start_marker / stop_marker / update_marker) called Project.dump() -- a full topology serialization + file write. A definition fan-out over 500 links therefore wrote the whole topology 500+ times (each blocking the event loop), which dominated the observed ~1 minute; the NIO round-trips themselves were negligible.
Add a dump: bool = True parameter to the three per-link operations and inherit_marker (matching the existing dump param on Link.add_node). The bulk paths -- definition create fan-out, definition-update sync and re-fan-out, definition-delete cleanup, pause/resume, new-link inheritance -- pass dump=False and their caller dumps once after. apply_defs_to_new_link suppresses per-def dumps too: link create / project open dump once after, so opening a 500-link project with N definitions no longer does 500xN topology writes.
The per-definition fan-out applied markers to links in a serial loop -- one compute round-trip per link. On a 1000-link project that serializes N HTTP round-trips (minutes on remote computes). Fan out with asyncio.gather + Semaphore(32): links are independent (own _markers/_link_data), per-link ControllerError stays isolated, and Project.dump is synchronous + atomic (tmp + rename) so concurrent dumps cannot corrupt the topology file.
Converts the definition-create fan-out, the definition-update sync and re-fan-out loops, and the definition-delete cleanup to the shared _marker_apply_concurrently helper. apply_defs_to_new_link stays serial deliberately: all definitions share one link and each push carries the link's full marker set, so concurrent pushes would race and lose markers.
Markers now work on serial links (Cisco HDLC / PPP / Frame Relay / ATM), not just Ethernet. A marker carries a data_link_type (default DLT_EN10MB); at the uBridge boundary it becomes the 'mark ... linktype <dlt>' keyword so the BPF compiles and the pcap is written with the matching link-layer.
- MarkerCreate / MarkerDefinitionCreate gain data_link_type (default DLT_EN10MB). Per-link it is create-only; definitions are updatable (a change re-fans-out).
- base_node._marker_linktype() normalizes the GNS3 DLT name (strip DLT_, uppercase, None for EN10MB). Single source is SerialPort.data_link_types, so Cisco PPP -> PPP_SERIAL (50), matching the capture path -- no second mapping table.
- _ubridge_add_marker_filter (generic) and the IOU marker loop append 'linktype <dlt>'.
- Definition fan-out branches on link_type in inherit_marker: Ethernet is always EN10MB; a serial link uses the definition's WAN encapsulation, or is SKIPPED when none was chosen (an EN10MB pcap on serial is undecodable). One definition covers a mixed topology.
- MCP marker_definition exposes data_link_type (None = not forwarded).
- No uBridge rebuild on a data_link_type change -- only that one marker's filter is swapped (delete + re-add), mirroring a BPF change; reset_packet_filters preserves sibling mark filters.
Requires the uBridge build with 'mark ... linktype' support.
A marker definition validated its BPF N times — once per link in the fan-out
(start_marker runs validate_bpf_syntax on every copy), spawning one tcpdump -d
subprocess per link for the same expression. Definitions did not validate BPF
at all; only direction was checked.
Move the single validation point to the definition layer (create/update), and
validate each definition's BPF on project load (dropping any that have gone
invalid, like private markers). The inherited fan-out (start_marker) and def
sync (update_marker) now skip validate_bpf_syntax for inherited copies, since
the BPF comes from an already-validated definition. Private per-link markers
still validate inline as before. uBridge still runs pcap_compile at install, so
an invalid expression can never slip through.
Creating a definition over N links now runs one tcpdump instead of N.
The 409 on a tx/rx definition now recommends encoding direction in the BPF
(e.g. icmp[icmptype]==8) as the primary fix, with per-link markers as the
single-link fallback. Doc updated: per-def rejects tx/rx (why + BPF), the
pause section no longer claims bpf changes reset+reapply (they rebuild one
filter), and a new Capture files section covers pcap cleanup + reset-preserves-mark.
Restore a private marker's direction (and highlight_duration) when a
project is reopened — _create_link_from_topology_data previously dropped
them, silently reverting rx/tx markers to "both".
Reject tx/rx direction on marker definitions with HTTP 409: a definition
auto-selects its capture node per link and direction is relative to that
node, so a fixed tx/rx has no stable project-wide meaning. Per-link
markers still support tx/rx; only the project-wide definition is restricted
to "both" (the default).
The project-wide mute (POST /markers/pause|resume + _markers_paused) paused
every marker with one button. The actual need is per-rule control: pause one
definition and toggle only its inherited global-{name} copies across all links.
- Drop project-level: _markers_paused (init/asdict/load/start_all), the
pause_all/resume_all_markers methods, and the /markers/pause|resume routes.
- Add per-definition: a persisted `paused` flag on each definition;
pause/resume_marker_definition fan out update_marker(enabled) to every
global-{name} copy — instant, via the existing enable_packet_filter toggle
(no NIO rebuild, pcap/emitted preserved). New links inherit a paused
definition already off (inherit_marker passes enabled=not paused).
- start_marker takes an enabled kwarg; update_marker's enabled-only short-circuit
now also covers inherited copies so def pause/resume is instant.
- Routes: POST /marker-definitions/{name}/pause|resume.
- Docs + tests updated.
pause/resume was fire-and-forget: the controller sent marker pause/resume but
stored nothing, so the Web UI could only keep a local optimistic flag that was
lost on panel reopen. Treat the mute as a project-level config (like per-marker
enabled): record _markers_paused, persist it in the topology (asdict + load),
and echo it on the project object so the UI renders from server truth.
Because marker pause is a uBridge runtime flag that resets on node restart,
start_all re-applies the mute to freshly started uBridges after a project
reopen — a paused project stays paused across close/reopen.
Wire gns3-server to uBridge's real-time marker controls (contract
../ubridge/doc/gns3server-integration.md §3.2), in three layers:
A. enabled reaches uBridge — _markers_for_node no longer drops disabled
markers controller-side; it carries `enabled` in the NIO spec. apply
installs every marker then issues `enable_packet_filter … off` for the
disabled ones (base_node bridge / iou iol_bridge; old-ubridge errors
downgrade to a warning so a toggle can't break link create).
B. instant per-filter toggle — apply records name→bridge so a new
`_ubridge_set_marker_filter_state` can flip a running filter with
`enable_packet_filter on|off` (iou overrides for iol_bridge + bay/unit).
Each marker-capable node type gains PUT /markers/{name}; update_marker
short-circuits to it when only `enabled` changes (no NIO rebuild, no pcap
flush), falling back to reset+reapply if the route is unavailable.
C. global pause/resume — `_ubridge_marker_pause/resume` send `marker pause`
/ `marker resume` direct to the hypervisor (pause stops signal+pcap,
resume instant, sink retained). Six node-type routes add POST
/markers/pause|resume; project.pause_all/resume_all_markers fan out to
each capture node (deduped, best-effort); REST exposes
POST /projects/{id}/markers/pause|resume.
Toggling enabled and pause/resume are now both instant — only marker create
or a bpf change still go through reset+reapply.
direction was settable but not clearable: once a marker or project-level
definition had direction=tx/rx, no update path could return it to "both
directions", and the definition-sync fan-out silently kept stale values on
every inherited copy.
Introduce a _UNSET sentinel (link.py) distinct from None so updaters can
tell "caller omitted direction" (preserve) from "caller passed None"
(clear). Thread it through UDPLink.update_marker and
Project.update_marker_definition; the two REST routes use Pydantic v2
model_fields_set to translate an explicit JSON null into the sentinel.
MCP follows with a "both" token: link_marker / marker_definition handlers
map direction="both" to a null in the REST body (tri-state: omit=preserve,
tx/rx=set, both=clear), and the tool descriptions/docstrings document it.
Backward compatible: omitting direction or passing tx/rx behaves exactly
as before; only an explicit null / "both" clears.
Direction field in marker.match events
=======================================
Read ubridge dir=<tx|rx> from MARK signal datagrams and forward it
as a "dir" key in the marker.match notification event. The field is
additive -- older ubridge builds omit it and the parser leaves it null,
so consumers fall back to undirected rendering with no version coupling.
Semantics are relative to the capture node (the signal's node=<id>):
tx = capture node is sending (ingressed device-side NIO), rx = it is
receiving (ingressed link-side NIO).
Per-marker direction filter (opt-in, server-side pipeline)
==========================================================
Add a direction field to MarkerCreate and MarkerDefinitionCreate
schemas ("tx" | "rx" | null). Plumb it through the full pipeline:
Schema -> controller (start_marker/update_marker, marker_entry,
_markers_for_node, update_marker_definition sync)
-> REST/MCP handlers -> compute _ubridge_add_marker_filter +
IOU _ubridge_apply_markers -> bridge add_packet_filter dir <tx|rx>
When set, ubridge only fires the mark handler (signal + pcap) for
packets matching the chosen direction. null (default/legacy) = both
directions -- zero behavioural change for existing markers.
Docs and tests
==============
- docs/features/marker-traffic-insight.md: signal format updated,
new Direction section with NIO mapping, arrow mapping, and additive
compatibility note.
- tests/compute/marker/test_marker_manager.py: 3 new parser tests
(dir tx/rx/absent) plus existing test extended to assert dir=None.
13 files, +123/-22, 72 tests pass (zero breakage)
Project.delete() calls open() to rebuild the internal data structures
needed for cleanup. open() schedules start_all() when the project has
auto_start enabled, so deleting an auto-start project actually launched
every node process, allocating ports and consuming resources, only for
close() to kill them moments later.
open() now takes an auto_start argument (default True, so normal opens
are unchanged) and delete() passes auto_start=False.
Fixes#2784
delete_marker_definition removed the def but left the inherited global-*
copies on every link: it called stop_marker(), which rejects inherited
markers (409), and the ControllerError was swallowed as a warning. The
orphaned copies were in-memory only (_persist_markers filters inherited
markers) so a restart hid the symptom, but during a running session they
were undeletable via either the per-link or project API.
Add the same inherited=True bypass that update_marker already has, and
pass it from the def-delete fan-out so the copies are removed for real.
Per-marker UI hint (milliseconds, ge=1) for how long the Web UI keeps a
marker highlighted after a match. Mirrors color: stored on the link,
persisted in the topology, inherited via project-level definitions, and
never sent to uBridge. Omitted = null = frontend uses its own default.
The default is intentionally NOT set in the schema: MarkerCreate also
backs PUT updates, so a schema default would make every partial update
silently reset the value. None means "not provided" (keep existing on
update / frontend decides on create).
Bugs found via end-to-end testing of project-level marker definitions:
1. New links didn't inherit — apply_defs_to_new_link is async but was
called without await in UDPLink.create().
2. Project load crashed — load_project passed marker_definitions to
Project.__init__. Now popped in load_project and restored separately
in Project.open() (it backs a read-only property).
3. PUT on a definition didn't sync to links — update_marker's guard
rejected even the project-layer sync call. Added an `inherited`
bypass flag used by update_marker_definition.
4. _ubridge_add_marker_filter raised re.PatternError — the name regex
used (?i)(?!global) look-around, invalid in Python's re module.
Dropped the prefix check there: "global-*" names are legitimate at
the uBridge boundary (inherited definitions); forbidden only at the
user-facing schema.
5. GET /links hid inherited markers — asdict()'s runtime branch used
_persist_markers() (which filters inherited markers). Restored
self._markers for the runtime branch; only the topology_dump branch
filters (inherited markers are rebuilt from definitions on load).
6. Duplicate "already exists" warnings on project open — open() fanned
out definitions to all links, but UDPLink.create() had already done
so via its inheritance hook. Removed the redundant fan-out in open().
Project-level marker definitions fan out to every link (existing and new).
A definition is stored once on Project._marker_definitions; when applied to
a link the marker is named "global-{def_name}" — the "global" prefix was
pre-reserved in the schema, so inherited and per-link markers can never
collide, nor will their registry keys.
Key behavior:
- POST /projects/{pid}/marker-definitions → fan out to all existing links
- PUT /projects/{pid}/marker-definitions/{name} → sync all inherited copies
- DELETE → remove every inherited copy from every link
- New links auto-inherit all active defs (hook in UDPLink.create)
- Per-link DELETE/PUT of a "global-*" marker is rejected (409)
- Inherited markers are NOT persisted in the topology; they are re-created
from _marker_definitions on project load
- Compute side is untouched — the marker reaches uBridge via the existing
start_marker→update→NIO→ubridge pipeline
Files:
- controller/project.py — _marker_definitions + CRUD + fanout + topology load
- controller/link.py — Link.inherit_marker() + asdict() filter
- controller/udp_link.py — guards on stop/update + create() inheritance hook
- controller/topology.py — persist marker_definitions in project topology
- schemas/controller/links.py — MarkerDefinitionCreate schema
- api/routes/controller/projects.py — REST endpoints (marker-definitions)
Add a read-only `markers` property on Project that flattens every link's
markers into a single dict keyed by "{link_id}/{name}", each entry
carrying the parent link_id and capture-side node_id. Expose it via
GET /projects/{pid}/markers (Project.Audit) so the frontend can fetch all
markers in one round-trip instead of enumerating links first.
Also surface a marker count in project.stats().
Markers now follow exactly the same apply pattern as packet filters:
state lives in Link._markers, application goes through NIO
(update() -> PUT /nio -> _ubridge_apply_markers). The former
immediate-apply REST endpoints (/markers/start, /markers/stop on
the compute side) and the per-node start_marker/stop_marker methods
are removed — they were a legacy of the original capture-inspired
design and have been superseded by the NIO flow.
Changes:
- controller/udp_link: start_marker/stop_marker/update_marker now
set _markers state + call self.update() (mirrors update_filters).
Removed _marker_capture_nodes runtime dict and its helpers.
- controller/project: _create_link_from_topology_data restores
_markers directly from persisted data (with BPF validation,
like filter reload). No long calls start_marker during load.
- compute: _ubridge_apply_markers swallows BPF compile errors
(warn+skip), matching _ubridge_apply_filters behaviour so a
single bad expression cannot break link creation / node restart.
- Removed: /markers/start,stop endpoints (6 handlers across
vpcs/qemu/docker route files), node start_marker/stop_marker
methods (3 VM files), _ubridge_delete_marker_filter,
_marker_capture_nodes, MarkerDelete schema.
Net: ~280 lines of dead code removed; marker and packet filter now
share a single, unified apply path via the NIO.
_create_link_from_topology_data now restores traffic-insight markers
(mirroring how filters are restored via update_filters), so markers —
including their color — survive project close/reopen and server restart.
Guard start_marker's uBridge POST with 'if self._created' (exactly as
update_filters guards its update() call): during project load the link
is not yet created, so only _markers state is recorded and the marker
is applied once via the NIO flow in create()/_ubridge_apply_markers —
no double application.
Previously the disconnected compute check ran after open(), which would
block for 120s trying to connect to unreachable remote computes before
rejecting the deletion. Now reads the topology file directly to extract
compute IDs and checks connectivity before calling open(), enabling
immediate rejection of deletions involving offline computes.
Also removes the redundant post-open() check since the early check
covers both opened and closed project states.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _iou_id_lock must cover _create_node() because get_next_application_id()
checks in-memory nodes (self._nodes), which are only registered after
_create_node() completes. Without this serialization, concurrent IOU
node creation produces duplicate application IDs.
- Narrow IOU lock scope to only cover application_id allocation,
allowing concurrent IOU node creation via Pool(concurrency=5)
- Parallelize link creation during project.open() using Pool(concurrency=5)
instead of sequential processing
- Add batch UDP port allocation endpoint on compute to allocate N ports
in a single HTTP call
- Pre-allocate UDP ports per compute before link creation during project
loading, reducing HTTP round-trips
- UDPLink.create() falls back to individual port allocation if no
pre-allocated port is available
Move the is_running() check from _fast_duplication() to
duplicate() to avoid the error message being wrapped by
the except Exception handler. This ensures the error
message is clean and prevents wasted fast duplication
attempts on running projects.
Add is_running() check at the beginning of _fast_duplication()
to prevent duplicating a project while nodes are running.
Previously, only the export/import fallback path had this check,
which meant running nodes were not detected when fast duplication
succeeded. This aligns with the duplicate API behavior and
provides a consistent safeguard against data inconsistencies.
When renaming a project that has running Docker containers, the containers
were unnecessarily stopped, removed, and recreated, even though the project
name change doesn't affect container configuration.
Root cause:
- Client sends complete project object including variables: [] during rename
- Controller unconditionally notified all computes about the update
- Docker nodes rebuild containers on any project update notification
Solution:
- Only notify compute nodes when variables field has actual content
- Treat None and [] as semantically equivalent (no variables)
- Empty variables don't affect running containers, so no need to update
Impact:
- Project rename operations no longer trigger ~7 second container rebuilds
- Only actual variable changes trigger container recreation
- Fixes issue #2760
Fixes#2759
When renaming a project:
- Update self._filename to match the new project name
- Rename the .gns3 file on disk to keep it in sync
- Add error handling for file rename failures
When duplicating a project:
- Use self._filename (actual filename) instead of self.name
- This handles the case where a project has been renamed
- Prevents 'No such file or directory' errors
The root cause was that project renaming only updated the project name
in memory and in the .gns3 file content, but did not update the actual
.gns3 filename. This caused duplicate operations to fail because they
tried to read a file with the new name that didn't exist.