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
+
+
+
+ | Name |
+ Type |
+ Description |
+
+
+
+ | filename |
+ string |
+ Name 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
+
+
+
+ | Name |
+ Type |
+ Description |
+
+
+
+ | filename |
+ string |
+ Name 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
+
+
+
+ | Name |
+ Mandatory |
+ Type |
+ Description |
+
+
+ | content | ✔ | string | New file content |
+
+
+
+Output
+*******
+
+.. raw:: html
+
+
+
+ | Name |
+ Mandatory |
+ Type |
+ Description |
+
+
+ | template_id | ✔ | string | Template UUID |
+ | filename | ✔ | string | Base configuration filename |
+ | content | ✔ | string | Updated file content |
+
+
+
+Sample session
+***************
+
+.. literalinclude:: ../../../examples/controller_put_templateidbaseconfig.txt
\ No newline at end of file
diff --git a/gns3server/api/routes/controller/templates.py b/gns3server/api/routes/controller/templates.py
index a8d0be8a3..d991ac086 100644
--- a/gns3server/api/routes/controller/templates.py
+++ b/gns3server/api/routes/controller/templates.py
@@ -35,7 +35,7 @@ from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.services.templates import TemplatesService
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.images import ImagesRepository
-from gns3server.controller.controller_error import ControllerError
+from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError
from gns3server.utils.images import get_builtin_disks
from .dependencies.authentication import get_current_active_user
@@ -230,3 +230,57 @@ async def duplicate_template(
template = await TemplatesService(templates_repo).duplicate_template(template_id)
return template
+
+@router.get("/{template_id}/base-config/{filename}")
+async def get_base_config(
+ template_id: UUID,
+ filename: str,
+ templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
+):
+ service = TemplatesService(templates_repo)
+
+ await service.get_template(template_id)
+
+ content = service.get_file(str(template_id), filename)
+
+ return {
+ "template_id": str(template_id),
+ "filename": os.path.basename(filename),
+ "content": content
+ }
+
+
+@router.put("/{template_id}/base-config/{filename}")
+async def update_base_config(
+ template_id: UUID,
+ filename: str,
+ body: dict,
+ templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
+):
+ if not body or "content" not in body:
+ raise ControllerBadRequestError("Missing 'content' field")
+
+ service = TemplatesService(templates_repo)
+
+ await service.get_template(template_id)
+
+ service.update_file(str(template_id), filename, body["content"])
+
+ return {
+ "template_id": str(template_id),
+ "filename": os.path.basename(filename),
+ "content": body["content"]
+ }
+
+
+@router.get("/{template_id}/base-configs")
+async def list_base_configs(
+ template_id: UUID,
+ templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
+):
+ service = TemplatesService(templates_repo)
+
+ await service.get_template(template_id)
+
+ return service.list_files(str(template_id))
+
diff --git a/gns3server/db/repositories/templates.py b/gns3server/db/repositories/templates.py
index ec8215af8..ed835a37a 100644
--- a/gns3server/db/repositories/templates.py
+++ b/gns3server/db/repositories/templates.py
@@ -49,6 +49,9 @@ class TemplatesRepository(BaseRepository):
super().__init__(db_session)
+ def configs_path(self) -> str:
+ return os.path.join(os.getcwd(), "configs")
+
async def get_template(self, template_id: UUID) -> Union[None, models.Template]:
query = select(models.Template).\
diff --git a/gns3server/services/templates.py b/gns3server/services/templates.py
index 92b78633e..5284e2324 100644
--- a/gns3server/services/templates.py
+++ b/gns3server/services/templates.py
@@ -174,6 +174,9 @@ class TemplatesService:
if builtin_template["template_id"] == template_id:
return jsonable_encoder(builtin_template)
+ def _base_path(self):
+ return self._templates_repo.configs_path()
+
async def get_templates(self) -> List[dict]:
templates = []
@@ -336,3 +339,45 @@ class TemplatesService:
self._controller.notification.controller_emit("template.deleted", {"template_id": str(template_id)})
else:
raise ControllerNotFoundError(f"Template '{template_id}' not found")
+
+ def _template_path(self, template_id: str) -> str:
+ return os.path.join(self._base_path(), str(template_id))
+
+ def list_files(self, template_id: str):
+ path = self._template_path(template_id)
+
+ if not os.path.exists(path):
+ return []
+
+ return [
+ {"filename": f}
+ for f in sorted(os.listdir(path))
+ if os.path.isfile(os.path.join(path, f))
+ ]
+
+ def get_file(self, template_id: str, filename: str):
+ safe_filename = os.path.basename(filename)
+ path = os.path.join(self._template_path(template_id), safe_filename)
+
+ if not os.path.isfile(path):
+ raise ControllerNotFoundError(f"File '{safe_filename}' not found")
+
+ try:
+ with open(path, encoding="utf-8", errors="ignore") as f:
+ return f.read()
+ except OSError as e:
+ raise ControllerError(str(e))
+
+ def update_file(self, template_id: str, filename: str, content: str):
+ safe_filename = os.path.basename(filename)
+
+ dir_path = self._template_path(template_id)
+ path = os.path.join(dir_path, safe_filename)
+
+ os.makedirs(dir_path, exist_ok=True)
+
+ try:
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(content)
+ except OSError as e:
+ raise ControllerError(str(e))
\ No newline at end of file
diff --git a/tests/api/routes/controller/test_templates.py b/tests/api/routes/controller/test_templates.py
index 3ae7e9383..0c61b3ab0 100644
--- a/tests/api/routes/controller/test_templates.py
+++ b/tests/api/routes/controller/test_templates.py
@@ -239,6 +239,80 @@ class TestTemplateRoutes:
# mock.assert_called_with(id, x=42, y=12, compute_id=None)
# assert response.status_code == status.HTTP_201_CREATED
+ async def test_get_base_config(self, app: FastAPI, client: AsyncClient):
+
+ create_resp = await client.post(app.url_path_for("create_template"), json={
+ "name": "TEST",
+ "compute_id": "local",
+ "template_type": "vpcs"
+ })
+
+ assert create_resp.status_code == 201
+ template_id = create_resp.json()["template_id"]
+
+ #создаём файл перед чтением
+ await client.put(
+ app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"),
+ json={"content": "hello"}
+ )
+
+ response = await client.get(
+ app.url_path_for(
+ "get_base_config",
+ template_id=template_id,
+ filename="test.txt"
+ )
+ )
+
+ assert response.status_code == 200
+ assert response.json()["content"] == "hello"
+
+ async def test_update_base_config(self, app: FastAPI, client: AsyncClient):
+ template_id = str(uuid.uuid4())
+ filename = "test.txt"
+
+ await client.post(app.url_path_for("create_template"), json={
+ "template_id": template_id,
+ "name": "TEST",
+ "compute_id": "local",
+ "template_type": "vpcs"
+ })
+
+ payload = {"content": "hello world"}
+
+ response = await client.put(
+ app.url_path_for("update_base_config", template_id=template_id, filename=filename),
+ json=payload
+ )
+
+ assert response.status_code == 200
+ assert response.json()["content"] == "hello world"
+
+ async def test_update_base_config_missing_content(self, app: FastAPI, client: AsyncClient):
+ template_id = str(uuid.uuid4())
+ filename = "test.txt"
+
+ await client.post(app.url_path_for("create_template"), json={
+ "template_id": template_id,
+ "name": "TEST",
+ "compute_id": "local",
+ "template_type": "vpcs"
+ })
+
+ response = await client.put(
+ app.url_path_for("update_base_config", template_id=template_id, filename=filename),
+ json={}
+ )
+
+ assert response.status_code in (400, 422)
+
+ async def test_base_config_template_not_found(self, app: FastAPI, client: AsyncClient):
+ response = await client.get(
+ app.url_path_for("get_base_config", template_id=str(uuid.uuid4()), filename="x.txt")
+ )
+
+ assert response.status_code == 404
+
class TestDuplicateTemplates: