Skip to content

Commit

Permalink
Allow importing/exporting disk images from virt namespace
Browse files Browse the repository at this point in the history
(cherry picked from commit d34423e)
  • Loading branch information
Qubad786 authored and bugclerk committed Feb 14, 2025
1 parent dcdbe40 commit 4e727f4
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 1 deletion.
25 changes: 24 additions & 1 deletion src/middlewared/middlewared/api/v25_04_0/virt_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

from pydantic import Field, field_validator

from middlewared.api.base import BaseModel, LocalGID, LocalUID, NonEmptyString
from middlewared.api.base import BaseModel, LocalGID, LocalUID, NonEmptyString, single_argument_args


__all__ = [
'DeviceType', 'InstanceType', 'VirtDeviceUSBChoicesArgs', 'VirtDeviceUSBChoicesResult',
'VirtDeviceGPUChoicesArgs', 'VirtDeviceGPUChoicesResult', 'VirtDeviceDiskChoicesArgs',
'VirtDeviceDiskChoicesResult', 'VirtDeviceNICChoicesArgs', 'VirtDeviceNICChoicesResult',
'VirtDeviceExportDiskImageArgs', 'VirtDeviceExportDiskImageResult', 'VirtDeviceImportDiskImageArgs',
'VirtDeviceImportDiskImageResult',
]


Expand Down Expand Up @@ -152,3 +154,24 @@ class VirtDeviceNICChoicesArgs(BaseModel):

class VirtDeviceNICChoicesResult(BaseModel):
result: dict[str, str]


@single_argument_args('virt_device_import_disk_image')
class VirtDeviceImportDiskImageArgs(BaseModel):
diskimg: NonEmptyString
zvol: NonEmptyString


class VirtDeviceImportDiskImageResult(BaseModel):
result: bool


@single_argument_args('virt_device_export_disk_image')
class VirtDeviceExportDiskImageArgs(BaseModel):
format: NonEmptyString
directory: NonEmptyString
zvol: NonEmptyString


class VirtDeviceExportDiskImageResult(BaseModel):
result: bool
178 changes: 178 additions & 0 deletions src/middlewared/middlewared/plugins/virt/disk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import errno
import os
import re
import shlex
import subprocess

from middlewared.api import api_method
from middlewared.api.current import (
VirtDeviceImportDiskImageArgs, VirtDeviceImportDiskImageResult, VirtDeviceExportDiskImageArgs,
VirtDeviceExportDiskImageResult,
)
from middlewared.plugins.zfs_.utils import zvol_name_to_path
from middlewared.service import CallError, job, Service


# Valid Disk Formats we can export
VALID_DISK_FORMATS = ('qcow2', 'qed', 'raw', 'vdi', 'vpc', 'vmdk')


class VirtDeviceService(Service):

class Config:
namespace = 'virt.device'
cli_namespace = 'virt.device'

@api_method(VirtDeviceImportDiskImageArgs, VirtDeviceImportDiskImageResult, roles=['VIRT_INSTANCE_WRITE'])
@job(lock_queue_size=1, lock=lambda args: f"virt_zvol_disk_image_{args[-1]['zvol']}")
def import_disk_image(self, job, data):
"""
Imports a specified disk image.
Utilized qemu-img with the auto-detect functionality to auto-convert
any supported disk image format to RAW -> ZVOL
As of this implementation it supports:
- QCOW2
- QED
- RAW
- VDI
- VPC
- VMDK
`diskimg` is a required parameter for the incoming disk image
`zvol` is the required target for the imported disk image
"""
if not self.middleware.call_sync('zfs.dataset.query', [('id', '=', data['zvol'])]):
raise CallError(f"zvol {data['zvol']} does not exist.", errno.ENOENT)

if os.path.exists(data['diskimg']) is False:
raise CallError('Disk Image does not exist.', errno.ENOENT)

zvol_device_path = str(zvol_name_to_path(data['zvol']))

if os.path.exists(zvol_device_path) is False:
raise CallError('Zvol device does not exist.', errno.ENOENT)

