1922 Commits
Author SHA1 Message Date
Guobin Yue 5238bd3b39 Merge branch '3.1' into fix/duplicate-device-configs-merge 2026-10-07 23:59:30 +08:00
Mark Paronyan 241c2b65e7 feat: add optional ETag/If-Match concurrency for projects, nodes and links 2026-10-07 16:53:11 +03:00
Mark Paronyan db031416a1 feat: add response models for endpoints returning untyped objects 2026-10-07 16:49:31 +03:00
Mark Paronyan 6ed9e44f00 feat(api): generate unique readable OpenAPI operationIds 2026-10-07 15:53:00 +03:00
Mark Paronyan 11a310dd9b feat(api): type link filters and available_filters response 2026-10-07 15:48:52 +03:00
Mark Paronyan a9e87df644 fix(links): reserve ports on add, accept port name and auto, add port_in_use code 2026-10-07 15:41:04 +03:00
Mark Paronyan e1a27dadcf feat(api): add strict_names and batch node and link creation 2026-10-07 15:35:45 +03:00
Mark Paronyan 6a416d1a2a feat(api): return node and add wait option on node lifecycle calls 2026-10-07 15:35:11 +03:00
Mark Paronyan ead44787f3 feat(api): add Snapshot.created and type NodeFile timestamps as date-time 2026-10-07 15:25:15 +03:00
Mark Paronyan 8cb92e7685 fix(api): derive OpenAPI version from package, add servers, hide AI stub routes 2026-10-07 15:21:30 +03:00
Mark Paronyan 86a279330d feat(api): add Notification schema and document notification streams 2026-10-07 15:14:50 +03:00
Jeremy Grossmann 0836e01525 Merge pull request #2950 from markparonyan/api-issue-14-yywwso
fix(api): return 200 instead of 201 for ACL endpoints, IOU license up…
2026-10-07 13:50:53 +02:00
Mark Paronyan a4f20e0966 feat(api): add discriminators to link iface, appliance version and template settings schemas 2026-10-07 14:22:18 +03:00
Mark Paronyan fcfa968cc5 feat(api): return token lifetimes and document API keys as bearer tokens 2026-10-07 14:18:18 +03:00
Mark Paronyan 289f9394b6 feat(api): add machine-readable error codes and align OpenAPI error schemas 2026-10-07 14:15:41 +03:00
Mark Paronyan ba0465f191 fix(api): declare binary and streaming bodies in OpenAPI 2026-10-07 14:09:02 +03:00
Mark Paronyan 97d5480241 fix(api): return 200 instead of 201 for ACL endpoints, IOU license update 2026-10-07 13:56:08 +03:00
Mark Paronyan 852dc5be0f feat(api): add exact-match name filter to list endpoints 2026-10-07 13:40:34 +03:00
YueGuobin 980254b9e6 Merge branch 'feat/e2e-ipv6-dualstack' into fix/duplicate-device-configs-merge 2026-10-07 13:20:22 +08:00
YueGuobin 6459122113 test(e2e): run every traffic checkpoint dual-stack (IPv6)
The live suite asserted the datapath over IPv4 only. Give every guest a
ULA IPv6 address next to its v4 one and twin every v4 assertion point,
sharing the impairment window (one filter application, both families
asserted) so the runtime grows by minutes, not double.

- harness: configure_ios(eth_ipv6=), ping() dispatches to `ping ipv6`,
  docker_bring_up_ipv6 (the Docker daemon leaves eth0 with
  disable_ipv6=1 on v6-less bridge networks, so the interfaces-file
  route cannot work), family-aware docker_wait_address, pcap_records
  scans every record (a leading DAD/MLD frame must not mask the
  sampled ethertype), and pcap_icmp_rtt measures the wire round trip
  from capture timestamps;
