v0.1.0-dev4
This commit is contained in:
		@@ -137,7 +137,7 @@ class CloudInit:
 | 
			
		||||
        subprocess.run(
 | 
			
		||||
            ['/usr/sbin/mkfs.vfat', '-n', 'CIDATA', '-C', str(disk), '1024'],
 | 
			
		||||
            check=True,
 | 
			
		||||
            stderr=subprocess.DEVNULL,
 | 
			
		||||
            stdout=subprocess.DEVNULL,
 | 
			
		||||
        )
 | 
			
		||||
        self._write_to_disk(
 | 
			
		||||
            disk=disk,
 | 
			
		||||
 
 | 
			
		||||
@@ -92,6 +92,8 @@ class DiskConfig(DeviceConfig):
 | 
			
		||||
                encoding='unicode',
 | 
			
		||||
                pretty_print=True,
 | 
			
		||||
            ).strip()
 | 
			
		||||
        source = xml.find('source')
 | 
			
		||||
        target = xml.find('target')
 | 
			
		||||
        driver = xml.find('driver')
 | 
			
		||||
        cachetype = driver.get('cache')
 | 
			
		||||
        disk_params = {
 | 
			
		||||
@@ -102,14 +104,14 @@ class DiskConfig(DeviceConfig):
 | 
			
		||||
                type=driver.get('type'),
 | 
			
		||||
                **({'cache': cachetype} if cachetype else {}),
 | 
			
		||||
            ),
 | 
			
		||||
            'source': xml.find('source').get('file'),
 | 
			
		||||
            'target': xml.find('target').get('dev'),
 | 
			
		||||
            'bus': xml.find('target').get('bus'),
 | 
			
		||||
            'source': source.get('file') if source is not None else None,
 | 
			
		||||
            'target': target.get('dev') if target is not None else None,
 | 
			
		||||
            'bus': target.get('bus') if target is not None else None,
 | 
			
		||||
            'is_readonly': False if xml.find('readonly') is None else True,
 | 
			
		||||
        }
 | 
			
		||||
        for param in disk_params:
 | 
			
		||||
            if disk_params[param] is None:
 | 
			
		||||
                msg = f"missing XML tag '{param}'"
 | 
			
		||||
                msg = f"missing tag '{param}'"
 | 
			
		||||
                raise InvalidDeviceConfigError(msg, xml_str)
 | 
			
		||||
            if param == 'driver':
 | 
			
		||||
                driver = disk_params[param]
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ __all__ = ['Instance', 'InstanceConfig', 'InstanceInfo']
 | 
			
		||||
import logging
 | 
			
		||||
import time
 | 
			
		||||
from typing import NamedTuple
 | 
			
		||||
from uuid import UUID
 | 
			
		||||
 | 
			
		||||
import libvirt
 | 
			
		||||
from lxml import etree
 | 
			
		||||