# Use quotes safely and assemble the command
imgsafe = shlex.quote(data['diskimg'])
devsafe = shlex.quote(zvol_device_path)
command = f"qemu-img convert -p -O raw {imgsafe} {devsafe}"
self.logger.info('Running Disk Import using: "' + command + '"')

cp = subprocess.Popen(
command, shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
universal_newlines=True
)

re_progress = re.compile(r'(\d+\.\d+)')
stderr = ''

for line in iter(cp.stdout.readline, ""):
progress = re_progress.search(line.lstrip())
if progress:
try:
progress = round(float(progress.group(1)))
job.set_progress(progress, "Disk Import Progress")
except ValueError:
self.logger.warning('Invalid progress in: "' + progress.group(1) + '"')
else:
stderr += line
self.logger.warning('No progress reported from qemu-img: "' + line.lstrip() + '"')
cp.wait()

if cp.returncode:
raise CallError(f'Failed to import disk: {stderr}')

return True

@api_method(VirtDeviceExportDiskImageArgs, VirtDeviceExportDiskImageResult, roles=['VIRT_INSTANCE_WRITE'])
@job(lock_queue_size=1, lock=lambda args: f"virt_zvol_disk_image_{args[-1]['zvol']}")
def export_disk_image(self, job, data):
"""
Exports a zvol to a formatted VM disk image.
Utilized qemu-img with the conversion functionality to export a zvol to
any supported disk image format, from RAW -> ${OTHER}. The resulting file
will be set to inherit the permissions of the target directory.
As of this implementation it supports the following {format} options :
- QCOW2
- QED
- RAW
- VDI
- VPC
- VMDK
`format` is a required parameter for the exported disk image
`directory` is a required parameter for the export disk image
`zvol` is the source for the disk image
"""
if not self.middleware.call_sync('zfs.dataset.query', [('id', '=', data['zvol'])]):
raise CallError(f"zvol {data['zvol']} does not exist.", errno.ENOENT)

if os.path.isdir(data['directory']) is False:
raise CallError(f"Export directory {data['directory']} does not exist.", errno.ENOENT)

if os.path.exists(zvol_name_to_path(data['zvol'])) is False:
raise CallError('Zvol device does not exist.', errno.ENOENT)

# Check that a supported format was specified
format = data['format'].lower()
if format not in VALID_DISK_FORMATS:
raise CallError('Invalid disk format specified.', errno.ENOENT)

# Grab the owner / group of the parent directory
parent_stat = os.stat(data['directory'])
owner = parent_stat.st_uid
group = parent_stat.st_gid

# Get the raw zvol device path
zvol_device_path = str(zvol_name_to_path(data['zvol']))

# Set the target file location
zvolbasename = os.path.basename(data['zvol'])
targetfile = f"{data['directory']}/vmdisk-{zvolbasename}.{format}"

# Use quotes safely and assemble the command
filesafe = shlex.quote(targetfile)
devsafe = shlex.quote(zvol_device_path)
command = f"qemu-img convert -p -f raw -O {data['format']} {devsafe} {filesafe}"
self.logger.info('Running Disk export using: "' + command + '"')

cp = subprocess.Popen(
command, shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
universal_newlines=True
)

re_progress = re.compile(r'(\d+\.\d+)')
stderr = ''

for line in iter(cp.stdout.readline, ""):
progress = re_progress.search(line.lstrip())
if progress:
try:
progress = round(float(progress.group(1)))
job.set_progress(progress, "Disk Export Progress")
except ValueError:
self.logger.warning('Invalid progress in: "' + progress.group(1) + '"')
else:
stderr += line
self.logger.warning('No progress reported from qemu-img: "' + line.lstrip() + '"')
cp.wait()

if cp.returncode:
raise CallError(f'Failed to export disk: {stderr}')

# Set the owner / group of the target file to inherit that of the saved parent directory
os.chown(targetfile, owner, group)

return True

0 comments on commit 4e727f4

Please sign in to comment.