diff --git a/docs/features/docker-exec-console.md b/docs/features/docker-exec-console.md index a121ea23c..332e22af4 100644 --- a/docs/features/docker-exec-console.md +++ b/docs/features/docker-exec-console.md @@ -31,17 +31,20 @@ they let a vendor NOS run as a first-class GNS3 Docker router node. > schema changes, so existing Docker nodes (FRR, ipterm, …) are unaffected. > `console_type: "docker_exec"` is added to the `ConsoleType` enum. -## The three environment knobs +## Environment knobs -All three are read from the node's `environment` field. Entries prefixed with +All are read from the node's `environment` field. Entries prefixed with `GNS3_` are **not** forwarded into the container (existing GNS3 behaviour), so -they stay host-side configuration. +they stay host-side configuration. The console-relevant ones (the full vendor +set, incl. `GNS3_SHM_SIZE` / `GNS3_DEVICES` / `GNS3_MASK_UDEV` / +`GNS3_STOP_TIMEOUT`, is documented in [vendor-nos-xrd.md](./vendor-nos-xrd.md)): | Variable | Purpose | |----------|---------| | `GNS3_SKIP_INIT=1` | Do **not** prepend `/gns3/init.sh` to the entrypoint. Vendor NOS images must run their own entrypoint (e.g. SR Linux's `sr_linux`); GNS3's init script (busybox bootstrap, `ifup`, eth wait) interferes with them. | | `GNS3_INTERFACE_NAMES=mgmt0,e1-1,e1-2,e1-3` | Rename the injected interfaces in adapter order instead of the default `eth{N}`. SR Linux expects `mgmt0` + `e1-N`; without this it does not recognise its datapath. | | `GNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli` | Command run by the `docker_exec` console inside the container. | +| `GNS3_CONSOLE_RESIZE=0` | Ignore client-driven console resizes (WS terminal-size control frames / telnet NAWS) and keep the tall no-paging PTY geometry. Set for CLIs that page on the PTY window size (IOS-XR) — see [Terminal geometry](#terminal-geometry-and-size-forwarding). | ## Architecture: `VendorDockerVM` subclass @@ -87,18 +90,21 @@ Setting `console_type: "docker_exec"` makes the node's primary console port run ```mermaid graph LR - A[Web UI xterm.js] -->|console WS| B[GNS3 Compute telnet server] - B -->|binary pty stream| C[Docker exec API] - C -->|Tty:true pty| D[sr_cli / vendor CLI] - A -.->|NAWS size| B - B -.->|POST exec/.../resize| C + A[Web UI xterm.js] -->|console WS: text frames| B[Controller forward] + B -->|WS: text + binary| C[GNS3 Compute telnet server] + C -->|binary pty stream| D[Docker exec API] + D -->|Tty:true pty| E[sr_cli / vendor CLI] + A -.->|binary control frame {"cols","rows"}| B + B -.-> C + C -.->|POST exec/.../resize| D ``` The console uses GNS3's **existing shared/broadcast telnet model**: a single exec instance (one CLI session) is broadcast to every console client, exactly like the primary console shares one PID 1. There is deliberately **no per-client session isolation** — this matches how every other GNS3 console -behaves. +behaves. The PTY geometry is likewise shared: the last client resize wins +(see [Terminal geometry](#terminal-geometry-and-size-forwarding)). ### Implementation @@ -159,8 +165,11 @@ reconnect logic is unit-tested. non-multiplexed bidirectional pty byte stream — no frame demux needed. 5. **NAWS → exec resize.** The telnet server runs with `naws=True`; the - `window_size_changed_callback` calls `POST exec/{eid}/resize?h=&w=` so the - TUI lays out for the xterm.js window size. + `window_size_changed_callback` (`_on_naws`, gated by + `GNS3_CONSOLE_RESIZE`) calls `POST exec/{eid}/resize?h=&w=` so the TUI + lays out for the client's window size. The internal `_resize_exec` path + (creation-time default, restore-on-idle) is not gated. See + [Terminal geometry](#terminal-geometry-and-size-forwarding). 6. **Binary passthrough + redraw.** `binary=True` so TUI escape sequences reach xterm.js intact; `echo=False` (the pty echoes). On every client (re)connect @@ -168,9 +177,66 @@ reconnect logic is unit-tested. screen for a previous client redraws for the new one (otherwise a reconnect shows a blank screen until the next output). +### Terminal geometry and size forwarding + +The exec PTY geometry is a shared resource with three consumers that want +different things: + +- **Browser clients (xterm.js)** need the PTY to match their real window, or + TUI CLIs misrender and over-render (below). +- **Non-NAWS clients** (netmiko, bare telnet — no terminal-size negotiation) + need the PTY *tall*: CLIs that page on the PTY window size (the IOS-XR + pager ignores `terminal length 0`) park at `--More--` on a 24-row PTY. +- **Concurrent sessions share one exec** — one browser resize changes what + every attached client sees. + +Resolution: + +1. **Tall default.** The exec is created at 511×10000 (width 511 matches + netmiko's `terminal width 511` convention). Non-NAWS clients get no paging + and no hard wrapping. +2. **WS terminal-size forwarding.** Console WebSocket clients may send + **binary control frames** — UTF-8 JSON `{"cols": N, "rows": N}` — + alongside text frames carrying terminal data (xterm.js's AttachAddon only + sends text, so binary is an unambiguous side channel; valid ranges are + cols 2–5000, rows 2–100000, anything else is silently ignored). The + controller forwards binary frames (previously only text was forwarded — + and a binary frame would have crashed the old `receive_text` loop), and + the compute side turns them into a telnet NAWS subnegotiation for + telnet-based consoles (docker_exec included) or an asyncssh + `change_terminal_size` for SSH consoles + (`base_node.py` `start_websocket_console`). +3. **Races.** A size frame that arrives before/during the exec creation is + remembered and applied right after creation — it is **not** overwritten by + the tall default. When the **last** client disconnects the exec goes back + to 511×10000, so a later non-NAWS client attaching to the still-live exec + doesn't inherit a browser geometry and hit PTY-window paging. +4. **`GNS3_CONSOLE_RESIZE=0`** makes the console ignore client resizes + entirely (the tall default is then permanent). Set it for paging CLIs + where a browser resize would break concurrent netmiko sessions on the + shared exec — XRd, which is line-oriented and doesn't need browser + resizing at all. + +**Why the browser must send its size — the SR Linux flicker.** `sr_cli` is a +prompt_toolkit TUI that anchors its layout with cursor-position requests +(CPR), which xterm.js answers. On a 10000-row PTY canvas the CPR-anchored +model conflicts with the winsize model, and every incremental render re-emits +the accumulated output: measured with a CPR-answering client, one `info` +command produces **~145 KB instead of ~60 KB** (~7× duplicated lines either +way — the CLI re-renders its output region as a scroll-append stream; that +part is inherent to `sr_cli` and identical outside GNS3, verified via manual +`docker exec`). The inflation is driven by **rows** (24/32 → normal, 10000 → +pathological, at any width) and is invisible without CPR answers — which is +why plain-telnet probes and real xterm.js sessions behaved so differently. +In the Web UI the excess renders as frequent full-screen clear/redraw — the +"flicker". With the browser's real size forwarded (rows ≈ 30), output volume +and rendering return to normal. + **File**: `gns3server/compute/base_node.py` — the console WebSocket guard now allows `docker_exec` (alongside `telnet`/`ssh`), since the WS bridge connects to -the console TCP port exactly as it does for telnet. +the console TCP port exactly as it does for telnet. The same WS handler also +intercepts binary control frames and propagates client terminal sizes (see +[Terminal geometry](#terminal-geometry-and-size-forwarding)). ### Why earlier approaches failed (context) @@ -418,17 +484,35 @@ present on the host. effect. If it does not, the image needs the bridge earlier (a vendor-specific entrypoint wrapper, not covered by this prototype). +**11. Web console flickers (full-screen clear/redraw) on every command** +- The PTY is stuck at the tall 511×10000 default while a CPR-answering client + is attached — see + [Terminal geometry](#terminal-geometry-and-size-forwarding). Check that the + Web UI actually sends the binary size control frames on connect/resize + (F12 → the console WS should show outgoing binary frames), and that the + server is new enough to forward them (the controller used to forward text + frames only). A client that never negotiates/forwards size (old Web UI, + bare telnet without NAWS) cannot trigger the fix — but also never answers + CPR, so it doesn't flicker either. +- The much milder per-keystroke/5 s cursor toggles (`\e[?25l…\e[?25h`) from + the TUI are normal and not this bug. + ## Limitations 1. **Shared session (broadcast).** All console clients share one CLI session and can see each other's input — identical to GNS3's existing primary - console model. There is no per-client independent session. + console model. There is no per-client independent session. The PTY + geometry is shared too (last resize wins): two browsers of different sizes + disagree harmlessly, but a browser on a *paging* CLI needs + `GNS3_CONSOLE_RESIZE=0` to stop resizing on behalf of concurrent netmiko + sessions (see [Terminal geometry](#terminal-geometry-and-size-forwarding)). 2. **`reset_console` not wired.** The console-reset action only handles `telnet`/`ssh`; it is a no-op for `docker_exec` (non-blocking; reconnect works fine). 3. **Prototype knobs.** `GNS3_SKIP_INIT` / `GNS3_INTERFACE_NAMES` / - `GNS3_CONSOLE_CMD` are environment-driven; they are not yet first-class node - schema fields and are not declared in the appliance (`gns3a`) schema. + `GNS3_CONSOLE_CMD` / `GNS3_CONSOLE_RESIZE` are environment-driven; they are + not yet first-class node schema fields and are not declared in the + appliance (`gns3a`) schema. 4. **Rootful-Docker assumption** (`UsernsMode: host`, set for all GNS3 Docker nodes) so the container-side chown acts on the host files' real uid/gid (see the volume-persistence section). @@ -448,7 +532,11 @@ present on the host. `_get_container_ifname`, `_cleanup_console_resources`). - `gns3server/compute/docker/__init__.py` — `Docker._select_node_class` / `create_node` factory. -- `gns3server/compute/base_node.py` — console WebSocket guard. +- `gns3server/compute/base_node.py` — console WebSocket guard; binary + terminal-size control frames → NAWS / asyncssh resize + (`start_websocket_console`). +- `gns3server/api/routes/controller/nodes.py` — console WS forwarding + (text and binary frames). - `gns3server/schemas/common.py` — `ConsoleType.docker_exec`. - containerlab `nodes/srl/srl.go` — reference for SR Linux launch command and interface naming. @@ -457,6 +545,7 @@ present on the host. | Version | Date | Changes | |---------|------|---------| +| 1.6 | 2026-08-20 | Terminal geometry and size forwarding: WS binary control frames `{"cols","rows"}` → NAWS / asyncssh resize (controller now forwards binary frames; compute intercepts them); tall 511×10000 default kept for non-NAWS clients, applied post-creation and restored on last disconnect (client size racing exec creation wins over the default); new `GNS3_CONSOLE_RESIZE=0` knob for paging CLIs (XRd) where a browser resize would break concurrent netmiko sessions on the shared exec; documented the SR Linux flicker root cause (tall rows × CPR-answering client → ~2.4× re-emitted output; rows-driven, width-independent). | | 1.5 | 2026-08-13 | Add appliance (`gns3a`) packaging section: 35-adapter full-chassis design, the three server-side schema fixes (DockerConsoleType, ApplianceV1_6.custom_adapters, extra_volumes passthrough), and the symbol-theme caveat (any `:/symbols/` symbol is rewritten to the category default at load). | | 1.4 | 2026-08-13 | Reconnect fix: drop the while-true wrapper (it restarted the CLI with no client to answer CPR → blank screen on reconnect); the exec is now recreated on connect when the upstream has died. `_LazyExecTelnetServer` extracted to module level and unit-tested. | | 1.3 | 2026-08-12 | Document runtime ownership safety (root processes, self-healing daemons, ACL evidence for SR Linux), the boot-ordering caveat (bridge after boot → verify save/stop/start closed loop), and troubleshooting #10. | diff --git a/docs/features/vendor-nos-xrd.md b/docs/features/vendor-nos-xrd.md index a512b9b35..6875cfb70 100644 --- a/docs/features/vendor-nos-xrd.md +++ b/docs/features/vendor-nos-xrd.md @@ -100,6 +100,7 @@ Note: "journal corrupted" messages with varying machine-IDs come from the ``` GNS3_SKIP_INIT=1 GNS3_CONSOLE_CMD=/pkg/bin/xr_cli.sh +GNS3_CONSOLE_RESIZE=0 GNS3_MASK_UDEV=1 GNS3_SHM_SIZE=1024 GNS3_DEVICES=/dev/fuse @@ -109,6 +110,15 @@ XR_MGMT_INTERFACES=linux:eth0,xr_name=Mg0/RP0/CPU0/0,chksum,snoop_v4,snoop_v6 XR_INTERFACES=linux:eth1,xr_name=Gi0/0/0/0;linux:eth2,xr_name=Gi0/0/0/1;... ``` +`GNS3_CONSOLE_RESIZE=0` (console geometry lock): the XR pager pages on the +PTY window size and ignores `terminal length 0`, so the exec PTY must stay at +the tall no-paging default for **every** client. The docker_exec console is a +single shared exec — one browser's terminal-size resize (WS control frames → +NAWS) would change the geometry concurrent netmiko/copilot sessions see and +bring `--More--` back. XRd's CLI is line-oriented, so browsers lose nothing +by not resizing. See +[docker-exec-console.md](./docker-exec-console.md#terminal-geometry-and-size-forwarding). + XRd-specific gotchas (image-side, not GNS3): - Management interface xr_name is **`Mg0/RP0/CPU0/0`** (short prefix, `CPU0` @@ -201,6 +211,7 @@ sequenceDiagram | Version | Date | Changes | |---------|------|---------| +| 1.5 | 2026-08-20 | Appliance env gains `GNS3_CONSOLE_RESIZE=0`: client-driven console resizes are ignored so the shared exec PTY stays at the tall no-paging geometry for concurrent netmiko/copilot sessions (browsers included). | | 1.4 | 2026-08-15 | Code-review hardening: stop-query HTTP timeout scales with `GNS3_STOP_TIMEOUT` (values >300 s no longer abort); overlapping mask/config bind targets deduplicated (Docker "Duplicate mount point"); `ExtraConfig.target` validated at save time and directory forms rejected; host-readiness check no longer aborts on one unreadable `/proc/sys` key; base env parser strips trailing commas; vendor env knobs re-parsed on create (PUT environment takes effect); graceful stop limited to explicit user stop (delete/update/close keep the immediate kill); extra_configs under a persisted volume warns. | | 1.3 | 2026-08-14 | Persistence corrected: XR's live data layer is `/xr-storage` (the image's symlink farm is materialized into real directories at bootstrap; `/xr-storage-shadow` is a pristine spare) — the appliance persists both. Vendor containers now stop gracefully (SIGTERM + `GNS3_STOP_TIMEOUT` grace, default 60 s) instead of being SIGKILLed on the spot. | | 1.2 | 2026-08-14 | Link UDP self-loop root-caused to a port-allocation race and fixed — see [link-udp-self-loop](../bugs/link-udp-self-loop.md) (now marked Fixed). |