@@ -66,7 +67,7 @@ class InstanceConfig(EntityConfig):
 | 
			
		||||
        self.emulator = schema.emulator
 | 
			
		||||
        self.arch = schema.arch
 | 
			
		||||
        self.boot = schema.boot
 | 
			
		||||
        self.network_interfaces = schema.network_interfaces
 | 
			
		||||
        self.network = schema.network
 | 
			
		||||
 | 
			
		||||
    def _gen_cpu_xml(self, cpu: CPUSchema) -> etree.Element:
 | 
			
		||||
        options = {
 | 
			
		||||
@@ -119,6 +120,7 @@ class InstanceConfig(EntityConfig):
 | 
			
		||||
        return E.interface(
 | 
			
		||||
            E.source(network=interface.source),
 | 
			
		||||
            E.mac(address=interface.mac),
 | 
			
		||||
            E.model(type=interface.model),
 | 
			
		||||
            type='network',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -165,8 +167,9 @@ class InstanceConfig(EntityConfig):
 | 
			
		||||
        )
 | 
			
		||||
        devices = E.devices()
 | 
			
		||||
        devices.append(E.emulator(str(self.emulator)))
 | 
			
		||||
        for interface in self.network_interfaces:
 | 
			
		||||
            devices.append(self._gen_network_interface_xml(interface))
 | 
			
		||||
        if self.network:
 | 
			
		||||
            for interface in self.network.interfaces:
 | 
			
		||||
                devices.append(self._gen_network_interface_xml(interface))
 | 
			
		||||
        devices.append(E.graphics(type='vnc', autoport='yes'))
 | 
			
		||||
        devices.append(E.input(type='tablet', bus='usb'))
 | 
			
		||||
        devices.append(
 | 
			
		||||
@@ -212,18 +215,14 @@ class Instance:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, domain: libvirt.virDomain):
 | 
			
		||||
        """
 | 
			
		||||
        Initialise Instance.
 | 
			
		||||
 | 
			
		||||
        :ivar libvirt.virDomain domain: domain object
 | 
			
		||||
        :ivar libvirt.virConnect connection: connection object
 | 
			
		||||
        :ivar str name: domain name
 | 
			
		||||
        :ivar GuestAgent guest_agent: :class:`GuestAgent` object
 | 
			
		||||
        Initialise Compute Instance object.
 | 
			
		||||
 | 
			
		||||
        :param domain: libvirt domain object
 | 
			
		||||
        """
 | 
			
		||||
        self._domain = domain
 | 
			
		||||
        self._connection = domain.connect()
 | 
			
		||||
        self._name = domain.name()
 | 
			
		||||
        self._uuid = domain.UUID()
 | 
			
		||||
        self._guest_agent = GuestAgent(domain)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
@@ -241,6 +240,11 @@ class Instance:
 | 
			
		||||
        """Instance name."""
 | 
			
		||||
        return self._name
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def uuid(self) -> UUID:
 | 
			
		||||
        """Instance UUID."""
 | 
			
		||||
        return UUID(bytes=self._uuid)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def guest_agent(self) -> GuestAgent:
 | 
			
		||||
        """:class:`GuestAgent` object."""
 | 
			
		||||
@@ -287,10 +291,9 @@ class Instance:
 | 
			
		||||
 | 
			
		||||
    def is_running(self) -> bool:
 | 
			
		||||
        """Return True if instance is running, else return False."""
 | 
			
		||||
        if self.domain.isActive() != 1:
 | 
			
		||||
            # 0 - is inactive, -1 - is error
 | 
			
		||||
            return False
 | 
			
		||||
        return True
 | 
			
		||||
        if self.domain.isActive() == 1:
 | 
			
		||||
            return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def is_autostart(self) -> bool:
 | 
			
		||||
        """Return True if instance autostart is enabled, else return False."""
 | 
			
		||||
@@ -318,7 +321,7 @@ class Instance:
 | 
			
		||||
        log.info("Starting instance '%s'", self.name)
 | 
			
		||||
        if self.is_running():
 | 
			
		||||
            log.warning(
 | 
			
		||||
                'Already started, nothing to do instance=%s', self.name
 | 
			
		||||
                "Instance '%s' is already started, nothing to do", self.name
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
        try:
 | 
			
		||||
@@ -347,8 +350,8 @@ class Instance:
 | 
			
		||||
            to unplugging machine from power. Internally send SIGTERM to
 | 
			
		||||
            instance process and destroy it gracefully.
 | 
			
		||||
 | 
			
		||||
        UNSAFE
 | 
			
		||||
            Force shutdown. Internally send SIGKILL to instance process.
 | 
			
		||||
        DESTROY
 | 
			
		||||
            Forced shutdown. Internally send SIGKILL to instance process.
 | 
			
		||||
            There is high data corruption risk!
 | 
			
		||||
 | 
			
		||||
        If method is None NORMAL method will used.
 | 
			
		||||
@@ -361,7 +364,7 @@ class Instance:
 | 
			
		||||
            'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
 | 
			
		||||
            'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT,
 | 
			
		||||
            'HARD': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL,
 | 
			
		||||
            'UNSAFE': libvirt.VIR_DOMAIN_DESTROY_DEFAULT,
 | 
			
		||||
            'DESTROY': libvirt.VIR_DOMAIN_DESTROY_DEFAULT,
 | 
			
		||||
        }
 | 
			
		||||
        if method is None:
 | 
			
		||||
            method = 'NORMAL'
 | 
			
		||||
@@ -372,11 +375,13 @@ class Instance:
 | 
			
		||||
        method = method.upper()
 | 
			
		||||
        if method not in methods:
 | 
			
		||||
            raise ValueError(f"Unsupported shutdown method: '{method}'")
 | 
			
		||||
        if method == 'SOFT' and self.guest_agent.is_available() is False:
 | 
			
		||||
            method = 'NORMAL'
 | 
			
		||||
        log.info("Performing instance shutdown with method '%s'", method)
 | 
			
		||||
        try:
 | 
			
		||||
            if method in ['SOFT', 'NORMAL']:
 | 
			
		||||
                self.domain.shutdownFlags(flags=methods[method])
 | 
			
		||||
            elif method in ['HARD', 'UNSAFE']:
 | 
			
		||||
            elif method in ['HARD', 'DESTROY']:
 | 
			
		||||
                self.domain.destroyFlags(flags=methods[method])
 | 
			
		||||
        except libvirt.libvirtError as e:
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
@@ -443,8 +448,7 @@ class Instance:
 | 
			
		||||
            self.domain.setAutostart(autostart)
 | 
			
		||||
        except libvirt.libvirtError as e:
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
                f'Cannot set autostart flag for instance={self.name} '
 | 
			
		||||
                f'{autostart=}: {e}'
 | 
			
		||||
                f"Cannot set {autostart=} flag for instance '{self.name}': {e}"
 | 
			
		||||
            ) from e
 | 
			
		||||
 | 
			
		||||
    def set_vcpus(self, nvcpus: int, *, live: bool = False) -> None:
 | 
			
		||||
