From 28b1247b7c34785b394b41cf84acae963b5ca2e9 Mon Sep 17 00:00:00 2001 From: Qubad786 Date: Wed, 15 Jan 2025 19:23:21 +0500 Subject: [PATCH] Allow cleaner way to consume ISO with virt VM instances (#15390) --- .../middlewared/api/v25_04_0/virt_instance.py | 6 +++- .../middlewared/plugins/virt/instance.py | 26 ++++++++++++-- tests/api2/test_virt_vm.py | 35 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 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 0971a951ff2ab..74fb138963341 100644 --- a/src/middlewared/middlewared/api/v25_04_0/virt_instance.py +++ b/src/middlewared/middlewared/api/v25_04_0/virt_instance.py @@ -75,7 +75,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'] = 'IMAGE' + source_type: Literal[None, 'IMAGE', 'ISO'] = 'IMAGE' + iso_volume: NonEmptyString | None = None image: Annotated[NonEmptyString, StringConstraints(max_length=200)] | None = None remote: REMOTE_CHOICES = 'LINUX_CONTAINERS' instance_type: InstanceType = 'CONTAINER' @@ -98,6 +99,9 @@ def validate_attrs(self): 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 == 'IMAGE' and self.image is None: raise ValueError('Image must be set when source type is "IMAGE"') diff --git a/src/middlewared/middlewared/plugins/virt/instance.py b/src/middlewared/middlewared/plugins/virt/instance.py index edc2ad7af59f1..5c988b9ce7e5c 100644 --- a/src/middlewared/middlewared/plugins/virt/instance.py +++ b/src/middlewared/middlewared/plugins/virt/instance.py @@ -135,7 +135,6 @@ async def query(self, filters, options): @private async def validate(self, new, schema_name, verrors, old=None): # Do not validate image_choices because its an expansive operation, just fail on creation - instance_type = new.get('instance_type') or (old or {}).get('type') if instance_type and not await self.middleware.call('virt.global.license_active', instance_type): verrors.add( @@ -169,6 +168,15 @@ async def validate(self, new, schema_name, verrors, old=None): 'enable_vnc': True, 'vnc_port': old['vnc_port'], }) + else: + # Creation case + if new['source_type'] == 'ISO' and not await self.middleware.call( + 'virt.volume.query', [['content_type', '=', 'ISO'], ['id', '=', new['iso_volume']]] + ): + verrors.add( + f'{schema_name}.iso_volume', + 'Invalid ISO volume selected. Please select a valid ISO volume.' + ) if instance_type == 'VM' and new.get('enable_vnc'): if not new.get('vnc_port'): @@ -267,7 +275,21 @@ async def do_create(self, job, data): await self.validate(data, 'virt_instance_create', verrors) devices = {} - for i in (data['devices'] or []): + data_devices = data['devices'] or [] + iso_volume = data.pop('iso_volume', None) + if data['source_type'] == 'ISO': + data['source_type'] = None + data_devices.append({ + 'name': iso_volume, + 'dev_type': 'DISK', + 'pool': 'default', + 'source': iso_volume, + 'destination': None, + 'readonly': False, + 'boot_priority': 1, + }) + + 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 d1d2004435dce..86f504a39d8df 100644 --- a/tests/api2/test_virt_vm.py +++ b/tests/api2/test_virt_vm.py @@ -149,6 +149,41 @@ def test_vm_iso_volume(vm, iso_volume): assert iso_vol['used_by'] == [VM_NAME], iso_vol +def test_vm_creation_with_iso_volume(vm, iso_volume): + virt_instance_name = 'test-iso-vm' + call('virt.instance.create', { + 'name': virt_instance_name, + 'instance_type': 'VM', + 'source_type': 'ISO', + 'iso_volume': ISO_VOLUME_NAME, + }, job=True) + + try: + vm_devices = call('virt.instance.device_list', virt_instance_name) + assert any(device['name'] == ISO_VOLUME_NAME for device in vm_devices), vm_devices + + iso_vol = call('virt.volume.get_instance', ISO_VOLUME_NAME) + assert iso_vol['used_by'] == [virt_instance_name], iso_vol + finally: + call('virt.instance.delete', virt_instance_name, job=True) + + +@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.'), +]) +def test_iso_param_validation_on_vm_create(virt_pool, iso_volume, error_msg): + with pytest.raises(ValidationErrors) as ve: + call('virt.instance.create', { + 'name': 'test-iso-vm2', + 'instance_type': 'VM', + 'source_type': 'ISO', + 'iso_volume': iso_volume + }, job=True) + + assert ve.value.errors[0].errmsg == error_msg + + @pytest.mark.parametrize('enable_vnc,vnc_port,error_msg', [ (True, None, 'Value error, VNC port must be set when VNC is enabled'), (True, 6901, 'VNC port is already in use by another virt instance'),