From 0f4b0c567eaf3e60954e39cef8dfc32b386d65da Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 6 May 2026 13:02:07 +0800 Subject: [PATCH 1/5] feat: add AI provider config files to .gitignore Add deepseek, claude, gemini, and grok settings files to .gitignore to prevent accidental commits of local AI provider configurations. --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index f80657cee..28a535038 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,10 @@ venv .claude/minmax-settings.json .claude/zhipu-settings.json .claude/settings.local.json +.claude/deepseek-settings.json +.claude/claude-settings.json +.claude/gemini-settings.json +.claude/grok-settings.json !.claude/development.md # Exception: allow development docs !.claude/skills/ # Exception: allow project skills !.claude/memory/ # Exception: allow project memory @@ -80,3 +84,4 @@ venv # Tiktoken cache files gns3server/agent/gns3_copilot/cache/tiktoken/ + From 4d06b2b7b672ead5f76f6007099b7f636f5bca52 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 6 May 2026 13:24:27 +0800 Subject: [PATCH 2/5] Fix: Skip add_compute for existing computes when opening projects --- gns3server/controller/project.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index cab967a99..96976730f 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -1147,7 +1147,9 @@ class Project: topology = project_data["topology"] for compute in topology.get("computes", []): - await self.controller.add_compute(**compute) + compute_id = compute.get("compute_id") + if compute_id not in self._controller._computes: + await self.controller.add_compute(**compute) # Get all compute used in the project # used to allocate application IDs for IOU nodes. From 8ecd449dd8a32d7ecc9122dc7c891d00dfec8daf Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Thu, 7 May 2026 09:37:51 +0800 Subject: [PATCH 3/5] Fix: Add error logging when closing/deleting projects on computes Previously errors during close() and delete_on_computes() were silently swallowed without any logging, making it difficult to diagnose failures when remote computes are unreachable. - close(): log warning instead of silent pass - delete_on_computes(): wrap HTTP DELETE in try/except with warning log Co-Authored-By: Claude Opus 4.6 --- gns3server/controller/project.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 96976730f..7d4a7cd37 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -890,9 +890,8 @@ class Project: for compute in list(self._project_created_on_compute): try: await compute.post(f"/projects/{self._id}/close", dont_connect=True) - # We don't care if a compute is down at this step - except (ComputeError, ControllerError, TimeoutError): - pass + except (ComputeError, ControllerError, TimeoutError) as e: + log.warning(f"Could not close project '{self._id}' on compute '{compute.id}': {e}") self._clean_pictures() self._status = "closed" if not ignore_notification: @@ -1075,7 +1074,10 @@ class Project: """ for compute in list(self._project_created_on_compute): if compute.id != "local": - await compute.delete(f"/projects/{self._id}") + try: + await compute.delete(f"/projects/{self._id}") + except (ComputeError, TimeoutError) as e: + log.warning(f"Could not delete project '{self._id}' on compute '{compute.id}': {e}") self._project_created_on_compute.remove(compute) @classmethod From 30ebde0cb09da012eb47afdf3998c823f9cc7836 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Thu, 7 May 2026 09:58:20 +0800 Subject: [PATCH 4/5] 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 | 68 +++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 7d4a7cd37..4a287a57b 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -1022,6 +1022,16 @@ 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() @@ -1029,28 +1039,6 @@ class Project: # 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 - # We need to check from the topology file because _project_created_on_compute - # gets reset during open() - disconnected_computes = [] - 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]) - 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() @@ -1068,6 +1056,42 @@ class Project: raise ControllerError(f"Cannot delete project directory {self.path}: {str(e)}") self.emit_controller_notification("project.deleted", self.asdict()) + 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 f1b11e7cae54d7d7bcbb34f768c783527a061c05 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Thu, 7 May 2026 10:48:18 +0800 Subject: [PATCH 5/5] Fix: Check compute connectivity before node creation in open() When opening a closed project with nodes on an offline remote compute, open() would block for 120s trying to connect before eventually failing. Now checks compute connectivity after loading the topology file but before creating nodes, allowing immediate failure with a clear error message. Co-Authored-By: Claude Opus 4.6 --- gns3server/controller/project.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 4a287a57b..e0a38838c 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -1184,6 +1184,16 @@ class Project: if compute_id not in self._computes: self._computes.append(compute_id) + # Check compute connectivity before creating nodes to avoid + # 120-second timeout when a remote compute is unreachable + disconnected = self._get_disconnected_computes() + if disconnected: + compute_names = ", ".join([f"'{c.name}'" for c in disconnected]) + raise ControllerError( + f"Cannot open project '{self.name}': {len(disconnected)} compute(s) are disconnected: {compute_names}. " + f"Please check the connection and try again." + ) + for node in topology.get("nodes", []): compute = self.controller.get_compute(node.pop("compute_id")) name = node.pop("name")