7801 Commits

Author SHA1 Message Date
YueGuobin
1db9f171ca
fix(controller): re-arm window_drop and expose kernel_datapath
Three fixes from live-calibrating the kernel impairment filters on an
isolated instance (Docker-to-Docker veth link, per-sequence evidence):

- update_filters skipped an unchanged filters dict as a no-op, so a
  one-shot window_drop outage that had already passed could never be
  re-armed: re-applying the same values produced no datapath command at
  all. A PUT still carrying window_drop now reconciles, restarting the
  schedule from the apply as documented. Measured: identical re-apply
  moves the outage window from seq 50-70 to 70-91 (arm at +8s of the
  new apply).

- gemodel parameter semantics were documented backwards in the
  validation rules, available_filters, the MCP/copilot descriptions and
  the feature docs. Measured against the kernel (per-direction counters,
  offered = sent + dropped): p is the good-to-bad transition chance, r
  the bad-to-good transition, 1-h the loss chance in the bad state (the
  good state loses nothing) - mean one-way loss p/(p+r)*(1-h), e.g.
  [10,30,100] -> 24.8%/25.7% measured vs 25% predicted, [5,45,100] ->
  10.8% both ways vs 10%.

- kernel_datapath was silently stripped from every REST/MCP link payload
  by the response model (LinkBase carried no such field), so clients
  could not tell a kernel link from a relay one.

Also verified on the same setup: the 3-argument window_drop form works
exactly as specified ([3000,5000,100] -> a single seq 31-55 outage,
[20000,6000,100] -> seq 115-144), and both generations of the command
builder emit the parameters unpadded.

Adds an API test for the field and controller tests for the no-op skip
and the window_drop re-arm exemption.
2026-10-06 00:21:08 +08:00
YueGuobin
5ac1521217
feat(mcp): accept project/node names as tool locators
LLM agents transcribe UUIDs unreliably — a 2-character slip invents an
id that never existed and is invisible to the eye. Tool parameters now
accept names wherever the server guarantees uniqueness: project names
among live projects, node names within a project. Links, drawings,
appliances and snapshots have no names and stay UUID-only.

Resolution sits in the single dispatch choke point (_run_handler_sync):
UUID-shaped values pass through untouched, names resolve with one list
call, and an unknown name fails fast listing the known names instead of
a confusing downstream 404. As a backstop for residual UUID typos,
error replies carrying "<UUID> doesn't exist" are matched against the
in-scope ids; a single candidate within edit distance 2 earns a
"Did you mean ...?" hint.
2026-10-06 00:21:08 +08:00
YueGuobin
935c00802f
docs(mcp): refresh filter vocabulary in the MCP/copilot tool descriptions
link_update, link_available_filters and the copilot packet-filter tool
still advertised the pre-kernel-datapath five types. List the full
thirteen with positional parameter formats, ranges and kernel-only
marking, point at link_available_filters as the per-link authority
(topology + compute capability gated), and document window_drop's
apply-relative schedule restart. Also add kernel_datapath to the link
field lists.

Description-only: handlers pass filters through unchanged.
2026-10-06 00:21:08 +08:00
YueGuobin
cecaf298bb
feat(controller): expose kernel_datapath on the link payload
Runtime fact recomputed from the NIO wiring on every load (not
persisted). Without it clients cannot tell a relay link from a kernel
link whose compute lacks a filter capability — both show up as
kernel-only types missing from available_filters.
2026-10-06 00:21:08 +08:00
YueGuobin
b4e4739d52
feat(compute): report uBridge tc capabilities and gate filters on them
Consume uBridge's ebpf_modes contract (feature/tc-window 40c36ae): a
stateful mode is usable iff ebpf=1 and its token is listed; builds
predating the field keep their shipped modes (nth/quota/flow), so the
pre-correction window semantics can never be installed silently.

- gns3server/utils/tc_capabilities.py: shared reply parser, mode
  policy and filter-type mappings (compute and controller gate on the
  same rules)
- gns3server/compute/ubridge/tc_probe.py: standalone probe spawning a
  throwaway uBridge as the server's own user, cached by binary
  identity (path/mtime/size - the version string cannot distinguish
  rebuilds); failures cache as unknown
- GET /v3/compute/capabilities gains ubridge_tc {netem, ebpf,
  ebpf_modes, cbpf}; the controller forwards it (Compute.capabilities)
  and available_filters hides kernel-datapath filter types an endpoint
  compute cannot run (both ends apply their own filters, so one
  incapable compute hides the type); computes without a report keep
  the full list and apply-time 409 stays the guard
- docker: _ubridge_apply_ebpf_drops gates per mode token; the cleanup
  path only sends "off" for modes the build declares
- split DockerVM's kernel-datapath machinery (veth lifecycle, carrier,
  marker primitives, tc filter translation, kernel NIO teardown) into
  compute/docker/docker_kernel_datapath.py (DockerKernelDatapathMixin)
2026-10-06 00:21:08 +08:00
YueGuobin
48dbdbe4a9
fix(docker): delete the per-link kernel bridge when a node stops
The bridge teardown only hung off the link-removal path, but deleting a
node or closing a project never issues a per-link delete — the veths
disappeared with their containers and left the bridge behind as an empty
orphan (reproduced: create a kernel link, DELETE the project, the
gns3{link_id} bridge survives). `_remove_kernel_veths` now deletes the
node's kernel-NIO bridges alongside the veth host ends: both endpoints do
it, the peer's still-enslaved port answers EBUSY (suppressed, last one
wins) exactly like link deletion, a relay NIO's uBridge bridge is left
alone (it dies with the process), and a node restart rebuilds the bridge
from the NIO in _connect_nio.

Found while cleaning up the window_drop e2e: seven such bridges had
accumulated on this host. Re-verified live: the bridge is gone after the
project delete, and the window_drop e2e (which includes a node restart
with the kernel link attached) still passes 15/15.
2026-10-06 00:21:08 +08:00
YueGuobin
316e7002c1
feat(docker): expose the eBPF time-window filter on kernel links
uBridge's window mode gained the semantics the B.2 spec asked for —
`tc window_drop <if> <start> <outage> <pct> [<period> [<jitter>]]`: a
single outage that traffic passes before *and* after, recurring flaps
with period >= outage, and a per-cycle jitter — so the kernel-only
`window_drop` filter type is now exposed:

- validation mirrors the tc grammar (3-5 params, ranges, period >=
  outage, jitter cap) and adds window_drop to KERNEL_ONLY_FILTERS;
  filter_inactive_filters keeps start=0 — unlike every other filter a
  zero first parameter means "the outage starts now", not "disabled".
- `_ubridge_apply_ebpf_drops` sends `tc window_drop` (positional
  pass-through) and turns the mode off explicitly when absent, under the
  same ebpf=1 gate.
- FILTERS entry (Start/Outage/Chance/Period/Jitter) + available_filters.

The schedule is relative to the apply: reconcile re-sets the classifier
on every apply, so any filter change, node restart or link reset restarts
the window.

Verified on a two-container kernel link, 15/15: a single outage
[1000, 1500, 100] arrives per-seq as [0..4, 13..19] (passing before the
window, dropped inside, passing after); recurring 1000/2000 ms = 52 %
round trip (duty cycle); period == outage = permanent outage; period <
outage = 409 before uBridge; jittered flap keeps the ~50 % duty cycle;
30 % chance on a permanent window = 53 % round trip against the
per-direction theory 1-0.7^2 = 51 %; coexist with netem; node restart
restores; available_filters kernel vs relay (409 + hidden); teardown
clean. P6b re-run as a regression on the same uBridge build: 13/13.

Worth recording for the next rebuild: the first delivered window program
loaded as root but reported ebpf=0 through gns3-server — the verifier's
non-root path loses the loop counter's bound, treats the catch-up loop as
unbounded and rejects the *whole* program (E2BIG, 8193 pending states),
which also took frequency_drop and quota down, with ebpf=0 as the only
symptom. Smaller trip caps did not help; the loop had to go (uBridge
572ca0c), on top of a Makefile header-dependency fix (53e34bb) that had
been shipping a stale program object.
2026-10-06 00:21:08 +08:00
YueGuobin
0c4ec8c38c
feat(docker): eBPF stateful packet filters on kernel links
Consume the eBPF impairment classifier delivered in uBridge
feature/tc-precision: frequency_drop becomes the exact every-Nth mode
(tc nth_drop; -1 = drop everything maps to every 1st) and the new
kernel-only quota type is the byte-cap mode (tc quota_drop, 100 = hard
cutoff). KERNEL_UNSUPPORTED_FILTERS is gone entirely — kernel/relay
eligibility is now purely topological. Both modes gate on ebpf=1 from
the per-process tc capabilities probe (error names the setcap fix); the
cleanup path (modes off) only talks to the eBPF commands when the probe
reported them. Reconcile is a plain re-set: the netem reset-before-set
already detaches the program and clears uBridge's per-interface
registry, so a mode set after it is a fresh load+attach.

