From d8f8b6ec700fe9c7446eba99ed97b94a6685ad0f Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 27 Feb 2026 16:35:11 +0100 Subject: [PATCH] fix(docker): handle container name conflict automatically When a Docker container with the same name already exists (e.g., from a previous crashed GNS3 session), Docker returns a 409 Conflict error when trying to create a new container with that name. This causes the project open operation to fail. This fix adds automatic cleanup of stale containers when encountering a name conflict: - Added DockerHttp409Error exception class - Updated http_query to detect 409 status codes - Modified create() to remove conflicting containers and retry Fixes the issue where opening a project fails with: "Docker has returned an error: 409 Conflict. The container name '/GNS3.xxx' is already in use by container 'xxx'" --- gns3server/compute/docker/__init__.py | 4 ++- gns3server/compute/docker/docker_error.py | 4 +++ gns3server/compute/docker/docker_vm.py | 36 +++++++++++++++++++---- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index a441af830..4c12314d5 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -33,7 +33,7 @@ from gns3server.config import Config from gns3server.utils.asyncio import locking from gns3server.compute.base_manager import BaseManager from gns3server.compute.docker.docker_vm import DockerVM -from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error +from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error, DockerHttp409Error log = logging.getLogger(__name__) @@ -236,6 +236,8 @@ class Docker(BaseManager): raise DockerHttp304Error("Docker has returned an error: {} {}".format(response.status, body)) elif response.status == 404: raise DockerHttp404Error("Docker has returned an error: {} {}".format(response.status, body)) + elif response.status == 409: + raise DockerHttp409Error("Docker has returned an error: {} {}".format(response.status, body)) else: raise DockerError("Docker has returned an error: {} {}".format(response.status, body)) return response diff --git a/gns3server/compute/docker/docker_error.py b/gns3server/compute/docker/docker_error.py index 5d2b9b1d8..17d45ebbe 100644 --- a/gns3server/compute/docker/docker_error.py +++ b/gns3server/compute/docker/docker_error.py @@ -32,3 +32,7 @@ class DockerHttp304Error(DockerError): class DockerHttp404Error(DockerError): pass + + +class DockerHttp409Error(DockerError): + pass diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 79b5f640b..7b58305c6 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -43,7 +43,8 @@ from ..nios.nio_udp import NIOUDP from .docker_error import ( DockerError, DockerHttp304Error, - DockerHttp404Error + DockerHttp404Error, + DockerHttp409Error ) import logging @@ -459,10 +460,35 @@ class DockerVM(BaseNode): if extra_hosts: params["Env"].append("GNS3_EXTRA_HOSTS={}".format(extra_hosts)) - # Support name in Doker: [a-zA-Z0-9][a-zA-Z0-9_.-] - result = await self.manager.query("POST", "containers/create?name={}".format(self.docker_name), data=params) - self._cid = result['Id'] - log.info("Docker container '{name}' [{id}] created".format(name=self._name, id=self._id)) + try: + # Supported names in Docker: [a-zA-Z0-9][a-zA-Z0-9_.-] + result = await self.manager.query("POST", f"containers/create?name={self.docker_name}", data=params) + except DockerHttp409Error: + # Container name already exists. This can happen when the server crashes + # and leaves containers behind. Try to remove the conflicting container. + log.warning(f"Container name '{self.docker_name}' is already in use, attempting to clean up the stale container...") + try: + # Try to get and remove the conflicting container + try: + container_info = await self.manager.query("GET", f"containers/{self.docker_name}/json") + container_id = container_info["Id"] + # Force remove the container + await self.manager.query("DELETE", f"containers/{container_id}", params={"force": 1, "v": 1}) + log.info(f"Removed stale container '{self.docker_name}' ({container_id})") + except DockerHttp404Error: + # Container doesn't exist anymore, race condition - just continue + pass + # Retry creating the container + result = await self.manager.query("POST", f"containers/create?name={self.docker_name}", data=params) + except DockerError as e: + log.error(f"Failed to clean up conflicting container '{self.docker_name}': {e}") + raise + self._cid = result["Id"] + log.info(f"Docker container '{self._name}' [{self._id}] created") + if self._cpus > 0: + log.info(f"CPU limit set to {self._cpus} CPUs") + if self._memory > 0: + log.info(f"Memory limit set to {self._memory} MB") return True def _format_env(self, variables, env):