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