various improvements
This commit is contained in:
		@@ -5,13 +5,13 @@
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# Ansible is distributed in the hope that it will be useful,
 | 
			
		||||
# Compute is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
from .guest_agent import GuestAgent
 | 
			
		||||
from .instance import Instance, InstanceConfig
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										128
									
								
								compute/instance/devices.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								compute/instance/devices.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
			
		||||
# This file is part of Compute
 | 
			
		||||
#
 | 
			
		||||
# Compute is free software: you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU General Public License as published by
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# Compute is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
# ruff: noqa: SIM211, UP007, A003
 | 
			
		||||
 | 
			
		||||
"""Virtual devices configs."""
 | 
			
		||||
 | 
			
		||||
from dataclasses import dataclass, field
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
from lxml import etree
 | 
			
		||||
from lxml.builder import E
 | 
			
		||||
 | 
			
		||||
from compute.common import DeviceConfig
 | 
			
		||||
from compute.exceptions import InvalidDeviceConfigError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class DiskDriver:
 | 
			
		||||
    """Disk driver description for libvirt."""
 | 
			
		||||
 | 
			
		||||
    name: str
 | 
			
		||||
    type: str
 | 
			
		||||
    cache: str
 | 
			
		||||
 | 
			
		||||
    def __call__(self):
 | 
			
		||||
        """Return self."""
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class DiskConfig(DeviceConfig):
 | 
			
		||||
    """
 | 
			
		||||
    Disk config builder.
 | 
			
		||||
 | 
			
		||||
    Generate XML config for attaching or detaching storage volumes
 | 
			
		||||
    to compute instances.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    type: str
 | 
			
		||||
    source: str | Path
 | 
			
		||||
    target: str
 | 
			
		||||
    is_readonly: bool = False
 | 
			
		||||
    device: str = 'disk'
 | 
			
		||||
    bus: str = 'virtio'
 | 
			
		||||
    driver: DiskDriver = field(
 | 
			
		||||
        default_factory=DiskDriver(
 | 
			
		||||
            name='qemu',
 | 
			
		||||
            type='qcow2',
 | 
			
		||||
            cache='writethrough',
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def to_xml(self) -> str:
 | 
			
		||||
        """Return XML config for libvirt."""
 | 
			
		||||
        xml = E.disk(type=self.type, device=self.device)
 | 
			
		||||
        xml.append(
 | 
			
		||||
            E.driver(
 | 
			
		||||
                name=self.driver.name,
 | 
			
		||||
                type=self.driver.type,
 | 
			
		||||
                cache=self.driver.cache,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        if self.source and self.type == 'file':
 | 
			
		||||
            xml.append(E.source(file=str(self.source)))
 | 
			
		||||
        xml.append(E.target(dev=self.target, bus=self.bus))
 | 
			
		||||
        if self.is_readonly:
 | 
			
		||||
            xml.append(E.readonly())
 | 
			
		||||
        return etree.tostring(xml, encoding='unicode', pretty_print=True)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_xml(cls, xml: Union[str, etree.Element]) -> 'DiskConfig':
 | 
			
		||||
        """
 | 
			
		||||
        Create :class:`DiskConfig` instance from XML config.
 | 
			
		||||
 | 
			
		||||
        :param xml: Disk device XML configuration as :class:`str` or lxml
 | 
			
		||||
            :class:`etree.Element` object.
 | 
			
		||||
        """
 | 
			
		||||
        if isinstance(xml, str):
 | 
			
		||||
            xml_str = xml
 | 
			
		||||
            xml = etree.fromstring(xml)
 | 
			
		||||
        else:
 | 
			
		||||
            xml_str = etree.tostring(
 | 
			
		||||
                xml,
 | 
			
		||||
                encoding='unicode',
 | 
			
		||||
                pretty_print=True,
 | 
			
		||||
            ).strip()
 | 
			
		||||
        driver = xml.find('driver')
 | 
			
		||||
        disk_params = {
 | 
			
		||||
            'type': xml.get('type'),
 | 
			
		||||
            'device': xml.get('device'),
 | 
			
		||||
            'driver': DiskDriver(
 | 
			
		||||
                name=driver.get('name'),
 | 
			
		||||
                type=driver.get('type'),
 | 
			
		||||
                cache=driver.get('cache'),
 | 
			
		||||
            ),
 | 
			
		||||
            'source': xml.find('source').get('file'),
 | 
			
		||||
            'target': xml.find('target').get('dev'),
 | 
			
		||||
            'bus': xml.find('target').get('bus'),
 | 
			
		||||
            '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}'"
 | 
			
		||||
                raise InvalidDeviceConfigError(msg, xml_str)
 | 
			
		||||
            if param == 'driver':
 | 
			
		||||
                driver = disk_params[param]
 | 
			
		||||
                for driver_param in [driver.name, driver.type, driver.cache]:
 | 
			
		||||
                    if driver_param is None:
 | 
			
		||||
                        msg = (
 | 
			
		||||
                            "'driver' tag must have "
 | 
			
		||||
                            "'name', 'type' and 'cache' attributes"
 | 
			
		||||
                        )
 | 
			
		||||
                        raise InvalidDeviceConfigError(msg, xml_str)
 | 
			
		||||
        return cls(**disk_params)
 | 
			
		||||
@@ -5,13 +5,13 @@
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# Ansible is distributed in the hope that it will be useful,
 | 
			
		||||
# Compute is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
"""Interacting with the QEMU Guest Agent."""
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,19 +5,20 @@
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# Ansible is distributed in the hope that it will be useful,
 | 
			
		||||
# Compute is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
"""Manage compute instances."""
 | 
			
		||||
 | 
			
		||||
__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo']
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
import time
 | 
			
		||||
from typing import NamedTuple
 | 
			
		||||
 | 
			
		||||
import libvirt
 | 
			
		||||
@@ -29,9 +30,9 @@ from compute.exceptions import (
 | 
			
		||||
    GuestAgentCommandNotSupportedError,
 | 
			
		||||
    InstanceError,
 | 
			
		||||
)
 | 
			
		||||
from compute.storage import DiskConfig
 | 
			
		||||
from compute.utils import units
 | 
			
		||||
 | 
			
		||||
from .devices import DiskConfig
 | 
			
		||||
from .guest_agent import GuestAgent
 | 
			
		||||
from .schemas import (
 | 
			
		||||
    CPUEmulationMode,
 | 
			
		||||
@@ -282,7 +283,7 @@ class Instance:
 | 
			
		||||
 | 
			
		||||
    def start(self) -> None:
 | 
			
		||||
        """Start defined instance."""
 | 
			
		||||
        log.info('Starting instnce=%s', self.name)
 | 
			
		||||
        log.info("Starting instance '%s'", self.name)
 | 
			
		||||
        if self.is_running():
 | 
			
		||||
            log.warning(
 | 
			
		||||
                'Already started, nothing to do instance=%s', self.name
 | 
			
		||||
@@ -292,7 +293,7 @@ class Instance:
 | 
			
		||||
            self.domain.create()
 | 
			
		||||
        except libvirt.libvirtError as e:
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
                f'Cannot start instance={self.name}: {e}'
 | 
			
		||||
                f"Cannot start instance '{self.name}': {e}"
 | 
			
		||||
            ) from e
 | 
			
		||||
 | 
			
		||||
    def shutdown(self, method: str | None = None) -> None:
 | 
			
		||||
@@ -323,6 +324,7 @@ class Instance:
 | 
			
		||||
        :param method: Method used to shutdown instance
 | 
			
		||||
        """
 | 
			
		||||
        if not self.is_running():
 | 
			
		||||
            log.warning('Instance is not running, nothing to do')
 | 
			
		||||
            return
 | 
			
		||||
        methods = {
 | 
			
		||||
            'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
 | 
			
		||||
@@ -339,6 +341,7 @@ class Instance:
 | 
			
		||||
        method = method.upper()
 | 
			
		||||
        if method not in methods:
 | 
			
		||||
            raise ValueError(f"Unsupported shutdown method: '{method}'")
 | 
			
		||||
        log.info("Performing instance shutdown with method '%s'", method)
 | 
			
		||||
        try:
 | 
			
		||||
            if method in ['SOFT', 'NORMAL']:
 | 
			
		||||
                self.domain.shutdownFlags(flags=methods[method])
 | 
			
		||||
@@ -346,7 +349,7 @@ class Instance:
 | 
			
		||||
                self.domain.destroyFlags(flags=methods[method])
 | 
			
		||||
        except libvirt.libvirtError as e:
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
                f'Cannot shutdown instance={self.name} ' f'{method=}: {e}'
 | 
			
		||||
                f"Cannot shutdown instance '{self.name}' with '{method=}': {e}"
 | 
			
		||||
            ) from e
 | 
			
		||||
 | 
			
		||||
    def reboot(self) -> None:
 | 
			
		||||
@@ -375,7 +378,7 @@ class Instance:
 | 
			
		||||
            self.domain.reset()
 | 
			
		||||
        except libvirt.libvirtError as e:
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
                f'Cannot reset instance={self.name}: {e}'
 | 
			
		||||
                f"Cannot reset instance '{self.name}': {e}"
 | 
			
		||||
            ) from e
 | 
			
		||||
 | 
			
		||||
    def power_reset(self) -> None:
 | 
			
		||||
@@ -389,7 +392,13 @@ class Instance:
 | 
			
		||||
        configuration change in libvirt and you need to restart the
 | 
			
		||||
        instance to apply the new configuration.
 | 
			
		||||
        """
 | 
			
		||||
        self.shutdown(method='NORMAL')
 | 
			
		||||
        log.debug("Performing power reset for instance '%s'", self.name)
 | 
			
		||||
        self.shutdown('NORMAL')
 | 
			
		||||
        time.sleep(3)
 | 
			
		||||
        # TODO @ge: do safe shutdown insted of this shit
 | 
			
		||||
        if self.is_running():
 | 
			
		||||
            self.shutdown('HARD')
 | 
			
		||||
        time.sleep(1)
 | 
			
		||||
        self.start()
 | 
			
		||||
 | 
			
		||||
    def set_autostart(self, *, enabled: bool) -> None:
 | 
			
		||||
@@ -550,7 +559,9 @@ class Instance:
 | 
			
		||||
                return
 | 
			
		||||
        self.domain.detachDeviceFlags(device.to_xml(), flags=flags)
 | 
			
		||||
 | 
			
		||||
    def get_disk(self, name: str) -> DiskConfig | None:
 | 
			
		||||
    def get_disk(
 | 
			
		||||
        self, name: str, *, persistent: bool = False
 | 
			
		||||
    ) -> DiskConfig | None:
 | 
			
		||||
        """
 | 
			
		||||
        Return :class:`DiskConfig` by disk target name.
 | 
			
		||||
 | 
			
		||||
@@ -558,20 +569,27 @@ class Instance:
 | 
			
		||||
 | 
			
		||||
        :param name: Disk name e.g. `vda`, `sda`, etc. This name may
 | 
			
		||||
            not match the name of the disk inside the guest OS.
 | 
			
		||||
        :param persistent: If True get only persistent volumes described
 | 
			
		||||
            in instance XML config.
 | 
			
		||||
        """
 | 
			
		||||
        xml = etree.fromstring(self.dump_xml())
 | 
			
		||||
        xml = etree.fromstring(self.dump_xml(inactive=persistent))
 | 
			
		||||
        child = xml.xpath(f'/domain/devices/disk/target[@dev="{name}"]')
 | 
			
		||||
        if len(child) == 0:
 | 
			
		||||
            return None
 | 
			
		||||
        return DiskConfig.from_xml(child[0].getparent())
 | 
			
		||||
 | 
			
		||||
    def list_disks(self) -> list[DiskConfig]:
 | 
			
		||||
        """Return list of attached disk devices."""
 | 
			
		||||
        xml = etree.fromstring(self.dump_xml())
 | 
			
		||||
    def list_disks(self, *, persistent: bool = False) -> list[DiskConfig]:
 | 
			
		||||
        """
 | 
			
		||||
        Return list of attached disk devices.
 | 
			
		||||
 | 
			
		||||
        :param persistent: If True list only persistent volumes described
 | 
			
		||||
            in instance XML config.
 | 
			
		||||
        """
 | 
			
		||||
        xml = etree.fromstring(self.dump_xml(inactive=persistent))
 | 
			
		||||
        disks = xml.xpath('/domain/devices/disk')
 | 
			
		||||
        return [DiskConfig.from_xml(disk) for disk in disks]
 | 
			
		||||
 | 
			
		||||
    def detach_disk(self, name: str) -> None:
 | 
			
		||||
    def detach_disk(self, name: str, *, live: bool = False) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Detach disk device by target name.
 | 
			
		||||
 | 
			
		||||
@@ -580,15 +598,17 @@ class Instance:
 | 
			
		||||
 | 
			
		||||
        :param name: Disk name e.g. `vda`, `sda`, etc. This name may
 | 
			
		||||
            not match the name of the disk inside the guest OS.
 | 
			
		||||
        :param live: Affect a running instance. Not supported for CDROM
 | 
			
		||||
            devices.
 | 
			
		||||
        """
 | 
			
		||||
        disk = self.get_disk(name)
 | 
			
		||||
        disk = self.get_disk(name, persistent=live)
 | 
			
		||||
        if disk is None:
 | 
			
		||||
            log.warning(
 | 
			
		||||
                "Volume with target '%s' is already detached",
 | 
			
		||||
                name,
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
        self.detach_device(disk, live=True)
 | 
			
		||||
        self.detach_device(disk, live=live)
 | 
			
		||||
 | 
			
		||||
    def resize_disk(
 | 
			
		||||
        self, name: str, capacity: int, unit: units.DataUnit
 | 
			
		||||
@@ -601,6 +621,7 @@ class Instance:
 | 
			
		||||
        :param capacity: New capacity.
 | 
			
		||||
        :param unit: Capacity unit.
 | 
			
		||||
        """
 | 
			
		||||
        # TODO @ge: check actual size before making changes
 | 
			
		||||
        self.domain.blockResize(
 | 
			
		||||
            name,
 | 
			
		||||
            units.to_bytes(capacity, unit=unit),
 | 
			
		||||
@@ -619,7 +640,7 @@ class Instance:
 | 
			
		||||
 | 
			
		||||
    def list_ssh_keys(self, user: str) -> list[str]:
 | 
			
		||||
        """
 | 
			
		||||
        Return list of SSH keys on guest for specific user.
 | 
			
		||||
        Return list of authorized SSH keys in guest for specific user.
 | 
			
		||||
 | 
			
		||||
        :param user: Username.
 | 
			
		||||
        """
 | 
			
		||||
@@ -655,7 +676,7 @@ class Instance:
 | 
			
		||||
        append: bool = False,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Add SSH keys to guest for specific user.
 | 
			
		||||
        Add authorized SSH keys to guest for specific user.
 | 
			
		||||
 | 
			
		||||
        :param user: Username.
 | 
			
		||||
        :param keys: List of authorized SSH keys.
 | 
			
		||||
@@ -666,7 +687,7 @@ class Instance:
 | 
			
		||||
        qemu_ga_commands = ['guest-ssh-add-authorized-keys']
 | 
			
		||||
        if remove and append:
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
                "'append' and 'remove' parameters is mutually exclusive"
 | 
			
		||||
                "'append' and 'remove' parameters are mutually exclusive"
 | 
			
		||||
            )
 | 
			
		||||
        if not self.is_running():
 | 
			
		||||
            raise InstanceError(
 | 
			
		||||
@@ -693,7 +714,7 @@ class Instance:
 | 
			
		||||
        """
 | 
			
		||||
        Set new user password in guest OS.
 | 
			
		||||
 | 
			
		||||
        This action performs by guest agent inside the guest.
 | 
			
		||||
        This action is performed by guest agent inside the guest.
 | 
			
		||||
 | 
			
		||||
        :param user: Username.
 | 
			
		||||
        :param password: Password.
 | 
			
		||||
@@ -702,6 +723,7 @@ class Instance:
 | 
			
		||||
        """
 | 
			
		||||
        self.guest_agent.raise_for_commands(['guest-set-user-password'])
 | 
			
		||||
        flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0
 | 
			
		||||
        log.debug("Setting up password for user '%s'", user)
 | 
			
		||||
        self.domain.setUserPassword(user, password, flags=flags)
 | 
			
		||||
 | 
			
		||||
    def dump_xml(self, *, inactive: bool = False) -> str:
 | 
			
		||||
@@ -709,11 +731,19 @@ class Instance:
 | 
			
		||||
        flags = libvirt.VIR_DOMAIN_XML_INACTIVE if inactive else 0
 | 
			
		||||
        return self.domain.XMLDesc(flags)
 | 
			
		||||
 | 
			
		||||
    def delete(self) -> None:
 | 
			
		||||
        """Delete instance with local disks."""
 | 
			
		||||
    def delete(self, *, with_volumes: bool = False) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Delete instance with local volumes.
 | 
			
		||||
 | 
			
		||||
        :param with_volumes: If True delete local volumes with instance.
 | 
			
		||||
        """
 | 
			
		||||
        self.shutdown(method='HARD')
 | 
			
		||||
        for disk in self.list_disks():
 | 
			
		||||
            if disk.disk_type == 'file':
 | 
			
		||||
        disks = self.list_disks(persistent=True)
 | 
			
		||||
        log.debug('Disks list: %s', disks)
 | 
			
		||||
        for disk in disks:
 | 
			
		||||
            if with_volumes and disk.type == 'file':
 | 
			
		||||
                volume = self.connection.storageVolLookupByPath(disk.source)
 | 
			
		||||
                log.debug('Delete volume: %s', volume.path())
 | 
			
		||||
                volume.delete()
 | 
			
		||||
        log.debug('Undefine instance')
 | 
			
		||||
        self.domain.undefine()
 | 
			
		||||
 
 | 
			
		||||
@@ -5,13 +5,13 @@
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# Ansible is distributed in the hope that it will be useful,
 | 
			
		||||
# Compute is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
# along with Compute.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
"""Compute instance related objects schemas."""
 | 
			
		||||
 | 
			
		||||
@@ -19,7 +19,8 @@ import re
 | 
			
		||||
from enum import StrEnum
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from pydantic import validator
 | 
			
		||||
from pydantic import ValidationError, validator
 | 
			
		||||
from pydantic.error_wrappers import ErrorWrapper
 | 
			
		||||
 | 
			
		||||
from compute.common import EntityModel
 | 
			
		||||
from compute.utils.units import DataUnit
 | 
			
		||||
@@ -73,15 +74,26 @@ class VolumeCapacitySchema(EntityModel):
 | 
			
		||||
    unit: DataUnit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiskDriverSchema(EntityModel):
 | 
			
		||||
    """Virtual disk driver model."""
 | 
			
		||||
 | 
			
		||||
    name: str
 | 
			
		||||
    type: str  # noqa: A003
 | 
			
		||||
    cache: str = 'writethrough'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VolumeSchema(EntityModel):
 | 
			
		||||
    """Storage volume model."""
 | 
			
		||||
 | 
			
		||||
    type: VolumeType  # noqa: A003
 | 
			
		||||
    target: str
 | 
			
		||||
    capacity: VolumeCapacitySchema
 | 
			
		||||
    driver: DiskDriverSchema
 | 
			
		||||
    capacity: VolumeCapacitySchema | None
 | 
			
		||||
    source: str | None = None
 | 
			
		||||
    is_readonly: bool = False
 | 
			
		||||
    is_system: bool = False
 | 
			
		||||
    bus: str = 'virtio'
 | 
			
		||||
    device: str = 'disk'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NetworkInterfaceSchema(EntityModel):
 | 
			
		||||
@@ -118,10 +130,10 @@ class InstanceSchema(EntityModel):
 | 
			
		||||
 | 
			
		||||
    @validator('name')
 | 
			
		||||
    def _check_name(cls, value: str) -> str:  # noqa: N805
 | 
			
		||||
        if not re.match(r'^[a-z0-9_]+$', value):
 | 
			
		||||
        if not re.match(r'^[a-z0-9_-]+$', value):
 | 
			
		||||
            msg = (
 | 
			
		||||
                'Name can contain only lowercase letters, numbers '
 | 
			
		||||
                'and underscore.'
 | 
			
		||||
                'Name can contain only lowercase letters, numbers, '
 | 
			
		||||
                'minus sign and underscore.'
 | 
			
		||||
            )
 | 
			
		||||
            raise ValueError(msg)
 | 
			
		||||
        return value
 | 
			
		||||
@@ -140,13 +152,22 @@ 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)
 | 
			
		||||
        vol_with_source = 0
 | 
			
		||||
        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:
 | 
			
		||||
                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')
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user