api endpoint implemented

This commit is contained in:
Volobue 2026-05-18 21:49:01 +03:00
parent 0fdeb34f1a
commit b74ebafb8f
9 changed files with 383 additions and 1 deletions

View File

@ -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"
}

View File

@ -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"
}
]

View File

@ -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"
}

View File

@ -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
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>filename</td>
<td>string</td>
<td>Name of the base configuration file</td>
</tr>
</table>
Sample session
***************
.. literalinclude:: ../../../examples/controller_get_templatesbaseconfigs.txt

View File

@ -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
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>filename</td>
<td>string</td>
<td>Name of the base configuration file</td>
</tr>
</table>
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
<table>
<tr>
<th>Name</th>
<th>Mandatory</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr><td>content</td><td>&#10004;</td><td>string</td><td>New file content</td></tr>
</table>
Output
*******
.. raw:: html
<table>
<tr>
<th>Name</th>
<th>Mandatory</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr><td>template_id</td><td>&#10004;</td><td>string</td><td>Template UUID</td></tr>
<tr><td>filename</td><td>&#10004;</td><td>string</td><td>Base configuration filename</td></tr>
<tr><td>content</td><td>&#10004;</td><td>string</td><td>Updated file content</td></tr>
</table>
Sample session
***************
.. literalinclude:: ../../../examples/controller_put_templateidbaseconfig.txt

View File

@ -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))

View File

@ -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).\

View File

@ -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))

View File

@ -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: