From 9106ac45228afc9e93ee2ebc9295309e6ec5e5b7 Mon Sep 17 00:00:00 2001 From: Volobue Date: Mon, 18 May 2026 21:49:01 +0300 Subject: [PATCH 01/14] api endpoint implemented --- .../controller_get_templateidbaseconfig.txt | 17 +++ .../controller_get_templatesbaseconfigs.txt | 44 ++++++ .../controller_put_templateidbaseconfig.txt | 17 +++ .../template/templatesbaseconfigs.rst | 38 ++++++ .../templatestemplateidbaseconfigfilename.rst | 90 ++++++++++++ .../api/controller/template_handler.py | 129 ++++++++++++++++++ .../handlers/api/controller/test_template.py | 81 ++++++++++- 7 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 docs/api/examples/controller_get_templateidbaseconfig.txt create mode 100644 docs/api/examples/controller_get_templatesbaseconfigs.txt create mode 100644 docs/api/examples/controller_put_templateidbaseconfig.txt create mode 100644 docs/api/v2/controller/template/templatesbaseconfigs.rst create mode 100644 docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst diff --git a/docs/api/examples/controller_get_templateidbaseconfig.txt b/docs/api/examples/controller_get_templateidbaseconfig.txt new file mode 100644 index 000000000..9e3d9c25c --- /dev/null +++ b/docs/api/examples/controller_get_templateidbaseconfig.txt @@ -0,0 +1,17 @@ +curl -i -X 'http://localhost:3080/v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt' + +GET /v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt HTTP/1.1 + +HTTP/1.1 200 OK +Connection: close +X-Route: /v2/templates/{template_id}/base-config/{filename} +Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f +Content-Type: application/json +Content-Length: 144 +Date: Mon, 18 May 2026 18:34:10 GMT + +{ + "content": "# test config\n\ndhcp\n", + "filename": "vpcs_base_config.txt", + "template_id": "19021f99-e36f-394d-b4a1-8aaa902ab9cc" +} \ No newline at end of file diff --git a/docs/api/examples/controller_get_templatesbaseconfigs.txt b/docs/api/examples/controller_get_templatesbaseconfigs.txt new file mode 100644 index 000000000..263cbfa1f --- /dev/null +++ b/docs/api/examples/controller_get_templatesbaseconfigs.txt @@ -0,0 +1,44 @@ +curl -i -X 'http://localhost:3080/v2/templates/base-configs' + +GET /v2/templates/base-configs HTTP/1.1 + +HTTP/1.1 200 OK +Connection: close +X-Route: /v2/templates/base-configs +Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f +Content-Type: application/json +Content-Length: 576 +Date: Mon, 18 May 2026 18:28:53 GMT + +[ + { + "filename": "config1.txt" + }, + { + "filename": "config2.txt" + }, + { + "filename": "ios_base_startup-config.txt" + }, + { + "filename": "ios_etherswitch_startup-config.txt" + }, + { + "filename": "iou_l2_base_startup-config.txt" + }, + { + "filename": "iou_l3_base_startup-config.txt" + }, + { + "filename": "test_config.txt" + }, + { + "filename": "update_test.txt" + }, + { + "filename": "vpcs_base_config.txt" + }, + { + "filename": "vpcs_base_config2.txt" + } +] \ No newline at end of file diff --git a/docs/api/examples/controller_put_templateidbaseconfig.txt b/docs/api/examples/controller_put_templateidbaseconfig.txt new file mode 100644 index 000000000..c70e315b8 --- /dev/null +++ b/docs/api/examples/controller_put_templateidbaseconfig.txt @@ -0,0 +1,17 @@ +curl -i -X 'http://localhost:3080/v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt' + +PUT /v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt HTTP/1.1 + +HTTP/1.1 200 OK +Connection: close +X-Route: /v2/templates/{template_id}/base-config/{filename} +Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f +Content-Type: application/json +Content-Length: 144 +Date: Mon, 18 May 2026 18:37:57 GMT + +{ + "content": "# test config\n\ndhcp\n", + "filename": "vpcs_base_config.txt", + "template_id": "19021f99-e36f-394d-b4a1-8aaa902ab9cc" +} \ No newline at end of file diff --git a/docs/api/v2/controller/template/templatesbaseconfigs.rst b/docs/api/v2/controller/template/templatesbaseconfigs.rst new file mode 100644 index 000000000..dc4de3e8c --- /dev/null +++ b/docs/api/v2/controller/template/templatesbaseconfigs.rst @@ -0,0 +1,38 @@ +GET /v2/templates/base-configs +------------------------------------------------------------------------------------------------------------------------------------------ + +.. contents:: + +GET /v2/templates/base-configs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +List all available base configuration files + +Response status codes +********************** +- **200**: List of base configuration files returned + +Output +******* + +.. raw:: html + + + + + + + + + + + + + + +
NameTypeDescription
filenamestringName of the base configuration file
+ +Sample session +*************** + +.. literalinclude:: ../../../examples/controller_get_templatesbaseconfigs.txt \ No newline at end of file diff --git a/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst b/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst new file mode 100644 index 000000000..98b356270 --- /dev/null +++ b/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst @@ -0,0 +1,90 @@ +/v2/templates/{template_id}/base-config/{filename} +------------------------------------------------------------------------------------------------------------------------------------------ + +.. contents:: + +GET /v2/templates/**{template_id}**/base-configs/**{filename}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +List all available base configuration files + +Response status codes +********************** +- **200**: List of base configuration files returned + +Output +******* + +.. raw:: html + + + + + + + + + + + + + + +
NameTypeDescription
filenamestringName of the base configuration file
+ +Sample session +*************** + +.. literalinclude:: ../../../examples/controller_get_templateidbaseconfig.txt + + +PUT /v2/templates/**{template_id}**/base-config/**{filename}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Update base configuration file content + +Response status codes +********************** +- **200**: File updated +- **404**: File not found + +Input +******* + +.. raw:: html + + + + + + + + + + + +
NameMandatoryTypeDescription
contentstringNew file content
+ +Output +******* + +.. raw:: html + + + + + + + + + + + + + +
NameMandatoryTypeDescription
template_idstringTemplate UUID
filenamestringBase configuration filename
contentstringUpdated file content
+ +Sample session +*************** + +.. literalinclude:: ../../../examples/controller_put_templateidbaseconfig.txt \ No newline at end of file diff --git a/gns3server/handlers/api/controller/template_handler.py b/gns3server/handlers/api/controller/template_handler.py index 277349429..278782276 100644 --- a/gns3server/handlers/api/controller/template_handler.py +++ b/gns3server/handlers/api/controller/template_handler.py @@ -22,6 +22,7 @@ from gns3server.schemas.template import TEMPLATE_USAGE_SCHEMA import hashlib import json +import os from gns3server.schemas.template import ( TEMPLATE_OBJECT_SCHEMA, @@ -173,3 +174,131 @@ class TemplateHandler: compute_id=request.json.get("compute_id")) response.set_status(201) response.json(node) + + @Route.get( + r"/templates/{template_id}/base-config/{filename}", + description="Get base configuration file content", + ) + def get_base_config(request, response): + + controller = Controller.instance() + + template_id = request.match_info["template_id"] + + template = controller.template_manager.get_template(template_id) + + filename = os.path.basename(request.match_info["filename"]) + + path = os.path.join(controller.configs_path(), filename) + + try: + if not os.path.exists(path): + response.set_status(404) + response.json({"message": "File not found"}) + return + + with open(path, encoding="utf-8", errors="ignore") as f: + content = f.read() + + except Exception as e: + response.set_status(500) + response.json({"message": str(e)}) + return + + response.set_status(200) + response.json({ + "template_id": template.id, + "filename": filename, + "content": content + }) + + @Route.put( + r"/templates/{template_id}/base-config/{filename}", + description="Update base configuration file content", + parameters={ + "template_id": "Template UUID", + "filename": "Base config filename" + }, + status_codes={ + 200: "File updated", + 400: "Invalid request", + 404: "Template or file not found" + }, + ) + def update_base_config(request, response): + + controller = Controller.instance() + + template_id = request.match_info["template_id"] + filename = os.path.basename(request.match_info["filename"]) + + try: + template = controller.template_manager.get_template(template_id) + except Exception: + response.set_status(404) + response.json({"message": "Template not found"}) + return + + path = os.path.join(controller.configs_path(), filename) + + body = request.json + + if not body or "content" not in body: + response.set_status(400) + response.json({"message": "Missing 'content' field"}) + return + + content = body["content"] + + try: + os.makedirs(controller.configs_path(), exist_ok=True) + + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + except OSError as e: + response.set_status(500) + response.json({"message": str(e)}) + return + + response.set_status(200) + response.json({ + "template_id": template.id, + "filename": filename, + "content": content + }) + + @Route.get( + r"/templates/base-configs", + description="List all available base configuration files", + status_codes={ + 200: "List of base configuration files returned" + } + ) + def list_base_configs(request, response): + + controller = Controller.instance() + + configs_path = controller.configs_path() + + try: + files = [] + + if os.path.exists(configs_path): + for filename in os.listdir(configs_path): + + path = os.path.join(configs_path, filename) + + if os.path.isfile(path): + files.append({"filename": filename}) + + except OSError as e: + response.set_status(500) + response.json({ + "message": str(e) + }) + return + + files = sorted(files, key=lambda x: x["filename"]) + response.set_status(200) + response.json(files) diff --git a/tests/handlers/api/controller/test_template.py b/tests/handlers/api/controller/test_template.py index 2accbffde..610b7da4b 100644 --- a/tests/handlers/api/controller/test_template.py +++ b/tests/handlers/api/controller/test_template.py @@ -14,8 +14,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - - +import os import uuid from tests.utils import asyncio_patch @@ -955,3 +954,81 @@ async def test_create_node_from_template(controller_api, controller, project): mock.assert_called_with(id, x=42, y=12, name=None, compute_id=None) assert response.route == "/projects/{project_id}/templates/{template_id}" assert response.status == 201 + + +async def test_get_base_config(controller_api, controller): + + template_id = str(uuid.uuid4()) + + controller.template_manager._templates[template_id] = Template(template_id, { + "template_type": "vpcs", + "category": 0, + "name": "test", + "symbol": "guest.svg", + "default_name_format": "{name}-{0}", + "compute_id": "local" + }) + + config_path = os.path.join(controller.configs_path(), "test_config.txt") + + with open(config_path, "w") as f: + f.write("hello config") + + response = await controller_api.get( + f"/templates/{template_id}/base-config/test_config.txt" + ) + + assert response.status == 200 + assert response.json["filename"] == "test_config.txt" + assert response.json["content"] == "hello config" + +async def test_update_base_config(controller_api, controller): + + template_id = str(uuid.uuid4()) + + controller.template_manager._templates[template_id] = Template(template_id, { + "template_type": "vpcs", + "category": 0, + "name": "test", + "symbol": "guest.svg", + "default_name_format": "{name}-{0}", + "compute_id": "local" + }) + + config_path = os.path.join(controller.configs_path(), "update_test.txt") + + with open(config_path, "w") as f: + f.write("old content") + + response = await controller_api.put( + f"/templates/{template_id}/base-config/update_test.txt", + { + "content": "new updated content" + } + ) + + assert response.status == 200 + assert response.json["content"] == "new updated content" + + with open(config_path) as f: + assert f.read() == "new updated content" + +async def test_list_base_configs(controller_api, controller): + + config1 = os.path.join(controller.configs_path(), "config1.txt") + config2 = os.path.join(controller.configs_path(), "config2.txt") + + with open(config1, "w") as f: + f.write("test1") + + with open(config2, "w") as f: + f.write("test2") + + response = await controller_api.get("/templates/base-configs") + + assert response.status == 200 + + filenames = [item["filename"] for item in response.json] + + assert "config1.txt" in filenames + assert "config2.txt" in filenames \ No newline at end of file From 8499ed3d65f34182cc265d0fcd44866474faeb8a Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 6 Jul 2026 18:05:28 +0200 Subject: [PATCH 02/14] Update GitHub Actions workflows --- .github/workflows/add-new-issues-to-project.yml | 2 +- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/docker-build.yml | 6 +++--- .github/workflows/publish-api-documentation.yml | 4 ++-- .github/workflows/testing.yml | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/add-new-issues-to-project.yml b/.github/workflows/add-new-issues-to-project.yml index aa8252552..5296e7a1f 100644 --- a/.github/workflows/add-new-issues-to-project.yml +++ b/.github/workflows/add-new-issues-to-project.yml @@ -10,7 +10,7 @@ jobs: name: Add issue to project runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@v1.0.1 + - uses: actions/add-to-project@v2 with: project-url: https://github.com/orgs/GNS3/projects/3 github-token: ${{ secrets.ADD_NEW_ISSUES_TO_PROJECT }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 06ab2cc62..7479a13b2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -56,11 +56,11 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -88,6 +88,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index fec449ba2..8c17f15f4 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -13,17 +13,17 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/.github/workflows/publish-api-documentation.yml b/.github/workflows/publish-api-documentation.yml index 4df3f308e..079913aa2 100644 --- a/.github/workflows/publish-api-documentation.yml +++ b/.github/workflows/publish-api-documentation.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 ref: "gh-pages" - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.8 - name: Merge changes from 3.0 branch diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index cb84817ea..f80388526 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -21,9 +21,9 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 40644f33e6548115c1a05554ea58df216e20560c Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Thu, 7 May 2026 03:58:20 +0200 Subject: [PATCH 03/14] 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 04/14] 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 05/14] 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 06/14] 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): """ From d8f8b6ec700fe9c7446eba99ed97b94a6685ad0f Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 27 Feb 2026 16:35:11 +0100 Subject: [PATCH 07/14] 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): From a58fa8602ba00841aaf006acea25dee89ea60134 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 13 Jul 2026 10:05:01 +0200 Subject: [PATCH 08/14] remove wrongly merged code --- gns3server/compute/docker/docker_vm.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 7b58305c6..a5822d998 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -485,10 +485,6 @@ class DockerVM(BaseNode): 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): From 896b4fc56a99a13086e229018229b31069b0ba55 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 13 Jul 2026 11:08:17 +0200 Subject: [PATCH 09/14] UEFI improvements for Qemu VMs * Add OVMF firmware directory configuration * Automatically add a Random Number Generator (RNG) device when using uefi option is enabled --- conf/gns3_server.conf | 2 ++ gns3server/compute/qemu/qemu_vm.py | 21 ++++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/conf/gns3_server.conf b/conf/gns3_server.conf index aa54d57c8..7bb2aa6bd 100644 --- a/conf/gns3_server.conf +++ b/conf/gns3_server.conf @@ -103,6 +103,8 @@ enable_hardware_acceleration = True require_hardware_acceleration = False ; Allow unsafe additional command line options allow_unsafe_options = False +; Path to the OVMF firmware directory +ovmf_firmware_dir = "/usr/share/OVMF" [VMware] ; First vmnet interface of the range that can be managed by the GNS3 server diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index f97e5db31..11399a0ed 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -35,6 +35,7 @@ import time import json import psutil +from pathlib import Path from gns3server.utils import parse_version, shlex_quote from gns3server.utils.asyncio import subprocess_check_output, cancellable_wait_run_in_executor from .qemu_error import QemuError @@ -2057,15 +2058,17 @@ class QemuVM(BaseNode): options.extend(["-bios", self._bios_image.replace(",", ",,")]) elif self._uefi: - + ovmf_firmware_dir = self._manager.config.get_section_config("Qemu").get("ovmf_firmware_dir", "/usr/share/OVMF") + system_ovmf_firmware_dir = Path(ovmf_firmware_dir) + log.info("Using OVMF firmware directory: {}".format(system_ovmf_firmware_dir)) old_ovmf_vars_path = os.path.join(self.working_dir, "OVMF_VARS.fd") if os.path.exists(old_ovmf_vars_path): # the node has its own UEFI variables store already, we must also use the old UEFI firmware ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE.fd") else: - system_ovmf_firmware_path = "/usr/share/OVMF/OVMF_CODE_4M.fd" - if os.path.exists(system_ovmf_firmware_path): - ovmf_firmware_path = system_ovmf_firmware_path + system_ovmf_firmware_path = next(system_ovmf_firmware_dir.glob("OVMF_CODE_4M.fd", case_sensitive=False), None) + if system_ovmf_firmware_path: + ovmf_firmware_path = str(system_ovmf_firmware_path) else: # otherwise, get the UEFI firmware from the images directory ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE_4M.fd") @@ -2074,9 +2077,9 @@ class QemuVM(BaseNode): options.extend(["-drive", "if=pflash,format=raw,readonly,file={}".format(ovmf_firmware_path)]) # try to use the UEFI variables store from the system first - system_ovmf_vars_path = "/usr/share/OVMF/OVMF_VARS_4M.fd" - if os.path.exists(system_ovmf_vars_path): - ovmf_vars_path = system_ovmf_vars_path + system_ovmf_vars_path = next(system_ovmf_firmware_dir.glob("OVMF_VARS_4M.fd", case_sensitive=False), None) + if system_ovmf_vars_path: + ovmf_vars_path = str(system_ovmf_vars_path) else: # otherwise, get the UEFI variables store from the images directory ovmf_vars_path = self.manager.get_abs_image_path("OVMF_VARS_4M.fd") @@ -2092,6 +2095,10 @@ class QemuVM(BaseNode): except OSError as e: raise QemuError("Cannot copy OVMF_VARS_4M.fd file to the node working directory: {}".format(e)) options.extend(["-drive", "if=pflash,format=raw,file={}".format(ovmf_vars_node_path)]) + + # edk2 firmware requires a Random Number Generator (RNG) device in order to turn network adapters on + options.extend(["-object", "rng-random,filename=/dev/urandom,id=rng0 "]) + options.extend(["-device", "virtio-rng-pci,rng=rng0"]) return options def _linux_boot_options(self): From 61c7f4469dda6b755b93762e9efe95c63330d97c Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 13 Jul 2026 11:34:29 +0200 Subject: [PATCH 10/14] Search for system UEFI that is compatible with Python < 3.12 --- gns3server/compute/qemu/qemu_vm.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 11399a0ed..fa7757a62 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -2066,7 +2066,13 @@ class QemuVM(BaseNode): # the node has its own UEFI variables store already, we must also use the old UEFI firmware ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE.fd") else: - system_ovmf_firmware_path = next(system_ovmf_firmware_dir.glob("OVMF_CODE_4M.fd", case_sensitive=False), None) + # Use a manual case-insensitive search instead + try: + system_ovmf_firmware_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd") + if f.name.lower() == "ovmf_code_4m.fd"), None) + except (FileNotFoundError, StopIteration): + system_ovmf_firmware_path = None + if system_ovmf_firmware_path: ovmf_firmware_path = str(system_ovmf_firmware_path) else: @@ -2077,7 +2083,11 @@ class QemuVM(BaseNode): options.extend(["-drive", "if=pflash,format=raw,readonly,file={}".format(ovmf_firmware_path)]) # try to use the UEFI variables store from the system first - system_ovmf_vars_path = next(system_ovmf_firmware_dir.glob("OVMF_VARS_4M.fd", case_sensitive=False), None) + try: + system_ovmf_vars_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd") + if f.name.lower() == "ovmf_vars_4m.fd"), None) + except (FileNotFoundError, StopIteration): + system_ovmf_vars_path = None if system_ovmf_vars_path: ovmf_vars_path = str(system_ovmf_vars_path) else: From c528d2cd950b5eac072f5b499d145ab40c645db9 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 13 Jul 2026 18:19:52 +0200 Subject: [PATCH 11/14] fix(import): project import does not move images symlinks into place --- gns3server/controller/import_project.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py index a0f95bf00..56d4bb4f5 100644 --- a/gns3server/controller/import_project.py +++ b/gns3server/controller/import_project.py @@ -271,7 +271,7 @@ async def _upload_file(compute, project_id, file_path, path): async def _import_images(controller, images_path): """ - Copy images to the images directory or delete them if they already exists. + Copy images to the images directory or delete them if they already exist. """ image_dir = controller.images_path() @@ -279,11 +279,12 @@ async def _import_images(controller, images_path): for (dirpath, dirnames, filenames) in os.walk(root, followlinks=False): for filename in filenames: path = os.path.join(dirpath, filename) - if os.path.islink(path): - continue dst = os.path.join(image_dir, os.path.relpath(path, root)) os.makedirs(os.path.dirname(dst), exist_ok=True) - await wait_run_in_executor(shutil.move, path, dst) + if not os.path.exists(dst): + await wait_run_in_executor(shutil.move, path, dst) + if not os.path.islink(dst): + os.chmod(dst, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC) async def update_snapshots(snapshots_dir, project_path, project_name, project_id, reset_mac_addresses=True): From 1986bce538f4e12f43d8665ccb97dd10564035c0 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 15 Jul 2026 09:43:34 +0200 Subject: [PATCH 12/14] Only set IOU images to be executable when importing images --- gns3server/controller/import_project.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py index 56d4bb4f5..a1912208f 100644 --- a/gns3server/controller/import_project.py +++ b/gns3server/controller/import_project.py @@ -283,9 +283,15 @@ async def _import_images(controller, images_path): os.makedirs(os.path.dirname(dst), exist_ok=True) if not os.path.exists(dst): await wait_run_in_executor(shutil.move, path, dst) - if not os.path.islink(dst): - os.chmod(dst, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC) - + try: + with open(dst, "rb") as f: + # read the first 7 bytes of the file. + elf_header_start = f.read(7) + # IOU images must start with the ELF magic number, be 32-bit or 64-bit, little endian and have an ELF version of 1 + if elf_header_start == b'\x7fELF\x01\x01\x01' or elf_header_start == b'\x7fELF\x02\x01\x01': + os.chmod(dst, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC) + except OSError as e: + continue async def update_snapshots(snapshots_dir, project_path, project_name, project_id, reset_mac_addresses=True): """ From f088c55cfe8b4e1e5295f39b4dbce54edea78794 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 15 Jul 2026 18:32:12 +0200 Subject: [PATCH 13/14] Sync appliances --- gns3server/appliances/bird.gns3a | 47 ++- gns3server/appliances/fortimanager.gns3a | 490 +--------------------- gns3server/appliances/infix.gns3a | 28 ++ gns3server/appliances/ubuntu-docker.gns3a | 2 +- gns3server/appliances/westermo-weos.gns3a | 14 + 5 files changed, 81 insertions(+), 500 deletions(-) diff --git a/gns3server/appliances/bird.gns3a b/gns3server/appliances/bird.gns3a index b18e18e86..9ecf790cc 100644 --- a/gns3server/appliances/bird.gns3a +++ b/gns3server/appliances/bird.gns3a @@ -1,41 +1,56 @@ { - "appliance_id": "088df570-f637-46f5-8a68-85acde538e5e", + "appliance_id": "8fecbf89-5cd1-4aea-b735-5f36cf0efbb7", "name": "BIRD", "category": "router", "description": "The BIRD project aims to develop a fully functional dynamic IP routing daemon primarily targeted on (but not limited to) Linux, FreeBSD and other UNIX-like systems and distributed under the GNU General Public License.", "vendor_name": "CZ.NIC Labs", - "vendor_url": "http://bird.network.cz/", - "documentation_url": "http://bird.network.cz/?get_doc&f=bird.html", + "vendor_url": "https://bird.network.cz", + "documentation_url": "https://bird.network.cz/?get_doc&f=bird.html&v=20", "product_name": "BIRD internet routing daemon", "registry_version": 4, "status": "stable", - "maintainer": "GNS3 Team", - "maintainer_email": "developers@gns3.net", - "usage": "\n*** BIRD v1 is end-of-life ***\nPlease use the BIRD2 appliance.\n\nConfigure interfaces in /opt/bootlocal.sh, BIRD configuration is done in /usr/local/etc/bird", + "maintainer": "Bernhard Ehlers", + "maintainer_email": "dev-ehlers@mailbox.org", + "usage": "Username:\tgns3\nPassword:\tgns3\nTo become root, use \"sudo -s\".\n\nNetwork configuration:\nsudo nano /etc/network/interfaces\nsudo systemctl restart networking\n\nBIRD:\nRestart: sudo systemctl restart bird\nReconfigure: birdc configure", + "port_name_format": "eth{0}", "qemu": { - "adapter_type": "e1000", + "adapter_type": "virtio-net-pci", "adapters": 4, - "ram": 128, - "hda_disk_interface": "ide", + "ram": 512, + "hda_disk_interface": "scsi", "arch": "x86_64", "console_type": "telnet", "kvm": "allow" }, "images": [ { - "filename": "bird-tinycore64-1.5.0.img", - "version": "1.5.0", - "md5sum": "08d50ba2b1b262e2e03e4babf90abf69", - "filesize": 22413312, + "filename": "bird2-debian-2.14.qcow2", + "version": "2.14", + "md5sum": "029cf1756201ee79497c169502b08b88", + "filesize": 303717376, "download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", - "direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird-tinycore64-1.5.0.img" + "direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.14.qcow2" + }, + { + "filename": "bird2-debian-2.0.12.qcow2", + "version": "2.0.12", + "md5sum": "435218a2e90cba921cc7fde1d64a9419", + "filesize": 287965184, + "download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", + "direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.0.12.qcow2" } ], "versions": [ { - "name": "1.5.0", + "name": "2.14", "images": { - "hda_disk_image": "bird-tinycore64-1.5.0.img" + "hda_disk_image": "bird2-debian-2.14.qcow2" + } + }, + { + "name": "2.0.12", + "images": { + "hda_disk_image": "bird2-debian-2.0.12.qcow2" } } ] diff --git a/gns3server/appliances/fortimanager.gns3a b/gns3server/appliances/fortimanager.gns3a index b5de0248e..ae51af861 100644 --- a/gns3server/appliances/fortimanager.gns3a +++ b/gns3server/appliances/fortimanager.gns3a @@ -51,250 +51,12 @@ "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" }, { - "filename": "FMG_VM64_KVM-v7.4.3-build2487-FORTINET.out.kvm.qcow2", - "version": "7.4.3", - "md5sum": "b01d9f86aa27c538407d518df1326863", - "filesize": 346107904, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.4.2-build2397-FORTINET.out.kvm.qcow2", - "version": "7.4.2", - "md5sum": "36371fbf06210ded57c00b2ff290f2c5", - "filesize": 322514944, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.4.1-build2308-FORTINET.out.kvm.qcow2", - "version": "7.4.1", - "md5sum": "e542cc8f2d8f46e9c32b783bf31bef39", - "filesize": 309387264, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.2.5-build1574-FORTINET.out.kvm.qcow2", - "version": "7.2.5", - "md5sum": "754326845096afd909ec45d98f8d5a83", - "filesize": 278401024, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.2.4-build1460-FORTINET.out.kvm.qcow2", - "version": "7.2.4", - "md5sum": "98fa9830d9ecb5911a703d03b80026b6", - "filesize": 261992448, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.2.2-build1334-FORTINET.out.kvm.qcow2", - "version": "7.2.2", - "md5sum": "2ff1298257321cd485d2cad91d6ce510", - "filesize": 246083584, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.2.1-build1215-FORTINET.out.kvm.qcow2", - "version": "7.2.1", - "md5sum": "1a3eeff1204fa8f4243773f7521e12b5", - "filesize": 242814976, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.0.12-build0623-FORTINET.out.kvm.qcow2", - "version": "7.0.12", - "md5sum": "5b6f6a2b8bc00e56337aa7023a9025cf", - "filesize": 249520128, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.0.11-build0595-FORTINET.out.kvm.qcow2", - "version": "7.0.11", - "md5sum": "7b166222136e26190159f37cccbaab6e", - "filesize": 249360384, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.0.9-build0489-FORTINET.out.kvm.qcow2", - "version": "7.0.9", - "md5sum": "dbeb6a79b6e421000573dbbbdb50b8b5", - "filesize": 247955456, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.0.6-build0372-FORTINET.out.kvm.qcow2", - "version": "7.0.6", - "md5sum": "dfa4df9e976ed87e73cb9601a8a70323", - "filesize": 239190016, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v7.0.5-build0365-FORTINET.out.kvm.qcow2", - "version": "7.0.5", - "md5sum": "e8b9c992784cea766b52a427a5fe0279", - "filesize": 237535232, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6.4.14-build2660-FORTINET.out.kvm.qcow2", - "version": "6.4.14", - "md5sum": "0fe56e363b166c07b710bde795e36049", - "filesize": 219430912, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6.4.12-build2610-FORTINET.out.kvm.qcow2", - "version": "6.4.12", - "md5sum": "36c0dc531d921e5f1e1e09b030f7c813", - "filesize": 219455488, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build2288-FORTINET.out.kvm.qcow2", - "version": "6.4.5", - "md5sum": "bd2791984b03f55a6825297e83c6576a", - "filesize": 117014528, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build2253-FORTINET.out.kvm.qcow2", - "version": "6.4.4", - "md5sum": "3554a47fde2dc91d17eec16fd0dc10a3", - "filesize": 116621312, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build1183-FORTINET.out.kvm.qcow2", - "version": "6.2.2", - "md5sum": "f5051a8fe49d916bb554b9bae32a1eb4", - "filesize": 139145216, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build1050-FORTINET.out.kvm.qcow2", - "version": "6.2.0", - "md5sum": "c19d2527f91ad1bbafbde5bf08487867", - "filesize": 126894080, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build0349-FORTINET.out.kvm.qcow2", - "version": "6.0.6", - "md5sum": "d03f024c948ba6e2bb9e66c11ca8f34c", - "filesize": 112553984, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build0255-FORTINET.out.kvm.qcow2", - "version": "6.0.3", - "md5sum": "5f34d52d9289b0be2a4c04943446ea39", - "filesize": 115703808, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build0205-FORTINET.out.kvm.qcow2", - "version": "6.0.2", - "md5sum": "8f748649c537d9b5466b24c5b4e62017", - "filesize": 116981760, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v6-build0092-FORTINET.out.kvm.qcow2", - "version": "6.0.0", - "md5sum": "73bfe1bc70124521a524d857646b9c2e", - "filesize": 119066624, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1631-FORTINET.out.kvm.qcow2", - "version": "5.6.2", - "md5sum": "c81cc247e8eb03249b475fe0e847653e", - "filesize": 106946560, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1619-FORTINET.out.kvm.qcow2", - "version": "5.6.1", - "md5sum": "8cc553842564d232af295d6a0c784c1f", - "filesize": 106831872, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1557-FORTINET.out.kvm.qcow2", - "version": "5.6.0", - "md5sum": "f8bd600796f894f4ca1ea2d6b4066d3d", - "filesize": 108363776, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1225-FORTINET.out.kvm.qcow2", - "version": "5.4.4", - "md5sum": "53bc6e320fe7bde5d2b636bde95a910c", - "filesize": 89911296, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1187-FORTINET.out.kvm.qcow2", - "version": "5.4.3", - "md5sum": "53602c776d215d98e32163a10804fc49", - "filesize": 87425024, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1151-FORTINET.out.kvm.qcow2", - "version": "5.4.2", - "md5sum": "8e131ad40009c740f3efdee6dc3a0ac3", - "filesize": 86437888, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1082-FORTINET.out.kvm.qcow2", - "version": "5.4.1", - "md5sum": "fc1815410f3f0536e2e3a9c1c5c07f41", - "filesize": 83124224, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build1019-FORTINET.out.kvm.qcow2", - "version": "5.4.0", - "md5sum": "1cfb22671cb372d8bf3e47b9c3c55ded", - "filesize": 77541376, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build0786-FORTINET.out.kvm.qcow2", - "version": "5.2.10", - "md5sum": "377fe38bf07bc2435608e5b65f780f07", - "filesize": 64962560, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build0780-FORTINET.out.kvm.qcow2", - "version": "5.2.9", - "md5sum": "04268e779d3d5e6c928c6fd638423c52", - "filesize": 65007616, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build0777-FORTINET.out.kvm.qcow2", - "version": "5.2.8", - "md5sum": "6dbf148ace9bf309ad383757afd75fad", - "filesize": 65011712, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "FMG_VM64_KVM-v5-build0757-FORTINET.out.kvm.qcow2", - "version": "5.2.7", - "md5sum": "d37dbaa49d7522324681eeba19f7699b", - "filesize": 65056768, - "download_url": "https://support.fortinet.com/Download/FirmwareImages.aspx" - }, - { - "filename": "empty30G.qcow2", + "filename": "empty500G.qcow2", "version": "1.0", - "md5sum": "3411a599e822f2ac6be560a26405821a", - "filesize": 197120, + "md5sum": "658c825441b9b3080ba00f9eec002eaa", + "filesize": 204608, "download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/", - "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty30G.qcow2/download" + "direct_download_url": "https://sourceforge.net/projects/gns-3/files/Empty%20Qemu%20disk/empty500G.qcow2/download" } ], "versions": [ @@ -302,259 +64,21 @@ "name": "7.4.6", "images": { "hda_disk_image": "FMG_VM64_KVM-v7.4.6.M-build2588-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" + "hdb_disk_image": "empty500G.qcow2" } }, { "name": "7.4.5", "images": { "hda_disk_image": "FMG_VM64_KVM-v7.4.5.M-build2553-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" + "hdb_disk_image": "empty500G.qcow2" } }, { "name": "7.4.4", "images": { "hda_disk_image": "FMG_VM64_KVM-v7.4.4.F-build2550-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.4.3", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.4.3-build2487-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.4.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.4.2-build2397-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.4.1", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.4.1-build2308-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.2.5", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.2.5-build1574-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.2.4", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.2.4-build1460-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.2.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.2.2-build1334-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.2.1", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.2.1-build1215-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.0.12", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.0.12-build0623-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.0.11", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.0.11-build0595-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.0.9", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.0.9-build0489-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.0.6", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.0.6-build0372-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "7.0.5", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v7.0.5-build0365-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.4.14", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6.4.14-build2660-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.4.12", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6.4.12-build2610-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.4.5", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build2288-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.4.4", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build2253-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.2.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build1183-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.2.0", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build1050-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.0.6", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build0349-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.0.3", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build0255-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.0.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build0205-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "6.0.0", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v6-build0092-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.6.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1631-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.6.1", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1619-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.6.0", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1557-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.4.4", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1225-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.4.3", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1187-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.4.2", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1151-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.4.1", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1082-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.4.0", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build1019-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.2.10", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build0786-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.2.9", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build0780-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.2.8", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build0777-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" - } - }, - { - "name": "5.2.7", - "images": { - "hda_disk_image": "FMG_VM64_KVM-v5-build0757-FORTINET.out.kvm.qcow2", - "hdb_disk_image": "empty30G.qcow2" + "hdb_disk_image": "empty500G.qcow2" } } ] diff --git a/gns3server/appliances/infix.gns3a b/gns3server/appliances/infix.gns3a index 736863ba7..481cba0fd 100644 --- a/gns3server/appliances/infix.gns3a +++ b/gns3server/appliances/infix.gns3a @@ -132,9 +132,37 @@ "md5sum": "24cd1006734993dab338e5c75f80b875", "version": "26.03.0", "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.03.0/infix-x86_64-v26.03.0.qcow2" + }, + { + "filename": "infix-x86_64-v26.05.0.qcow2", + "filesize": 330039296, + "md5sum": "60f7c36c389c33ab108acc021f41ccd5", + "version": "26.05.0", + "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.05.0/infix-x86_64-v26.05.0.qcow2" + }, + { + "filename": "infix-x86_64-v26.06.0.qcow2", + "filesize": 363593728, + "md5sum": "79ca8bd8534bbaa1af0ab874a49d6f4c", + "version": "26.06.0", + "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.06.0/infix-x86_64-v26.06.0.qcow2" } ], "versions": [ + { + "name": "26.06.0", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "infix-x86_64-v26.06.0.qcow2" + } + }, + { + "name": "26.05.0", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "infix-x86_64-v26.05.0.qcow2" + } + }, { "name": "26.03.0", "images": { diff --git a/gns3server/appliances/ubuntu-docker.gns3a b/gns3server/appliances/ubuntu-docker.gns3a index 9cde03e79..c69a1405d 100644 --- a/gns3server/appliances/ubuntu-docker.gns3a +++ b/gns3server/appliances/ubuntu-docker.gns3a @@ -14,7 +14,7 @@ "symbol": "linux_guest.svg", "docker": { "adapters": 1, - "image": "gns3/ubuntu:noble", + "image": "gns3/ubuntu:resolute", "console_type": "telnet" } } diff --git a/gns3server/appliances/westermo-weos.gns3a b/gns3server/appliances/westermo-weos.gns3a index 6ebce85d7..dbcf3a7eb 100644 --- a/gns3server/appliances/westermo-weos.gns3a +++ b/gns3server/appliances/westermo-weos.gns3a @@ -38,6 +38,13 @@ "filesize": 17825792, "direct_download_url": "https://dropzone.westermo.com/file.aspx?id=e6a7676d-85a7-4374-8961-c68aacb74921" }, + { + "filename": "WeOS-zero-5.29.0.disk", + "version": "5.29.0", + "filesize": 81788928, + "md5sum": "f464de7b2b424f4a8ad7c650108fee3d", + "direct_download_url": "https://dropzone.westermo.com/file.aspx?id=52655074-0fab-4119-ba01-b68200a733ab" + }, { "filename": "WeOS-zero-5.28.0.disk", "version": "5.28.0", @@ -54,6 +61,13 @@ } ], "versions": [ + { + "name": "5.29.0", + "images": { + "hda_disk_image": "WeOS-zero-5.29.0.disk", + "hdb_disk_image": "Config-zero-1.0.0.disk" + } + }, { "name": "5.28.0", "images": { From 803cc1741035cd8ae83cfde169baa17356d6714e Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 15 Jul 2026 18:36:26 +0200 Subject: [PATCH 14/14] Release v2.2.60 --- CHANGELOG | 14 ++++++++ gns3server/appliances/bird2.gns3a | 57 ------------------------------- gns3server/crash_report.py | 2 +- gns3server/version.py | 4 +-- 4 files changed, 17 insertions(+), 60 deletions(-) delete mode 100644 gns3server/appliances/bird2.gns3a diff --git a/CHANGELOG b/CHANGELOG index 42f09ab17..02d3d14f9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,19 @@ # Change Log +## 2.2.60 15/07/2026 + +* Sync appliances +* Only set IOU images to be executable when importing images +* fix(import): project import does not move images symlinks into place +* Search for system UEFI that is compatible with Python < 3.12 +* Add OVMF firmware directory configuration +* Automatically add a Random Number Generator (RNG) device when using uefi option is enabled +* fix(docker): handle container name conflict automatically +* fix: gns3-server crashes on startup if "Open this project in the background" is active but there is a problem with that project +* Handle HTTPNotFound exception when retrieving compute status +* Fix: Check compute connectivity before open() during project deletion +* API endpoints to manage base configuration files for templates + ## 2.2.59 08/05/2026 * Sync appliances diff --git a/gns3server/appliances/bird2.gns3a b/gns3server/appliances/bird2.gns3a deleted file mode 100644 index 670d56a83..000000000 --- a/gns3server/appliances/bird2.gns3a +++ /dev/null @@ -1,57 +0,0 @@ -{ - "appliance_id": "8fecbf89-5cd1-4aea-b735-5f36cf0efbb7", - "name": "BIRD2", - "category": "router", - "description": "The BIRD project aims to develop a fully functional dynamic IP routing daemon primarily targeted on (but not limited to) Linux, FreeBSD and other UNIX-like systems and distributed under the GNU General Public License.", - "vendor_name": "CZ.NIC Labs", - "vendor_url": "https://bird.network.cz", - "documentation_url": "https://bird.network.cz/?get_doc&f=bird.html&v=20", - "product_name": "BIRD internet routing daemon", - "registry_version": 4, - "status": "stable", - "maintainer": "Bernhard Ehlers", - "maintainer_email": "dev-ehlers@mailbox.org", - "usage": "Username:\tgns3\nPassword:\tgns3\nTo become root, use \"sudo -s\".\n\nNetwork configuration:\nsudo nano /etc/network/interfaces\nsudo systemctl restart networking\n\nBIRD:\nRestart: sudo systemctl restart bird\nReconfigure: birdc configure", - "port_name_format": "eth{0}", - "qemu": { - "adapter_type": "virtio-net-pci", - "adapters": 4, - "ram": 512, - "hda_disk_interface": "scsi", - "arch": "x86_64", - "console_type": "telnet", - "kvm": "allow" - }, - "images": [ - { - "filename": "bird2-debian-2.14.qcow2", - "version": "2.14", - "md5sum": "029cf1756201ee79497c169502b08b88", - "filesize": 303717376, - "download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", - "direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.14.qcow2" - }, - { - "filename": "bird2-debian-2.0.12.qcow2", - "version": "2.0.12", - "md5sum": "435218a2e90cba921cc7fde1d64a9419", - "filesize": 287965184, - "download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/", - "direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.0.12.qcow2" - } - ], - "versions": [ - { - "name": "2.14", - "images": { - "hda_disk_image": "bird2-debian-2.14.qcow2" - } - }, - { - "name": "2.0.12", - "images": { - "hda_disk_image": "bird2-debian-2.0.12.qcow2" - } - } - ] -} diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 4dce82c80..5b3250a0a 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -57,7 +57,7 @@ class CrashReport: Report crash to a third party service """ - DSN = "https://646d66c1f0b65d59af9c4577a68f3f87@o19455.ingest.us.sentry.io/38482" + DSN = "https://deee946d000cb5548acebb1cd7c1a1d3@o19455.ingest.us.sentry.io/38482" _instance = None def __init__(self): diff --git a/gns3server/version.py b/gns3server/version.py index 8d4ff1a3f..a384e3124 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,8 +23,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.60.dev1" -__version_info__ = (2, 2, 60, 99) +__version__ = "2.2.60" +__version_info__ = (2, 2, 60, 0) if "dev" in __version__: try: