mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 12:30:13 +03:00
docker: add GNS3_CONSOLE_RESIZE knob for paging CLIs
The exec behind a docker_exec console is shared by every console client, so a browser's terminal-size resize (WS control frames -> NAWS) also changes the geometry concurrent netmiko sessions see. SR Linux doesn't care (no pager, no hard wrapping), but CLIs that page on the PTY window size (IOS-XR) would park at --More-- again the moment a browser is connected. Split the client-driven NAWS path (_on_naws) from the internal resize (_resize_exec): GNS3_CONSOLE_RESIZE=0 makes the console ignore client resizes entirely and keep the tall 511x10000 no-paging default, while the creation-time default and the restore-on-last-disconnect still go through the internal path. XRd appliance templates should set it.
This commit is contained in:
parent
abd0b8e274
commit
5741e85b65
@ -55,6 +55,11 @@ class VendorDockerVM(DockerVM):
|
||||
(adapter order) instead of default ``eth{N}``.
|
||||
* ``GNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli`` — command run inside the
|
||||
container by the ``docker_exec`` console (defaults to ``/bin/sh``).
|
||||
* ``GNS3_CONSOLE_RESIZE=0`` — ignore client-driven console resizes
|
||||
(WS terminal-size frames / telnet NAWS). Set for CLIs that page on the
|
||||
PTY window size (IOS-XR): the exec PTY must stay at the tall
|
||||
no-NAWS default for every client, including concurrent netmiko
|
||||
sessions on the shared exec.
|
||||
* ``GNS3_STOP_TIMEOUT=60`` — SIGTERM grace period in seconds when stopping
|
||||
the container (default 60; Docker SIGKILLs once it expires).
|
||||
"""
|
||||
@ -77,6 +82,7 @@ class VendorDockerVM(DockerVM):
|
||||
self._gns3_init = True
|
||||
self._interface_names = []
|
||||
self._console_cmd = None
|
||||
self._console_resize = True
|
||||
self._stop_timeout = 60
|
||||
if self._environment:
|
||||
for _line in self._environment.splitlines():
|
||||
@ -89,6 +95,8 @@ class VendorDockerVM(DockerVM):
|
||||
]
|
||||
elif _line.startswith("GNS3_CONSOLE_CMD="):
|
||||
self._console_cmd = _line.split("=", 1)[1].strip()
|
||||
elif _line.startswith("GNS3_CONSOLE_RESIZE="):
|
||||
self._console_resize = _line.split("=", 1)[1].strip().lower() not in ("0", "false", "no")
|
||||
elif _line.startswith("GNS3_STOP_TIMEOUT="):
|
||||
try:
|
||||
timeout = int(_line.split("=", 1)[1].strip())
|
||||
@ -357,7 +365,13 @@ class VendorDockerVM(DockerVM):
|
||||
Command from GNS3_CONSOLE_CMD.
|
||||
"""
|
||||
|
||||
telnet = _LazyExecTelnetServer(self, self.manager, self._cid, self._console_cmd or "/bin/sh")
|
||||
telnet = _LazyExecTelnetServer(
|
||||
self,
|
||||
self.manager,
|
||||
self._cid,
|
||||
self._console_cmd or "/bin/sh",
|
||||
allow_resize=self._console_resize,
|
||||
)
|
||||
try:
|
||||
self._telnet_servers.append(
|
||||
await telnet.start(self._manager.port_manager.console_host, self.console)
|
||||
@ -384,7 +398,7 @@ class _LazyExecTelnetServer(AsyncioTelnetServer):
|
||||
CPR, producing a blank/degraded screen on reconnect.
|
||||
"""
|
||||
|
||||
def __init__(self, vm, manager, cid, command):
|
||||
def __init__(self, vm, manager, cid, command, allow_resize=True):
|
||||
super().__init__(
|
||||
reader=None,
|
||||
writer=None,
|
||||
@ -397,6 +411,7 @@ class _LazyExecTelnetServer(AsyncioTelnetServer):
|
||||
self._manager = manager
|
||||
self._cid = cid
|
||||
self._command = command
|
||||
self._allow_resize = allow_resize
|
||||
self._exec_id = None
|
||||
self._broadcast_task = None
|
||||
self._lock = asyncio.Lock()
|
||||
@ -421,9 +436,9 @@ class _LazyExecTelnetServer(AsyncioTelnetServer):
|
||||
# inherit it and hit PTY-window paging (the IOS-XR --More-- trap).
|
||||
if self._exec_id and not await self._get_connections_snapshot():
|
||||
with contextlib.suppress(Exception):
|
||||
await self._on_naws(511, 10000)
|
||||
await self._resize_exec(511, 10000)
|
||||
|
||||
async def _on_naws(self, columns, rows):
|
||||
async def _resize_exec(self, columns, rows):
|
||||
if self._exec_id:
|
||||
try:
|
||||
await self._manager.query(
|
||||
@ -434,6 +449,15 @@ class _LazyExecTelnetServer(AsyncioTelnetServer):
|
||||
except DockerError:
|
||||
pass
|
||||
|
||||
async def _on_naws(self, columns, rows):
|
||||
# Client-driven resize (WS terminal-size control frames, telnet NAWS).
|
||||
# Ignored for paging CLIs (GNS3_CONSOLE_RESIZE=0): with the exec shared
|
||||
# by all clients, one browser resize would break concurrent netmiko
|
||||
# sessions that rely on the tall no-paging geometry.
|
||||
if not self._allow_resize:
|
||||
return
|
||||
await self._resize_exec(columns, rows)
|
||||
|
||||
async def run(self, network_reader, network_writer):
|
||||
"""Catch and log any exception that kills the client session."""
|
||||
try:
|
||||
@ -523,7 +547,7 @@ class _LazyExecTelnetServer(AsyncioTelnetServer):
|
||||
# WebUI clients resize to their real geometry right after
|
||||
# connecting, via WS terminal-size control frames turned
|
||||
# into NAWS by start_websocket_console.
|
||||
await self._on_naws(511, 10000)
|
||||
await self._resize_exec(511, 10000)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
|
||||
@ -463,12 +463,15 @@ def test_cleanup_console_resources_no_writer(compute_project, manager):
|
||||
# _LazyExecTelnetServer — upstream aliveness + reconnect/recreate logic
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_lazy_server(compute_project, manager):
|
||||
def _make_lazy_server(compute_project, manager, environment="GNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli"):
|
||||
"""Build a _LazyExecTelnetServer with _create_exec mocked out (no docker)."""
|
||||
vm = _make_vm(compute_project, manager, environment="GNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli")
|
||||
srv = _LazyExecTelnetServer(vm, manager, "e90e34656842", "/opt/srlinux/bin/sr_cli")
|
||||
vm = _make_vm(compute_project, manager, environment=environment)
|
||||
srv = _LazyExecTelnetServer(
|
||||
vm, manager, "e90e34656842", "/opt/srlinux/bin/sr_cli",
|
||||
allow_resize=vm._console_resize,
|
||||
)
|
||||
srv._create_exec = AsyncioMagicMock()
|
||||
srv._on_naws = AsyncioMagicMock()
|
||||
srv._resize_exec = AsyncioMagicMock()
|
||||
return srv
|
||||
|
||||
|
||||
@ -539,7 +542,46 @@ async def test_first_connect_sets_tall_default_pty_geometry(compute_project, man
|
||||
|
||||
srv = _make_lazy_server(compute_project, manager)
|
||||
await srv.client_connected_hook()
|
||||
srv._on_naws.assert_called_once_with(511, 10000)
|
||||
srv._resize_exec.assert_called_once_with(511, 10000)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_naws_resizes_exec_by_default(compute_project, manager):
|
||||
"""Client-driven NAWS (WS terminal-size frames) reaches the exec resize."""
|
||||
|
||||
srv = _make_lazy_server(compute_project, manager)
|
||||
await srv._on_naws(120, 40)
|
||||
srv._resize_exec.assert_called_once_with(120, 40)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_naws_ignored_when_resize_disabled(compute_project, manager):
|
||||
"""GNS3_CONSOLE_RESIZE=0: client resizes must not change the shared exec
|
||||
geometry (paging CLIs need the tall default for concurrent netmiko)."""
|
||||
|
||||
srv = _make_lazy_server(
|
||||
compute_project, manager,
|
||||
environment="GNS3_CONSOLE_RESIZE=0",
|
||||
)
|
||||
assert srv._allow_resize is False
|
||||
await srv._on_naws(120, 40)
|
||||
srv._resize_exec.assert_not_called()
|
||||
# the tall default is still applied at exec creation (internal path)
|
||||
await srv.client_connected_hook()
|
||||
srv._resize_exec.assert_called_once_with(511, 10000)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_last_client_disconnect_restores_tall_default(compute_project, manager):
|
||||
"""When the last console client leaves, the exec goes back to the tall
|
||||
no-NAWS default so a later non-NAWS client (netmiko) doesn't inherit a
|
||||
browser geometry and hit PTY-window paging."""
|
||||
|
||||
srv = _make_lazy_server(compute_project, manager)
|
||||
srv._exec_id = "abc"
|
||||
writer = AsyncioMagicMock()
|
||||
await srv._disconnect_client(writer)
|
||||
srv._resize_exec.assert_called_once_with(511, 10000)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user