Documented semantic difference: the relay counts both directions
through one bridge counter, the kernel counts per direction — N=3
round-trip loss is 1-(2/3)^2 = 55.6% on kernel vs ~1/N on relay
(measured 57%). window_drop is NOT exposed: the delivered
implementation advances the window with no gap, making 'outside the
window packets pass' unreachable (needs outage/period fields —
recorded in the spec); flow_drop awaits a parameter-shape decision.

Also fixes a pre-existing restart bug the e2e exposed: _bridges (relay
bridge names) was never cleared on stop, so restarting a node with a
relay link skipped 'bridge create' and failed on add_nio_udp.

e2e (isolated instance): 13/13 — pref-1 tc_impair classifier on both
veths, 57% round-trip for N=3, -1 = exact 100%, quota hard cutoff,
delay coexistence, clean teardown, suspend/resume, node restart,
relay-leg frequency_drop regression through an Ethernet switch. Full
suite: 1925 passed.
2026-10-06 00:21:08 +08:00
YueGuobin
d4b57d49fb
feat(docker): expose netem-extension packet filters on kernel links
Surface the netem extensions delivered in uBridge feature/tc-netem-ext
as GNS3 filter types: rate (tc-style bandwidth string), reorder (needs
delay), gemodel (Gilbert-Elliot bursty loss, exclusive with
packet_loss), duplicate, seed and limit, plus the delay jitter
distribution and the loss/dup correlation parameters. All are
kernel-datapath only — the relay's packet-filter registry has no
equivalent, so available_filters hides them on relay links, setting one
on a created relay link returns 409, and the project-load path drops
them with a warning (update_filters runs before the datapath is
decided, so it cannot reject there). Validation mirrors the tc grammar:
reorder requires delay, gemodel and packet_loss are mutually exclusive,
a distribution requires jitter > 0, rate is integer+unit capped at
100gbit.

The translation in DockerVM._netem_command_parts emits the frozen
grammar order and gates extension keywords on the 'tc capabilities'
netem token list (probed once per uBridge process; a plain
delay/loss/corrupt/dup filter never probes, keeping old uBridge builds
untouched; the correl suffix has no token and gates on 'rate' as the
extension-build marker).

Reconcile became reset-before-set: the kernel's netem change merges
optional attributes (rate, correlation, reorder, corrupt, gemodel,
distribution, seed keep their previous value when the new message omits
them), so removing a parameter needs an explicit 'tc reset' — the
bpf_drop filters are re-added right after in the same flow, so the
clsact churn is invisible. The capability check runs before the reset
so a failed update leaves the previous qdisc in place.

e2e (isolated instance, kernel link): 24/24 — rate 512kbit adds exactly
2x the per-packet serialization to the RTT (45.5 ms for 1400-byte
pings), paretonormal jitter + rate-cleared leak check, reorder gap 5,
gemodel 50% bursty loss, duplicate, loss 30% correl 50%, seed 42 +
limit 5000 visible in the qdisc dump, suspend/resume and node restart
keep the filters, relay link rejects rate with 409, teardown leaves no
netem. Full unit suite: 1916 passed.
2026-10-06 00:21:08 +08:00
YueGuobin
b1eb0984bc
fix(docker): bring the veth host end up before a relay attach
The unified veth host end is born admin-down (carrier off until a link
attaches), and libpcap cannot open a packet socket on an admin-down
interface — its netlink promiscuous-mode transaction returns ENOENT, so
'bridge add_nio_ethernet' failed on every runtime relay link (and would
on any relay link at container start). Bring the interface up for the
attach; the carrier pass in the caller refines the state afterwards (a
suspended NIO sets it back down). Verified live with a direct uBridge
probe: attach on a down veth -> ENOENT, after 'link set up' -> OK.
2026-10-06 00:21:08 +08:00
YueGuobin
2b45f29b2c
feat(docker): bpf packet filter on kernel links via cls_bpf match-drop
The GNS3 bpf filter (one expression per line, any line matching drops the
packet) now runs on the kernel datapath: uBridge's tc bpf_drop compiles
each line with libpcap (same compiler as the relay filter) into classic
BPF, attaches it to the veth host end's clsact egress as a cls_bpf
classifier with a gact TC_ACT_SHOT action, and the kernel migrates the
cBPF to eBPF internally -- no CAP_BPF needed.

