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=<seconds> (default 60,
clamped to 1-600, invalid values keep the default) and use it in
VendorDockerVM._terminate_container().
This commit is contained in:
YueGuobin 2026-08-14 22:11:34 +08:00
parent e9339faaa7
commit 62076c1727
No known key found for this signature in database
3 changed files with 45 additions and 7 deletions

View File

@ -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/<unit>` | `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. |

View File

@ -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

View File

@ -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})