various improvements
This commit is contained in:
		@@ -1,4 +1,4 @@
 | 
			
		||||
"""Manage QEMU guest agent."""
 | 
			
		||||
"""Interacting with the QEMU Guest Agent."""
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
@@ -19,9 +19,6 @@ from compute.exceptions import (
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
QEMU_TIMEOUT = 60
 | 
			
		||||
POLL_INTERVAL = 0.3
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GuestExecOutput(NamedTuple):
 | 
			
		||||
    """QEMU guest-exec command output."""
 | 
			
		||||
@@ -35,7 +32,7 @@ class GuestExecOutput(NamedTuple):
 | 
			
		||||
class GuestAgent:
 | 
			
		||||
    """Class for interacting with QEMU guest agent."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, domain: libvirt.virDomain, timeout: int | None = None):
 | 
			
		||||
    def __init__(self, domain: libvirt.virDomain, timeout: int = 60):
 | 
			
		||||
        """
 | 
			
		||||
        Initialise GuestAgent.
 | 
			
		||||
 | 
			
		||||
@@ -43,7 +40,7 @@ class GuestAgent:
 | 
			
		||||
        :param timeout: QEMU timeout
 | 
			
		||||
        """
 | 
			
		||||
        self.domain = domain
 | 
			
		||||
        self.timeout = timeout or QEMU_TIMEOUT
 | 
			
		||||
        self.timeout = timeout
 | 
			
		||||
        self.flags = libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT
 | 
			
		||||
        self.last_pid = None
 | 
			
		||||
 | 
			
		||||
@@ -65,9 +62,6 @@ class GuestAgent:
 | 
			
		||||
            return json.loads(output)
 | 
			
		||||
        except libvirt.libvirtError as e:
 | 
			
		||||
            if e.get_error_code() == libvirt.VIR_ERR_AGENT_UNRESPONSIVE:
 | 
			
		||||
                log.exception(
 | 
			
		||||
                    'Guest agent is unavailable on instanse=%s', self.name
 | 
			
		||||
                )
 | 
			
		||||
                raise GuestAgentUnavailableError(e) from e
 | 
			
		||||
            raise GuestAgentError(e) from e
 | 
			
		||||
 | 
			
		||||
@@ -95,9 +89,7 @@ class GuestAgent:
 | 
			
		||||
 | 
			
		||||
    def raise_for_commands(self, commands: list[str]) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Check QEMU guest agent command availability.
 | 
			
		||||
 | 
			
		||||
        Raise exception if command is not available.
 | 
			
		||||
        Raise exception if QEMU GA command is not available.
 | 
			
		||||
 | 
			
		||||
        :param commands: List of required commands
 | 
			
		||||
        :raise: GuestAgentCommandNotSupportedError
 | 
			
		||||
@@ -164,12 +156,15 @@ class GuestAgent:
 | 
			
		||||
            stderr = b64decode(stderr or '').decode('utf-8')
 | 
			
		||||
        return GuestExecOutput(exited, exitcode, stdout, stderr)
 | 
			
		||||
 | 
			
		||||
    def guest_exec_status(self, pid: int, *, poll: bool = False) -> dict:
 | 
			
		||||
    def guest_exec_status(
 | 
			
		||||
        self, pid: int, *, poll: bool = False, poll_interval: float = 0.3
 | 
			
		||||
    ) -> dict:
 | 
			
		||||
        """
 | 
			
		||||
        Execute guest-exec-status and return output.
 | 
			
		||||
 | 
			
		||||
        :param pid: PID in guest
 | 
			
		||||
        :param poll: If True poll command status with POLL_INTERVAL
 | 
			
		||||
        :param pid: PID in guest.
 | 
			
		||||
        :param poll: If True poll command status.
 | 
			
		||||
        :param poll_interval: Time between attempts to obtain command status.
 | 
			
		||||
        :return: Command output
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        """
 | 
			
		||||
@@ -185,7 +180,7 @@ class GuestAgent:
 | 
			
		||||
            command_status = self.execute(command)
 | 
			
		||||
            if command_status['return']['exited']:
 | 
			
		||||
                break
 | 
			
		||||
            sleep(POLL_INTERVAL)
 | 
			
		||||
            sleep(poll_interval)
 | 
			
		||||
            now = time()
 | 
			
		||||
            if now - start_time > self.timeout:
 | 
			
		||||
                raise GuestAgentTimeoutExceededError(self.timeout)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
"""Manage compute instances."""
 | 
			
		||||
 | 
			
		||||
__all__ = ['Instance', 'InstanceConfig']
 | 
			
		||||
__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo']
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from typing import NamedTuple
 | 
			
		||||
 | 
			
		||||
import libvirt
 | 
			
		||||
from lxml import etree
 | 
			
		||||
@@ -16,14 +16,19 @@ from compute.exceptions import (
 | 
			
		||||
from compute.utils import units
 | 
			
		||||
 | 
			
		||||
from .guest_agent import GuestAgent
 | 
			
		||||
from .schemas import CPUSchema, InstanceSchema, NetworkInterfaceSchema
 | 
			
		||||
from .schemas import (
 | 
			
		||||
    CPUEmulationMode,
 | 
			
		||||
    CPUSchema,
 | 
			
		||||
    InstanceSchema,
 | 
			
		||||
    NetworkInterfaceSchema,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InstanceConfig:
 | 
			
		||||
    """Compute instance description for libvirt."""
 | 
			
		||||
    """Compute instance config builder."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, schema: InstanceSchema):
 | 
			
		||||
        """
 | 
			
		||||
@@ -46,21 +51,33 @@ class InstanceConfig:
 | 
			
		||||
        self.network_interfaces = schema.network_interfaces
 | 
			
		||||
 | 
			
		||||
    def _gen_cpu_xml(self, cpu: CPUSchema) -> etree.Element:
 | 
			
		||||
        xml = E.cpu(match='exact', mode=cpu.emulation_mode)
 | 
			
		||||
        xml.append(E.model(cpu.model, fallback='forbid'))
 | 
			
		||||
        xml.append(E.vendor(cpu.vendor))
 | 
			
		||||
        xml.append(
 | 
			
		||||
            E.topology(
 | 
			
		||||
                sockets=str(cpu.topology.sockets),
 | 
			
		||||
                dies=str(cpu.topology.dies),
 | 
			
		||||
                cores=str(cpu.topology.cores),
 | 
			
		||||
                threads=str(cpu.topology.threads),
 | 
			
		||||
        options = {
 | 
			
		||||
            'mode': cpu.emulation_mode,
 | 
			
		||||
            'match': 'exact',
 | 
			
		||||
            'check': 'partial',
 | 
			
		||||
        }
 | 
			
		||||
        if cpu.emulation_mode == CPUEmulationMode.HOST_PASSTHROUGH:
 | 
			
		||||
            options['check'] = 'none'
 | 
			
		||||
            options['migratable'] = 'on'
 | 
			
		||||
        xml = E.cpu(**options)
 | 
			
		||||
        if cpu.model:
 | 
			
		||||
            xml.append(E.model(cpu.model, fallback='forbid'))
 | 
			
		||||
        if cpu.vendor:
 | 
			
		||||
            xml.append(E.vendor(cpu.vendor))
 | 
			
		||||
        if cpu.topology:
 | 
			
		||||
            xml.append(
 | 
			
		||||
                E.topology(
 | 
			
		||||
                    sockets=str(cpu.topology.sockets),
 | 
			
		||||
                    dies=str(cpu.topology.dies),
 | 
			
		||||
                    cores=str(cpu.topology.cores),
 | 
			
		||||
                    threads=str(cpu.topology.threads),
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        for feature in cpu.features.require:
 | 
			
		||||
            xml.append(E.feature(policy='require', name=feature))
 | 
			
		||||
        for feature in cpu.features.disable:
 | 
			
		||||
            xml.append(E.feature(policy='disable', name=feature))
 | 
			
		||||
        if cpu.features:
 | 
			
		||||
            for feature in cpu.features.require:
 | 
			
		||||
                xml.append(E.feature(policy='require', name=feature))
 | 
			
		||||
            for feature in cpu.features.disable:
 | 
			
		||||
                xml.append(E.feature(policy='disable', name=feature))
 | 
			
		||||
        return xml
 | 
			
		||||
 | 
			
		||||
    def _gen_vcpus_xml(self, vcpus: int, max_vcpus: int) -> etree.Element:
 | 
			
		||||
@@ -89,15 +106,15 @@ class InstanceConfig:
 | 
			
		||||
 | 
			
		||||
    def to_xml(self) -> str:
 | 
			
		||||
        """Return XML config for libvirt."""
 | 
			
		||||
        xml = E.domain(
 | 
			
		||||
            E.name(self.name),
 | 
			
		||||
            E.title(self.title),
 | 
			
		||||
            E.description(self.description),
 | 
			
		||||
            E.metadata(),
 | 
			
		||||
            E.memory(str(self.memory * 1024), unit='KiB'),
 | 
			
		||||
            E.currentMemory(str(self.memory * 1024), unit='KiB'),
 | 
			
		||||
            type='kvm',
 | 
			
		||||
        )
 | 
			
		||||
        xml = E.domain(type='kvm')
 | 
			
		||||
        xml.append(E.name(self.name))
 | 
			
		||||
        if self.title:
 | 
			
		||||
            xml.append(E.title(self.title))
 | 
			
		||||
        if self.description:
 | 
			
		||||
            xml.append(E.description(self.description))
 | 
			
		||||
        xml.append(E.metadata())
 | 
			
		||||
        xml.append(E.memory(str(self.max_memory * 1024), unit='KiB'))
 | 
			
		||||
        xml.append(E.currentMemory(str(self.memory * 1024), unit='KiB'))
 | 
			
		||||
        xml.append(
 | 
			
		||||
            E.vcpu(
 | 
			
		||||
                str(self.max_vcpus),
 | 
			
		||||
@@ -148,8 +165,14 @@ class InstanceConfig:
 | 
			
		||||
        return etree.tostring(xml, encoding='unicode', pretty_print=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class InstanceInfo:
 | 
			
		||||
class InstanceInfo(NamedTuple):
 | 
			
		||||
    """
 | 
			
		||||
    Store compute instance info.
 | 
			
		||||
 | 
			
		||||
    Reference:
 | 
			
		||||
    https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    state: str
 | 
			
		||||
    max_memory: int
 | 
			
		||||
    memory: int
 | 
			
		||||
@@ -193,13 +216,8 @@ class Instance:
 | 
			
		||||
        }
 | 
			
		||||
        return states[state]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def info(self) -> InstanceInfo:
 | 
			
		||||
        """
 | 
			
		||||
        Return instance info.
 | 
			
		||||
 | 
			
		||||
        https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo
 | 
			
		||||
        """
 | 
			
		||||
    def get_info(self) -> InstanceInfo:
 | 
			
		||||
        """Return instance info."""
 | 
			
		||||
        _info = self.domain.info()
 | 
			
		||||
        return InstanceInfo(
 | 
			
		||||
            state=self._expand_instance_state(_info[0]),
 | 
			
		||||
@@ -209,8 +227,7 @@ class Instance:
 | 
			
		||||
            cputime=_info[4],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def status(self) -> str:
 | 
			
		||||
    def get_status(self) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Return instance state: 'running', 'shutoff', etc.
 | 
			
		||||
 | 
			
		||||
@@ -225,7 +242,6 @@ class Instance:
 | 
			
		||||
            ) from e
 | 
			
		||||
        return self._expand_instance_state(state)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_running(self) -> bool:
 | 
			
		||||
        """Return True if instance is running, else return False."""
 | 
			
		||||
        if self.domain.isActive() != 1:
 | 
			
		||||