- reconcile is flush + one add per line at priorities 10, 11, ... (OR
  semantics, same as the relay's per-line filters); tc reset (link
  delete / clear) is the full restore in uBridge and clears them
- capability-gated: the compute probes "tc capabilities" once per uBridge
  process (cached, invalidated on stop); without cbpf=1 the filter apply
  fails with an actionable upgrade error, and old uBridge builds without
  the tc module are never touched when no bpf filter is present
- a line that compiled for the controller's tcpdump but not for the
  compute's uBridge libpcap is skipped with a warning, mirroring the
  relay path
- KERNEL_UNSUPPORTED_FILTERS shrinks to frequency_drop (its eBPF stateful
  classifier is still pending, spec part B)

e2e (22/22): bpf "icmp" = 100% loss; size-discriminating expression
("greater 150") passes 98-byte and drops 300-byte pings (real byte-level
match); two-line OR; coexistence with netem (delay 50 -> small pings
100.2 ms while big dropped); clear removes clsact; suspend/resume keeps
the drops; link delete + recreate leaves no residual filters.
2026-10-06 00:21:08 +08:00
YueGuobin
bd1c205651
docs: frozen uBridge spec for kernel impairment (netem extensions, eBPF/cBPF classifiers) 2026-10-06 00:21:08 +08:00
YueGuobin
0a8d430d50
feat(docker): packet filters on kernel-datapath links via tc netem
Impairment filters with a netem equivalent (delay, packet_loss, corrupt)
now run on the kernel datapath as one tc netem qdisc per veth host end,
pushed through uBridge's netlink tc module (tc netem set / tc reset).
This removes the relay delay filter's nanosleep bottleneck (upstream

Both link endpoints carry the filters: each end's qdisc egress covers
the traffic entering that container, so every direction is impaired
exactly once -- the same net effect as the relay, where both directions
cross the single filtered bridge.

- controller: eligibility keys on relay-only types (frequency_drop, bpf)
  instead of any filter; kernel links push the real filters (the
  synthetic suspend frequency_drop no longer flips a suspended link to
  relay on reopen/reset); update_filters 409s only relay-only types;
  available_filters hides them on kernel links
- compute: create_nio whitelists netem-capable filter types; the NIO
  update path keeps its own guard (PUT mutates the NIO in place);
  _ubridge_apply_netem translates the filter dict into one atomic
  netem-set (full re-apply IS the reconcile); node restart restores the
  qdisc, link deletion detaches it (the veth survives the link)
- e2e (27/27): delay 100 -> 200.2 ms RTT, atomic replace 50 -> 100.2 ms,
  clear -> baseline, packet_loss 30 -> 48 % round-trip (expected 51 %),
  suspend/resume and node restart keep the qdisc, link delete leaves no
  residual impairment
2026-10-06 00:21:08 +08:00
YueGuobin
94b5495683
docs: Docker kernel datapath implementation document
Cover the four-stage stack (kernel datapath, AF_PACKET capture, markers,
veth-everywhere): adapter interface types per node class, datapath
selection, veth lifecycle, link operations, suspend/capture/markers
semantics, uBridge command surface and the tc netem filters roadmap.
Cross-link from the traffic-insight marker doc for the kernel variant.
2026-10-06 00:21:08 +08:00
YueGuobin
672efda592
feat(docker): unify adapter interfaces on veth, allow kernel links at runtime
Every Docker adapter is now born as a veth pair at container start
(unified interface) regardless of NIO type: guest end in the container
namespace, host end in the root namespace. Kernel links enslave the host
end into the per-link Linux bridge; relay links attach it to the uBridge
relay via AF_PACKET (bridge add_nio_ethernet) — the TAP path is gone.

This makes the datapath a runtime decision:
- links can be created on running containers (kernel or relay) without
  touching container interfaces — the controller stopped-node
  eligibility check is removed