@@ -466,7 +470,7 @@ class Instance:
 | 
			
		||||
            raise InstanceError('vCPUs count is greather than max_vcpus')
 | 
			
		||||
        if nvcpus == self.get_info().nproc:
 | 
			
		||||
            log.warning(
 | 
			
		||||
                'Instance instance=%s already have %s vCPUs, nothing to do',
 | 
			
		||||
                "Instance '%s' already have %s vCPUs, nothing to do",
 | 
			
		||||
                self.name,
 | 
			
		||||
                nvcpus,
 | 
			
		||||
            )
 | 
			
		||||
@@ -492,18 +496,17 @@ class Instance:
 | 
			
		||||
                        self.domain.setVcpusFlags(nvcpus, flags=flags)
 | 
			
		||||
                    except GuestAgentCommandNotSupportedError:
 | 
			
		||||
                        log.warning(
 | 
			
		||||
                            'Cannot set vCPUs in guest via agent, you may '
 | 
			
		||||
                            'need to apply changes in guest manually.'
 | 
			
		||||
                            "'guest-set-vcpus' command is not supported, '"
 | 
			
		||||
                            'you may need to enable CPUs in guest manually.'
 | 
			
		||||
                        )
 | 
			
		||||
                else:
 | 
			
		||||
                    log.warning(
 | 
			
		||||
                        'Cannot set vCPUs in guest OS on instance=%s. '
 | 
			
		||||
                        'You may need to apply CPUs in guest manually.',
 | 
			
		||||
                        self.name,
 | 
			
		||||
                        'Guest agent is not installed or not connected, '
 | 
			
		||||
                        'you may need to enable CPUs in guest manually.'
 | 
			
		||||
                    )
 | 
			
		||||
        except libvirt.libvirtError as e:
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
                f'Cannot set vCPUs for instance={self.name}: {e}'
 | 
			
		||||
                f"Cannot set vCPUs for instance '{self.name}': {e}"
 | 
			
		||||
            ) from e
 | 
			
		||||
 | 
			
		||||
    def set_memory(self, memory: int, *, live: bool = False) -> None:
 | 
			
		||||
 
 | 
			
		||||
@@ -16,11 +16,11 @@
 | 
			
		||||
"""Compute instance related objects schemas."""
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
from collections import Counter
 | 
			
		||||
from enum import StrEnum
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from pydantic import ValidationError, validator
 | 
			
		||||
from pydantic.error_wrappers import ErrorWrapper
 | 
			
		||||
from pydantic import validator
 | 
			
		||||
 | 
			
		||||
from compute.abstract import EntityModel
 | 
			
		||||
from compute.utils.units import DataUnit
 | 
			
		||||
@@ -74,12 +74,30 @@ class VolumeCapacitySchema(EntityModel):
 | 
			
		||||
    unit: DataUnit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiskCache(StrEnum):
 | 
			
		||||
    """Possible disk cache mechanisms enumeration."""
 | 
			
		||||
 | 
			
		||||
    NONE = 'none'
 | 
			
		||||
    WRITETHROUGH = 'writethrough'
 | 
			
		||||
    WRITEBACK = 'writeback'
 | 
			
		||||
    DIRECTSYNC = 'directsync'
 | 
			
		||||
    UNSAFE = 'unsafe'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiskDriverSchema(EntityModel):
 | 
			
		||||
    """Virtual disk driver model."""
 | 
			
		||||
 | 
			
		||||
    name: str
 | 
			
		||||
    type: str  # noqa: A003
 | 
			
		||||
    cache: str = 'writethrough'
 | 
			
		||||
    cache: DiskCache = DiskCache.WRITETHROUGH
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiskBus(StrEnum):
 | 
			
		||||
    """Possible disk buses enumeration."""
 | 
			
		||||
 | 
			
		||||
    VIRTIO = 'virtio'
 | 
			
		||||
    IDE = 'ide'
 | 
			
		||||
    SATA = 'sata'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VolumeSchema(EntityModel):
 | 
			
		||||
