YueGuobin c05f667f3d feat(templates): safely access settings dict keys
Use dict.get() method to safely access 'image' and 'path' keys in settings dictionary to avoid KeyError exceptions when these keys are missing. This improves error handling and prevents server crashes when creating templates with incomplete settings.
2026-02-23 01:02:32 +08:00

330 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,
},
{
"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")
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")