- scenarios: connectivity/delay/suspend/isolation/churn checks in both
  families, captures assert 86dd next to 0800, the libpcap bpf drops
  are asserted family-selective (icmp vs icmp6, each killing its own
  family and leaving the other alive), the eBPF classifier modes on
  both families, marker v6 twins with an echo-specific expression
  (bare icmp6 also matches DAD/MLD and would break the exact counts),
  the v6 sharkd replay, and the link-local matrix gains the 86dd
  all-nodes row; serial (HDLC) legs stay v4-only;
- the dynamips-emulated classic IOS under-reports its `ping ipv6` RTT
  by ~40 % of a delayed round trip (pcap-verified: the wire carries the
  v6 echo exchange in the same ~230 ms as v4 while the console reports
  ~130 ms), so the dynamips-family scenarios assert the v6 delay from
  the capture's own timestamps — recorded in tests/e2e/README.md.

Full suite: 17 passed in 58m50s.
2026-10-07 13:19:38 +08:00
Jeremy Grossmann 49f047d07a Merge branch '3.1' into fix/duplicate-device-configs-merge 2026-10-06 17:09:32 +02:00
Jeremy Grossmann f184883aa7 Merge branch '3.1' into images-nested-folders-support 2026-10-06 16:50:40 +02:00
YueGuobin 96be37baa1 test: cover the namespace-failure veth cleanup and BridgeNIO field validation
Both fixes from the code-review pass were carried by other tests'
side effects: the move_to_ns failure path (the branch that used to skip
the delete_veth) and the required-but-nullable bridge field on the NIO
create routes.
2026-10-06 22:42:31 +08:00
Jeremy Grossmann 2d8ddb5ed7 Merge branch '3.1' into fix/duplicate-device-configs-merge 2026-10-06 16:34:48 +02:00
Jeremy Grossmann e2161fb66c Merge branch '3.1' into fix-api 2026-10-06 16:12:18 +02:00
Jeremy Grossmann 0170d2c8ce Merge branch '3.1' into fix/duplicate-device-configs-merge 2026-10-06 16:10:23 +02:00
Mark Paronyan d9678ce98c refactor: derive update schemas with PartialUpdateModel 2026-10-06 16:46:22 +03:00
YueGuobin 35cf964882 test: cover the kernel-datapath review fixes
Regression coverage added with the fixes:
- capture-slot semantics: second-port refusal, failed-start rollback, and
  a non-owner port's stop not reaching `capture stop_kernel`;
- QEMU stop/start teardown on a mid-body or pre-launch failure, IOU start
  cleanup and filter guards, Dynamips half-attach undo plus retry;
- switch VLAN reconcile skipping a deferred anchor and rolling the
  ports_mapping back on failure;
- Docker/QEMU occupied-port refusal, probe ctime invalidation, probe
  survival of a failing uBridge start;
- env-marker dialects (mid-line mentions, falsy values) and the agent
  merge rejecting entries without a device_name.

