mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 20:40:13 +03:00
Fixes #1658 Add validation in the update_template method to check if a template with the same name already exists before updating. This prevents users from creating duplicate template names by editing existing templates. The check excludes the current template being edited to allow updating other properties without changing the name.
339 lines
14 KiB
Python
339 lines
14 KiB
Python
#
|
|
# Copyright (C) 2021 GNS3 Technologies Inc.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import os
|
|
import uuid
|
|
import pydantic
|
|
|
|
from uuid import UUID
|
|
from fastapi.encoders import jsonable_encoder
|
|
from typing import List
|
|
|
|
from gns3server import schemas
|
|
from gns3server.config import Config
|
|
import gns3server.db.models as models
|
|
from gns3server.db.repositories.templates import TemplatesRepository
|
|
from gns3server.controller.controller_error import (
|
|
ControllerError,
|
|
ControllerBadRequestError,
|
|
ControllerNotFoundError,
|
|
ControllerForbiddenError,
|
|
)
|
|
|
|
TEMPLATE_TYPE_TO_SCHEMA = {
|
|
"cloud": schemas.CloudTemplate,
|
|
"ethernet_hub": schemas.EthernetHubTemplate,
|
|
"ethernet_switch": schemas.EthernetSwitchTemplate,
|
|
"docker": schemas.DockerTemplate,
|
|
"dynamips": schemas.DynamipsTemplate,
|
|
"vpcs": schemas.VPCSTemplate,
|
|
"virtualbox": schemas.VirtualBoxTemplate,
|
|
"vmware": schemas.VMwareTemplate,
|
|
"iou": schemas.IOUTemplate,
|
|
"qemu": schemas.QemuTemplate,
|
|
}
|
|
|
|
TEMPLATE_TYPE_TO_UPDATE_SCHEMA = {
|
|
"cloud": schemas.CloudTemplateUpdate,
|
|
"ethernet_hub": schemas.EthernetHubTemplateUpdate,
|
|
"ethernet_switch": schemas.EthernetSwitchTemplateUpdate,
|
|
"docker": schemas.DockerTemplateUpdate,
|
|
"vpcs": schemas.VPCSTemplateUpdate,
|
|
"virtualbox": schemas.VirtualBoxTemplateUpdate,
|
|
"vmware": schemas.VMwareTemplateUpdate,
|
|
"iou": schemas.IOUTemplateUpdate,
|
|
"qemu": schemas.QemuTemplateUpdate,
|
|
}
|
|
|
|
DYNAMIPS_PLATFORM_TO_SCHEMA = {
|
|
"c7200": schemas.C7200DynamipsTemplate,
|
|
"c3745": schemas.C3745DynamipsTemplate,
|
|
"c3725": schemas.C3725DynamipsTemplate,
|
|
"c3600": schemas.C3600DynamipsTemplate,
|
|
"c2691": schemas.C2691DynamipsTemplate,
|
|
"c2600": schemas.C2600DynamipsTemplate,
|
|
"c1700": schemas.C1700DynamipsTemplate,
|
|
}
|
|
|
|
DYNAMIPS_PLATFORM_TO_UPDATE_SCHEMA = {
|
|
"c7200": schemas.C7200DynamipsTemplateUpdate,
|
|
"c3745": schemas.C3745DynamipsTemplateUpdate,
|
|
"c3725": schemas.C3725DynamipsTemplateUpdate,
|
|
"c3600": schemas.C3600DynamipsTemplateUpdate,
|
|
"c2691": schemas.C2691DynamipsTemplateUpdate,
|
|
"c2600": schemas.C2600DynamipsTemplateUpdate,
|
|
"c1700": schemas.C1700DynamipsTemplateUpdate,
|
|
}
|
|
|
|
# built-in templates have their compute_id set to None to tell clients to select a compute
|
|
BUILTIN_TEMPLATES = [
|
|
{
|
|
"template_id": uuid.uuid5(uuid.NAMESPACE_X500, "cloud"),
|
|
"template_type": "cloud",
|
|
"name": "Cloud",
|
|
"default_name_format": "Cloud{0}",
|
|
"category": "guest",
|
|
"symbol": "cloud",
|
|
"compute_id": None,
|
|
"builtin": True,
|
|
},
|
|
{
|
|
"template_id": uuid.uuid5(uuid.NAMESPACE_X500, "nat"),
|
|
"template_type": "nat",
|
|
"name": "NAT",
|
|
"default_name_format": "NAT{0}",
|
|
"category": "guest",
|
|
"symbol": "nat",
|
|
"compute_id": None,
|
|
"builtin": True,
|
|
},
|
|
{
|
|
"template_id": uuid.uuid5(uuid.NAMESPACE_X500, "vpcs"),
|
|
"template_type": "vpcs",
|
|
"name": "VPCS",
|
|
"default_name_format": "PC{0}",
|
|
"category": "guest",
|
|
"symbol": "vpcs_guest",
|
|
"base_script_file": "vpcs_base_config.txt",
|
|
"compute_id": None,
|
|
"builtin": True,
|
|
"tags": ["device_type:gns3_vpcs_telnet"],
|
|
},
|
|
{
|
|
"template_id": uuid.uuid5(uuid.NAMESPACE_X500, "ethernet_switch"),
|
|
"template_type": "ethernet_switch",
|
|
"name": "Ethernet switch",
|
|
"console_type": "none",
|
|
"default_name_format": "Switch{0}",
|
|
"category": "switch",
|
|
"symbol": "ethernet_switch",
|
|
"compute_id": None,
|
|
"builtin": True,
|
|
},
|
|
{
|
|
"template_id": uuid.uuid5(uuid.NAMESPACE_X500, "ethernet_hub"),
|
|
"template_type": "ethernet_hub",
|
|
"name": "Ethernet hub",
|
|
"default_name_format": "Hub{0}",
|
|
"category": "switch",
|
|
"symbol": "hub",
|
|
"compute_id": None,
|
|
"builtin": True,
|
|
},
|
|
{
|
|
"template_id": uuid.uuid5(uuid.NAMESPACE_X500, "frame_relay_switch"),
|
|
"template_type": "frame_relay_switch",
|
|
"name": "Frame Relay switch",
|
|
"default_name_format": "FRSW{0}",
|
|
"category": "switch",
|
|
"symbol": "frame_relay_switch",
|
|
"compute_id": None,
|
|
"builtin": True,
|
|
},
|
|
{
|
|
"template_id": uuid.uuid5(uuid.NAMESPACE_X500, "atm_switch"),
|
|
"template_type": "atm_switch",
|
|
"name": "ATM switch",
|
|
"default_name_format": "ATMSW{0}",
|
|
"category": "switch",
|
|
"symbol": "atm_switch",
|
|
"compute_id": None,
|
|
"builtin": True,
|
|
},
|
|
]
|
|
|
|
|
|
class TemplatesService:
|
|
|
|
def __init__(self, templates_repo: TemplatesRepository):
|
|
|
|
self._templates_repo = templates_repo
|
|
from gns3server.controller import Controller
|
|
self._controller = Controller.instance()
|
|
|
|
# resolve built-in template symbols
|
|
for builtin_template in BUILTIN_TEMPLATES:
|
|
builtin_template["symbol"] = self._controller.symbols.resolve_symbol(builtin_template["symbol"])
|
|
|
|
def get_builtin_template(self, template_id: UUID) -> dict:
|
|
|
|
for builtin_template in BUILTIN_TEMPLATES:
|
|
if builtin_template["template_id"] == template_id:
|
|
return jsonable_encoder(builtin_template)
|
|
|
|
async def get_templates(self) -> List[dict]:
|
|
|
|
templates = []
|
|
db_templates = await self._templates_repo.get_templates()
|
|
for db_template in db_templates:
|
|
templates.append(db_template.asjson())
|
|
if Config.instance().settings.Server.enable_builtin_templates:
|
|
for builtin_template in BUILTIN_TEMPLATES:
|
|
templates.append(jsonable_encoder(builtin_template))
|
|
return templates
|
|
|
|
async def _find_image(self, image_path: str):
|
|
|
|
image = await self._templates_repo.get_image(image_path)
|
|
if not image:
|
|
raise ControllerNotFoundError(f"Image '{image_path}' could not be found in the controller database")
|
|
if not os.path.exists(image.path):
|
|
raise ControllerNotFoundError(f"Image '{image.path}' could not be found on disk")
|
|
return image
|
|
|
|
async def _find_images(self, template_type: str, settings: dict) -> List[models.Image]:
|
|
|
|
images_to_add_to_template = []
|
|
if template_type == "dynamips":
|
|
if settings.get("image"):
|
|
image = await self._find_image(settings["image"])
|
|
if image.image_type != "ios":
|
|
raise ControllerBadRequestError(
|
|
f"Image '{image.filename}' type is not 'ios' but '{image.image_type}'"
|
|
)
|
|
images_to_add_to_template.append(image)
|
|
elif template_type == "iou":
|
|
if settings.get("path"):
|
|
image = await self._find_image(settings["path"])
|
|
if image.image_type != "iou":
|
|
raise ControllerBadRequestError(
|
|
f"Image '{image.filename}' type is not 'iou' but '{image.image_type}'"
|
|
)
|
|
images_to_add_to_template.append(image)
|
|
elif template_type == "qemu":
|
|
for key, value in settings.items():
|
|
if key.endswith("_image") and value:
|
|
image = await self._find_image(value)
|
|
if image.image_type != "qemu":
|
|
raise ControllerBadRequestError(
|
|
f"Image '{image.filename}' type is not 'qemu' but '{image.image_type}'"
|
|
)
|
|
if image not in images_to_add_to_template:
|
|
images_to_add_to_template.append(image)
|
|
return images_to_add_to_template
|
|
|
|
async def create_template(self, template_create: schemas.TemplateCreate) -> dict:
|
|
|
|
if await self._templates_repo.get_template_by_name_and_version(template_create.name, template_create.version):
|
|
if template_create.version:
|
|
raise ControllerError(f"A template with name '{template_create.name}' and "
|
|
f"version {template_create.version} already exists")
|
|
else:
|
|
raise ControllerError(f"A template with name '{template_create.name}' already exists")
|
|
|
|
try:
|
|
# get the default template settings
|
|
create_settings = jsonable_encoder(template_create, exclude_unset=True)
|
|
template_schema = TEMPLATE_TYPE_TO_SCHEMA[template_create.template_type]
|
|
template_settings = template_schema.model_validate(create_settings).model_dump()
|
|
if template_create.template_type == "dynamips":
|
|
# special case for Dynamips to cover all platform types that contain specific settings
|
|
dynamips_template_schema = DYNAMIPS_PLATFORM_TO_SCHEMA[template_settings["platform"]]
|
|
template_settings = dynamips_template_schema.model_validate(create_settings).model_dump()
|
|
except pydantic.ValidationError as e:
|
|
raise ControllerBadRequestError(f"JSON schema error received while creating new template: {e}")
|
|
|
|
# resolve the template symbol
|
|
template_settings["symbol"] = self._controller.symbols.resolve_symbol(template_settings["symbol"])
|
|
images_to_add_to_template = await self._find_images(template_create.template_type, template_settings)
|
|
db_template = await self._templates_repo.create_template(template_create.template_type, template_settings)
|
|
for image in images_to_add_to_template:
|
|
await self._templates_repo.add_image_to_template(db_template.template_id, image)
|
|
template = db_template.asjson()
|
|
self._controller.notification.controller_emit("template.created", template)
|
|
return template
|
|
|
|
async def get_template(self, template_id: UUID) -> dict:
|
|
|
|
db_template = await self._templates_repo.get_template(template_id)
|
|
if db_template:
|
|
template = db_template.asjson()
|
|
else:
|
|
template = self.get_builtin_template(template_id)
|
|
if not template:
|
|
raise ControllerNotFoundError(f"Template '{template_id}' not found")
|
|
return template
|
|
|
|
async def _remove_image(self, template_id: UUID, image_path:str) -> None:
|
|
|
|
image = await self._templates_repo.get_image(image_path)
|
|
await self._templates_repo.remove_image_from_template(template_id, image)
|
|
|
|
async def update_template(self, template_id: UUID, template_update: schemas.TemplateUpdate) -> dict:
|
|
|
|
if self.get_builtin_template(template_id):
|
|
raise ControllerForbiddenError(f"Template '{template_id}' cannot be updated because it is built-in")
|
|
|
|
db_template = await self._templates_repo.get_template(template_id)
|
|
if not db_template:
|
|
raise ControllerNotFoundError(f"Template '{template_id}' not found")
|
|
|
|
# Check for duplicate name (excluding current template)
|
|
if template_update.name is not None:
|
|
existing_template = await self._templates_repo.get_template_by_name_and_version(
|
|
template_update.name, db_template.version
|
|
)
|
|
if existing_template and existing_template.template_id != template_id:
|
|
raise ControllerError(f"A template with name '{template_update.name}' already exists")
|
|
|
|
try:
|
|
# validate the update settings
|
|
update_settings = jsonable_encoder(template_update, exclude_unset=True)
|
|
if db_template.template_type == "dynamips":
|
|
template_schema = DYNAMIPS_PLATFORM_TO_UPDATE_SCHEMA[db_template.platform]
|
|
else:
|
|
template_schema = TEMPLATE_TYPE_TO_UPDATE_SCHEMA[db_template.template_type]
|
|
template_settings = template_schema.model_validate(update_settings).model_dump(exclude_unset=True)
|
|
except pydantic.ValidationError as e:
|
|
raise ControllerBadRequestError(f"JSON schema error received while updating template: {e}")
|
|
|
|
images_to_add_to_template = await self._find_images(db_template.template_type, template_settings)
|
|
if db_template.template_type == "dynamips" and "image" in template_settings:
|
|
await self._remove_image(db_template.template_id, db_template.image)
|
|
elif db_template.template_type == "iou" and "path" in template_settings:
|
|
await self._remove_image(db_template.template_id, db_template.path)
|
|
elif db_template.template_type == "qemu":
|
|
for key in template_update.model_dump().keys():
|
|
if key.endswith("_image") and key in template_settings:
|
|
await self._remove_image(db_template.template_id, db_template.__dict__[key])
|
|
|
|
db_template = await self._templates_repo.update_template(db_template, template_settings)
|
|
for image in images_to_add_to_template:
|
|
await self._templates_repo.add_image_to_template(db_template.template_id, image)
|
|
template = db_template.asjson()
|
|
self._controller.notification.controller_emit("template.updated", template)
|
|
return template
|
|
|
|
async def duplicate_template(self, template_id: UUID) -> dict:
|
|
|
|
if self.get_builtin_template(template_id):
|
|
raise ControllerForbiddenError(f"Template '{template_id}' cannot be duplicated because it is built-in")
|
|
db_template = await self._templates_repo.duplicate_template(template_id)
|
|
if not db_template:
|
|
raise ControllerNotFoundError(f"Template '{template_id}' not found")
|
|
template = db_template.asjson()
|
|
self._controller.notification.controller_emit("template.created", template)
|
|
return template
|
|
|
|
async def delete_template(self, template_id: UUID) -> None:
|
|
|
|
if self.get_builtin_template(template_id):
|
|
raise ControllerForbiddenError(f"Template '{template_id}' cannot be deleted because it is built-in")
|
|
if await self._templates_repo.delete_template(template_id):
|
|
self._controller.notification.controller_emit("template.deleted", {"template_id": str(template_id)})
|
|
else:
|
|
raise ControllerNotFoundError(f"Template '{template_id}' not found")
|