- project reopen re-prepares relay links and upgrades eligible ones to
  the kernel datapath
- packet capture start/stop now key on the NIO type (not veth presence)
  so relay-over-veth captures use the relay capture, kernel ones the
  AF_PACKET module
- vendor unix-socket containers keep their wiring: no veth is required
  on the relay path
2026-10-06 00:21:08 +08:00
YueGuobin
61020d3bbc
feat(docker): traffic-insight markers on kernel-datapath links via AF_PACKET
Markers no longer disqualify the kernel datapath. They ride the NIO like
on the relay path and attach to the veth host end through uBridge's
marker add_kernel/delete_kernel/enable_kernel commands (AF_PACKET taps,
same BPF/direction/signal semantics as the relay mark filter).

- base_node: marker anchors are now polymorphic (relay bridge name or
  kernel veth host interface); the reconcile loop is datapath-agnostic
  and DockerVM overrides the add/delete/enable primitives to translate
  the commands. Marker-name validation extracted for both add paths.
- docker_vm: kernel NIO connect/update apply markers on the veth anchor;
  link deletion tears markers down explicitly (the veth survives the
  link, unlike a relay bridge whose death drops its filters).
- controller: eligibility and start_marker no longer exclude markers or
  project-level marker definitions; the kernel NIO carries the capture
  node's marker specs.
- NIO creation accepts markers on kernel NIOs (filters still rejected).
2026-10-06 00:21:07 +08:00
YueGuobin
f6b869be63
feat(docker): packet capture on kernel-datapath links via AF_PACKET
Replace the capture rejection with wiring to uBridge's capture module:
an AF_PACKET socket bound to the veth host end writes into the same pcap
file the relay path uses (capture start_kernel <if> "<pcap>" /
capture stop_kernel), so the pcap streaming to clients is unchanged.

- capture survives node restarts: _connect_nio re-arms it when the NIO
  is already capturing, mirroring the relay path
- uBridge's kernel capture is a singleton per process: a concurrent
  second capture on the same node surfaces uBridge's EALREADY error
- update the datapath docs: filters and markers remain relay-only,
  capture is now served on both datapaths
2026-10-06 00:21:07 +08:00
YueGuobin
58b97b4ce5
fix(controller): don't push the synthetic suspend filter to kernel links
update_suspend() piggybacks on the filter system: get_active_filters()
returns {"frequency_drop": [-1]} while a link is suspended so relay-based
node types drop all traffic at the uBridge filter. Kernel-datapath links
implement suspend natively via interface carrier, and pushing the
synthetic filter tripped the compute-side guard, making suspend fail
with 409 on every kernel link (and leaving controller/kernel state torn).
_get_node_filters() now returns empty filters for kernel links.
2026-10-06 00:21:07 +08:00
YueGuobin
37d7b9121e
feat(docker): wire Docker-to-Docker links through a kernel bridge
Eligible links (both endpoints Docker, same compute, no filters/markers,
stopped nodes, no unix-socket NIO containers) are wired as veth pairs
enslaved into a per-link Linux kernel bridge (gns3 + 11 hex of the link
id, filling IFNAMSIZ) instead of the uBridge UDP relay, so frames flow
entirely in the kernel.

- NIOBridge NIO type: the controller decides the datapath in
  UDPLink._prepare and both computes enslave their deterministic gv*/gc*
  veth pair via uBridge's docker/brctl modules
- carrier/suspend = host-end admin state; both endpoints race-safely
  create/delete the bridge (EEXIST/EBUSY/ENOENT tolerated)
- veth adapters can still attach relay (UDP) links on a running node:
  lazy relay bridge over AF_PACKET on the host end (add_nio_ethernet)
- filters/markers/capture are rejected with explicit errors on this
  datapath (they live in the uBridge relay); vendor unix-socket
  containers are excluded; Server.enable_kernel_datapath is the
  kill-switch (default on)
- host-end veths survive container death and are removed on stop plus
  swept by name on the next start (crash/stale recovery)