Test-body imports of product modules move to the shared header.
2026-10-06 19:55:31 +08:00
Cristi a7f0c7174b fix(templates): Fixed template test formatting to pass CI 2026-10-06 00:04:26 +03:00
Mark Paronyan cbd4f457be Merge branch '3.1' into fix-api 2026-10-05 21:23:03 +03:00
YueGuobin 5841138cef docs(e2e): complete the README — requirements, image matrix, host notes
Adds what running the suite on a real machine actually requires (uBridge
with caps, tc for the filter assertions, a Docker daemon, cc -static for
the raw-frame probe), the full image-selection matrix (preferred
filenames, fallback patterns, per-folder layout, the alpine digest
ladder; missing images skip, never false-fail), the NetworkManager host
hazard and its record, timing/load sensitivity notes, the complete
GNS3_E2E_* environment-variable list — and the link-local scenario,
which the scenario list was missing.
2026-10-06 00:21:11 +08:00
YueGuobin f688e654e1 test(e2e): widen the fast-path RTT bound to 100 ms via harness.FAST_RTT_MS
The 'impairment cleared' asserts used avg < 50 on three-packet IOS pings;
the unimpaired baseline is 20-45 ms with 4 ms quantization, and host
scheduling jitter pushed averages to 54/56 in two full-suite runs
(esw, dynamips) — a margin, not an impairment (the qdisc asserts pass,
success is 100 %). The contrasted delayed regime is >= 150 ms, so a
100 ms bound still separates the two cleanly. Docker's sub-millisecond
veth asserts keep their 50 ms bound (three orders of magnitude of
headroom, never observed to flake).
2026-10-06 00:21:11 +08:00
YueGuobin f21d7be99c test(e2e): derive busybox ping loss from replies when the summary is lost
The exec stream can truncate after busybox's 4294967 ms RTT timer-wrap
artifact, dropping the summary line — docker_ping then reported a
phantom 100 % loss and made the corrupt-filter scenario fail spuriously.
Factor the parsing into _busybox_ping_parse: the summary stays
authoritative when present; otherwise the loss is derived from the
seq= replies actually seen (no replies at all still reports 100 %).
2026-10-06 00:21:11 +08:00
YueGuobin 15fbef93fb test(e2e): unmanage the TAP anchors in the remaining kernel scenarios
Same guard the ethernet-switch scenario already carries: NetworkManager
can release an NM-managed TAP from its bridge shortly after
(re-)enslavement (docs/bugs/networkmanager-tap-release.md). Applied to
the Dynamips, QEMU, IOU, IOL-runner and switch-cascade scenarios after
their nodes have booted (no-op where nmcli/NetworkManager is absent).
This keeps those tests deterministic on un-declared hosts.
2026-10-06 00:21:11 +08:00
YueGuobin 1c85befbab test(e2e): take the switch anchors out of NetworkManager's reach
The esw kernel fast-path scenario deletes and re-creates a link, which
re-enslaves the peer anchor — and on NetworkManager hosts NM's netdev
state machine can release an NM-managed TAP from its bridge some 60-70 ms
after enslavement, ~50% of re-enslavements, while the tap is in NM's
not-yet-assumed state. The freed port stays administratively up and no
GNS3 process logs anything: the membership assert sees the join, then the
ping measures a silent blackhole (this was the line-277 e2e failure).

Root-caused with a server-free harness (one uBridge, one fresh tap, one
bridge, an addif/delif loop): the same tap frees 20/40 while NM-managed,
0/40 after `nmcli device set <tap> managed no`, 20/40 again when
re-managed. Docker's veth host ends are not NM-managed, which is why only
TAP anchors ever showed it — including the "anchor lost its bridge
membership after a ports_mapping update (TAP peers only)" incident from
the switch's development, whose conclusion is corrected in the docs.

Unmanage the anchors once the routers have booted, before the scenario
re-enslaves them; document the failure signature and the host-side remedy
(per-device nmcli call, or NM unmanaged-devices configuration) in the
L2-anchor spec and the switch feature doc.
2026-10-06 00:21:11 +08:00
YueGuobin 7d04d08dba fix: repair imports lost in the 3.1 rebase and meet the new lint baseline
The rebase onto upstream 3.1 (mypy + ruff I001/F401 waves) left the tree
short of the newly enforced baselines in three ways:

* contextlib / UbridgeError imports dropped as false duplicates during
  conflict resolution left NameErrors in iou_vm and qemu_vm (F821);
  the Dynamips router grabbed the compute-level NIOTAP instead of the
  hypervisor one, breaking TAP anchoring (test failure).
* unused imports (KERNEL_UNSUPPORTED_FILTERS, UbridgeError, List) and
  unsorted blocks left behind where the stack's later commits moved the
  last usage elsewhere — ruff I001/F401/RUF022 now enforce these.
* mypy, now baseline-free: cache dict annotations in tc_probe, the
  locators candidate list, the resolved-params None branch in the MCP
  runner, and validate_bpf_syntax keeps the upstream Dict[str, Any]
  return (the strict Optional[str] was wrong for the "valid" bool entry).

The e2e raw-frame probe resolves its compiler via shutil.which (S607)
and zips with strict= (B905); the remaining subprocess call is S603 by
nature and gets a per-file ignore like other test files.