@@ -92,15 +110,30 @@ class VolumeSchema(EntityModel):
 | 
			
		||||
    source: str | None = None
 | 
			
		||||
    is_readonly: bool = False
 | 
			
		||||
    is_system: bool = False
 | 
			
		||||
    bus: str = 'virtio'
 | 
			
		||||
    bus: DiskBus = DiskBus.VIRTIO
 | 
			
		||||
    device: str = 'disk'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NetworkAdapterModel(StrEnum):
 | 
			
		||||
    """Network adapter models."""
 | 
			
		||||
 | 
			
		||||
    VIRTIO = 'virtio'
 | 
			
		||||
    E1000 = 'e1000'
 | 
			
		||||
    RTL8139 = 'rtl8139'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NetworkInterfaceSchema(EntityModel):
 | 
			
		||||
    """Network inerface model."""
 | 
			
		||||
 | 
			
		||||
    source: str
 | 
			
		||||
    mac: str
 | 
			
		||||
    model: NetworkAdapterModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NetworkSchema(EntityModel):
 | 
			
		||||
    """Network configuration schema."""
 | 
			
		||||
 | 
			
		||||
    interfaces: list[NetworkInterfaceSchema]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BootOptionsSchema(EntityModel):
 | 
			
		||||
@@ -134,7 +167,7 @@ class InstanceSchema(EntityModel):
 | 
			
		||||
    arch: str
 | 
			
		||||
    boot: BootOptionsSchema
 | 
			
		||||
    volumes: list[VolumeSchema]
 | 
			
		||||
    network_interfaces: list[NetworkInterfaceSchema]
 | 
			
		||||
    network: NetworkSchema | None | bool
 | 
			
		||||
    image: str | None = None
 | 
			
		||||
    cloud_init: CloudInitSchema | None = None
 | 
			
		||||
 | 
			
		||||
@@ -142,7 +175,7 @@ class InstanceSchema(EntityModel):
 | 
			
		||||
    def _check_name(cls, value: str) -> str:  # noqa: N805
 | 
			
		||||
        if not re.match(r'^[a-z0-9_-]+$', value):
 | 
			
		||||
            msg = (
 | 
			
		||||
                'Name can contain only lowercase letters, numbers, '
 | 
			
		||||
                'Name must contain only lowercase letters, numbers, '
 | 
			
		||||
                'minus sign and underscore.'
 | 
			
		||||
            )
 | 
			
		||||
            raise ValueError(msg)
 | 
			
		||||
@@ -162,27 +195,33 @@ class InstanceSchema(EntityModel):
 | 
			
		||||
        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)
 | 
			
		||||
        for vol in volumes:
 | 
			
		||||
            if vol.source is None and vol.capacity is None:
 | 
			
		||||
                raise ValidationError(
 | 
			
		||||
                    [
 | 
			
		||||
                        ErrorWrapper(
 | 
			
		||||
                            Exception(
 | 
			
		||||
                                "capacity is required if 'source' is unset"
 | 
			
		||||
                            ),
 | 
			
		||||
                            loc='X.capacity',
 | 
			
		||||
                        )
 | 
			
		||||
                    ],
 | 
			
		||||
                    model=VolumeSchema,
 | 
			
		||||
                )
 | 
			
		||||
            if vol.is_system is True and vol.is_readonly is True:
 | 
			
		||||
        index = 0
 | 
			
		||||
        for volume in volumes:
 | 
			
		||||
            index += 1
 | 
			
		||||
            if volume.source is None and volume.capacity is None:
 | 
			
		||||
                msg = f"{index}: capacity is required if 'source' is unset"
 | 
			
		||||
                raise ValueError(msg)
 | 
			
		||||
            if volume.is_system is True and volume.is_readonly is True:
 | 
			
		||||
                msg = 'volume marked as system cannot be readonly'
 | 
			
		||||
                raise ValueError(msg)
 | 
			
		||||
        sources = [v.source for v in volumes if v.source is not None]
 | 
			
		||||
        targets = [v.target for v in volumes]
 | 
			
		||||
        for item in [sources, targets]:
 | 
			
		||||
            duplicates = Counter(item) - Counter(set(item))
 | 
			
		||||
            if duplicates:
 | 
			
		||||
                msg = f'find duplicate values: {list(duplicates)}'
 | 
			
		||||
                raise ValueError(msg)
 | 
			
		||||
        return volumes
 | 
			
		||||
 | 
			
		||||
    @validator('network_interfaces')
 | 
			
		||||
    def _check_network_interfaces(cls, value: list) -> list:  # noqa: N805
 | 
			
		||||
        if not value:
 | 
			
		||||
            msg = 'Network interfaces list must contain at least one element'
 | 
			
		||||
    @validator('network')
 | 
			
		||||
    def _check_network(
 | 
			
		||||
        cls,  # noqa: N805
 | 
			
		||||
        network: NetworkSchema | None | bool,
 | 
			
		||||
    ) -> NetworkSchema | None | bool:
 | 
			
		||||
        if network is True:
 | 
			
		||||
            msg = (
 | 
			
		||||
                "'network' cannot be True, set it to False "
 | 
			
		||||
                'or provide network configuration'
 | 
			
		||||
            )
 | 
			
		||||
            raise ValueError(msg)
 | 
			
		||||
        return value
 | 
			
		||||
        return network
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user