2026-10-06 00:21:07 +08:00
Jeremy Grossmann
65c858d455
Merge pull request #2942 from markparonyan/ruff/pr03-fstrings
refactor: use f-strings
2026-10-05 14:35:56 +02:00
Mark Paronyan
b3b1ba0d4e
Merge branch '3.1' into ruff/pr03-fstrings 2026-10-04 20:56:13 +03:00
Jeremy Grossmann
9f9be983cd
Merge pull request #2939 from markparonyan/patch-1
fix: bump asyncssh - at least 2.24.1 for CVE-2026-62949 fixes
2026-10-04 18:15:03 +02:00
Mark Paronyan
1c5c3d1f76
fix: bump asyncssh - at least 2.24.1 for CVE-2026-62949 fixes
Updated asyncssh version to 2.24.1 in requirements.
2026-10-04 19:01:30 +03:00
Jeremy Grossmann
38cc7cc0a0
Merge pull request #2938 from markparonyan/ruff/pr02-unused-imports
Ruff/pr02 unused imports
2026-10-03 19:33:45 +02:00
Mark Paronyan
7e1bc67be2
chore(ruff): drop UP032 from the ignore baseline 2026-10-03 19:44:02 +03:00
Mark Paronyan
3135c8b7e3
refactor: use f-strings in remaining modules, scripts and tests 2026-10-03 19:44:02 +03:00
Mark Paronyan
be943fcdf3
refactor: use f-strings in controller tests 2026-10-03 19:44:02 +03:00
Mark Paronyan
bee6b3f8b1
refactor: use f-strings in compute tests 2026-10-03 19:44:01 +03:00
Mark Paronyan
7a9c600f54
refactor: use f-strings in controller modules 2026-10-03 19:44:01 +03:00
Mark Paronyan
552c0ca3e0
refactor: use f-strings in remaining compute modules 2026-10-03 19:44:01 +03:00
Mark Paronyan
bbebf20825
refactor: use f-strings in dynamips manager 2026-10-03 19:44:01 +03:00
Mark Paronyan
c458459626
refactor: use f-strings in dynamips NIO modules 2026-10-03 19:44:01 +03:00
Mark Paronyan
fba3ecc5fc
refactor: use f-strings in dynamips nodes 2026-10-03 19:44:00 +03:00
Mark Paronyan
a5d2bca921
refactor: use f-strings in dynamips router node 2026-10-03 19:44:00 +03:00
Mark Paronyan
97f6805d0a
refactor: use f-strings in iou compute modules 2026-10-03 19:44:00 +03:00
Mark Paronyan
d5ad4c67c6
refactor: use f-strings in qemu compute modules 2026-10-03 19:44:00 +03:00
Mark Paronyan
7f2c0c7e9c
refactor: use f-strings in docker compute modules 2026-10-03 19:44:00 +03:00
Mark Paronyan
72fae7a1ce
refactor: declare new image sync models in the public API 2026-10-03 19:43:59 +03:00
Mark Paronyan
d5e2832649
chore(ruff): drop F401 from the ignore baseline 2026-10-03 19:43:59 +03:00
Mark Paronyan
b622ccae7b
refactor: declare explicit public API in package __init__ modules 2026-10-03 19:43:59 +03:00
Mark Paronyan
01d890bc40
style: tidy import block after unused-import removal 2026-10-03 19:43:59 +03:00
Mark Paronyan
306d8fbde4
refactor: remove unused imports 2026-10-03 19:43:58 +03:00
Jeremy Grossmann
084004586c
Merge pull request #2934 from markparonyan/ruff/pr01-imports
refactor: ruff fixes
2026-10-03 17:20:41 +02:00
Mark Paronyan
1ed8320e31
Merge branch '3.1' into ruff/pr01-imports 2026-10-03 14:21:22 +03:00
grossmj
64e2a62a04
fix: improve warning message for missing mtools in config disk creation. Ref #2778 2026-10-02 16:08:38 +02:00
Mark Paronyan
b78672a8ca style: sort imports in the new image management modules 2026-10-02 12:43:26 +03:00
Mark Paronyan
a64379cb78 chore(ruff): drop I001 from the ignore baseline 2026-10-02 12:43:26 +03:00
Mark Paronyan
83e7fbe63b style: sort imports in remaining tests and scripts 2026-10-02 12:43:26 +03:00
Mark Paronyan
6d16d923c7 style: sort imports in compute and controller tests 2026-10-02 12:43:26 +03:00
Mark Paronyan
b46b9a1d26 style: sort imports in api tests 2026-10-02 12:43:26 +03:00