1
0
mirror of https://github.com/GNS3/gns3-server synced 2025-02-03 11:51:31 +00:00

Support to create empty disk images on the controller

This commit is contained in:
grossmj 2023-09-25 17:51:14 +10:00
parent c1507b4155
commit 1ae6d13022
3 changed files with 91 additions and 21 deletions

View File

@ -23,13 +23,15 @@ import logging
import urllib.parse import urllib.parse
from fastapi import APIRouter, Request, Depends, status from fastapi import APIRouter, Request, Depends, status
from fastapi.encoders import jsonable_encoder
from starlette.requests import ClientDisconnect from starlette.requests import ClientDisconnect
from sqlalchemy.orm.exc import MultipleResultsFound from sqlalchemy.orm.exc import MultipleResultsFound
from typing import List, Optional from typing import List, Optional
from gns3server import schemas from gns3server import schemas
from gns3server.config import Config 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.images import ImagesRepository
from gns3server.db.repositories.templates import TemplatesRepository from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.rbac import RbacRepository
@ -50,6 +52,53 @@ log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.post(
"/{image_path:path}",
response_model=schemas.Image,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Image.Allocate"))]
)
async def create_image(
image_path: str,
image_data: schemas.QemuDiskImageCreate,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> schemas.Image:
"""
Create a new blank 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
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( @router.get(
"", "",
response_model=List[schemas.Image], response_model=List[schemas.Image],

View File

@ -21,6 +21,8 @@ Qemu server module.
import asyncio import asyncio
import os import os
import platform import platform
import shutil
import shlex
import sys import sys
import re import re
import subprocess import subprocess
@ -159,6 +161,44 @@ class Qemu(BaseManager):
return qemus 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 @staticmethod
async def get_qemu_version(qemu_path): async def get_qemu_version(qemu_path):
""" """
@ -178,25 +218,6 @@ class Qemu(BaseManager):
except (OSError, subprocess.SubprocessError) as e: except (OSError, subprocess.SubprocessError) as e:
raise QemuError(f"Error while looking for the Qemu version: {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 @staticmethod
async def get_swtpm_version(swtpm_path): async def get_swtpm_version(swtpm_path):
""" """

View File

@ -82,4 +82,4 @@ from .compute.vmware_nodes import VMwareCreate, VMwareUpdate, VMware
from .compute.virtualbox_nodes import VirtualBoxCreate, VirtualBoxUpdate, VirtualBox from .compute.virtualbox_nodes import VirtualBoxCreate, VirtualBoxUpdate, VirtualBox
# Schemas for both controller and compute # Schemas for both controller and compute
from .qemu_disk_image import QemuDiskImageCreate, QemuDiskImageUpdate from .qemu_disk_image import QemuDiskImageFormat, QemuDiskImageCreate, QemuDiskImageUpdate