@@ -233,7 +249,6 @@ class Instance:
 | 
			
		||||
            return False
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_autostart(self) -> bool:
 | 
			
		||||
        """Return True if instance autostart is enabled, else return False."""
 | 
			
		||||
        try:
 | 
			
		||||
@@ -244,10 +259,18 @@ class Instance:
 | 
			
		||||
                f'instance={self.name}: {e}'
 | 
			
		||||
            ) from e
 | 
			
		||||
 | 
			
		||||
    def get_max_memory(self) -> int:
 | 
			
		||||
        """Maximum memory value for domain in KiB."""
 | 
			
		||||
        return self.domain.maxMemory()
 | 
			
		||||
 | 
			
		||||
    def get_max_vcpus(self) -> int:
 | 
			
		||||
        """Maximum vCPUs number for domain."""
 | 
			
		||||
        return self.domain.maxVcpus()
 | 
			
		||||
 | 
			
		||||
    def start(self) -> None:
 | 
			
		||||
        """Start defined instance."""
 | 
			
		||||
        log.info('Starting instnce=%s', self.name)
 | 
			
		||||
        if self.is_running:
 | 
			
		||||
        if self.is_running():
 | 
			
		||||
            log.warning(
 | 
			
		||||
                'Already started, nothing to do instance=%s', self.name
 | 
			
		||||
            )
 | 
			
		||||
