From a76336121c3c90facefffea3b5d0b25da44fd268 Mon Sep 17 00:00:00 2001 From: "M. Rehan" Date: Wed, 27 Nov 2024 16:28:45 +0500 Subject: [PATCH] Remove accepts style decorator from VM plugin --- .../middlewared/api/v25_04_0/__init__.py | 2 + .../middlewared/api/v25_04_0/vm.py | 440 ++++++++++++++++++ .../middlewared/api/v25_04_0/vm_device.py | 334 +++++++++++++ .../middlewared/plugins/vm/capabilities.py | 12 +- .../middlewared/plugins/vm/clone.py | 10 +- .../middlewared/plugins/vm/devices/cdrom.py | 15 +- .../middlewared/plugins/vm/devices/device.py | 12 +- .../middlewared/plugins/vm/devices/display.py | 14 +- .../middlewared/plugins/vm/devices/nic.py | 10 +- .../middlewared/plugins/vm/devices/pci.py | 6 +- .../plugins/vm/devices/storage_devices.py | 28 +- .../middlewared/plugins/vm/devices/usb.py | 21 +- .../middlewared/plugins/vm/disk_utils.py | 21 +- .../middlewared/plugins/vm/info.py | 42 +- .../middlewared/plugins/vm/lifecycle.py | 8 - .../middlewared/plugins/vm/memory.py | 6 +- src/middlewared/middlewared/plugins/vm/pci.py | 58 +-- src/middlewared/middlewared/plugins/vm/usb.py | 30 +- .../middlewared/plugins/vm/vm_devices.py | 55 +-- .../middlewared/plugins/vm/vm_display_info.py | 46 +- .../middlewared/plugins/vm/vm_lifecycle.py | 36 +- .../middlewared/plugins/vm/vm_memory_info.py | 42 +- src/middlewared/middlewared/plugins/vm/vms.py | 107 +---- tests/api2/test_541_vm.py | 3 +- ...t_account_privilege_role_private_fields.py | 4 +- 25 files changed, 924 insertions(+), 438 deletions(-) create mode 100644 src/middlewared/middlewared/api/v25_04_0/vm.py create mode 100644 src/middlewared/middlewared/api/v25_04_0/vm_device.py diff --git a/src/middlewared/middlewared/api/v25_04_0/__init__.py b/src/middlewared/middlewared/api/v25_04_0/__init__.py index fd3e5097fef0a..bd8488ad22341 100644 --- a/src/middlewared/middlewared/api/v25_04_0/__init__.py +++ b/src/middlewared/middlewared/api/v25_04_0/__init__.py @@ -44,3 +44,5 @@ from .virt_device import * # noqa from .virt_global import * # noqa from .virt_instance import * # noqa +from .vm import * # noqa +from .vm_device import * # noqa diff --git a/src/middlewared/middlewared/api/v25_04_0/vm.py b/src/middlewared/middlewared/api/v25_04_0/vm.py new file mode 100644 index 0000000000000..0a377c9a935fe --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/vm.py @@ -0,0 +1,440 @@ +import uuid + +from typing import Literal + +from pydantic import ConfigDict, Field, field_validator + +from middlewared.api.base import ( + BaseModel, Excluded, excluded_field, ForUpdateMetaclass, NonEmptyString, single_argument_args, + single_argument_result, +) + +from .vm_device import VMDisplayDevice, VMDeviceEntry + + +__all__ = [ + 'VMEntry', 'VMCreateArgs', 'VMCreateResult', 'VMUpdateArgs', 'VMUpdateResult', 'VMDeleteArgs', 'VMDeleteResult', + 'VMBootloaderOVMFChoicesArgs', 'VMBootloaderOVMFChoicesResult', 'VMBootloaderOptionsArgs', + 'VMBootloaderOptionsResult', 'VMStatusArgs', 'VMStatusResult', 'VMLogFilePathArgs', 'VMLogFilePathResult', + 'VMLogFileDownloadArgs', 'VMLogFileDownloadResult', 'VMGuestArchitectureMachineChoicesArgs', + 'VMGuestArchitectureMachineChoicesResult', 'VMCloneArgs', 'VMCloneResult', 'VMImportDiskImageArgs', + 'VMImportDiskImageResult', 'VMExportDiskImageArgs', 'VMExportDiskImageResult', 'VMSupportsVirtualizationArgs', + 'VMSupportsVirtualizationResult', 'VMVirtualizationDetailsArgs', 'VMVirtualizationDetailsResult', + 'VMMaximumSupportedVCPUsArgs', 'VMMaximumSupportedVCPUsResult', 'VMFlagsArgs', 'VMFlagsResult', 'VMGetConsoleArgs', + 'VMGetConsoleResult', 'VMCPUModelChoicesArgs', 'VMCPUModelChoicesResult', 'VMGetMemoryUsageArgs', + 'VMGetMemoryUsageResult', 'VMPortWizardArgs', 'VMPortWizardResult', 'VMResolutionChoicesArgs', + 'VMResolutionChoicesResult', 'VMGetDisplayDevicesArgs', 'VMGetDisplayDevicesResult', 'VMDisplayWebURIArgs', + 'VMDisplayWebURIResult', 'VMStartArgs', 'VMStartResult', 'VMStopArgs', 'VMStopResult', 'VMRestartArgs', + 'VMRestartResult', 'VMResumeArgs', 'VMResumeResult', 'VMPoweroffArgs', 'VMPoweroffResult', 'VMSuspendArgs', + 'VMSuspendResult', 'VMGetVMemoryInUseArgs', 'VMGetVMemoryInUseResult', 'VMGetAvailableMemoryArgs', + 'VMGetAvailableMemoryResult', 'VMGetVMMemoryInfoArgs', 'VMGetVMMemoryInfoResult', 'VMRandomMacArgs', + 'VMRandomMacResult', +] + + +class VMStatus(BaseModel): + state: NonEmptyString + pid: int | None + domain_state: NonEmptyString + + +class VMEntry(BaseModel): + command_line_args: str = '' + cpu_mode: Literal['CUSTOM', 'HOST-MODEL', 'HOST_PASSTHROUGH'] = 'CUSTOM' + cpu_model: str | None = None + name: NonEmptyString + description: str = '' + vcpus: int = Field(ge=1, default=1) + cores: int = Field(ge=1, default=1) + threads: int = Field(ge=1, default=1) + cpuset: str | None = None # TODO: Add validation for numeric set + nodeset: str | None = None # TODO: Same as above + enable_cpu_topology_extension: bool = False + pin_vcpus: bool = False + suspend_on_snapshot: bool = False + trusted_platform_module: bool = False + memory: int = Field(ge=20) + min_memory: int | None = Field(ge=20, default=None) + hyperv_enlightenments: bool = False + bootloader: Literal['UEFI_CSM', 'UEFI'] = 'UEFI' + bootloader_ovmf: str = 'OVMF_CODE.fd' + autostart: bool = True + hide_from_msr: bool = False + ensure_display_device: bool = True + time: Literal['LOCAL', 'UTC'] = 'LOCAL' + shutdown_timeout: int = Field(ge=5, le=300, default=90) + arch_type: str | None = None + machine_type: str | None = None + uuid: str | None = None + devices: list[VMDeviceEntry] + display_available: bool + id: int + status: VMStatus + + +class VMCreate(VMEntry): + status: Excluded = excluded_field() + id: Excluded = excluded_field() + display_available: Excluded = excluded_field() + devices: Excluded = excluded_field() + + @field_validator('uuid') + def validate_uuid(cls, value): + if value is not None: + try: + uuid.UUID(value, version=4) + except ValueError: + raise ValueError('UUID is not valid version 4') + return value + + +@single_argument_args('vm_create') +class VMCreateArgs(VMCreate): + pass + + +class VMCreateResult(BaseModel): + result: VMEntry + + +class VMUpdate(VMCreate, metaclass=ForUpdateMetaclass): + pass + + +class VMUpdateArgs(BaseModel): + id: int + vm_update: VMUpdate + + +class VMUpdateResult(BaseModel): + result: VMEntry + + +class VMDeleteOptions(BaseModel): + zvols: bool = False + force: bool = False + + +class VMDeleteArgs(BaseModel): + id: int + options: VMDeleteOptions = VMDeleteOptions() + + +class VMDeleteResult(BaseModel): + result: bool + + +class VMBootloaderOVMFChoicesArgs(BaseModel): + pass + + +@single_argument_result +class VMBootloaderOVMFChoicesResult(BaseModel): + model_config = ConfigDict(extra='allow') + + +class VMBootloaderOptionsArgs(BaseModel): + pass + + +@single_argument_result +class VMBootloaderOptionsResult(BaseModel): + UEFI: Literal['UEFI'] = 'UEFI' + UEFI_CSM: Literal['Legacy BIOS'] = 'Legacy BIOS' + + +class VMStatusArgs(BaseModel): + id: int + + +class VMStatusResult(BaseModel): + result: VMStatus + + +class VMLogFilePathArgs(BaseModel): + id: int + + +class VMLogFilePathResult(BaseModel): + result: NonEmptyString | None + + +class VMLogFileDownloadArgs(BaseModel): + id: int + + +class VMLogFileDownloadResult(BaseModel): + result: None + + +class VMGuestArchitectureMachineChoicesArgs(BaseModel): + pass + + +@single_argument_result +class VMGuestArchitectureMachineChoicesResult(BaseModel): + model_config = ConfigDict(extra='allow') + + +class VMCloneArgs(BaseModel): + id: int + name: NonEmptyString | None = None + + +class VMCloneResult(BaseModel): + result: bool + + +@single_argument_args('vm_import_disk_image') +class VMImportDiskImageArgs(BaseModel): + diskimg: NonEmptyString + zvol: NonEmptyString + + +class VMImportDiskImageResult(BaseModel): + result: bool + + +@single_argument_args('vm_export_disk_image') +class VMExportDiskImageArgs(BaseModel): + format: NonEmptyString + directory: NonEmptyString + zvol: NonEmptyString + + +class VMExportDiskImageResult(BaseModel): + result: bool + + +class VMSupportsVirtualizationArgs(BaseModel): + pass + + +class VMSupportsVirtualizationResult(BaseModel): + result: bool + + +class VMVirtualizationDetailsArgs(BaseModel): + pass + + +@single_argument_result +class VMVirtualizationDetailsResult(BaseModel): + supported: bool + error: str | None + + +class VMMaximumSupportedVCPUsArgs(BaseModel): + pass + + +class VMMaximumSupportedVCPUsResult(BaseModel): + result: int + + +class VMFlagsArgs(BaseModel): + pass + + +@single_argument_result +class VMFlagsResult(BaseModel): + intel_vmx: bool + unrestricted_guest: bool + amd_rvi: bool + amd_asids: bool + + +class VMGetConsoleArgs(BaseModel): + id: int + + +class VMGetConsoleResult(BaseModel): + result: NonEmptyString + + +class VMCPUModelChoicesArgs(BaseModel): + pass + + +@single_argument_result +class VMCPUModelChoicesResult(BaseModel): + model_config = ConfigDict(extra='allow') + + +class VMGetMemoryUsageArgs(BaseModel): + id: int + + +class VMGetMemoryUsageResult(BaseModel): + result: int + + +class VMPortWizardArgs(BaseModel): + pass + + +@single_argument_result +class VMPortWizardResult(BaseModel): + port: int + '''Available server port''' + web: int + '''Web port to be used based on available port''' + + +class VMResolutionChoicesArgs(BaseModel): + pass + + +class VMResolutionChoicesResult(BaseModel): + result: dict[str, str] + + +class VMGetDisplayDevicesArgs(BaseModel): + id: int + + +class GetDisplayDevice(VMDisplayDevice): + password_configured: bool + + +class DisplayDevice(VMDeviceEntry): + attributes: GetDisplayDevice + + +class VMGetDisplayDevicesResult(BaseModel): + result: list[DisplayDevice] + + +class DisplayWebURIOptions(BaseModel): + protocol: Literal['HTTP', 'HTTPS'] = 'HTTP' + + +class VMDisplayWebURIArgs(BaseModel): + id: int + host: str = '' + options: DisplayWebURIOptions = DisplayWebURIOptions() + + +@single_argument_result +class VMDisplayWebURIResult(BaseModel): + error: str | None + uri: str | None + + +class VMStartOptions(BaseModel): + overcommit: bool = False + + +class VMStartArgs(BaseModel): + id: int + options: VMStartOptions = VMStartOptions() + + +class VMStartResult(BaseModel): + result: None + + +class VMStopOptions(BaseModel): + force: bool = False + force_after_timeout: bool = False + + +class VMStopArgs(BaseModel): + id: int + options: VMStopOptions = VMStopOptions() + + +class VMStopResult(BaseModel): + result: None + + +class VMPoweroffArgs(BaseModel): + id: int + + +class VMPoweroffResult(BaseModel): + result: None + + +class VMRestartArgs(BaseModel): + id: int + + +class VMRestartResult(BaseModel): + result: None + + +class VMSuspendArgs(BaseModel): + id: int + + +class VMSuspendResult(BaseModel): + result: None + + +class VMResumeArgs(BaseModel): + id: int + + +class VMResumeResult(BaseModel): + result: None + + +class VMGetVMemoryInUseArgs(BaseModel): + pass + + +@single_argument_result +class VMGetVMemoryInUseResult(BaseModel): + RNP: int + '''Running but not provisioned''' + PRD: int + '''Provisioned but not running''' + RPRD: int + '''Running and provisioned''' + + +class VMGetAvailableMemoryArgs(BaseModel): + overcommit: bool = False + + +class VMGetAvailableMemoryResult(BaseModel): + result: int + + +class VMGetVMMemoryInfoArgs(BaseModel): + id: int + + +@single_argument_result +class VMGetVMMemoryInfoResult(BaseModel): + minimum_memory_requested: int | None + '''Minimum memory requested by the VM''' + total_memory_requested: int + '''Maximum / total memory requested by the VM''' + overcommit_required: bool + '''Overcommit of memory is required to start VM''' + memory_req_fulfilled_after_overcommit: bool + '''Memory requirements of VM are fulfilled if over-committing memory is specified''' + arc_to_shrink: int | None + '''Size of ARC to shrink in bytes''' + current_arc_max: int + '''Current size of max ARC in bytes''' + arc_min: int + '''Minimum size of ARC in bytes''' + arc_max_after_shrink: int + '''Size of max ARC in bytes after shrinking''' + actual_vm_requested_memory: int + ''' + VM memory in bytes to consider when making calculations for available/required memory. If VM ballooning is + specified for the VM, the minimum VM memory specified by user will be taken into account otherwise total VM + memory requested will be taken into account. + ''' + + +class VMRandomMacArgs(BaseModel): + pass + + +class VMRandomMacResult(BaseModel): + result: str diff --git a/src/middlewared/middlewared/api/v25_04_0/vm_device.py b/src/middlewared/middlewared/api/v25_04_0/vm_device.py new file mode 100644 index 0000000000000..c6e6f7dfef302 --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/vm_device.py @@ -0,0 +1,334 @@ +from typing import Annotated, Literal, TypeAlias + +from pydantic import ConfigDict, Field, model_validator, RootModel, Secret + +from middlewared.api.base import ( + BaseModel, Excluded, excluded_field, ForUpdateMetaclass, NonEmptyString, single_argument_args, + single_argument_result, +) + + +__all__ = [ + 'VMCDROMDevice', 'VMDisplayDevice', 'VMNICDevice', 'VMPCIDevice', 'VMRAWDevice', 'VMDiskDevice', 'VMUSBDevice', + 'VMDeviceType', 'VMDeviceEntry', 'VMDeviceCreateArgs', 'VMDeviceCreateResult', 'VMDeviceUpdateArgs', + 'VMDeviceUpdateResult', 'VMDeviceDeleteArgs', 'VMDeviceDeleteResult', 'VMDeviceDiskChoicesArgs', + 'VMDeviceDiskChoicesResult', 'VMDeviceIOTypeArgs', 'VMDeviceIOTypeResult', 'VMDeviceNICAttachChoicesArgs', + 'VMDeviceNICAttachChoicesResult', 'VMDeviceBindChoicesArgs', 'VMDeviceBindChoicesResult', + 'VMDevicePassthroughDeviceArgs', 'VMDevicePassthroughDeviceResult', 'VMDeviceIOMMUEnabledArgs', + 'VMDeviceIOMMUEnabledResult', 'VMDevicePassthroughDeviceChoicesArgs', 'VMDevicePassthroughDeviceChoicesResult', + 'VMDevicePPTDevChoicesArgs', 'VMDevicePPTDevChoicesResult', 'VMDeviceGetPCIIdsForIsolationArgs', + 'VMDeviceGetPCIIdsForIsolationResult', 'VMDeviceUSBPassthroughDeviceArgs', 'VMDeviceUSBPassthroughDeviceResult', + 'VMDeviceUSBPassthroughDeviceChoicesArgs', 'VMDeviceUSBPassthroughDeviceChoicesResult', + 'VMDeviceUSBControllerChoicesArgs', 'VMDeviceUSBControllerChoicesResult', +] + + +class VMCDROMDevice(BaseModel): + dtype: Literal['CDROM'] + path: NonEmptyString = Field(pattern=r'^/mnt/[^{}]*$') + '''Path must not contain "{", "}" characters, and it should start with "/mnt/"''' + + +class VMDisplayDevice(BaseModel): + dtype: Literal['DISPLAY'] + resolution: Literal[ + '1920x1200', '1920x1080', '1600x1200', '1600x900', + '1400x1050', '1280x1024', '1280x720', + '1024x768', '800x600', '640x480', + ] = '1024x768' + port: int | None = Field(default=None, ge=5900, le=65535) + web_port: int | None = Field(default=None, ge=5900, le=65535) + bind: NonEmptyString = '127.0.0.1' + wait: bool = False + password: Secret[NonEmptyString] + web: bool = True + type_: Literal['SPICE'] = Field(alias='type', default='SPICE') + + +class VMNICDevice(BaseModel): + dtype: Literal['NIC'] + trust_guest_rx_filters: bool = False + type_: Literal['E1000', 'VIRTIO'] = Field(alias='type', default='E1000') + nic_attach: str | None = None + mac: str | None = Field(default=None, pattern=r'^([0-9A-Fa-f]{2}[:-]?){5}([0-9A-Fa-f]{2})$') + + +class VMPCIDevice(BaseModel): + dtype: Literal['PCI'] + pptdev: NonEmptyString + + +class VMRAWDevice(BaseModel): + dtype: Literal['RAW'] + path: NonEmptyString = Field(pattern=r'^[^{}]*$', description='Path must not contain "{", "}" characters') + type_: Literal['AHCI', 'VIRTIO'] = Field(alias='type', default='AHCI') + exists: bool = False + boot: bool = False + size: int | None = None + logical_sectorsize: Literal[None, 512, 4096] | None = None + physical_sectorsize: Literal[None, 512, 4096] | None = None + iotype: Literal['NATIVE', 'THREADS', 'IO_URING'] = 'THREADS' + serial: NonEmptyString | None = None + + +class VMDiskDevice(BaseModel): + dtype: Literal['DISK'] + path: NonEmptyString | None = None + type_: Literal['AHCI', 'VIRTIO'] = Field(alias='type', default='AHCI') + create_zvol: bool = False + zvol_name: str | None = None + zvol_volsize: int | None = None + logical_sectorsize: Literal[None, 512, 4096] | None = None + physical_sectorsize: Literal[None, 512, 4096] | None = None + iotype: Literal['NATIVE', 'THREADS', 'IO_URING'] = 'THREADS' + serial: NonEmptyString | None = None + + @model_validator(mode='after') + def validate_attrs(self): + if self.path is not None and self.create_zvol is True: + raise ValueError('Path should not be provided if create_zvol is set') + if self.path is None and self.create_zvol is None: + raise ValueError('Either `path` or `create_zvol` should be set') + if self.path is None and self.create_zvol is False: + raise ValueError('Path must be specified if create_zvol is not set') + + return self + + +class USBAttributes(BaseModel): + vendor_id: NonEmptyString = Field(pattern=r'^0x.*') + '''Vendor id must start with "0x" prefix e.g 0x16a8''' + product_id: NonEmptyString = Field(pattern=r'^0x.*') + '''Product id must start with "0x" prefix e.g 0x16a8''' + + +class VMUSBDevice(BaseModel): + dtype: Literal['USB'] + usb: USBAttributes | None = None + controller_type: Literal[ + 'piix3-uhci', 'piix4-uhci', 'ehci', 'ich9-ehci1', + 'vt82c686b-uhci', 'pci-ohci', 'nec-xhci', 'qemu-xhci', + ] = 'nec-xhci' + device: NonEmptyString | None = None + + +VMDeviceType: TypeAlias = Annotated[ + VMCDROMDevice | VMDisplayDevice | VMNICDevice | VMPCIDevice | VMRAWDevice | VMDiskDevice | VMUSBDevice, + Field(discriminator='dtype') +] + + +# VM Device Service models + + +class VMDeviceEntry(BaseModel): + id: int + attributes: VMDeviceType + vm: int + order: int + + +class VMDeviceCreate(VMDeviceEntry): + order: int | None = None + id: Excluded = excluded_field() + + +@single_argument_args('vm_device_create') +class VMDeviceCreateArgs(VMDeviceCreate): + pass + + +class VMDeviceCreateResult(BaseModel): + result: VMDeviceEntry + + +class VMDeviceUpdate(VMDeviceCreate, metaclass=ForUpdateMetaclass): + pass + + +class VMDeviceUpdateArgs(BaseModel): + id: int + vm_device_update: VMDeviceUpdate + + +class VMDeviceUpdateResult(BaseModel): + result: VMDeviceEntry + + +class VMDeviceDeleteOptions(BaseModel): + force: bool = False + raw_file: bool = False + zvol: bool = False + + +class VMDeviceDeleteArgs(BaseModel): + id: int + options: VMDeviceDeleteOptions = VMDeviceDeleteOptions() + + +class VMDeviceDeleteResult(BaseModel): + result: bool + + +class VMDeviceDiskChoicesArgs(BaseModel): + pass + + +class VMDeviceDiskChoices(BaseModel): + model_config = ConfigDict(extra='allow') + + +class VMDeviceDiskChoicesResult(BaseModel): + result: VMDeviceDiskChoices + + +class VMDeviceIOTypeArgs(BaseModel): + pass + + +@single_argument_result +class VMDeviceIOTypeResult(BaseModel): + NATIVE: str = 'NATIVE' + THREADS: str = 'THREADS' + IO_URING: str = 'IO_URING' + + +class VMDeviceNICAttachChoicesArgs(BaseModel): + pass + + +@single_argument_result +class VMDeviceNICAttachChoicesResult(BaseModel): + model_config = ConfigDict(extra='allow') + + +class VMDeviceBindChoicesArgs(BaseModel): + pass + + +@single_argument_result +class VMDeviceBindChoicesResult(BaseModel): + model_config = ConfigDict(extra='allow') + + +class VMDeviceIOMMUEnabledArgs(BaseModel): + pass + + +class VMDeviceIOMMUEnabledResult(BaseModel): + result: bool + + +class VMDevicePassthroughDeviceArgs(BaseModel): + device: NonEmptyString + + +class VMDeviceCapability(BaseModel): + class_: str | None = Field(alias='class') + domain: str | None + bus: str | None + slot: str | None + function: str | None + product: str | None + vendor: str | None + + +class VMDeviceIOMMUGroupAddress(BaseModel): + domain: str + bus: str + slot: str + function: str + + +class VMDeviceIOMMUGroup(BaseModel): + number: int + addresses: list[VMDeviceIOMMUGroupAddress] + + +class VMDevicePassthroughDevice(BaseModel): + capability: VMDeviceCapability + controller_type: str | None + iommu_group: VMDeviceIOMMUGroup | None = None + available: bool + drivers: list[str] + error: str | None + reset_mechanism_defined: bool + description: str + critical: bool + device_path: str | None + + +class VMDevicePassthroughDeviceResult(BaseModel): + result: VMDevicePassthroughDevice + + +class VMDevicePassthroughInfo(RootModel[dict[str, VMDevicePassthroughDevice]]): + pass + + +class VMDevicePassthroughDeviceChoicesArgs(BaseModel): + pass + + +class VMDevicePassthroughDeviceChoicesResult(BaseModel): + result: VMDevicePassthroughInfo + + +class VMDevicePPTDevChoicesArgs(BaseModel): + pass + + +class VMDevicePPTDevChoicesResult(BaseModel): + result: VMDevicePassthroughInfo + + +class VMDeviceGetPCIIdsForIsolationArgs(BaseModel): + gpu_pci_id: NonEmptyString + + +class VMDeviceGetPCIIdsForIsolationResult(BaseModel): + result: list[NonEmptyString] + + +class USBCapability(BaseModel): + product: str | None + product_id: str | None + vendor: str | None + vendor_id: str | None + bus: str | None + device: str | None + + +class VMDeviceUSBPassthroughDeviceArgs(BaseModel): + device: NonEmptyString + + +class USBPassthroughDevice(BaseModel): + capability: USBCapability + available: bool + error: str | None + + +class USBPassthroughInfo(RootModel[dict[str, USBPassthroughDevice]]): + pass + + +class VMDeviceUSBPassthroughDeviceResult(BaseModel): + result: USBPassthroughDevice + + +class VMDeviceUSBPassthroughDeviceChoicesArgs(BaseModel): + pass + + +class VMDeviceUSBPassthroughDeviceChoicesResult(BaseModel): + result: USBPassthroughInfo + + +class VMDeviceUSBControllerChoicesArgs(BaseModel): + pass + + +@single_argument_result +class VMDeviceUSBControllerChoicesResult(BaseModel): + model_config = ConfigDict(extra='allow') diff --git a/src/middlewared/middlewared/plugins/vm/capabilities.py b/src/middlewared/middlewared/plugins/vm/capabilities.py index b9daa80787096..5d6f5ec1b7e35 100644 --- a/src/middlewared/middlewared/plugins/vm/capabilities.py +++ b/src/middlewared/middlewared/plugins/vm/capabilities.py @@ -1,7 +1,8 @@ from collections import defaultdict from xml.etree import ElementTree as etree -from middlewared.schema import accepts, Dict, returns +from middlewared.api import api_method +from middlewared.api.current import VMGuestArchitectureMachineChoicesArgs, VMGuestArchitectureMachineChoicesResult from middlewared.service import private, Service from .connection import LibvirtConnectionMixin @@ -27,14 +28,7 @@ def update_capabilities_cache(self): self.CAPABILITIES = supported_archs - @accepts(roles=['VM_READ']) - @returns(Dict( - additional_attrs=True, - example={ - 'x86_64': ['pc-i440fx-5.2', 'pc-q35-5.2', 'pc-i440fx-2.7'], - 'i686': ['pc-i440fx-3.0', 'xenfv'], - } - )) + @api_method(VMGuestArchitectureMachineChoicesArgs, VMGuestArchitectureMachineChoicesResult, roles=['VM_READ']) async def guest_architecture_and_machine_choices(self): """ Retrieve choices for supported guest architecture types and machine choices. diff --git a/src/middlewared/middlewared/plugins/vm/clone.py b/src/middlewared/middlewared/plugins/vm/clone.py index e0d9af3f7772a..03d0b4dbad81e 100644 --- a/src/middlewared/middlewared/plugins/vm/clone.py +++ b/src/middlewared/middlewared/plugins/vm/clone.py @@ -2,8 +2,9 @@ import re import uuid +from middlewared.api import api_method +from middlewared.api.current import VMCloneArgs, VMCloneResult from middlewared.plugins.zfs_.utils import zvol_name_to_path, zvol_path_to_name -from middlewared.schema import accepts, Bool, Int, returns, Str from middlewared.service import CallError, item_method, Service, private from middlewared.service_exception import ValidationErrors @@ -69,12 +70,7 @@ async def __clone_zvol(self, name, zvol, created_snaps, created_clones): return clone_dst @item_method - @accepts( - Int('id'), - Str('name', default=None), - roles=['VM_WRITE'] - ) - @returns(Bool()) + @api_method(VMCloneArgs, VMCloneResult, roles=['VM_WRITE']) async def clone(self, id_, name): """ Clone the VM `id`. diff --git a/src/middlewared/middlewared/plugins/vm/devices/cdrom.py b/src/middlewared/middlewared/plugins/vm/devices/cdrom.py index ba6161bc9aa9f..4a492602375ec 100644 --- a/src/middlewared/middlewared/plugins/vm/devices/cdrom.py +++ b/src/middlewared/middlewared/plugins/vm/devices/cdrom.py @@ -1,10 +1,11 @@ import os +from middlewared.api.current import VMCDROMDevice from middlewared.plugins.boot import BOOT_POOL_NAME -from middlewared.schema import Dict, File, Str +from middlewared.schema import Dict from middlewared.service import CallError from middlewared.utils.zfs import query_imported_fast_impl -from middlewared.validators import check_path_resides_within_volume_sync, Match +from middlewared.validators import check_path_resides_within_volume_sync from .device import Device from .utils import create_element, disk_from_number, LIBVIRT_USER @@ -14,16 +15,8 @@ class CDROM(Device): schema = Dict( 'attributes', - File( - 'path', required=True, validators=[ - Match( - r'^/mnt/[^{}]*$', - explanation='Path must not contain "{", "}" characters, and it should start with "/mnt/"' - ), - ], empty=False - ), - Str('dtype', enum=['CDROM'], required=True), ) + schema_model = VMCDROMDevice def identity(self): return self.data['attributes']['path'] diff --git a/src/middlewared/middlewared/plugins/vm/devices/device.py b/src/middlewared/middlewared/plugins/vm/devices/device.py index 31e2bb9d8301a..dc721428d829a 100644 --- a/src/middlewared/middlewared/plugins/vm/devices/device.py +++ b/src/middlewared/middlewared/plugins/vm/devices/device.py @@ -1,11 +1,13 @@ from abc import ABC -from middlewared.validators import validate_schema +from middlewared.api.base.handler.accept import validate_model +from middlewared.schema import ValidationErrors class Device(ABC): schema = NotImplemented + schema_model = NotImplementedError def __init__(self, data, middleware=None): self.data = data @@ -39,8 +41,12 @@ def pre_start_vm_device_setup(self, *args, **kwargs): pass def validate(self, device, old=None, vm_instance=None, update=True): - verrors = validate_schema(list(self.schema.attrs.values()), device['attributes']) - verrors.check() + if self.schema_model is NotImplementedError: + raise NotImplementedError('schema_model is not implemented for this device') + + dump = validate_model(self.schema_model, device['attributes']) + device['attributes'] = dump + verrors = ValidationErrors() self._validate(device, verrors, old, vm_instance, update) verrors.check() diff --git a/src/middlewared/middlewared/plugins/vm/devices/display.py b/src/middlewared/middlewared/plugins/vm/devices/display.py index 8b5a23e0d9ded..d932bd9d9870f 100644 --- a/src/middlewared/middlewared/plugins/vm/devices/display.py +++ b/src/middlewared/middlewared/plugins/vm/devices/display.py @@ -3,8 +3,8 @@ from urllib.parse import urlencode, quote_plus -from middlewared.schema import Bool, Dict, Int, Password, Str, ValidationErrors -from middlewared.validators import Range +from middlewared.api.current import VMDisplayDevice +from middlewared.schema import Dict, ValidationErrors from .device import Device from .utils import create_element, NGINX_PREFIX @@ -20,16 +20,8 @@ class DISPLAY(Device): schema = Dict( 'attributes', - Str('dtype', enum=['DISPLAY'], required=True), - Str('resolution', enum=RESOLUTION_ENUM, default='1024x768'), - Int('port', default=None, null=True, validators=[Range(min_=5900, max_=65535)]), - Int('web_port', default=None, null=True, validators=[Range(min_=5900, max_=65535)]), - Str('bind', default='127.0.0.1'), - Bool('wait', default=False), - Password('password', required=True, null=False, empty=False), - Bool('web', default=True), - Str('type', default='SPICE', enum=['SPICE']), ) + schema_model = VMDisplayDevice def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/middlewared/middlewared/plugins/vm/devices/nic.py b/src/middlewared/middlewared/plugins/vm/devices/nic.py index 9dea0b633d90e..73db2db4cb52a 100644 --- a/src/middlewared/middlewared/plugins/vm/devices/nic.py +++ b/src/middlewared/middlewared/plugins/vm/devices/nic.py @@ -1,9 +1,9 @@ import random +from middlewared.api.current import VMNICDevice from middlewared.plugins.interface.netif import netif -from middlewared.schema import Bool, Dict, Str +from middlewared.schema import Dict from middlewared.service import CallError -from middlewared.validators import MACAddr from .device import Device from .utils import create_element @@ -13,12 +13,8 @@ class NIC(Device): schema = Dict( 'attributes', - Bool('trust_guest_rx_filters', default=False), - Str('type', enum=['E1000', 'VIRTIO'], default='E1000'), - Str('nic_attach', default=None, null=True), - Str('mac', default=None, null=True, validators=[MACAddr(separator=':')]), - Str('dtype', enum=['NIC'], required=True), ) + schema_model = VMNICDevice def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/middlewared/middlewared/plugins/vm/devices/pci.py b/src/middlewared/middlewared/plugins/vm/devices/pci.py index 8028509c34012..05ed28d932b10 100644 --- a/src/middlewared/middlewared/plugins/vm/devices/pci.py +++ b/src/middlewared/middlewared/plugins/vm/devices/pci.py @@ -1,7 +1,8 @@ import subprocess +from middlewared.api.current import VMPCIDevice from middlewared.service import CallError -from middlewared.schema import Dict, Str +from middlewared.schema import Dict from middlewared.utils import filter_list from .device import Device @@ -32,9 +33,8 @@ class PCI(PCIBase): schema = Dict( 'attributes', - Str('pptdev', required=True, empty=False), - Str('dtype', enum=['PCI'], required=True), ) + schema_model = VMPCIDevice def vm_device_filters(self): return [['attributes.pptdev', '=', self.passthru_device()], ['attributes.dtype', '=', 'PCI']] diff --git a/src/middlewared/middlewared/plugins/vm/devices/storage_devices.py b/src/middlewared/middlewared/plugins/vm/devices/storage_devices.py index 0dca275ccc416..40bac82ccd57e 100644 --- a/src/middlewared/middlewared/plugins/vm/devices/storage_devices.py +++ b/src/middlewared/middlewared/plugins/vm/devices/storage_devices.py @@ -1,11 +1,11 @@ import errno import os +from middlewared.api.current import VMDiskDevice, VMRAWDevice from middlewared.plugins.zfs_.utils import zvol_name_to_path, zvol_path_to_name from middlewared.plugins.zfs_.validation_utils import check_zvol_in_boot_pool_using_path -from middlewared.schema import Bool, Dict, Int, Str +from middlewared.schema import Dict from middlewared.utils.crypto import generate_string -from middlewared.validators import Match from .device import Device from .utils import create_element, disk_from_number @@ -83,19 +83,8 @@ class RAW(StorageDevice): schema = Dict( 'attributes', - Str('path', required=True, validators=[Match( - r'^[^{}]*$', explanation='Path should not contain "{", "}" characters' - )], empty=False), - Str('type', enum=['AHCI', 'VIRTIO'], default='AHCI'), - Bool('exists'), - Bool('boot', default=False), - Int('size', default=None, null=True), - Int('logical_sectorsize', enum=[None, 512, 4096], default=None, null=True), - Int('physical_sectorsize', enum=[None, 512, 4096], default=None, null=True), - Str('iotype', enum=IOTYPE_CHOICES, default='THREADS'), - Str('serial'), - Str('dtype', enum=['RAW'], required=True), ) + schema_model = VMRAWDevice def create_source_element(self): return create_element('source', file=self.data['attributes']['path']) @@ -128,17 +117,8 @@ class DISK(StorageDevice): schema = Dict( 'attributes', - Str('path'), - Str('type', enum=['AHCI', 'VIRTIO'], default='AHCI'), - Bool('create_zvol'), - Str('zvol_name'), - Int('zvol_volsize'), - Int('logical_sectorsize', enum=[None, 512, 4096], default=None, null=True), - Int('physical_sectorsize', enum=[None, 512, 4096], default=None, null=True), - Str('iotype', enum=IOTYPE_CHOICES, default='THREADS'), - Str('serial'), - Str('dtype', enum=['DISK'], required=True), ) + schema_model = VMDiskDevice def create_source_element(self): return create_element('source', dev=self.data['attributes']['path']) diff --git a/src/middlewared/middlewared/plugins/vm/devices/usb.py b/src/middlewared/middlewared/plugins/vm/devices/usb.py index db77e81224ce1..bc64b8fc86b1d 100644 --- a/src/middlewared/middlewared/plugins/vm/devices/usb.py +++ b/src/middlewared/middlewared/plugins/vm/devices/usb.py @@ -1,5 +1,5 @@ -from middlewared.schema import Dict, Str -from middlewared.validators import Match +from middlewared.api.current import VMUSBDevice +from middlewared.schema import Dict from .pci import PCIBase from .utils import create_element @@ -15,23 +15,8 @@ class USB(PCIBase): schema = Dict( 'attributes', - Dict( - 'usb', - Str( - 'vendor_id', empty=False, required=True, validators=[Match(r'^0x.*')], - description='Vendor id must start with "0x" prefix e.g 0x0451' - ), - Str( - 'product_id', empty=False, required=True, validators=[Match(r'^0x.*')], - description='Product id must start with "0x" prefix e.g 0x16a8' - ), - default=None, - null=True, - ), - Str('controller_type', empty=False, default='nec-xhci', enum=USB_CONTROLLER_CHOICES), - Str('device', empty=False, null=True, default=None), - Str('dtype', enum=['USB'], required=True), ) + schema_model = VMUSBDevice @property def usb_device(self): diff --git a/src/middlewared/middlewared/plugins/vm/disk_utils.py b/src/middlewared/middlewared/plugins/vm/disk_utils.py index 4df6206217b7e..f0c39d97f50a4 100644 --- a/src/middlewared/middlewared/plugins/vm/disk_utils.py +++ b/src/middlewared/middlewared/plugins/vm/disk_utils.py @@ -4,21 +4,20 @@ import shlex import subprocess +from middlewared.api import api_method +from middlewared.api.current import ( + VMImportDiskImageArgs, VMImportDiskImageResult, VMExportDiskImageArgs, VMExportDiskImageResult, +) from middlewared.plugins.zfs_.utils import zvol_name_to_path -from middlewared.schema import accepts, Bool, Dict, returns, Str from middlewared.service import CallError, Service, job + # Valid Disk Formats we can export VALID_DISK_FORMATS = ['qcow2', 'qed', 'raw', 'vdi', 'vpc', 'vmdk' ] class VMService(Service): - @accepts(Dict( - 'vm_info', - Str('diskimg', required=True), - Str('zvol', required=True) - )) - @returns(Bool()) + @api_method(VMImportDiskImageArgs, VMImportDiskImageResult, roles=['VM_WRITE']) @job(lock_queue_size=1, lock=lambda args: f"zvol_disk_image_{args[-1]['zvol']}") def import_disk_image(self, job, data): @@ -81,13 +80,7 @@ def import_disk_image(self, job, data): return True - @accepts(Dict( - 'vm_info', - Str('format', required=True), - Str('directory', required=True), - Str('zvol', required=True) - )) - @returns(Bool()) + @api_method(VMExportDiskImageArgs, VMExportDiskImageResult, roles=['VM_WRITE']) @job(lock_queue_size=1, lock=lambda args: f"zvol_disk_image_{args[-1]['zvol']}") def export_disk_image(self, job, data): diff --git a/src/middlewared/middlewared/plugins/vm/info.py b/src/middlewared/middlewared/plugins/vm/info.py index 1b8683d25a182..f6a267da92110 100644 --- a/src/middlewared/middlewared/plugins/vm/info.py +++ b/src/middlewared/middlewared/plugins/vm/info.py @@ -3,8 +3,13 @@ import subprocess from xml.etree import ElementTree as etree -from middlewared.schema import Bool, Dict, Int, returns, Str -from middlewared.service import accepts, private, Service +from middlewared.api import api_method +from middlewared.api.current import ( + VMSupportsVirtualizationArgs, VMSupportsVirtualizationResult, VMVirtualizationDetailsArgs, + VMVirtualizationDetailsResult, VMMaximumSupportedVCPUsArgs, VMMaximumSupportedVCPUsResult, VMFlagsArgs, + VMFlagsResult, VMGetConsoleArgs, VMGetConsoleResult, VMCPUModelChoicesArgs, VMCPUModelChoicesResult, +) +from middlewared.service import private, Service from middlewared.utils import run from .connection import LibvirtConnectionMixin @@ -20,8 +25,7 @@ class VMService(Service, LibvirtConnectionMixin): CPU_MODEL_CHOICES = {} - @accepts(roles=['VM_READ']) - @returns(Bool()) + @api_method(VMSupportsVirtualizationArgs, VMSupportsVirtualizationResult, roles=['VM_READ']) def supports_virtualization(self): """ Returns "true" if system supports virtualization, "false" otherwise @@ -41,11 +45,7 @@ async def license_active(self): return can_run_vms - @accepts(roles=['VM_READ']) - @returns(Dict( - Bool('supported', required=True), - Str('error', null=True, required=True), - )) + @api_method(VMVirtualizationDetailsArgs, VMVirtualizationDetailsResult, roles=['VM_READ']) def virtualization_details(self): """ Retrieve details if virtualization is supported on the system and in case why it's not supported if it isn't. @@ -55,22 +55,14 @@ def virtualization_details(self): 'error': None if self._is_kvm_supported() else 'Your CPU does not support KVM extensions', } - @accepts(roles=['VM_READ']) - @returns(Int()) + @api_method(VMMaximumSupportedVCPUsArgs, VMMaximumSupportedVCPUsResult, roles=['VM_READ']) async def maximum_supported_vcpus(self): """ Returns maximum supported VCPU's """ return 255 - @accepts(roles=['VM_READ']) - @returns(Dict( - 'cpu_flags', - Bool('intel_vmx', required=True), - Bool('unrestricted_guest', required=True), - Bool('amd_rvi', required=True), - Bool('amd_asids', required=True), - )) + @api_method(VMFlagsArgs, VMFlagsResult, roles=['VM_READ']) async def flags(self): """ Returns a dictionary with CPU flags for the hypervisor. @@ -106,8 +98,7 @@ async def flags(self): return flags - @accepts(Int('id')) - @returns(Str('console_device')) + @api_method(VMGetConsoleArgs, VMGetConsoleResult, roles=['VM_READ']) async def get_console(self, id_): """ Get the console device from a given guest. @@ -115,14 +106,7 @@ async def get_console(self, id_): vm = await self.middleware.call('vm.get_instance', id_) return f'{vm["id"]}_{vm["name"]}' - @accepts(roles=['VM_READ']) - @returns(Dict( - additional_attrs=True, - example={ - '486': '486', - 'pentium': 'pentium', - } - )) + @api_method(VMCPUModelChoicesArgs, VMCPUModelChoicesResult, roles=['VM_READ']) def cpu_model_choices(self): """ Retrieve CPU Model choices which can be used with a VM guest to emulate the CPU in the guest. diff --git a/src/middlewared/middlewared/plugins/vm/lifecycle.py b/src/middlewared/middlewared/plugins/vm/lifecycle.py index a3205acba23a7..b2338a21eb7c9 100644 --- a/src/middlewared/middlewared/plugins/vm/lifecycle.py +++ b/src/middlewared/middlewared/plugins/vm/lifecycle.py @@ -1,7 +1,6 @@ import asyncio import contextlib -from middlewared.schema import accepts, Bool, Dict from middlewared.service import CallError, private, Service from middlewared.utils.asyncio_ import asyncio_map @@ -71,13 +70,6 @@ async def start_on_boot(self): self.middleware.logger.error(f'Failed to start VM {vm["name"]}: {e}') @private - @accepts( - Dict( - 'deinitialize_vms_options', - Bool('reload_ui', default=True), - Bool('stop_libvirt', default=True), - ) - ) async def deinitialize_vms(self, options): await self.middleware.call('vm.close_libvirt_connection') if options['reload_ui']: diff --git a/src/middlewared/middlewared/plugins/vm/memory.py b/src/middlewared/middlewared/plugins/vm/memory.py index 8fa8bbb211477..4370b3330dfab 100644 --- a/src/middlewared/middlewared/plugins/vm/memory.py +++ b/src/middlewared/middlewared/plugins/vm/memory.py @@ -1,6 +1,7 @@ import errno -from middlewared.schema import accepts, Int, returns +from middlewared.api import api_method +from middlewared.api.current import VMGetMemoryUsageArgs, VMGetMemoryUsageResult from middlewared.service import CallError, private, Service from .utils import ACTIVE_STATES @@ -55,8 +56,7 @@ async def teardown_guest_vmemory(self, vm_id): f'Not giving back memory to ARC because new arc_max ({new_arc_max}) <= arc_min ({arc_min})' ) - @accepts(Int('vm_id'), roles=['VM_READ']) - @returns(Int('memory_usage', description='Memory usage of a VM in bytes')) + @api_method(VMGetMemoryUsageArgs, VMGetMemoryUsageResult, roles=['VM_READ']) def get_memory_usage(self, vm_id): return self.get_memory_usage_internal(self.middleware.call_sync('vm.get_instance', vm_id)) diff --git a/src/middlewared/middlewared/plugins/vm/pci.py b/src/middlewared/middlewared/plugins/vm/pci.py index 31960f04bbfde..010c6f0193208 100644 --- a/src/middlewared/middlewared/plugins/vm/pci.py +++ b/src/middlewared/middlewared/plugins/vm/pci.py @@ -4,7 +4,13 @@ from pyudev import Context -from middlewared.schema import accepts, Bool, Dict, Int, List, Ref, returns, Str +from middlewared.api import api_method +from middlewared.api.current import ( + VMDeviceIOMMUEnabledArgs, VMDeviceIOMMUEnabledResult, VMDevicePassthroughDeviceArgs, + VMDevicePassthroughDeviceResult, VMDevicePassthroughDeviceChoicesArgs, VMDevicePassthroughDeviceChoicesResult, + VMDevicePPTDevChoicesArgs, VMDevicePPTDevChoicesResult, VMDeviceGetPCIIdsForIsolationArgs, + VMDeviceGetPCIIdsForIsolationResult, +) from middlewared.service import private, Service, ValidationErrors from middlewared.utils.gpu import get_gpus from middlewared.utils.iommu import get_iommu_groups_info @@ -21,8 +27,7 @@ class VMDeviceService(Service): class Config: namespace = 'vm.device' - @accepts(roles=['VM_DEVICE_READ']) - @returns(Bool()) + @api_method(VMDeviceIOMMUEnabledArgs, VMDeviceIOMMUEnabledResult, roles=['VM_DEVICE_READ']) def iommu_enabled(self): """Returns "true" if iommu is enabled, "false" otherwise""" return os.path.exists('/sys/kernel/iommu_groups') @@ -41,7 +46,7 @@ def get_pci_device_default_data(self): }, 'controller_type': None, 'critical': False, - 'iommu_group': {}, + 'iommu_group': None, 'available': False, 'drivers': [], 'error': None, @@ -111,41 +116,7 @@ def get_single_pci_device_details(self, pcidev): result[key] = self.get_pci_device_details(i, iommu_info) return result - @accepts(Str('device'), roles=['VM_DEVICE_READ']) - @returns(Dict( - 'passthrough_device', - Dict( - 'capability', - Str('class', null=True, required=True), - Str('domain', null=True, required=True), - Str('bus', null=True, required=True), - Str('slot', null=True, required=True), - Str('function', null=True, required=True), - Str('product', null=True, required=True), - Str('vendor', null=True, required=True), - required=True, - ), - Str('controller_type', null=True, required=True), - Dict( - 'iommu_group', - Int('number', required=True), - List('addresses', items=[Dict( - 'address', - Str('domain', required=True), - Str('bus', required=True), - Str('slot', required=True), - Str('function', required=True), - )]), - required=True, - ), - Bool('available', required=True), - List('drivers', items=[Str('driver', required=False)], required=True), - Str('error', null=True, required=True), - Str('device_path', null=True, required=True), - Bool('reset_mechanism_defined', required=True), - Str('description', empty=True, required=True), - register=True, - )) + @api_method(VMDevicePassthroughDeviceArgs, VMDevicePassthroughDeviceResult, roles=['VM_DEVICE_READ']) def passthrough_device(self, device): """Retrieve details about `device` PCI device""" self.middleware.call_sync('vm.check_setup_libvirt') @@ -157,20 +128,17 @@ def passthrough_device(self, device): 'error': 'Device not found', } - @accepts(roles=['VM_DEVICE_READ']) - @returns(List(items=[Ref('passthrough_device')], register=True)) + @api_method(VMDevicePassthroughDeviceChoicesArgs, VMDevicePassthroughDeviceChoicesResult, roles=['VM_DEVICE_READ']) def passthrough_device_choices(self): """Available choices for PCI passthru devices""" return self.get_all_pci_devices_details() - @accepts() - @returns(Ref('passthrough_device_choices')) + @api_method(VMDevicePPTDevChoicesArgs, VMDevicePPTDevChoicesResult, roles=['VM_DEVICE_READ']) def pptdev_choices(self): """Available choices for PCI passthru device""" return self.get_all_pci_devices_details() - @accepts(Str('gpu_pci_id', empty=False)) - @returns(List(items=[Str('pci_ids')])) + @api_method(VMDeviceGetPCIIdsForIsolationArgs, VMDeviceGetPCIIdsForIsolationResult, roles=['VM_DEVICE_READ']) def get_pci_ids_for_gpu_isolation(self, gpu_pci_id): """ Get PCI IDs of devices which are required to be isolated for `gpu_pci_id` GPU isolation. diff --git a/src/middlewared/middlewared/plugins/vm/usb.py b/src/middlewared/middlewared/plugins/vm/usb.py index 5e2f261538127..46778f7a694fe 100644 --- a/src/middlewared/middlewared/plugins/vm/usb.py +++ b/src/middlewared/middlewared/plugins/vm/usb.py @@ -2,7 +2,11 @@ from xml.etree import ElementTree as etree -from middlewared.schema import accepts, Bool, Dict, List, Ref, Str, returns +from middlewared.api import api_method +from middlewared.api.current import ( + VMDeviceUSBPassthroughDeviceArgs, VMDeviceUSBPassthroughDeviceResult, VMDeviceUSBPassthroughDeviceChoicesArgs, + VMDeviceUSBPassthroughDeviceChoicesResult, VMDeviceUSBControllerChoicesArgs, VMDeviceUSBControllerChoicesResult, +) from middlewared.service import CallError, private, Service from middlewared.utils import run @@ -18,8 +22,7 @@ class VMDeviceService(Service): class Config: namespace = 'vm.device' - @accepts() - @returns(Dict(*[Str(k, enum=[k]) for k in USB_CONTROLLER_CHOICES])) + @api_method(VMDeviceUSBControllerChoicesArgs, VMDeviceUSBControllerChoicesResult, roles=['VM_DEVICE_READ']) async def usb_controller_choices(self): """ Retrieve USB controller type choices @@ -52,21 +55,7 @@ def get_capability_keys(self): 'device': None, } - @accepts(Str('device', empty=False), roles=['VM_DEVICE_READ']) - @returns(Dict( - Dict( - 'capability', - Str('product', required=True, null=True), - Str('product_id', required=True, null=True), - Str('vendor', required=True, null=True), - Str('vendor_id', required=True, null=True), - Str('bus', required=True, null=True), - Str('device', required=True, null=True), - ), - Bool('available', required=True), - Str('error', required=True, null=True), - register=True, - )) + @api_method(VMDeviceUSBPassthroughDeviceArgs, VMDeviceUSBPassthroughDeviceResult, roles=['VM_DEVICE_READ']) async def usb_passthrough_device(self, device): """ Retrieve details about `device` USB device. @@ -99,8 +88,9 @@ async def get_basic_usb_passthrough_device_data(self): 'error': None, } - @accepts(roles=['VM_DEVICE_READ']) - @returns(List(items=[Ref('usb_passthrough_device')])) + @api_method( + VMDeviceUSBPassthroughDeviceChoicesArgs, VMDeviceUSBPassthroughDeviceChoicesResult, roles=['VM_DEVICE_READ'] + ) async def usb_passthrough_choices(self): """ Available choices for USB passthrough devices. diff --git a/src/middlewared/middlewared/plugins/vm/vm_devices.py b/src/middlewared/middlewared/plugins/vm/vm_devices.py index 0546671fac7f6..b98466b2247ed 100644 --- a/src/middlewared/middlewared/plugins/vm/vm_devices.py +++ b/src/middlewared/middlewared/plugins/vm/vm_devices.py @@ -4,9 +4,15 @@ import middlewared.sqlalchemy as sa +from middlewared.api import api_method +from middlewared.api.current import ( + VMDeviceEntry, VMDeviceCreateArgs, VMDeviceCreateResult, VMDeviceUpdateArgs, VMDeviceUpdateResult, + VMDeviceDeleteArgs, VMDeviceDeleteResult, VMDeviceDiskChoicesArgs, VMDeviceDiskChoicesResult, + VMDeviceIOTypeArgs, VMDeviceIOTypeResult, VMDeviceNICAttachChoicesArgs, VMDeviceNICAttachChoicesResult, + VMDeviceBindChoicesArgs, VMDeviceBindChoicesResult, +) from middlewared.plugins.vm.devices.storage_devices import IOTYPE_CHOICES from middlewared.plugins.zfs_.utils import zvol_name_to_path, zvol_path_to_name -from middlewared.schema import accepts, Bool, Dict, Int, OROperator, Patch, returns, Str from middlewared.service import CallError, CRUDService, private from middlewared.utils import run from middlewared.async_validators import check_path_resides_within_volume @@ -29,22 +35,15 @@ class VMDeviceModel(sa.Model): class VMDeviceService(CRUDService): - ENTRY = Patch( - 'vmdevice_create', 'vm_device_entry', - ('add', Int('id')), - ('rm', {'name': 'attributes'}), - ('add', OROperator(*[device.schema for device in DEVICES.values()], name='attributes')), - ) - class Config: namespace = 'vm.device' datastore = 'vm.device' datastore_extend = 'vm.device.extend_device' cli_namespace = 'service.vm.device' role_prefix = 'VM_DEVICE' + entry = VMDeviceEntry - @accepts() - @returns(Dict(additional_attrs=True, example={'vms/test 1': '/dev/zvol/vms/test+1'})) + @api_method(VMDeviceDiskChoicesArgs, VMDeviceDiskChoicesResult, roles=['VM_DEVICE_READ']) async def disk_choices(self): """ Returns disk choices for device type "DISK". @@ -63,10 +62,7 @@ async def disk_choices(self): return out - @accepts() - @returns(Dict( - *[Str(k, enum=[k]) for k in IOTYPE_CHOICES] - )) + @api_method(VMDeviceIOTypeArgs, VMDeviceIOTypeResult, roles=['VM_DEVICE_READ']) async def iotype_choices(self): """ IO-type choices for storage devices. @@ -86,16 +82,14 @@ async def extend_device(self, device): device['order'] = 1002 return device - @accepts(roles=['VM_DEVICE_READ']) - @returns(Dict(additional_attrs=True)) + @api_method(VMDeviceNICAttachChoicesArgs, VMDeviceNICAttachChoicesResult, roles=['VM_DEVICE_READ']) def nic_attach_choices(self): """ Available choices for NIC Attach attribute. """ return self.middleware.call_sync('interface.choices', {'exclude': ['epair', 'tap', 'vnet']}) - @accepts(roles=['VM_DEVICE_READ']) - @returns(Dict(additional_attrs=True)) + @api_method(VMDeviceBindChoicesArgs, VMDeviceBindChoicesResult, roles=['VM_DEVICE_READ']) async def bind_choices(self): """ Available choices for Bind attribute. @@ -139,19 +133,7 @@ async def update_device(self, data, old=None): return data - @accepts( - Dict( - 'vmdevice_create', - Int('vm', required=True), - Dict( - 'attributes', - Str('dtype', enum=['NIC', 'DISK', 'CDROM', 'PCI', 'DISPLAY', 'RAW', 'USB'], required=True), - additional_attrs=True, - ), - Int('order', default=None, null=True), - register=True, - ), - ) + @api_method(VMDeviceCreateArgs, VMDeviceCreateResult) async def do_create(self, data): """ Create a new device for the VM of id `vm`. @@ -173,6 +155,7 @@ async def do_create(self, data): return await self.get_instance(id_) + @api_method(VMDeviceUpdateArgs, VMDeviceUpdateResult) async def do_update(self, id_, data): """ Update a VM device of `id`. @@ -215,15 +198,7 @@ async def delete_resource(self, options, device): except OSError: raise CallError(f'Failed to destroy {device["attributes"]["path"]}') - @accepts( - Int('id'), - Dict( - 'vm_device_delete', - Bool('zvol', default=False), - Bool('raw_file', default=False), - Bool('force', default=False), - ) - ) + @api_method(VMDeviceDeleteArgs, VMDeviceDeleteResult) async def do_delete(self, id_, options): """ Delete a VM device of `id`. diff --git a/src/middlewared/middlewared/plugins/vm/vm_display_info.py b/src/middlewared/middlewared/plugins/vm/vm_display_info.py index 415de0aa457be..f0948fd25d106 100644 --- a/src/middlewared/middlewared/plugins/vm/vm_display_info.py +++ b/src/middlewared/middlewared/plugins/vm/vm_display_info.py @@ -1,6 +1,10 @@ from socket import AF_INET6 -from middlewared.schema import accepts, Dict, Int, List, returns, Str +from middlewared.api import api_method +from middlewared.api.current import ( + VMPortWizardArgs, VMPortWizardResult, VMResolutionChoicesArgs, VMResolutionChoicesResult, VMGetDisplayDevicesArgs, + VMGetDisplayDevicesResult, VMDisplayWebURIArgs, VMDisplayWebURIResult, +) from middlewared.service import pass_app, private, Service from .devices import DISPLAY @@ -9,12 +13,7 @@ class VMService(Service): - @accepts(roles=['VM_READ']) - @returns(Dict( - 'available_display_port', - Int('port', required=True, description='Available server port'), - Int('web', required=True, description='Web port to be used based on available `port`'), - )) + @api_method(VMPortWizardArgs, VMPortWizardResult, roles=['VM_READ']) async def port_wizard(self): """ It returns the next available Display Server Port and Web Port. @@ -40,28 +39,14 @@ async def all_used_display_device_ports(self, additional_filters=None): all_ports.extend([device['attributes']['port'], device['attributes']['web_port']]) return all_ports - @accepts() - @returns(Dict( - *[Str(r, enum=[r]) for r in DISPLAY.RESOLUTION_ENUM] - )) + @api_method(VMResolutionChoicesArgs, VMResolutionChoicesResult, roles=['VM_READ']) async def resolution_choices(self): """ Retrieve supported resolution choices for VM Display devices. """ return {r: r for r in DISPLAY.RESOLUTION_ENUM} - @accepts(Int('id'), roles=['VM_READ']) - @returns(List( - 'vmdevice', items=[ - Dict( - 'vmdevice', - Int('id'), - DISPLAY.schema, - Int('order'), - Int('vm'), - ), - ] - )) + @api_method(VMGetDisplayDevicesArgs, VMGetDisplayDevicesResult, roles=['VM_READ']) async def get_display_devices(self, id_): """ Get the display devices from a given guest. If a display device has password configured, @@ -75,20 +60,7 @@ async def get_display_devices(self, id_): devices.append(device) return devices - @accepts( - Int('id'), - Str('host', default=''), - Dict( - 'options', - Str('protocol', default='HTTP', enum=['HTTP', 'HTTPS']), - ), - roles=['VM_READ'] - ) - @returns(Dict( - 'display_devices_uri', - Str('error', null=True), - Str('uri', null=True), - )) + @api_method(VMDisplayWebURIArgs, VMDisplayWebURIResult, roles=['VM_READ']) @pass_app() async def get_display_web_uri(self, app, id_, host, options): """ diff --git a/src/middlewared/middlewared/plugins/vm/vm_lifecycle.py b/src/middlewared/middlewared/plugins/vm/vm_lifecycle.py index acb4763a7c2c1..85c5049c8c905 100644 --- a/src/middlewared/middlewared/plugins/vm/vm_lifecycle.py +++ b/src/middlewared/middlewared/plugins/vm/vm_lifecycle.py @@ -1,4 +1,8 @@ -from middlewared.schema import accepts, Bool, Dict, Int, returns +from middlewared.api import api_method +from middlewared.api.current import ( + VMStartArgs, VMStartResult, VMStopArgs, VMStopResult, VMRestartArgs, VMRestartResult, VMPoweroffArgs, + VMPoweroffResult, VMSuspendArgs, VMSuspendResult, VMResumeArgs, VMResumeResult, +) from middlewared.service import CallError, item_method, job, private, Service from .vm_supervisor import VMSupervisorMixin @@ -12,12 +16,7 @@ async def lifecycle_action_check(self): raise CallError('Requested action cannot be performed as system is not licensed to use VMs') @item_method - @accepts( - Int('id'), - Dict('options', Bool('overcommit', default=False)), - roles=['VM_WRITE'] - ) - @returns() + @api_method(VMStartArgs, VMStartResult, roles=['VM_WRITE']) async def start(self, id_, options): """ Start a VM. @@ -64,16 +63,7 @@ async def start(self, id_, options): await self.middleware.call('service.reload', 'http') @item_method - @accepts( - Int('id'), - Dict( - 'options', - Bool('force', default=False), - Bool('force_after_timeout', default=False), - ), - roles=['VM_WRITE'] - ) - @returns() + @api_method(VMStopArgs, VMStopResult, roles=['VM_WRITE']) @job(lock=lambda args: f'stop_vm_{args[0]}') def stop(self, job, id_, options): """ @@ -98,8 +88,7 @@ def stop(self, job, id_, options): self._poweroff(vm_data['name']) @item_method - @accepts(Int('id'), roles=['VM_WRITE']) - @returns() + @api_method(VMPoweroffArgs, VMPoweroffResult, roles=['VM_WRITE']) def poweroff(self, id_): """ Poweroff a VM. @@ -110,8 +99,7 @@ def poweroff(self, id_): self._poweroff(vm_data['name']) @item_method - @accepts(Int('id'), roles=['VM_WRITE']) - @returns() + @api_method(VMRestartArgs, VMRestartResult, roles=['VM_WRITE']) @job(lock=lambda args: f'restart_vm_{args[0]}') def restart(self, job, id_): """ @@ -127,8 +115,7 @@ def restart(self, job, id_): self.middleware.call_sync('vm.start', id_, {'overcommit': True}) @item_method - @accepts(Int('id'), roles=['VM_WRITE']) - @returns() + @api_method(VMSuspendArgs, VMSuspendResult, roles=['VM_WRITE']) def suspend(self, id_): """ Suspend `id` VM. @@ -139,8 +126,7 @@ def suspend(self, id_): self._suspend(vm['name']) @item_method - @accepts(Int('id'), roles=['VM_WRITE']) - @returns() + @api_method(VMResumeArgs, VMResumeResult, roles=['VM_WRITE']) def resume(self, id_): """ Resume suspended `id` VM. diff --git a/src/middlewared/middlewared/plugins/vm/vm_memory_info.py b/src/middlewared/middlewared/plugins/vm/vm_memory_info.py index 2e94d63606295..a7c2e7916ed4d 100644 --- a/src/middlewared/middlewared/plugins/vm/vm_memory_info.py +++ b/src/middlewared/middlewared/plugins/vm/vm_memory_info.py @@ -1,8 +1,11 @@ import psutil -from middlewared.schema import accepts, Bool, Dict, Int, returns, Str +from middlewared.api import api_method +from middlewared.api.current import ( + VMGetVMMemoryInfoArgs, VMGetVMMemoryInfoResult, VMGetAvailableMemoryArgs, VMGetAvailableMemoryResult, + VMGetVMemoryInUseArgs, VMGetVMemoryInUseResult, VMRandomMacArgs, VMRandomMacResult, +) from middlewared.service import CallError, Service -from middlewared.validators import MACAddr from .devices import NIC from .utils import ACTIVE_STATES @@ -10,13 +13,7 @@ class VMService(Service): - @accepts() - @returns(Dict( - 'vmemory_in_use', - Int('RNP', required=True, description='Running but not provisioned'), - Int('PRD', required=True, description='Provisioned but not running'), - Int('RPRD', required=True, description='Running and provisioned'), - )) + @api_method(VMGetVMemoryInUseArgs, VMGetVMemoryInUseResult, roles=['VM_READ']) async def get_vmemory_in_use(self): """ The total amount of virtual memory in MB used by guests @@ -37,8 +34,7 @@ async def get_vmemory_in_use(self): return memory_allocation - @accepts(Bool('overcommit', default=False), roles=['VM_READ']) - @returns(Int('available_memory')) + @api_method(VMGetAvailableMemoryArgs, VMGetAvailableMemoryResult, roles=['VM_READ']) async def get_available_memory(self, overcommit): """ Get the current maximum amount of available memory to be allocated for VMs. @@ -85,26 +81,7 @@ async def get_available_memory(self, overcommit): return max(0, total_free - vms_memory_used) - @accepts(Int('vm_id'), roles=['VM_READ']) - @returns(Dict( - Int('minimum_memory_requested', description='Minimum memory requested by the VM'), - Int('total_memory_requested', description='Maximum / total memory requested by the VM'), - Bool('overcommit_required', description='Overcommit of memory is required to start VM'), - Bool( - 'memory_req_fulfilled_after_overcommit', - description='Memory requirements of VM are fulfilled if over-committing memory is specified' - ), - Int('arc_to_shrink', description='Size of ARC to shrink in bytes', null=True), - Int('current_arc_max', description='Current size of max ARC in bytes'), - Int('arc_min', description='Minimum size of ARC in bytes'), - Int('arc_max_after_shrink', description='Size of max ARC in bytes after shrinking'), - Int( - 'actual_vm_requested_memory', - description='VM memory in bytes to consider when making calculations for available/required memory.' - ' If VM ballooning is specified for the VM, the minimum VM memory specified by user will' - ' be taken into account otherwise total VM memory requested will be taken into account.' - ), - )) + @api_method(VMGetVMMemoryInfoArgs, VMGetVMMemoryInfoResult, roles=['VM_READ']) async def get_vm_memory_info(self, vm_id): """ Returns memory information for `vm_id` VM if it is going to be started. @@ -144,8 +121,7 @@ async def get_vm_memory_info(self, vm_id): 'actual_vm_requested_memory': vm_requested_memory, } - @accepts() - @returns(Str('mac', validators=[MACAddr(separator=':')]),) + @api_method(VMRandomMacArgs, VMRandomMacResult, roles=['VM_READ']) def random_mac(self): """ Create a random mac address. diff --git a/src/middlewared/middlewared/plugins/vm/vms.py b/src/middlewared/middlewared/plugins/vm/vms.py index aaaa68f2776c0..3200dc3693937 100644 --- a/src/middlewared/middlewared/plugins/vm/vms.py +++ b/src/middlewared/middlewared/plugins/vm/vms.py @@ -9,11 +9,16 @@ import middlewared.sqlalchemy as sa +from middlewared.api import api_method +from middlewared.api.current import ( + VMEntry, VMCreateArgs, VMCreateResult, VMUpdateArgs, VMUpdateResult, VMDeleteArgs, VMDeleteResult, + VMBootloaderOVMFChoicesArgs, VMBootloaderOVMFChoicesResult, VMBootloaderOptionsArgs, VMBootloaderOptionsResult, + VMStatusArgs, VMStatusResult, VMLogFilePathArgs, VMLogFilePathResult, VMLogFileDownloadArgs, + VMLogFileDownloadResult, +) from middlewared.plugins.zfs_.utils import zvol_path_to_name -from middlewared.schema import accepts, Bool, Dict, Int, List, Patch, returns, Str, ValidationErrors -from middlewared.service import CallError, CRUDService, item_method, job, private -from middlewared.validators import Range, UUID -from middlewared.plugins.vm.numeric_set import parse_numeric_set, NumericSet +from middlewared.service import CallError, CRUDService, item_method, job, private, ValidationErrors +from middlewared.plugins.vm.numeric_set import parse_numeric_set from .utils import ACTIVE_STATES, get_default_status, get_vm_nvram_file_name, SYSTEM_NVRAM_FOLDER_PATH from .vm_supervisor import VMSupervisorMixin @@ -74,23 +79,9 @@ class Config: datastore_extend_context = 'vm.extend_context' cli_namespace = 'service.vm' role_prefix = 'VM' + entry = VMEntry - ENTRY = Patch( - 'vm_create', - 'vm_entry', - ('add', List('devices')), - ('add', Dict( - 'status', - Str('state', required=True), - Int('pid', null=True, required=True), - Str('domain_state', required=True), - )), - ('add', Bool('display_available')), - ('add', Int('id')), - ) - - @accepts(roles=['VM_READ']) - @returns(Dict(additional_attrs=True)) + @api_method(VMBootloaderOVMFChoicesArgs, VMBootloaderOVMFChoicesResult, roles=['VM_READ']) def bootloader_ovmf_choices(self): """ Retrieve bootloader ovmf choices @@ -113,10 +104,7 @@ def extend_context(self, rows, extra): 'status': status, } - @accepts(roles=['VM_READ']) - @returns(Dict( - *[Str(k, enum=[v]) for k, v in BOOT_LOADER_OPTIONS.items()], - )) + @api_method(VMBootloaderOptionsArgs, VMBootloaderOptionsResult, roles=['VM_READ']) async def bootloader_options(self): """ Supported motherboard firmware options. @@ -134,39 +122,7 @@ async def extend_vm(self, vm, context): vm['status'] = context['status'][vm['id']] return vm - @accepts(Dict( - 'vm_create', - Str('command_line_args', default=''), - Str('cpu_mode', default='CUSTOM', enum=[ - 'CUSTOM', 'HOST-MODEL', 'HOST-PASSTHROUGH']), - Str('cpu_model', default=None, null=True), - Str('name', required=True), - Str('description'), - Int('vcpus', default=1), - Int('cores', default=1), - Int('threads', default=1), - Str('cpuset', default=None, null=True, validators=[NumericSet()]), - Str('nodeset', default=None, null=True, validators=[NumericSet()]), - Bool('enable_cpu_topology_extension', default=False), - Bool('pin_vcpus', default=False), - Bool('suspend_on_snapshot', default=False), - Bool('trusted_platform_module', default=False), - Int('memory', required=True, validators=[Range(min_=20)]), - Int('min_memory', null=True, validators=[Range(min_=20)], default=None), - Bool('hyperv_enlightenments', default=False), - Str('bootloader', enum=list(BOOT_LOADER_OPTIONS.keys()), default='UEFI'), - Str('bootloader_ovmf', default='OVMF_CODE.fd'), - Bool('autostart', default=True), - Bool('hide_from_msr', default=False), - Bool('ensure_display_device', default=True), - Str('time', enum=['LOCAL', 'UTC'], default='LOCAL'), - Int('shutdown_timeout', default=90, - validators=[Range(min_=5, max_=300)]), - Str('arch_type', null=True, default=None), - Str('machine_type', null=True, default=None), - Str('uuid', null=True, default=None, validators=[UUID()]), - register=True, - )) + @api_method(VMCreateArgs, VMCreateResult) async def do_create(self, data): """ Create a Virtual Machine (VM). @@ -327,17 +283,7 @@ async def common_validation(self, verrors, schema_name, data, old=None): # with reports of users having thousands of disks # Let's validate that the VM has the correct no of slots available to accommodate currently configured devices - @accepts( - Int('id', required=True), - Patch( - 'vm_entry', - 'vm_update', - ('rm', {'name': 'devices'}), - ('rm', {'name': 'display_available'}), - ('rm', {'name': 'status'}), - ('attr', {'update': True}), - ) - ) + @api_method(VMUpdateArgs, VMUpdateResult) async def do_update(self, id_, data): """ Update all information of a specific VM. @@ -398,14 +344,7 @@ async def do_update(self, id_, data): return await self.get_instance(id_) - @accepts( - Int('id'), - Dict( - 'vm_delete', - Bool('zvols', default=False), - Bool('force', default=False), - ), - ) + @api_method(VMDeleteArgs, VMDeleteResult) async def do_delete(self, id_, data): """ Delete a VM. @@ -461,20 +400,14 @@ async def do_delete(self, id_, data): await self.middleware.call('vm.device.delete', device['id'], {'force': data['force']}) result = await self.middleware.call('datastore.delete', 'vm.vm', id_) if not await self.middleware.call('vm.query'): - await self.middleware.call('vm.deinitialize_vms') + await self.middleware.call('vm.deinitialize_vms', {'reload_ui': False}) self._clear() else: await self.middleware.call('etc.generate', 'libvirt_guests') return result @item_method - @accepts(Int('id'), roles=['VM_READ']) - @returns(Dict( - 'vm_status', - Str('state', required=True), - Int('pid', null=True, required=True), - Str('domain_state', required=True), - )) + @api_method(VMStatusArgs, VMStatusResult, roles=['VM_READ']) def status(self, id_): """ Get the status of `id` VM. @@ -498,8 +431,7 @@ def status_impl(self, vm): return get_default_status() - @accepts(Int('id'), roles=['VM_READ']) - @returns(Str(null=True)) + @api_method(VMLogFilePathArgs, VMLogFilePathResult, roles=['VM_READ']) def log_file_path(self, vm_id): """ Retrieve log file path of `id` VM. @@ -510,8 +442,7 @@ def log_file_path(self, vm_id): path = f'/var/log/libvirt/qemu/{vm["id"]}_{vm["name"]}.log' return path if os.path.exists(path) else None - @accepts(Int('id'), roles=['VM_READ']) - @returns() + @api_method(VMLogFileDownloadArgs, VMLogFileDownloadResult, roles=['VM_READ']) @job(pipes=['output']) def log_file_download(self, job, vm_id): """ diff --git a/tests/api2/test_541_vm.py b/tests/api2/test_541_vm.py index a51d253ffb83e..3bccf9da4921e 100644 --- a/tests/api2/test_541_vm.py +++ b/tests/api2/test_541_vm.py @@ -154,7 +154,7 @@ def test_013_delete_vm_devices(vm_name, request): def test_014_start_vm(vm_name, request): depends(request, ['VM_CREATED']) _id = VmAssets.VM_INFO[vm_name]['query_response']['id'] - call('vm.start', _id) + call('vm.start', _id, {'overcommit': True}) vm_status = call('vm.status', _id) assert all((vm_status[key] == 'RUNNING' for key in ('state', 'domain_state'))) assert all((vm_status['pid'], isinstance(vm_status['pid'], int))) @@ -206,6 +206,7 @@ def test_021_resume_vm(vm_name, request): else: assert False, f'Timed out after {retry} seconds waiting on {vm_name!r} to resume' + @pytest.mark.skip(reason='Takes > 60 seconds and is flaky') @pytest.mark.parametrize('vm_name', VmAssets.VM_NAMES) @pytest.mark.dependency(name='VM_RESTARTED') diff --git a/tests/api2/test_account_privilege_role_private_fields.py b/tests/api2/test_account_privilege_role_private_fields.py index 40db7537c0475..0557ccfa26c39 100644 --- a/tests/api2/test_account_privilege_role_private_fields.py +++ b/tests/api2/test_account_privilege_role_private_fields.py @@ -122,8 +122,8 @@ def vm_device(): "attributes": { "dtype": "DISPLAY", "bind": "127.0.0.1", - "port": 1, - "web_port": 1, + "port": 5900, + "web_port": 5901, "password": "pass", } }