From 62076c1727d22169eda2ef8e21fc4c3ce5701bd2 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 14 Aug 2026 22:11:34 +0800 Subject: [PATCH] docker: make the vendor graceful-stop grace period configurable (GNS3_STOP_TIMEOUT) The 60 s SIGTERM grace was hardcoded, unlike every other vendor knob (GNS3_SHM_SIZE, GNS3_DEVICES, GNS3_MASK_UDEV, ...) which rides the environment line. Parse GNS3_STOP_TIMEOUT= (default 60, clamped to 1-600, invalid values keep the default) and use it in VendorDockerVM._terminate_container(). --- docs/features/vendor-nos-xrd.md | 4 +-- gns3server/compute/docker/vendor_docker_vm.py | 21 +++++++++++---- tests/compute/docker/test_vendor_docker_vm.py | 27 +++++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/docs/features/vendor-nos-xrd.md b/docs/features/vendor-nos-xrd.md index 41e08a7f8..8a9083005 100644 --- a/docs/features/vendor-nos-xrd.md +++ b/docs/features/vendor-nos-xrd.md @@ -65,7 +65,7 @@ graph TB | config injection | `extra_configs: [{target, content}]` (template/node/appliance schema field) | content written under the node dir, bind-mounted **read-only** at `target` — seeds NOS startup configs without rebuilding the image | `docker_vm.py` `_mount_binds()`; persisted in `docker_templates.extra_configs` (Alembic migration) | | udev masking | `GNS3_MASK_UDEV=1` | `/dev/null` over the 5 udev systemd units **and** `/bin|/sbin|/usr/bin/udevadm` | `docker_vm.py` `create()` | | generic unit mask | `GNS3_MASK_SYSTEMD=u1,u2` | `/dev/null` over arbitrary `/etc/systemd/system/` | `docker_vm.py` `create()` | -| graceful stop | automatic for vendor containers (`docker_exec`) | stop sends SIGTERM and waits up to 60 s (Docker SIGKILLs after the grace period) instead of the base class' immediate kill — systemd NOS images require a graceful shutdown | `vendor_docker_vm.py` `_terminate_container()` | +| graceful stop | `GNS3_STOP_TIMEOUT=60` (seconds; default 60) | vendor containers are stopped with SIGTERM + a grace period (Docker SIGKILLs once it expires) instead of the base class' immediate kill — systemd NOS images require a graceful shutdown | `vendor_docker_vm.py` `_terminate_container()` | | host check | automatic at Docker connect | read-only `/proc` check of inotify/file-max/FUSE; warns with exact fix commands (server is unprivileged — it can only check) | `compute/docker/__init__.py` `_check_host_readiness()` | `GNS3_*` variables are consumed host-side only and never forwarded into the @@ -200,7 +200,7 @@ sequenceDiagram | Version | Date | Changes | |---------|------|---------| -| 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 + 60 s grace) instead of being SIGKILLed on the spot. | +| 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). | | 1.1 | 2026-08-14 | Datapath validated end-to-end (XRd brings its own interfaces up; ARP/ICMP bidirectional). Add troubleshooting entry for the one-way-link symptom (GNS3 link UDP self-loop bug, see bugs/link-udp-self-loop.md). | | 1.0 | 2026-08-14 | Initial documentation of the XRd control-plane adaptation: vendor path requirement, shm/devices/extra_configs/udev-mask mechanisms, host-disturbance root causes, appliance recipe. | diff --git a/gns3server/compute/docker/vendor_docker_vm.py b/gns3server/compute/docker/vendor_docker_vm.py index e7b155f34..ca85c1813 100644 --- a/gns3server/compute/docker/vendor_docker_vm.py +++ b/gns3server/compute/docker/vendor_docker_vm.py @@ -55,6 +55,8 @@ 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_STOP_TIMEOUT=60`` — SIGTERM grace period in seconds when stopping + the container (default 60; Docker SIGKILLs once it expires). """ def __init__(self, *args, **kwargs): @@ -66,6 +68,7 @@ class VendorDockerVM(DockerVM): self._interface_names = [] self._console_cmd = None self._console_exec_writer = None + self._stop_timeout = 60 if self._environment: for _line in self._environment.splitlines(): @@ -78,6 +81,13 @@ class VendorDockerVM(DockerVM): ] elif _line.startswith("GNS3_CONSOLE_CMD="): self._console_cmd = _line.split("=", 1)[1].strip() + elif _line.startswith("GNS3_STOP_TIMEOUT="): + try: + timeout = int(_line.split("=", 1)[1].strip()) + if 1 <= timeout <= 600: + self._stop_timeout = timeout + except ValueError: + pass # ---- hook overrides --------------------------------------------------- @@ -142,13 +152,14 @@ class VendorDockerVM(DockerVM): """ Override: vendor NOS containers run systemd and require a graceful shutdown (e.g. Cisco XRd treats an abrupt SIGKILL as an unclean - shutdown). Send SIGTERM and wait up to 60 s for the services to stop; - Docker SIGKILLs the container itself once the grace period expires, - so no fallback kill is needed. The blocking stop call sits well - inside the manager's default 300 s query timeout. + shutdown). Send SIGTERM and wait up to ``GNS3_STOP_TIMEOUT`` seconds + (default 60) for the services to stop; Docker SIGKILLs the container + itself once the grace period expires, so no fallback kill is needed. + The blocking stop call sits well inside the manager's default 300 s + query timeout. """ try: - await self.manager.query("POST", f"containers/{self._cid}/stop", params={"t": 60}) + await self.manager.query("POST", f"containers/{self._cid}/stop", params={"t": self._stop_timeout}) except DockerHttp304Error: pass # already stopped diff --git a/tests/compute/docker/test_vendor_docker_vm.py b/tests/compute/docker/test_vendor_docker_vm.py index 1fbbbcc1b..33c5ce726 100644 --- a/tests/compute/docker/test_vendor_docker_vm.py +++ b/tests/compute/docker/test_vendor_docker_vm.py @@ -667,3 +667,30 @@ async def test_stop_uses_graceful_termination(compute_project, manager): ) as mock_term: await vm.stop() mock_term.assert_called_once() + + +def test_env_stop_timeout(compute_project, manager): + vm = _make_vm(compute_project, manager, environment="GNS3_STOP_TIMEOUT=120") + assert vm._stop_timeout == 120 + + +def test_env_stop_timeout_default_60(compute_project, manager): + vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1") + assert vm._stop_timeout == 60 + + +def test_env_stop_timeout_invalid_keeps_default(compute_project, manager): + vm = _make_vm(compute_project, manager, environment="GNS3_STOP_TIMEOUT=abc") + assert vm._stop_timeout == 60 + vm = _make_vm(compute_project, manager, environment="GNS3_STOP_TIMEOUT=9999") + assert vm._stop_timeout == 60 + + +@pytest.mark.asyncio +async def test_terminate_container_uses_env_timeout(compute_project, manager): + vm = _make_vm(compute_project, manager, environment="GNS3_STOP_TIMEOUT=120") + manager.query = AsyncioMagicMock() + + await vm._terminate_container() + + manager.query.assert_called_once_with("POST", "containers/e90e34656842/stop", params={"t": 120})