Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NAS-133584 / 25.04 / Allow bootstrapping virt VM with zvol #15400

Merged
merged 1 commit into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/middlewared/middlewared/api/v25_04_0/virt_instance.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from typing import Annotated, Literal, TypeAlias

from pydantic import Field, model_validator, StringConstraints
Expand Down Expand Up @@ -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'
Expand All @@ -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):
Expand All @@ -95,15 +102,27 @@ 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')

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

Expand Down
23 changes: 19 additions & 4 deletions src/middlewared/middlewared/plugins/virt/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,21 +274,36 @@ 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',
'source': iso_volume,
'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'],
Expand Down
28 changes: 28 additions & 0 deletions tests/api2/test_virt_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.'),
Expand Down
Loading