From 08693871ae0c09b4793249567aaa1afe2bfe76cd Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 3 Jan 2025 21:35:14 +0700 Subject: [PATCH] Support to create templates based on image checksums. --- gns3server/api/routes/controller/images.py | 37 +++++++++++++++++++++ tests/api/routes/controller/test_images.py | 30 +++++++++++++++++ tests/resources/empty100G.qcow2 | Bin 0 -> 198656 bytes 3 files changed, 67 insertions(+) create mode 100644 tests/resources/empty100G.qcow2 diff --git a/gns3server/api/routes/controller/images.py b/gns3server/api/routes/controller/images.py index d7fe1d35..c4a06a4f 100644 --- a/gns3server/api/routes/controller/images.py +++ b/gns3server/api/routes/controller/images.py @@ -192,6 +192,43 @@ async def prune_images( await images_repo.prune_images(skip_images) +@router.post( + "/install", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Image.Allocate"))] +) +async def install_images( + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) +) -> None: + """ + Attempt to automatically create templates based on image checksums. + + Required privilege: Image.Allocate + """ + + skip_images = get_builtin_disks() + images = await images_repo.get_images() + for image in images: + if skip_images and image.filename in skip_images: + log.debug(f"Skipping image '{image.path}' for image installation") + continue + templates = await images_repo.get_image_templates(image.image_id) + if templates: + # the image is already used by a template + log.warning(f"Image '{image.path}' is used by one or more templates") + continue + await Controller.instance().appliance_manager.install_appliances_from_image( + image.path, + image.checksum, + images_repo, + templates_repo, + None, + None, + os.path.dirname(image.path) + ) + + @router.get( "/{image_path:path}", response_model=schemas.Image, diff --git a/tests/api/routes/controller/test_images.py b/tests/api/routes/controller/test_images.py index 7a8ac4d7..abb1f353 100644 --- a/tests/api/routes/controller/test_images.py +++ b/tests/api/routes/controller/test_images.py @@ -19,6 +19,7 @@ import os import pytest import hashlib +from tests.utils import asyncio_patch from sqlalchemy.ext.asyncio import AsyncSession from fastapi import FastAPI, status from httpx import AsyncClient @@ -295,3 +296,32 @@ class TestImageRoutes: assert len(templates) == 1 assert templates[0].name == "Empty VM" assert templates[0].version == "30G" + await templates_repo.delete_template(templates[0].template_id) + + async def test_install_all( + self, app: FastAPI, + client: AsyncClient, + db_session: AsyncSession, + controller: Controller + ) -> None: + + image_path = "tests/resources/empty100G.qcow2" + image_name = os.path.basename(image_path) + with open(image_path, "rb") as f: + image_data = f.read() + response = await client.post( + app.url_path_for("upload_image", image_path=image_name), + content=image_data) + assert response.status_code == status.HTTP_201_CREATED + + controller.appliance_manager.load_appliances() # make sure appliances are loaded + with asyncio_patch("gns3server.api.routes.controller.images.get_builtin_disks", return_value=[]) as mock: + response = await client.post(app.url_path_for("install_images")) + assert mock.called + assert response.status_code == status.HTTP_204_NO_CONTENT + + templates_repo = TemplatesRepository(db_session) + templates = await templates_repo.get_templates() + assert len(templates) == 1 + assert templates[0].name == "Empty VM" + assert templates[0].version == "100G" \ No newline at end of file diff --git a/tests/resources/empty100G.qcow2 b/tests/resources/empty100G.qcow2 new file mode 100644 index 0000000000000000000000000000000000000000..8fda12e8d46c3ebe3cda76bb13fbc7f7c6d35823 GIT binary patch literal 198656 zcmeIuF%Ezr3;@8QKETxvnK=3jpX#4nBoGr8W*iOzDWyr9JNM&>h&i8jt@*JviMzyK zRVuG~H$|Or$@#1ECry9=0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0)G;i{xpnP2oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkKch(L