import re import textwrap from dataclasses import dataclass from enum import Enum from pathlib import Path from uuid import UUID from lxml.etree import SubElement, fromstring, tostring from ..utils import mac, xml class CPUMode(Enum): HOST_MODEL = 'host-model' HOST_PASSTHROUGH = 'host-passthrough' CUSTOM = 'custom' MAXIMUM = 'maximum' @classmethod def default(cls): return cls.HOST_MODEL @dataclass class CPUTopology: sockets: int cores: int threads: int def validate(self, vcpus: int) -> None: if self.sockets * self.cores * self.threads == vcpus: return raise ValueError("CPU topology must match the number of 'vcpus'") @dataclass class CPUInfo: vendor: str model: str required_features: list[str] disabled_features: list[str] @dataclass class VolumeInfo: name: str path: Path capacity: int @dataclass class CloudInitConfig: user_data: str = '' meta_data: str = '' class Boot(Enum): BIOS = 'bios' UEFI = 'uefi' @classmethod def default(cls): return cls.BIOS @dataclass class BootMenu: enabled: bool = False timeout: int = 3000 class VirtualMachineInstaller: def __init__(self, session: 'LibvirtSession'): self.connection = session.connection # libvirt.virConnect object self.domcaps = fromstring( self.connection.getDomainCapabilities()) self.arch = self.domcaps.xpath('/domainCapabilities/arch/text()')[0] self.virttype = self.domcaps.xpath( '/domainCapabilities/domain/text()')[0] self.emulator = self.domcaps.xpath( '/domainCapabilities/path/text()')[0] self.machine = self.domcaps.xpath( '/domainCapabilities/machine/text()')[0] def install( self, name: str | None = None, title: str | None = None, description: str = '', os: str | None = None, image: UUID | None = None, volumes: list['VolumeInfo'] | None = None, vcpus: int = 0, vcpu_info: CPUInfo | None = None, vcpu_mode: CPUMode = CPUMode.default(), vcpu_topology: CPUTopology | None = None, memory: int = 0, boot: Boot = Boot.default(), boot_menu: BootMenu = BootMenu(), boot_order: tuple[str] = ('cdrom', 'hd'), cloud_init: CloudInitConfig | None = None): """ Install virtual machine with passed parameters. If no `vcpu_info` is None select best CPU wich can be provided by hypervisor. Choosen CPU depends on `vcpu_mode`, default is 'custom'. See CPUMode for more info. Default `vcpu_topology` is: 1 socket, `vcpus` cores, 1 threads. `memory` must be integer value in mebibytes e.g. 4094 MiB = 4 GiB. Volumes must be passed as list of VolumeInfo objects. Minimum one volume is required. """ name = self._validate_name(name) if vcpu_topology is None: vcpu_topology = CPUTopology(sockets=1, cores=vcpus, threads=1) vcpu_topology.validate(vcpus) if vcpu_info is None: if not vcpu_mode: vcpu_mode = CPUMode.CUSTOM.value xml_cpu = self._choose_best_cpu(vcpu_mode) else: raise NotImplementedError('Custom CPU not implemented') xml_domain = xml.Constructor().gen_domain_xml( name=name, title=title if title else name, desc=description if description else '', vcpus=vcpus, # vcpu_topology=vcpu_topology, # vcpu_info=vcpu_info, memory=memory, domain_type='hvm', machine=self.machine, arch=self.arch, # boot_menu=boot_menu, boot_order=boot_order, cpu=xml_cpu, mac=mac.random_mac() ) xml_volume = xml.Constructor().gen_volume_xml( dev='vda', mode='rw', path='') virconn = self.connection virstor = virconn.storagePoolLookupByName('default') # Мб использовать storageVolLookupByPath вместо поиска по имени etalon_volume = virstor.storageVolLookupByName('debian_bookworm.qcow2') return xml_domain def _validate_name(self, name) -> str: if name is None: raise ValueError("'name' cannot be empty") if isinstance(name, str): if not re.match(r"^[a-z0-9_]+$", name, re.I): raise ValueError( "'name' can contain only letters, numbers " "and underscore.") return name.lower() raise TypeError(f"'name' must be 'str', not {type(name)}") def _choose_best_cpu(self, mode: CPUMode) -> str: if mode == 'host-passthrough': xml = '' elif mode == 'maximum': xml = '' elif mode in ['host-model', 'custom']: cpus = self.domcaps.xpath( f'/domainCapabilities/cpu/mode[@name="{mode}"]')[0] cpus.tag = 'cpu' for attr in cpus.attrib.keys(): del cpus.attrib[attr] arch = SubElement(cpus, 'arch') arch.text = self.arch xmlcpus = tostring(cpus, encoding='unicode', pretty_print=True) xml = self.connection.baselineHypervisorCPU( self.emulator, self.arch, self.machine, self.virttype, [xmlcpus]) else: raise ValueError( f'CPU mode must be in {[v.value for v in CPUMode]}, ' f"but passed '{mode}'") return textwrap.indent(xml, ' ' * 2) def _define(self, xml: str) -> None: self.connection.defineXML(xml)