ruff check/format clean, mypy at upstream parity (no new errors),
full unit suite 2167 passed.
2026-10-06 00:21:11 +08:00
YueGuobin e13e5f3804 fix(tools): merge same-device entries before device command execution
A batch carrying the same device twice lost all but the last entry's
commands: the per-device command map kept only the last entry while
result processing iterated every original entry, so each row reported
the last entry's output under its own command list.

c3c78f99a consolidated the merge into the MCP template renderer, but
that only runs when a template is passed — the MCP direct-commands
path and gns3-copilot's direct _run path were left uncovered.
Normalize once in each tool's _run (single point covering all entry
paths, restoring the c42b3a59b coverage) and keep the renderer merge
as boundary normalization. Blocked-command info now also accumulates
across same-device entries instead of last-winning in the per-device
map.
2026-10-06 00:21:11 +08:00
YueGuobin d0b0cdbd38 fix(compute): a passive absorbed anchor no longer resets the switch's tc state
The peer end of an Ethernet switch fast-path link is a NIOBridge with no
bridge of its own (the switch absorbed its anchor into the switch bridge).
Its update reconcile still ran the impairment pass with the empty filter
set the controller routes to that end — and an empty set is a plain
`tc reset` on the shared anchor, silently wiping the netem/cls_bpf/eBPF
the switch had just applied. Any link update did it (filters, markers,
suspend, capture start/stop), and the outcome depended on the link's
endpoint order: UdpLink.update() PUTs node1 then node2, so with the
switch first its apply ran first and the peer's reset ran last (API
green, data plane unimpaired), while with the peer first the reset ran
first and the filter survived. That order dependence is also why the
shipped e2e never caught it — it only created peer-first links.

Ownership is now explicit: only the end that owns the anchor's tc state
applies the impairment reconcile (_owns_anchor_impairments — a NIOBridge
with `bridge is None` is a passive carrier, the same idiom the
bridge-delete path already used). Markers, capture and the suspend
carrier still apply on both ends: markers reconcile per process, so
neither end can strip the other's, and the carrier converges by
agreement.

Red/green: two new docker unit tests fail on the unfixed tree with
`tc reset "<anchor>"` and pass after; the switch e2e also creates one
link switch-first and asserts the netem qdisc and the RTT hold under
that order (failed with the wipe, passes with the fix). Full unit suite
2110 passed; the switch e2e file passed end to end (378s).
2026-10-06 00:21:11 +08:00
YueGuobin df79773bc0 test(e2e): live link-local matrix on the kernel datapath
Two Alpine containers on a kernel link, a static raw-Ethernet probe built
at run time and docker-cp'd in (the image has no raw-frame tool; DockerDaemon
gains the cp), injecting per-destination-MAC frames from guest 1 while
guest 2 counts arrivals: ordinary multicast, STP, LACP, 802.1X and LLDP must
cross, PAUSE must not (the documented kernel limit). Asserts the anchors'
sysfs brport group_fwd_mask reads 0xfffd through the server's own wiring
(int-parsed: newer kernels print it hex), that the bridge level stays 0, and
that a container stop/start replays the mask with LACP surviving. Negative
control is the uBridge suite's setportgroupfwd 0 case.
2026-10-06 00:21:10 +08:00
YueGuobin 86651604c4 test(e2e): relay capture and markers on both relay engines
The relay engines' observable paths had no live coverage: every
capture/start call in the suite sat on a kernel anchor (AF_PACKET
capture start_kernel) and markers were only asserted on marker
add_kernel. The two relay engines' own command shapes were unit-tested
only:
  * bridge engine  - bridge start_capture / bridge add_packet_filter ... mark
  * iol_bridge engine - iol_bridge start_capture / iol_bridge add_packet_filter ... mark

Docker relay control (bridge engine, 1 passed 33s): the capture start
REST call writes the ICMP exchange to a pcap off the relay bridge; a
mark filter attached with capture_node_id n1 signals exactly 10
marker.match events over the dedicated marker WS for 5 pings (5
requests + 5 replies, both directions), carrying node_id/link_id/tag and
the busybox frame length, writes exactly those 10 frames to the marker
pcap, and deleting the marker takes the pcap with it.

