docker: graceful stop for vendor NOS containers (SIGTERM + 60s grace)

DockerVM.stop() terminated containers with an immediate SIGKILL — fine
for init.sh-based containers whose state is persisted beforehand, but a
systemd NOS (Cisco XRd, SR Linux) needs a graceful shutdown and treats
the abrupt kill as an unclean shutdown (exit 137 on every stop).

Extract the final termination into _terminate_container() and override
it in VendorDockerVM: POST /containers/{id}/stop?t=60 sends SIGTERM and
waits for systemd to stop services; Docker itself SIGKILLs the
container once the grace period expires, so no fallback is needed.
Docker's 304 (already stopped) is swallowed.
This commit is contained in:
YueGuobin committed 2026-08-14 22:02:58 +08:00
1 parent 350f2b24e7
commit e9339faaa7
4 files changed
+103 -16

No files matched your search

+13 -5
View File
@@ -1206,12 +1206,8 @@ class DockerVM(BaseNode):
state = await self._get_container_state()
if state != "stopped" and state != "exited":
# SIGKILL immediately. GNS3 has already persisted container state
# (permissions via _fix_permissions, /gns3volumes) before this
# point, and the business process (often an interactive shell)
# ignores SIGTERM — so a stop grace period buys nothing but latency.
try:
await self.manager.query("POST", f"containers/{self._cid}/kill")
await self._terminate_container()
log.debug(f"Docker container '{self._name}' [{self._image}] stopped")
except DockerHttp409Error:
# Container is already stopped
@@ -1222,6 +1218,18 @@ class DockerVM(BaseNode):
return
self.status = "stopped"
async def _terminate_container(self):
"""
Final termination of a still-running container: immediate SIGKILL.
GNS3 has already persisted container state (permissions via
_fix_permissions, /gns3volumes) before this point, and the business
process (often an interactive shell) ignores SIGTERM — a stop grace
period buys nothing but latency. Vendor NOS containers override this
with a graceful SIGTERM shutdown (see VendorDockerVM).
"""
await self.manager.query("POST", f"containers/{self._cid}/kill")
async def pause(self):
"""
Pauses this Docker container.
+15 -1
View File
@@ -35,7 +35,7 @@ import shutil
from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer
from gns3server.compute.docker.docker_vm import DockerVM
from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Error
from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error
log = logging.getLogger(__name__)
@@ -138,6 +138,20 @@ class VendorDockerVM(DockerVM):
pass
self._console_exec_writer = None
async def _terminate_container(self):
"""
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.
"""
try:
await self.manager.query("POST", f"containers/{self._cid}/stop", params={"t": 60})
except DockerHttp304Error:
pass # already stopped
async def start(self):
await super().start()
if self.status == "started" and not self._gns3_init: