mirror of
https://github.com/GNS3/gns3-server.git
synced 2024-11-17 09:14:52 +02:00
Merge pull request #2292 from GNS3/fix/3422
Support to create empty disk images on the controller
This commit is contained in:
commit
7215b150dd
@ -23,13 +23,15 @@ import logging
|
||||
import urllib.parse
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from starlette.requests import ClientDisconnect
|
||||
from sqlalchemy.orm.exc import MultipleResultsFound
|
||||
from typing import List, Optional
|
||||
from gns3server import schemas
|
||||
|
||||
from gns3server.config import Config
|
||||
from gns3server.utils.images import InvalidImageError, write_image
|
||||
from gns3server.compute.qemu import Qemu
|
||||
from gns3server.utils.images import InvalidImageError, write_image, read_image_info, default_images_directory
|
||||
from gns3server.db.repositories.images import ImagesRepository
|
||||
from gns3server.db.repositories.templates import TemplatesRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
@ -50,6 +52,53 @@ log = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/qemu/{image_path:path}",
|
||||
response_model=schemas.Image,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("Image.Allocate"))]
|
||||
)
|
||||
async def create_qemu_image(
|
||||
image_path: str,
|
||||
image_data: schemas.QemuDiskImageCreate,
|
||||
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
||||
|
||||
) -> schemas.Image:
|
||||
"""
|
||||
Create a new blank Qemu image.
|
||||
|
||||
Required privilege: Image.Allocate
|
||||
"""
|
||||
|
||||
allow_raw_image = Config.instance().settings.Server.allow_raw_images
|
||||
if image_data.format == schemas.QemuDiskImageFormat.raw and not allow_raw_image:
|
||||
raise ControllerBadRequestError("Raw images are not allowed")
|
||||
|
||||
disk_image_path = urllib.parse.unquote(image_path)
|
||||
image_dir, image_name = os.path.split(disk_image_path)
|
||||
# check if the path is within the default images directory
|
||||
base_images_directory = os.path.expanduser(Config.instance().settings.Server.images_path)
|
||||
full_path = os.path.abspath(os.path.join(base_images_directory, image_dir, image_name))
|
||||
if os.path.commonprefix([base_images_directory, full_path]) != base_images_directory:
|
||||
raise ControllerForbiddenError(f"Cannot write disk image, '{disk_image_path}' is forbidden")
|
||||
|
||||
if not image_dir:
|
||||
# put the image in the default images directory for Qemu
|
||||
directory = default_images_directory(image_type="qemu")
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
disk_image_path = os.path.abspath(os.path.join(directory, disk_image_path))
|
||||
|
||||
if await images_repo.get_image(disk_image_path):
|
||||
raise ControllerBadRequestError(f"Disk image '{disk_image_path}' already exists")
|
||||
|
||||
options = jsonable_encoder(image_data, exclude_unset=True)
|
||||
# FIXME: should we have the create_disk_image in the compute code since
|
||||
# this code is used to create images on the controller?
|
||||
await Qemu.instance().create_disk_image(disk_image_path, options)
|
||||
|
||||
image_info = await read_image_info(disk_image_path, "qemu")
|
||||
return await images_repo.add_image(**image_info)
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Image],
|
||||
|
@ -21,6 +21,8 @@ Qemu server module.
|
||||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import shlex
|
||||
import sys
|
||||
import re
|
||||
import subprocess
|
||||
@ -159,6 +161,44 @@ class Qemu(BaseManager):
|
||||
|
||||
return qemus
|
||||
|
||||
@staticmethod
|
||||
async def create_disk_image(disk_image_path, options):
|
||||
"""
|
||||
Create a Qemu disk (used by the controller to create empty disk images)
|
||||
|
||||
:param disk_image_path: disk image path
|
||||
:param options: disk creation options
|
||||
"""
|
||||
|
||||
qemu_img_path = shutil.which("qemu-img")
|
||||
if not qemu_img_path:
|
||||
raise QemuError(f"Could not find qemu-img binary")
|
||||
|
||||
try:
|
||||
if os.path.exists(disk_image_path):
|
||||
raise QemuError(f"Could not create disk image '{disk_image_path}', file already exists")
|
||||
except UnicodeEncodeError:
|
||||
raise QemuError(
|
||||
f"Could not create disk image '{disk_image_path}', "
|
||||
"Disk image name contains characters not supported by the filesystem"
|
||||
)
|
||||
|
||||
img_format = options.pop("format")
|
||||
img_size = options.pop("size")
|
||||
command = [qemu_img_path, "create", "-f", img_format]
|
||||
for option in sorted(options.keys()):
|
||||
command.extend(["-o", f"{option}={options[option]}"])
|
||||
command.append(disk_image_path)
|
||||
command.append(f"{img_size}M")
|
||||
command_string = " ".join(shlex.quote(s) for s in command)
|
||||
output = ""
|
||||
try:
|
||||
log.info(f"Executing qemu-img with: {command_string}")
|
||||
output = await subprocess_check_output(*command, stderr=True)
|
||||
log.info(f"Qemu disk image'{disk_image_path}' created")
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise QemuError(f"Could not create '{disk_image_path}' disk image: {e}\n{output}")
|
||||
|
||||
@staticmethod
|
||||
async def get_qemu_version(qemu_path):
|
||||
"""
|
||||
@ -178,25 +218,6 @@ class Qemu(BaseManager):
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise QemuError(f"Error while looking for the Qemu version: {e}")
|
||||
|
||||
@staticmethod
|
||||
async def _get_qemu_img_version(qemu_img_path):
|
||||
"""
|
||||
Gets the Qemu-img version.
|
||||
|
||||
:param qemu_img_path: path to Qemu-img executable.
|
||||
"""
|
||||
|
||||
try:
|
||||
output = await subprocess_check_output(qemu_img_path, "--version")
|
||||
match = re.search(r"version\s+([0-9a-z\-\.]+)", output)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
return version
|
||||
else:
|
||||
raise QemuError("Could not determine the Qemu-img version for '{}'".format(qemu_img_path))
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise QemuError("Error while looking for the Qemu-img version: {}".format(e))
|
||||
|
||||
@staticmethod
|
||||
async def get_swtpm_version(swtpm_path):
|
||||
"""
|
||||
|
@ -82,4 +82,4 @@ from .compute.vmware_nodes import VMwareCreate, VMwareUpdate, VMware
|
||||
from .compute.virtualbox_nodes import VirtualBoxCreate, VirtualBoxUpdate, VirtualBox
|
||||
|
||||
# Schemas for both controller and compute
|
||||
from .qemu_disk_image import QemuDiskImageCreate, QemuDiskImageUpdate
|
||||
from .qemu_disk_image import QemuDiskImageFormat, QemuDiskImageCreate, QemuDiskImageUpdate
|
||||
|
@ -22,10 +22,12 @@ import hashlib
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from fastapi import FastAPI, status
|
||||
from httpx import AsyncClient
|
||||
from tests.utils import AsyncioMagicMock
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.db.repositories.images import ImagesRepository
|
||||
from gns3server.db.repositories.templates import TemplatesRepository
|
||||
from gns3server.compute.qemu import Qemu
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
@ -104,6 +106,17 @@ def empty_image(tmpdir) -> str:
|
||||
|
||||
class TestImageRoutes:
|
||||
|
||||
async def test_create_image(self, app: FastAPI, client: AsyncClient, images_dir) -> None:
|
||||
|
||||
Qemu.instance().create_disk_image = AsyncioMagicMock()
|
||||
path = os.path.join(os.path.join(images_dir, "QEMU", "new_image.qcow2"))
|
||||
with open(path, "wb+") as f:
|
||||
f.write(b'QFI\xfb\x00\x00\x00')
|
||||
image_name = os.path.basename(path)
|
||||
response = await client.post(
|
||||
app.url_path_for("create_qemu_image", image_path=image_name), json={"format": "qcow2", "size": 30})
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"image_type, fixture_name, valid_request",
|
||||
(
|
||||
@ -151,7 +164,7 @@ class TestImageRoutes:
|
||||
|
||||
response = await client.get(app.url_path_for("get_images"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) == 4 # 4 valid images uploaded before
|
||||
assert len(response.json()) == 5 # 4 valid images uploaded before + 1 created
|
||||
|
||||
async def test_image_get(self, app: FastAPI, client: AsyncClient, qcow2_image: str) -> None:
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user