From 4444da9ffac6382cf02b506cfd6099e9a3eb72ba Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 6 May 2026 00:05:10 +0800 Subject: [PATCH 1/4] Fix: Check compute connection status before project deletion This commit addresses issue #2703 where deleting a project with nodes on remote compute nodes would result in long waits with no feedback if those computes were unreachable. Changes: 1. Compute connection status updates on connection failure - When a compute fails to connect, update connected=False and last_error - Send compute.updated notification to UI so users can see status - This allows Web UI to display real-time connection status 2. Project deletion checks compute status before attempting deletion - Check all computes used by the project are connected - If any compute is disconnected, immediately reject deletion - Provide clear error message indicating which computes are offline - This prevents long timeouts and gives users immediate feedback Benefits: - Immediate feedback instead of 120-second timeouts - Clear error messages about which computes are disconnected - Prevents orphaned resources on offline computes - Improves user experience by avoiding silent waits Related: #2703 Co-Authored-By: Claude Sonnet 4.6 --- gns3server/controller/compute.py | 5 +++++ gns3server/controller/project.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index 65bca3c34..9a9d94c29 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -375,6 +375,11 @@ class Compute: log.info(f"Connecting to compute '{self._id}'") response = await self._run_http_query("GET", "/capabilities") except ComputeError as e: + # Update connection status and notify UI + self._connected = False + self._last_error = str(e) + self._controller.notification.controller_emit("compute.updated", self.asdict()) + if report_failed_connection: raise log.warning(f"Cannot connect to compute '{self._id}': {e}") diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 8d8eeebee..9cf43d215 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -1029,6 +1029,20 @@ class Project: except ControllerError as e: # ignore missing images or other conflicts when deleting a project log.warning(f"Conflict while deleting project: {e}") + + # Check if all computes used by this project are connected before deletion + disconnected_computes = [] + for compute in self._project_created_on_compute: + if not compute.connected: + disconnected_computes.append(compute) + + if disconnected_computes: + compute_names = ", ".join([f"'{c.name}'" for c in disconnected_computes]) + raise ControllerForbiddenError( + f"Cannot delete project '{self.name}': {len(disconnected_computes)} compute(s) are disconnected: {compute_names}. " + f"Please fix the connection or delete the project manually on those computes." + ) + await self.delete_on_computes() await self.close() From 9c868911e11969bec3aa6f13174f67eb1b775dd8 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 6 May 2026 00:19:55 +0800 Subject: [PATCH 2/4] Fix: Use _computes instead of _project_created_on_compute for deletion check The initial fix used _project_created_on_compute to check for disconnected computes before deletion, but this set gets reset during project.open(), causing the check to fail. Now uses self._computes which is loaded from the topology file and persists through the open() call. Related: #2703 Co-Authored-By: Claude Sonnet 4.6 --- gns3server/controller/project.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 9cf43d215..cab967a99 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -1031,10 +1031,19 @@ class Project: log.warning(f"Conflict while deleting project: {e}") # Check if all computes used by this project are connected before deletion + # We need to check from the topology file because _project_created_on_compute + # gets reset during open() disconnected_computes = [] - for compute in self._project_created_on_compute: - if not compute.connected: - disconnected_computes.append(compute) + for compute_id in self._computes: + try: + compute = self._controller.get_compute(compute_id) + if not compute.connected: + disconnected_computes.append(compute) + except ControllerError: + # Compute doesn't exist anymore, consider it disconnected + log.warning(f"Compute '{compute_id}' not found in controller") + # We can't add it to disconnected_computes without the compute object + pass if disconnected_computes: compute_names = ", ".join([f"'{c.name}'" for c in disconnected_computes]) From c92188cad4f93a5ed0a74739f315605bf9c0488d Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 6 May 2026 00:47:24 +0800 Subject: [PATCH 3/4] Fix: Handle project deletion errors in autoidlepc When deleting the AUTOIDLEPC project in autoidlepc(), if a remote compute is disconnected, the deletion fails with ControllerForbiddenError due to our compute connection status check. This causes: - The project to remain in the controller's memory - Subsequent autoidlepc calls to fail - User must restart the server to recover This fix adds exception handling to ensure the project is always removed from the controller's memory, even if deletion fails. Related to #2703 --- gns3server/controller/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 9cfe2bcd7..602765109 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -842,7 +842,14 @@ class Controller: compute = self.get_compute(compute_id) for project in list(self._projects.values()): if project.name == "AUTOIDLEPC": - await project.delete() + try: + await project.delete() + except ControllerForbiddenError as e: + # Project couldn't be deleted due to disconnected computes + log.warning(f"Could not delete AUTOIDLEPC project '{project.id}': {e}") + except ControllerError as e: + log.warning(f"Error deleting AUTOIDLEPC project '{project.id}': {e}") + # Always remove from controller to allow creating a new one self.remove_project(project) project = await self.add_project(name="AUTOIDLEPC") node = await project.add_node( From a67cd66b60c965615305375998b29d994b21eeb9 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 6 May 2026 00:59:05 +0800 Subject: [PATCH 4/4] Revert "Fix: Handle project deletion errors in autoidlepc" This reverts commit c92188cad4f93a5ed0a74739f315605bf9c0488d. --- gns3server/controller/__init__.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 602765109..9cfe2bcd7 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -842,14 +842,7 @@ class Controller: compute = self.get_compute(compute_id) for project in list(self._projects.values()): if project.name == "AUTOIDLEPC": - try: - await project.delete() - except ControllerForbiddenError as e: - # Project couldn't be deleted due to disconnected computes - log.warning(f"Could not delete AUTOIDLEPC project '{project.id}': {e}") - except ControllerError as e: - log.warning(f"Error deleting AUTOIDLEPC project '{project.id}': {e}") - # Always remove from controller to allow creating a new one + await project.delete() self.remove_project(project) project = await self.add_project(name="AUTOIDLEPC") node = await project.add_node(