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) | | 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()` | | 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()` | | 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()` | | 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 `GNS3_*` variables are consumed host-side only and never forwarded into the
@ -200,7 +200,7 @@ sequenceDiagram
| Version | Date | Changes | | 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.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.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. | | 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}``. (adapter order) instead of default ``eth{N}``.
* ``GNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli`` — command run inside the * ``GNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli`` — command run inside the
container by the ``docker_exec`` console (defaults to ``/bin/sh``). 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): def __init__(self, *args, **kwargs):
@ -66,6 +68,7 @@ class VendorDockerVM(DockerVM):
self._interface_names = [] self._interface_names = []
self._console_cmd = None self._console_cmd = None
self._console_exec_writer = None self._console_exec_writer = None
self._stop_timeout = 60
if self._environment: if self._environment:
for _line in self._environment.splitlines(): for _line in self._environment.splitlines():
@ -78,6 +81,13 @@ class VendorDockerVM(DockerVM):
] ]
elif _line.startswith("GNS3_CONSOLE_CMD="): elif _line.startswith("GNS3_CONSOLE_CMD="):
self._console_cmd = _line.split("=", 1)[1].strip() 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 --------------------------------------------------- # ---- hook overrides ---------------------------------------------------
@ -142,13 +152,14 @@ class VendorDockerVM(DockerVM):
""" """
Override: vendor NOS containers run systemd and require a graceful Override: vendor NOS containers run systemd and require a graceful
shutdown (e.g. Cisco XRd treats an abrupt SIGKILL as an unclean 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; shutdown). Send SIGTERM and wait up to ``GNS3_STOP_TIMEOUT`` seconds
Docker SIGKILLs the container itself once the grace period expires, (default 60) for the services to stop; Docker SIGKILLs the container
so no fallback kill is needed. The blocking stop call sits well itself once the grace period expires, so no fallback kill is needed.
inside the manager's default 300 s query timeout. The blocking stop call sits well inside the manager's default 300 s
query timeout.
""" """
try: 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: except DockerHttp304Error:
pass # already stopped pass # already stopped

View File

@ -667,3 +667,30 @@ async def test_stop_uses_graceful_termination(compute_project, manager):
) as mock_term: ) as mock_term:
await vm.stop() await vm.stop()
mock_term.assert_called_once() 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})