IOU relay control (iol_bridge engine, 1 passed 173s): the same two
checks on iol_bridge start_capture and the port's mark filter, driven
by the real IOS ping through the fabric socket relay.

Not duplicated in the ESW/VPCS/cloud relay controls: they issue the same
uBridge bridge commands through their own call sites, which stay covered
by unit tests; the IOL container relay goes through the same iol_bridge
engine already exercised here.
2026-10-06 00:21:10 +08:00
YueGuobin ff41a54364 test(e2e): assert the §E.2 idle-silence window in every kernel scenario
The L2-anchor spec's behavioral guarantee — an idle link stays silent for
5 s once the enslavement burst settles — only ran in the Dynamips and
Docker suites. It now also runs in QEMU, IOU, the IOL container, the
switch fast path and the switch cascade, which covers every host-device
creation path:

* tap create (Dynamips/QEMU/IOU anchors), docker create_veth (Docker host
  ends; the cascade's race-loser end is hardened after create by the
  server), bridge add_nio_tap (the IOL container anchor, the fifth
  creator), and the brctl switch bridges, whose multicast snooping is off
  precisely so the bridge role can reach silence;
* each scenario silences its guests first (IOS interfaces shut through
  the console; Docker guests' links down) and re-verifies the ping after;
* the cascade measures 8 devices at once (four absorbed router anchors,
  both cascade ends, both switch bridges).

IOL 158 s, cascade 293 s, ESW fast path 378 s, QEMU 428 s, IOU 308 s —
all green.
2026-10-06 00:21:10 +08:00
YueGuobin 1f544c96a5 test(e2e): classifier spot check and filter persistence on TAP anchors
The filter matrix (cls_bpf, the eBPF classifier) only ran on Docker veth
host ends; the IOU and IOL-container scenarios now spot-check it on their
TAP anchors and prove a filter survives a node restart:

* bpf "icmp" drops everything with clsact on the anchor, both ends, and
  clears back to an empty qdisc;
* frequency_drop 3 (the eBPF every-nth mode) measured 56 % round-trip
  loss (1 - (2/3)^2 = 55.6 % expected) in both scenarios;
* the IOU test applies delay 100 before a router stop/start: the fresh
  anchor gets netem back (tc level) and the restarted router's pings
  measure RTT >= 150 ms until the filter is cleared (a re-attach that
  dropped the filter would show ~100 ms).

IOU file 492 s, IOL file 182 s, both green.
2026-10-06 00:21:10 +08:00
YueGuobin 9d937802d5 test(e2e): relay filters shape real traffic on both relay engines
Extend the docker and IOU relay negative controls with the filters the
relay itself supports (delay, frequency_drop, bpf), asserted through real
ICMP — the first live coverage of the relay filter path:

* bridge engine (docker): delay 100 grows the RTT by ~200 ms and comes
  back on clear; frequency_drop 2 kills the round trips (the filter counts
  every packet crossing the bridge, both directions sharing one counter,
  so on an alternating ping stream one whole direction dies; verified
  against uBridge directly), frequency_drop -1 blackholes, and bpf drops
  by libpcap expression ('icmp', then 'greater 150' discriminating by
  frame size).
* iol_bridge engine (IOU): the same delay and frequency_drop effects
  through the port's iol_bridge filter list, plus the available_filters
  spot check.

Docker 33 s, IOU 168 s, both green.
2026-10-06 00:21:10 +08:00
YueGuobin 4fc0398423 test(e2e): live coverage for markers on the kernel datapath
Two Alpine containers on a kernel-only link with traffic-insight markers
attached to a veth anchor (marker add_kernel), observed on the dedicated
marker WebSocket via a new harness piece (WebSocketCollector, a sync
facade over websockets' sync client). Real ICMP proves: the marker.match
identity is complete (filter/tag/link_id/capture node/ts/len and the
tx/rx direction set of a ping), the main project channel carries no
matches, the BPF and direction are enforced (a non-matching sibling and
a tx-only marker each see exactly their traffic), the host pcap holds
exactly the matched frames, pausing silences one marker while a sibling
keeps counting (silence is never a dead channel), tag replay serves the
paused tag's timeline with display-filter/link/window narrowing and
refuses with 409 while capturing, and deleting a marker takes its pcap
and signals with it.
2026-10-06 00:21:10 +08:00
YueGuobin ad39b22ab4 test(e2e): live coverage for the switch cascade kernel datapath
Two switches cascaded through the link-owned veth pair, two c7200
routers per switch, real ICMP crossing both bridges: the pair is born
UP with exactly one end per switch bridge, a dot1q trunk cascade
carries two VLANs at once, the symmetric access/trunk mode flip on the
cascade ends isolates and revives a VLAN in place (asymmetric flips are
semantically broken — the access side egresses untagged into the far
trunk's native PVID), a delay filter lands as netem on both ends (one
per direction, RTT ~= 2x delay), suspend admin-downs both ends, and a
link delete/re-create retires the whole pair and mints a new one under
the new link id — zero host residue after the project goes.
2026-10-06 00:21:10 +08:00
YueGuobin cb7f74c77b fix(esw): harden the cascade end the create race leaves unhardened
docker create_veth hardens only its FIRST end (the Docker shape: host
anchor plus a container leg that keeps its L3 life). A cascade end is
that second, unhardened end whenever the peer switch's create won the
race, and the kernel gives it an IPv6 link-local — a silent L3 identity
on the switch fabric (ubridge-l2-anchor-spec §E). Each switch now
hardens its OWN end with 'link l2only <end> on' once the pair exists,
whatever branch brought it there (fresh create, lost race, reuse);
idempotent, and best-effort with a warning on a uBridge without the
command.
2026-10-06 00:21:10 +08:00
YueGuobin 6e36b58a5d fix(compute): refuse a duplicate port NIO and wire before bookkeeping
The compute-side backstop of the controller's duplicate-port guard
(keep-port-links + live-port bind made the controller guard reliable;
this is the layer under it). IOU and Dynamips bound the adapter NIO
before the uBridge/hypervisor attach: a failed attach left the port's
binding replaced — the link that still owned the port lost its handle
and its host state leaked (the IOU EBUSY incident's 8 orphan TAPs).
Dynamips was worse: its wiring idempotence check silently skipped the
attach on a second binding and rebound only the bookkeeping. A port
that already carries an NIO is now refused, and both types bind the
NIO only after the wiring succeeded.
2026-10-06 00:21:10 +08:00
YueGuobin 4fd7e1aa07 test(e2e): live coverage for the QEMU node kernel datapath 2026-10-06 00:21:10 +08:00
YueGuobin 928757542e fix(controller): bind and release link members on their live ports
Link.delete cleared the port link through the port object stored at
add_node time; a node update since then rebuilt the port list, so that
object was stale and the live port kept its link — the next link on the
port was refused with "Port is already used". Masked until now by the
older rebuild bug (c2d68a65d): links never survived a rebuild, so the
guard never fired and the stale clear never mattered.
2026-10-06 00:21:10 +08:00
YueGuobin 1e999d916d fix(controller): keep port links across a port-list rebuild
Node._list_ports() rebuilds every Port object, and it runs on each node
update — including every compute node.updated notification (a node
reporting started is one). The links are unaffected by that rebuild, but
the fresh port objects started out with link=None, so after any node
update the "Port is already used" guard in Link.add_node stopped matching:
a second link on an occupied port reached the compute, where the port's
NIO was replaced before the attach failed on the already-bound anchor,
leaving the link half-torn-down (on IOU, deleting the project afterwards
left the node's anchor TAPs and a per-link bridge on the host).

Carry the link back-references over by (adapter_number, port_number),
first match wins, mirroring get_port; a port that still exists keeps its
link. Regression tests pin both the reference (test_node) and the guard
(test_link: the duplicate link is refused after a node update).
2026-10-06 00:21:10 +08:00