From 9106ac45228afc9e93ee2ebc9295309e6ec5e5b7 Mon Sep 17 00:00:00 2001 From: Volobue Date: Mon, 18 May 2026 21:49:01 +0300 Subject: [PATCH 1/2] 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 2/2] 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