From 40644f33e6548115c1a05554ea58df216e20560c Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Thu, 7 May 2026 03:58:20 +0200 Subject: [PATCH 1/4] Fix: Check compute connectivity before open() during project deletion Previously the disconnected compute check ran after open(), which would block for 120s trying to connect to unreachable remote computes before rejecting the deletion. Now reads the topology file directly to extract compute IDs and checks connectivity before calling open(), enabling immediate rejection of deletions involving offline computes. Also removes the redundant post-open() check since the early check covers both opened and closed project states. Co-Authored-By: Claude Opus 4.6 --- gns3server/controller/project.py | 49 +++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 9d633f526..ddec19b4f 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -905,12 +905,23 @@ class Project: async def delete(self): + # Check compute connectivity before open() to avoid 120s timeout + # when remote computes are unreachable + disconnected = self._get_disconnected_computes() + if disconnected: + compute_names = ", ".join([f"'{c.name}'" for c in disconnected]) + raise ControllerForbiddenError( + f"Cannot delete project '{self.name}': {len(disconnected)} compute(s) are disconnected: {compute_names}. " + f"Please fix the connection or delete the project manually on those computes." + ) + if self._status != "opened": try: await self.open() except aiohttp.web.HTTPConflict as e: # ignore missing images or other conflicts when deleting a project - log.warning("Conflict while deleting project: {}".format(e.text)) + log.warning(f"Conflict while deleting project: {e}") + await self.delete_on_computes() await self.close() try: @@ -922,6 +933,42 @@ class Project: raise aiohttp.web.HTTPConflict(text="Cannot delete project directory {}: {}".format(self.path, str(e))) self.emit_controller_notification("project.deleted", self.__json__()) + def _get_disconnected_computes(self): + """ + Check compute connectivity by reading the topology file directly, + without opening the project (which would try to connect to computes). + Returns a list of disconnected Compute objects. + """ + if self._status == "opened": + # Project is already open, use the already-loaded _computes list + compute_ids = self._computes + else: + # Read compute IDs from topology file without connecting + path = self._topology_file() + if not os.path.exists(path): + return [] + try: + project_data = load_topology(path) + except (ValueError, OSError) as e: + log.warning(f"Could not read topology file for project '{self._name}': {e}") + return [] + topology = project_data.get("topology", {}) + compute_ids = set() + for node in topology.get("nodes", []): + compute_id = node.get("compute_id") + if compute_id: + compute_ids.add(compute_id) + + disconnected = [] + for compute_id in compute_ids: + try: + compute = self._controller.get_compute(compute_id) + if not compute.connected: + disconnected.append(compute) + except ControllerError: + log.warning(f"Compute '{compute_id}' not found in controller") + return disconnected + async def delete_on_computes(self): """ Delete the project on computes but not on controller From 0ecaab4da01db9ddfd0bc5c759d628f61c93a48f Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 12 Jul 2026 11:05:15 +0200 Subject: [PATCH 2/4] Replace ControllerForbiddenError with aiohttp.web.HTTPForbidden for project deletion error handling --- gns3server/controller/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index ddec19b4f..de201f0f5 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -910,7 +910,7 @@ class Project: disconnected = self._get_disconnected_computes() if disconnected: compute_names = ", ".join([f"'{c.name}'" for c in disconnected]) - raise ControllerForbiddenError( + raise aiohttp.web.HTTPForbidden( f"Cannot delete project '{self.name}': {len(disconnected)} compute(s) are disconnected: {compute_names}. " f"Please fix the connection or delete the project manually on those computes." ) From e3e0a511b6b9b17f2596214c8974f76a49e31a83 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 12 Jul 2026 11:08:13 +0200 Subject: [PATCH 3/4] Handle HTTPNotFound exception when retrieving compute status --- gns3server/controller/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index de201f0f5..f26b1ff5f 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -965,7 +965,7 @@ class Project: compute = self._controller.get_compute(compute_id) if not compute.connected: disconnected.append(compute) - except ControllerError: + except aiohttp.web.HTTPNotFound: log.warning(f"Compute '{compute_id}' not found in controller") return disconnected From ea09cc2724297c4b98d3a56886eec8c4ae0ecdcf Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 12 Jul 2026 13:46:12 +0200 Subject: [PATCH 4/4] fix: gns3-server crashes on startup if "Open this project in the background" is active but there is a problem with that project --- gns3server/controller/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 34b5144b9..7bd932aa4 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -562,7 +562,15 @@ class Controller: for project in self._projects.values(): if project.auto_open: - await project.open() + try: + await project.open() + except aiohttp.web.HTTPClientError as e: + details = e.text or e.reason or str(e) + log.warning( + "Failed to auto-open project '%s': %s", + project.name, + details, + ) def get_free_project_name(self, base_name): """