From 927314b1f4fa74e23bc309893b07b7d4ce63c716 Mon Sep 17 00:00:00 2001 From: Qubad786 Date: Wed, 15 Jan 2025 23:33:48 +0500 Subject: [PATCH] Allow bootstrapping virt VM with zvol (#15400) --- .../middlewared/api/v25_04_0/virt_instance.py | 21 +++++++++++++- .../middlewared/plugins/virt/instance.py | 23 ++++++++++++--- tests/api2/test_virt_vm.py | 28 +++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/middlewared/middlewared/api/v25_04_0/virt_instance.py b/src/middlewared/middlewared/api/v25_04_0/virt_instance.py index 74fb138963341..51cfa989baa3e 100644 --- a/src/middlewared/middlewared/api/v25_04_0/virt_instance.py +++ b/src/middlewared/middlewared/api/v25_04_0/virt_instance.py @@ -1,3 +1,4 @@ +import os from typing import Annotated, Literal, TypeAlias from pydantic import Field, model_validator, StringConstraints @@ -75,8 +76,8 @@ class VirtInstanceEntry(BaseModel): @single_argument_args('virt_instance_create') class VirtInstanceCreateArgs(BaseModel): name: Annotated[NonEmptyString, StringConstraints(max_length=200)] - source_type: Literal[None, 'IMAGE', 'ISO'] = 'IMAGE' iso_volume: NonEmptyString | None = None + source_type: Literal[None, 'IMAGE', 'ZVOL', 'ISO'] = 'IMAGE' image: Annotated[NonEmptyString, StringConstraints(max_length=200)] | None = None remote: REMOTE_CHOICES = 'LINUX_CONTAINERS' instance_type: InstanceType = 'CONTAINER' @@ -87,6 +88,12 @@ class VirtInstanceCreateArgs(BaseModel): memory: MemoryType | None = None enable_vnc: bool = False vnc_port: int | None = Field(ge=5900, le=65535, default=None) + zvol_path: NonEmptyString | None = None + ''' + This is useful when a VM wants to be booted where a ZVOL already has a VM bootstrapped in it and needs + to be ported over to virt plugin. Virt will consume this zvol and add it as a DISK device to the instance + with boot priority set to 1 so the VM can be booted from it. + ''' @model_validator(mode='after') def validate_attrs(self): @@ -95,6 +102,8 @@ def validate_attrs(self): raise ValueError('Source type must be set to "IMAGE" when instance type is CONTAINER') if self.enable_vnc: raise ValueError('VNC is not supported for containers and `enable_vnc` should be unset') + if self.zvol_path: + raise ValueError('Zvol path is only supported for VMs') else: if self.enable_vnc and self.vnc_port is None: raise ValueError('VNC port must be set when VNC is enabled') @@ -102,8 +111,18 @@ def validate_attrs(self): if self.source_type == 'ISO' and self.iso_volume is None: raise ValueError('ISO volume must be set when source type is "ISO"') + if self.source_type == 'ZVOL': + if self.zvol_path is None: + raise ValueError('Zvol path must be set when source type is "ZVOL"') + if self.zvol_path.startswith('/dev/zvol/') is False: + raise ValueError('Zvol path must be a valid zvol path') + elif not os.path.exists(self.zvol_path): + raise ValueError(f'Zvol path {self.zvol_path} does not exist') + if self.source_type == 'IMAGE' and self.image is None: raise ValueError('Image must be set when source type is "IMAGE"') + elif self.source_type != 'IMAGE' and self.image: + raise ValueError('Image must not be set when source type is not "IMAGE"') return self diff --git a/src/middlewared/middlewared/plugins/virt/instance.py b/src/middlewared/middlewared/plugins/virt/instance.py index 5c988b9ce7e5c..20aa0f24a6243 100644 --- a/src/middlewared/middlewared/plugins/virt/instance.py +++ b/src/middlewared/middlewared/plugins/virt/instance.py @@ -274,12 +274,22 @@ async def do_create(self, job, data): verrors = ValidationErrors() await self.validate(data, 'virt_instance_create', verrors) - devices = {} data_devices = data['devices'] or [] iso_volume = data.pop('iso_volume', None) - if data['source_type'] == 'ISO': + root_device_to_add = None + zvol_path = data.pop('zvol_path', None) + if data['source_type'] == 'ZVOL': data['source_type'] = None - data_devices.append({ + root_device_to_add = { + 'name': 'ix_virt_zvol_root', + 'dev_type': 'DISK', + 'source': zvol_path, + 'destination': None, + 'readonly': False, + 'boot_priority': 1, + } + elif data['source_type'] == 'ISO': + root_device_to_add = { 'name': iso_volume, 'dev_type': 'DISK', 'pool': 'default', @@ -287,8 +297,13 @@ async def do_create(self, job, data): 'destination': None, 'readonly': False, 'boot_priority': 1, - }) + } + if root_device_to_add: + data['source_type'] = None + data_devices.append(root_device_to_add) + + devices = {} for i in data_devices: await self.middleware.call( 'virt.instance.validate_device', i, 'virt_instance_create', verrors, data['instance_type'], diff --git a/tests/api2/test_virt_vm.py b/tests/api2/test_virt_vm.py index 86f504a39d8df..26a09c86f9f4e 100644 --- a/tests/api2/test_virt_vm.py +++ b/tests/api2/test_virt_vm.py @@ -168,6 +168,34 @@ def test_vm_creation_with_iso_volume(vm, iso_volume): call('virt.instance.delete', virt_instance_name, job=True) +def test_vm_creation_with_zvol(virt_pool, vm, iso_volume): + virt_instance_name = 'test-zvol-vm' + zvol_name = f'{virt_pool["pool"]}/test_zvol' + call('zfs.dataset.create', { + 'name': zvol_name, + 'type': 'VOLUME', + 'properties': {'volsize': '514MiB'} + }) + call('virt.instance.create', { + 'name': virt_instance_name, + 'instance_type': 'VM', + 'source_type': 'ZVOL', + 'zvol_path': f'/dev/zvol/{zvol_name}', + }, job=True) + + try: + vm_devices = call('virt.instance.device_list', virt_instance_name) + assert any( + device['name'] == 'ix_virt_zvol_root' + and device['boot_priority'] == 1 + for device in vm_devices + ), vm_devices + + finally: + call('virt.instance.delete', virt_instance_name, job=True) + call('zfs.dataset.delete', zvol_name) + + @pytest.mark.parametrize('iso_volume,error_msg', [ (None, 'Value error, ISO volume must be set when source type is "ISO"'), ('test_iso123', 'Invalid ISO volume selected. Please select a valid ISO volume.'),