@@ -311,6 +334,15 @@ class Instance:
 | 
			
		||||
                f'Cannot shutdown instance={self.name} ' f'{method=}: {e}'
 | 
			
		||||
            ) from e
 | 
			
		||||
 | 
			
		||||
    def reboot(self) -> None:
 | 
			
		||||
        """Send ACPI signal to guest OS to reboot. OS may ignore this."""
 | 
			
		||||
        try:
 | 
			
		||||
            self.domain.reboot()
 | 
			
		||||
        except libvirt.libvirtError as e:
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
                f'Cannot reboot instance={self.name}: {e}'
 | 
			
		||||
            ) from e
 | 
			
		||||
 | 
			
		||||
    def reset(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Reset instance.
 | 
			
		||||
@@ -331,14 +363,19 @@ class Instance:
 | 
			
		||||
                f'Cannot reset instance={self.name}: {e}'
 | 
			
		||||
            ) from e
 | 
			
		||||
 | 
			
		||||
    def reboot(self) -> None:
 | 
			
		||||
        """Send ACPI signal to guest OS to reboot. OS may ignore this."""
 | 
			
		||||
        try:
 | 
			
		||||
            self.domain.reboot()
 | 
			
		||||
        except libvirt.libvirtError as e:
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
                f'Cannot reboot instance={self.name}: {e}'
 | 
			
		||||
            ) from e
 | 
			
		||||
    def power_reset(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Shutdown instance and start.
 | 
			
		||||
 | 
			
		||||
        By analogy with real hardware, this is a normal server shutdown,
 | 
			
		||||
        and then turning off from the power supply and turning it on again.
 | 
			
		||||
 | 
			
		||||
        This method is applicable in cases where there has been a
 | 
			
		||||
        configuration change in libvirt and you need to restart the
 | 
			
		||||
        instance to apply the new configuration.
 | 
			
		||||
        """
 | 
			
		||||
        self.shutdown(method='NORMAL')
 | 
			
		||||
        self.start()
 | 
			
		||||
 | 
			
		||||
    def set_autostart(self, *, enabled: bool) -> None:
 | 
			
		||||
        """
 | 
			
		||||
@@ -383,7 +420,7 @@ class Instance:
 | 
			
		||||
            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
			
		||||
            self.domain.setVcpusFlags(nvcpus, flags=flags)
 | 
			
		||||
            if live is True:
 | 
			
		||||
                if not self.is_running:
 | 
			
		||||
                if not self.is_running():
 | 
			
		||||
                    log.warning(
 | 
			
		||||
                        'Instance is not running, changes applied in '
 | 
			
		||||
                        'instance config.'
 | 
			
		||||
@@ -453,7 +490,7 @@ class Instance:
 | 
			
		||||
        :param device: Object with device description e.g. DiskConfig
 | 
			
		||||
        :param live: Affect a running instance
 | 
			
		||||
        """
 | 
			
		||||
        if live and self.is_running:
 | 
			
		||||
        if live and self.is_running():
 | 
			
		||||
            flags = (
 | 
			
		||||
                libvirt.VIR_DOMAIN_AFFECT_LIVE
 | 
			
		||||
                | libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
			
		||||
@@ -471,7 +508,7 @@ class Instance:
 | 
			
		||||
        :param device: Object with device description e.g. DiskConfig
 | 
			
		||||
        :param live: Affect a running instance
 | 
			
		||||
        """
 | 
			
		||||
        if live and self.is_running:
 | 
			
		||||
        if live and self.is_running():
 | 
			
		||||
            flags = (
 | 
			
		||||
                libvirt.VIR_DOMAIN_AFFECT_LIVE
 | 
			
		||||
                | libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
			
		||||
 
 | 
			
		||||
@@ -4,11 +4,20 @@ import re
 | 
			
		||||
from enum import StrEnum
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from pydantic import BaseModel, validator
 | 
			
		||||
from pydantic import BaseModel, Extra, validator
 | 
			
		||||
 | 
			
		||||
from compute.utils.units import DataUnit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EntityModel(BaseModel):
 | 
			
		||||
    """Basic entity model."""
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        """Do not allow extra fields."""
 | 
			
		||||
 | 
			
		||||
        extra = Extra.forbid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CPUEmulationMode(StrEnum):
 | 
			
		||||
    """CPU emulation mode enumerated."""
 | 
			
		||||
 | 
			
		||||
@@ -18,7 +27,7 @@ class CPUEmulationMode(StrEnum):
 | 
			
		||||
    MAXIMUM = 'maximum'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CPUTopologySchema(BaseModel):
 | 
			
		||||
class CPUTopologySchema(EntityModel):
 | 
			
		||||
    """CPU topology model."""
 | 
			
		||||
 | 
			
		||||
    sockets: int
 | 
			
		||||
@@ -27,67 +36,66 @@ class CPUTopologySchema(BaseModel):
 | 
			
		||||
    dies: int = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CPUFeaturesSchema(BaseModel):
 | 
			
		||||
class CPUFeaturesSchema(EntityModel):
 | 
			
		||||
    """CPU features model."""
 | 
			
		||||
 | 
			
		||||
    require: list[str]
 | 
			
		||||
    disable: list[str]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CPUSchema(BaseModel):
 | 
			
		||||
class CPUSchema(EntityModel):
 | 
			
		||||
    """CPU model."""
 | 
			
		||||
 | 
			
		||||
    emulation_mode: CPUEmulationMode
 | 
			
		||||
    model: str
 | 
			
		||||
    vendor: str
 | 
			
		||||
    topology: CPUTopologySchema
 | 
			
		||||
    features: CPUFeaturesSchema
 | 
			
		||||
    model: str | None
 | 
			
		||||
    vendor: str | None
 | 
			
		||||
    topology: CPUTopologySchema | None
 | 
			
		||||
    features: CPUFeaturesSchema | None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VolumeType(StrEnum):
 | 
			
		||||
    """Storage volume types enumeration."""
 | 
			
		||||
 | 
			
		||||
    FILE = 'file'
 | 
			
		||||
    NETWORK = 'network'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VolumeCapacitySchema(BaseModel):
 | 
			
		||||
class VolumeCapacitySchema(EntityModel):
 | 
			
		||||
    """Storage volume capacity field model."""
 | 
			
		||||
 | 
			
		||||
    value: int
 | 
			
		||||
    unit: DataUnit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VolumeSchema(BaseModel):
 | 
			
		||||
class VolumeSchema(EntityModel):
 | 
			
		||||
    """Storage volume model."""
 | 
			
		||||
 | 
			
		||||
    type: VolumeType  # noqa: A003
 | 
			
		||||
    source: Path
 | 
			
		||||
    target: str
 | 
			
		||||
    capacity: VolumeCapacitySchema
 | 
			
		||||
    readonly: bool = False
 | 
			
		||||
    source: str | None = None
 | 
			
		||||
    is_readonly: bool = False
 | 
			
		||||
    is_system: bool = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NetworkInterfaceSchema(BaseModel):
 | 
			
		||||
class NetworkInterfaceSchema(EntityModel):
 | 
			
		||||
    """Network inerface model."""
 | 
			
		||||
 | 
			
		||||
    source: str
 | 
			
		||||
    mac: str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BootOptionsSchema(BaseModel):
 | 
			
		||||
class BootOptionsSchema(EntityModel):
 | 
			
		||||
    """Instance boot settings."""
 | 
			
		||||
 | 
			
		||||
    order: tuple
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InstanceSchema(BaseModel):
 | 
			
		||||
class InstanceSchema(EntityModel):
 | 
			
		||||
    """Compute instance model."""
 | 
			
		||||
 | 
			
		||||
    name: str
 | 
			
		||||
    title: str
 | 
			
		||||
    description: str
 | 
			
		||||
    title: str | None
 | 
			
		||||
    description: str | None
 | 
			
		||||
    memory: int
 | 
			
		||||
    max_memory: int
 | 
			
		||||
    vcpus: int
 | 
			
		||||
@@ -96,10 +104,10 @@ class InstanceSchema(BaseModel):
 | 
			
		||||
    machine: str
 | 
			
		||||
    emulator: Path
 | 
			
		||||
    arch: str
 | 
			
		||||
    image: str
 | 
			
		||||
    boot: BootOptionsSchema
 | 
			
		||||
    volumes: list[VolumeSchema]
 | 
			
		||||
    network_interfaces: list[NetworkInterfaceSchema]
 | 
			
		||||
    image: str | None = None
 | 
			
		||||
 | 
			
		||||
    @validator('name')
 | 
			
		||||
    def _check_name(cls, value: str) -> str:  # noqa: N805
 | 
			
		||||
@@ -111,12 +119,28 @@ class InstanceSchema(BaseModel):
 | 
			
		||||
            raise ValueError(msg)
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    @validator('volumes')
 | 
			
		||||
    def _check_volumes(cls, value: list) -> list:  # noqa: N805
 | 
			
		||||
        if len([v for v in value if v.is_system is True]) != 1:
 | 
			
		||||
            msg = 'Volumes list must contain one system volume'
 | 
			
		||||
    @validator('cpu')
 | 
			
		||||
    def _check_topology(cls, cpu: int, values: dict) -> CPUSchema:  # noqa: N805
 | 
			
		||||
        topo = cpu.topology
 | 
			
		||||
        max_vcpus = values['max_vcpus']
 | 
			
		||||
        if topo and topo.sockets * topo.cores * topo.threads != max_vcpus:
 | 
			
		||||
            msg = f'CPU topology does not match with {max_vcpus=}'
 | 
			
		||||
            raise ValueError(msg)
 | 
			
		||||
        return value
 | 
			
		||||
        return cpu
 | 
			
		||||
 | 
			
		||||
    @validator('volumes')
 | 
			
		||||
    def _check_volumes(cls, volumes: list) -> list:  # noqa: N805
 | 
			
		||||
        if len([v for v in volumes if v.is_system is True]) != 1:
 | 
			
		||||
            msg = 'volumes list must contain one system volume'
 | 
			
		||||
            raise ValueError(msg)
 | 
			
		||||
        vol_with_source = 0
 | 
			
		||||
        for vol in volumes:
 | 
			
		||||
            if vol.is_system is True and vol.is_readonly is True:
 | 
			
		||||
                msg = 'volume marked as system cannot be readonly'
 | 
			
		||||
                raise ValueError(msg)
 | 
			
		||||
            if vol.source is not None:
 | 
			
		||||
                vol_with_source += 1
 | 
			
		||||
        return volumes
 | 
			
		||||
 | 
			
		||||
    @validator('network_interfaces')
 | 
			
		||||
    def _check_network_interfaces(cls, value: list) -> list:  # noqa: N805
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user