diff --git a/gns3server/api/routes/controller/appliances.py b/gns3server/api/routes/controller/appliances.py index 18532e05..550473e1 100644 --- a/gns3server/api/routes/controller/appliances.py +++ b/gns3server/api/routes/controller/appliances.py @@ -26,7 +26,12 @@ from uuid import UUID from gns3server import schemas from gns3server.controller import Controller -from gns3server.controller.controller_error import ControllerNotFoundError +from gns3server.controller.controller_error import ( + ControllerError, + ControllerBadRequestError, + ControllerNotFoundError +) + from gns3server.db.repositories.images import ImagesRepository from gns3server.db.repositories.templates import TemplatesRepository from gns3server.db.repositories.rbac import RbacRepository @@ -68,6 +73,31 @@ def get_appliance(appliance_id: UUID) -> schemas.Appliance: return appliance.asdict() +@router.post("/{appliance_id}/version", status_code=status.HTTP_201_CREATED) +def add_appliance_version(appliance_id: UUID, appliance_version: schemas.ApplianceVersion) -> schemas.Appliance: + """ + Add a version to an appliance + """ + + controller = Controller.instance() + appliance = controller.appliance_manager.appliances.get(str(appliance_id)) + if not appliance: + raise ControllerNotFoundError(message=f"Could not find appliance '{appliance_id}'") + + if not appliance.versions: + raise ControllerBadRequestError(message=f"Appliance '{appliance_id}' do not have versions") + + if not appliance_version.images: + raise ControllerBadRequestError(message=f"Version '{appliance_version.name}' must contain images") + + for version in appliance.versions: + if version.get("name") == appliance_version.name: + raise ControllerError(message=f"Appliance '{appliance_id}' already has version '{appliance_version.name}'") + + appliance.versions.append(appliance_version.dict(exclude_unset=True)) + return appliance.asdict() + + @router.post("/{appliance_id}/install", status_code=status.HTTP_204_NO_CONTENT) async def install_appliance( appliance_id: UUID, diff --git a/gns3server/schemas/__init__.py b/gns3server/schemas/__init__.py index f5c69e32..5a3e99a3 100644 --- a/gns3server/schemas/__init__.py +++ b/gns3server/schemas/__init__.py @@ -24,7 +24,7 @@ from .controller.links import LinkCreate, LinkUpdate, Link from .controller.computes import ComputeCreate, ComputeUpdate, AutoIdlePC, Compute from .controller.templates import TemplateCreate, TemplateUpdate, TemplateUsage, Template from .controller.images import Image, ImageType -from .controller.appliances import Appliance +from .controller.appliances import ApplianceVersion, Appliance from .controller.drawings import Drawing from .controller.gns3vm import GNS3VM from .controller.nodes import NodeCreate, NodeUpdate, NodeDuplicate, NodeCapture, Node diff --git a/gns3server/schemas/controller/appliances.py b/gns3server/schemas/controller/appliances.py index b1f0d61c..5af72108 100644 --- a/gns3server/schemas/controller/appliances.py +++ b/gns3server/schemas/controller/appliances.py @@ -317,7 +317,7 @@ class Compression(Enum): field_7z = '7z' -class Image(BaseModel): +class ApplianceImage(BaseModel): filename: str = Field(..., title='Filename') version: str = Field(..., title='Version of the file') @@ -335,7 +335,7 @@ class Image(BaseModel): ) -class Images(BaseModel): +class ApplianceVersionImages(BaseModel): kernel_image: Optional[str] = Field(None, title='Kernel image') initrd: Optional[str] = Field(None, title='Initrd disk image') @@ -348,11 +348,11 @@ class Images(BaseModel): cdrom_image: Optional[str] = Field(None, title='cdrom image') -class Version(BaseModel): +class ApplianceVersion(BaseModel): name: str = Field(..., title='Name of the version') idlepc: Optional[str] = Field(None, regex='^0x[0-9a-f]{8}') - images: Optional[Images] = Field(None, title='Images used for this version') + images: Optional[ApplianceVersionImages] = Field(None, title='Images used for this version') class DynamipsSlot(Enum): @@ -460,5 +460,5 @@ class Appliance(BaseModel): iou: Optional[Iou] = Field(None, title='IOU specific options') dynamips: Optional[Dynamips] = Field(None, title='Dynamips specific options') qemu: Optional[Qemu] = Field(None, title='Qemu specific options') - images: Optional[List[Image]] = Field(None, title='Images for this appliance') - versions: Optional[List[Version]] = Field(None, title='Versions of the appliance') + images: Optional[List[ApplianceImage]] = Field(None, title='Images for this appliance') + versions: Optional[List[ApplianceVersion]] = Field(None, title='Versions of the appliance') diff --git a/tests/api/routes/controller/test_appliances.py b/tests/api/routes/controller/test_appliances.py index a57b04ad..089466c2 100644 --- a/tests/api/routes/controller/test_appliances.py +++ b/tests/api/routes/controller/test_appliances.py @@ -66,3 +66,28 @@ class TestApplianceRoutes: appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance response = await client.post(app.url_path_for("install_appliance", appliance_id=appliance_id)) assert response.status_code == status.HTTP_400_BAD_REQUEST + + async def test_add_version_appliance(self, app: FastAPI, client: AsyncClient) -> None: + + appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance + new_version = { + "name": "99G", + "images": { + "hda_disk_image": "empty99G.qcow2" + } + } + response = await client.post(app.url_path_for("add_appliance_version", appliance_id=appliance_id), json=new_version) + assert response.status_code == status.HTTP_201_CREATED + assert new_version in response.json()["versions"] + + async def test_add_existing_version_appliance(self, app: FastAPI, client: AsyncClient) -> None: + + appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance + new_version = { + "name": "8G", + "images": { + "hda_disk_image": "empty8G.qcow2" + } + } + response = await client.post(app.url_path_for("add_appliance_version", appliance_id=appliance_id), json=new_version) + assert response.status_code == status.HTTP_409_CONFLICT