upd
This commit is contained in:
		@@ -4,7 +4,7 @@ Manage virtual machines.
 | 
				
			|||||||
Usage:  na-vmctl [options] status <machine>
 | 
					Usage:  na-vmctl [options] status <machine>
 | 
				
			||||||
        na-vmctl [options] is-running <machine>
 | 
					        na-vmctl [options] is-running <machine>
 | 
				
			||||||
        na-vmctl [options] start <machine>
 | 
					        na-vmctl [options] start <machine>
 | 
				
			||||||
        na-vmctl [options] shutdown <machine> [-f|--force] [-9|--sigkill]
 | 
					        na-vmctl [options] shutdown <machine>
 | 
				
			||||||
        na-vmctl [options] set-vcpus <machine> <nvcpus>
 | 
					        na-vmctl [options] set-vcpus <machine> <nvcpus>
 | 
				
			||||||
        na-vmctl [options] set-memory <machine> <memory>
 | 
					        na-vmctl [options] set-memory <machine> <memory>
 | 
				
			||||||
        na-vmctl [options] list [-a|--all]
 | 
					        na-vmctl [options] list [-a|--all]
 | 
				
			||||||
@@ -31,6 +31,9 @@ from ..vm import VirtualMachine, VMError, VMNotFound
 | 
				
			|||||||
logger = logging.getLogger(__name__)
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
levels = logging.getLevelNamesMapping()
 | 
					levels = logging.getLevelNamesMapping()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Supress libvirt errors
 | 
				
			||||||
 | 
					libvirt.registerErrorHandler(lambda userdata, err: None, ctx=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Color:
 | 
					class Color:
 | 
				
			||||||
    RED = '\033[31m'
 | 
					    RED = '\033[31m'
 | 
				
			||||||
@@ -89,19 +92,17 @@ def cli():
 | 
				
			|||||||
    if loglvl in levels:
 | 
					    if loglvl in levels:
 | 
				
			||||||
        logging.basicConfig(level=levels[loglvl])
 | 
					        logging.basicConfig(level=levels[loglvl])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with LibvirtSession(config) as session:
 | 
					    with LibvirtSession() as session:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            if args['list']:
 | 
					            if args['list']:
 | 
				
			||||||
                vms = session.list_domains()
 | 
					 | 
				
			||||||
                table = Table()
 | 
					                table = Table()
 | 
				
			||||||
                table.header(['NAME', 'STATE', 'AUTOSTART'])
 | 
					                table.header(['NAME', 'STATE', 'AUTOSTART'])
 | 
				
			||||||
                for vm_ in vms:
 | 
					                for vm_ in session.list_machines():
 | 
				
			||||||
                    vm_ = VirtualMachine(vm_)
 | 
					 | 
				
			||||||
                    table.row([vm_.name, vm_.status, vm_.is_autostart])
 | 
					                    table.row([vm_.name, vm_.status, vm_.is_autostart])
 | 
				
			||||||
                table.print()
 | 
					                table.print()
 | 
				
			||||||
                sys.exit()
 | 
					                sys.exit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            vm = VirtualMachine(session, machine)
 | 
					            vm = session.get_machine(machine)
 | 
				
			||||||
            if args['status']:
 | 
					            if args['status']:
 | 
				
			||||||
                print(vm.status)
 | 
					                print(vm.status)
 | 
				
			||||||
            if args['is-running']:
 | 
					            if args['is-running']:
 | 
				
			||||||
@@ -113,7 +114,7 @@ def cli():
 | 
				
			|||||||
                vm.start()
 | 
					                vm.start()
 | 
				
			||||||
                print(f'{vm.name} started')
 | 
					                print(f'{vm.name} started')
 | 
				
			||||||
            if args['shutdown']:
 | 
					            if args['shutdown']:
 | 
				
			||||||
                vm.shutdown(force=args['--force'], sigkill=args['sigkill'])
 | 
					                vm.shutdown('NORMAL')
 | 
				
			||||||
        except VMNotFound as nferr:
 | 
					        except VMNotFound as nferr:
 | 
				
			||||||
            sys.exit(f'{Color.RED}VM {machine} not found.{Color.NONE}')
 | 
					            sys.exit(f'{Color.RED}VM {machine} not found.{Color.NONE}')
 | 
				
			||||||
        except VMError as vmerr:
 | 
					        except VMError as vmerr:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,15 +14,19 @@ import logging
 | 
				
			|||||||
import pathlib
 | 
					import pathlib
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import libvirt
 | 
				
			||||||
from docopt import docopt
 | 
					from docopt import docopt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..session import LibvirtSession
 | 
					from ..session import LibvirtSession
 | 
				
			||||||
from ..vm import QemuAgent, QemuAgentError, VMNotFound
 | 
					from ..vm import GuestAgent, GuestAgentError, VMNotFound
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger(__name__)
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
levels = logging.getLevelNamesMapping()
 | 
					levels = logging.getLevelNamesMapping()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Supress libvirt errors
 | 
				
			||||||
 | 
					libvirt.registerErrorHandler(lambda userdata, err: None, ctx=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Color:
 | 
					class Color:
 | 
				
			||||||
    RED = '\033[31m'
 | 
					    RED = '\033[31m'
 | 
				
			||||||
@@ -45,16 +49,16 @@ def cli():
 | 
				
			|||||||
    if loglvl in levels:
 | 
					    if loglvl in levels:
 | 
				
			||||||
        logging.basicConfig(level=levels[loglvl])
 | 
					        logging.basicConfig(level=levels[loglvl])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with LibvirtSession(config) as session:
 | 
					    with LibvirtSession() as session:
 | 
				
			||||||
        shell = args['--shell']
 | 
					        shell = args['--shell']
 | 
				
			||||||
        cmd = args['<command>']
 | 
					        cmd = args['<command>']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            ga = QemuAgent(session, machine)
 | 
					            ga = session.get_guest_agent(machine)
 | 
				
			||||||
            exited, exitcode, stdout, stderr = ga.shellexec(
 | 
					            exited, exitcode, stdout, stderr = ga.shellexec(
 | 
				
			||||||
                cmd, executable=shell, capture_output=True, decode_output=True,
 | 
					                cmd, executable=shell, capture_output=True, decode_output=True,
 | 
				
			||||||
                timeout=int(args['--timeout']))
 | 
					                timeout=int(args['--timeout']))
 | 
				
			||||||
        except QemuAgentError as qemuerr:
 | 
					        except GuestAgentError as qemuerr:
 | 
				
			||||||
            errmsg = f'{Color.RED}{qemuerr}{Color.NONE}'
 | 
					            errmsg = f'{Color.RED}{qemuerr}{Color.NONE}'
 | 
				
			||||||
            if str(qemuerr).startswith('Polling command pid='):
 | 
					            if str(qemuerr).startswith('Polling command pid='):
 | 
				
			||||||
                errmsg = (errmsg + Color.YELLOW +
 | 
					                errmsg = (errmsg + Color.YELLOW +
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@ class ConfigLoader(UserDict):
 | 
				
			|||||||
        super().__init__(self._load())
 | 
					        super().__init__(self._load())
 | 
				
			||||||
        # todo: load deafult configuration
 | 
					        # todo: load deafult configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _load(self):
 | 
					    def _load(self) -> dict:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            with open(self.file, 'rb') as config:
 | 
					            with open(self.file, 'rb') as config:
 | 
				
			||||||
                return tomllib.load(config)
 | 
					                return tomllib.load(config)
 | 
				
			||||||
@@ -34,5 +34,5 @@ class ConfigLoader(UserDict):
 | 
				
			|||||||
            raise ConfigLoaderError(
 | 
					            raise ConfigLoaderError(
 | 
				
			||||||
                f'Cannot read config file: {self.file}: {readerr}') from readerr
 | 
					                f'Cannot read config file: {self.file}: {readerr}') from readerr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reload(self):
 | 
					    def reload(self) -> None:
 | 
				
			||||||
        self.data = self._load()
 | 
					        self.data = self._load()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
from contextlib import AbstractContextManager
 | 
					from contextlib import AbstractContextManager
 | 
				
			||||||
from pathlib import Path
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .config import ConfigLoader
 | 
					from .vm import GuestAgent, VirtualMachine, VMNotFound
 | 
				
			||||||
 | 
					from .volume import StoragePool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LibvirtSessionError(Exception):
 | 
					class LibvirtSessionError(Exception):
 | 
				
			||||||
@@ -12,9 +12,8 @@ class LibvirtSessionError(Exception):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class LibvirtSession(AbstractContextManager):
 | 
					class LibvirtSession(AbstractContextManager):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, config: Path | None = None):
 | 
					    def __init__(self, uri: str = 'qemu:///system'):
 | 
				
			||||||
        self.config = ConfigLoader(config)
 | 
					        self.connection = self._connect(uri)
 | 
				
			||||||
        self.session = self._connect(self.config['libvirt']['uri'])
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __enter__(self):
 | 
					    def __enter__(self):
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
@@ -22,30 +21,48 @@ class LibvirtSession(AbstractContextManager):
 | 
				
			|||||||
    def __exit__(self, exception_type, exception_value, exception_traceback):
 | 
					    def __exit__(self, exception_type, exception_value, exception_traceback):
 | 
				
			||||||
        self.close()
 | 
					        self.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _connect(self, connection_uri: str):
 | 
					    def _connect(self, connection_uri: str) -> libvirt.virConnect:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            return libvirt.open(connection_uri)
 | 
					            return libvirt.open(connection_uri)
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            raise LibvirtSessionError(
 | 
					            raise LibvirtSessionError(
 | 
				
			||||||
                f'Failed to open connection to the hypervisor: {err}') from err
 | 
					                f'Failed to open connection to the hypervisor: {err}') from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def close(self) -> None:
 | 
					    def _get_domain(self, name: str) -> libvirt.virDomain:
 | 
				
			||||||
        self.session.close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def list_domains(self):
 | 
					 | 
				
			||||||
        return self.session.listAllDomains()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_domain(self, name: str) -> libvirt.virDomain:
 | 
					 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            return self.session.lookupByName(name)
 | 
					            return self.connection.lookupByName(name)
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            if err.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
 | 
					            if err.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
 | 
				
			||||||
                raise VMNotFound(name)
 | 
					                raise VMNotFound(name) from err
 | 
				
			||||||
            else:
 | 
					            raise LibvirtSessionError(err) from err
 | 
				
			||||||
                raise LibvirtSessionError(err)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_storage_pool(self, name: str) -> libvirt.virStoragePool:
 | 
					    def _list_all_domains(self) -> list[libvirt.virDomain]:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            return self.session.storagePoolLookupByName(name)
 | 
					            return self.connection.listAllDomains()
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            raise LibvirtSessionError(err)
 | 
					            raise LibvirtSessionError(err) from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get_storage_pool(self, name: str) -> libvirt.virStoragePool:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return self.connection.storagePoolLookupByName(name)
 | 
				
			||||||
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
 | 
					            raise LibvirtSessionError(err) from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_machine(self, name: str) -> VirtualMachine:
 | 
				
			||||||
 | 
					        return VirtualMachine(self._get_domain(name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def list_machines(self) -> list[VirtualMachine]:
 | 
				
			||||||
 | 
					        return [VirtualMachine(dom) for dom in self._list_all_domains()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_guest_agent(self, name: str, timeout: int | None = None,
 | 
				
			||||||
 | 
					                        flags: int | None = None) -> GuestAgent:
 | 
				
			||||||
 | 
					        return GuestAgent(self._get_domain(name), timeout, flags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_storage_pool(self, name: str) -> StoragePool:
 | 
				
			||||||
 | 
					        return StoragePool(self._get_storage_pool(name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def list_storage_pools(self):
 | 
				
			||||||
 | 
					        return [StoragePool(p) for p in self.connection.listStoragePools()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def close(self) -> None:
 | 
				
			||||||
 | 
					        self.connection.close()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1 @@
 | 
				
			|||||||
from .mac import *
 | 
					from . import mac, xml
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,212 +0,0 @@
 | 
				
			|||||||
from pathlib import Path
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from lxml.builder import E
 | 
					 | 
				
			||||||
from lxml.etree import Element, QName, SubElement, tostring, fromstring
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
XPATH_DOM_NAME = '/domain/name'
 | 
					 | 
				
			||||||
XPATH_DOM_TITLE = '/domain/title'
 | 
					 | 
				
			||||||
XPATH_DOM_DESCRIPTION = '/domain/description'
 | 
					 | 
				
			||||||
XPATH_DOM_METADATA = '/domain/metadata'
 | 
					 | 
				
			||||||
XPATH_DOM_MEMORY = '/domain/memory'
 | 
					 | 
				
			||||||
XPATH_DOM_CURRENT_MEMORY = '/domain/currentMemory'
 | 
					 | 
				
			||||||
XPATH_DOM_VCPU = '/domain/vcpu'
 | 
					 | 
				
			||||||
XPATH_DOM_OS = '/domian/os'
 | 
					 | 
				
			||||||
XPATH_DOM_CPU = '/domain/cpu'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Reader:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(xml: str):
 | 
					 | 
				
			||||||
        self.xml = xml
 | 
					 | 
				
			||||||
        self.el = fromstring(self.xml)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_domcaps_machine(self):
 | 
					 | 
				
			||||||
        return self.el.xpath('/domainCapabilities/machine')[0].text
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_domcaps_cpus(self):
 | 
					 | 
				
			||||||
        # mode can be: custom, host-model, host-passthrough
 | 
					 | 
				
			||||||
        return self.el.xpath('/domainCapabilities/cpu/mode[@name="custom"]')[0]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Constructor:
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    The XML constructor. This class builds XML configs for libvirt.
 | 
					 | 
				
			||||||
    Features:
 | 
					 | 
				
			||||||
        - Generate basic virtual machine XML. See gen_domain_xml()
 | 
					 | 
				
			||||||
        - Generate virtual disk XML. See gen_volume_xml()
 | 
					 | 
				
			||||||
        - Add arbitrary metadata to XML from special structured dict
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, xml: str | None = None):
 | 
					 | 
				
			||||||
        self.xml_string = xml
 | 
					 | 
				
			||||||
        self.xml = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def domain_xml(self):
 | 
					 | 
				
			||||||
        return self.xml
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def gen_domain_xml(self,
 | 
					 | 
				
			||||||
                       name: str,
 | 
					 | 
				
			||||||
                       title: str,
 | 
					 | 
				
			||||||
                       vcpus: int,
 | 
					 | 
				
			||||||
                       vcpu_vendor: str,
 | 
					 | 
				
			||||||
                       vcpu_model: str,
 | 
					 | 
				
			||||||
                       mac_addr: str,
 | 
					 | 
				
			||||||
                       memory: int,
 | 
					 | 
				
			||||||
                       volume: Path,
 | 
					 | 
				
			||||||
                       vcpu_features: dict | None = None,
 | 
					 | 
				
			||||||
                       desc: str = "") -> None:
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Generate default domain XML configuration for virtual machines.
 | 
					 | 
				
			||||||
        See https://lxml.de/tutorial.html#the-e-factory for details.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        domain_xml = E.domain(
 | 
					 | 
				
			||||||
            E.name(name),
 | 
					 | 
				
			||||||
            E.title(title),
 | 
					 | 
				
			||||||
            E.description(desc),
 | 
					 | 
				
			||||||
            E.metadata(),
 | 
					 | 
				
			||||||
            E.memory(str(memory), unit='MB'),
 | 
					 | 
				
			||||||
            E.currentMemory(str(memory), unit='MB'),
 | 
					 | 
				
			||||||
            E.vcpu(str(vcpus), placement='static'),
 | 
					 | 
				
			||||||
            E.os(
 | 
					 | 
				
			||||||
                E.type('hvm', arch='x86_64'),
 | 
					 | 
				
			||||||
                E.boot(dev='cdrom'),
 | 
					 | 
				
			||||||
                E.boot(dev='hd'),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            E.features(
 | 
					 | 
				
			||||||
                E.acpi(),
 | 
					 | 
				
			||||||
                E.apic(),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            E.cpu(
 | 
					 | 
				
			||||||
                E.vendor(vcpu_vendor),
 | 
					 | 
				
			||||||
                E.model(vcpu_model, fallback='forbid'),
 | 
					 | 
				
			||||||
                E.topology(sockets='1', dies='1', cores=str(vcpus),
 | 
					 | 
				
			||||||
                           threads='1'),
 | 
					 | 
				
			||||||
                mode='custom',
 | 
					 | 
				
			||||||
                match='exact',
 | 
					 | 
				
			||||||
                check='partial',
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            E.on_poweroff('destroy'),
 | 
					 | 
				
			||||||
            E.on_reboot('restart'),
 | 
					 | 
				
			||||||
            E.on_crash('restart'),
 | 
					 | 
				
			||||||
            E.pm(
 | 
					 | 
				
			||||||
                E('suspend-to-mem', enabled='no'),
 | 
					 | 
				
			||||||
                E('suspend-to-disk', enabled='no'),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            E.devices(
 | 
					 | 
				
			||||||
                E.emulator('/usr/bin/qemu-system-x86_64'),
 | 
					 | 
				
			||||||
                E.disk(
 | 
					 | 
				
			||||||
                    E.driver(name='qemu', type='qcow2', cache='writethrough'),
 | 
					 | 
				
			||||||
                    E.source(file=volume),
 | 
					 | 
				
			||||||
                    E.target(dev='vda', bus='virtio'),
 | 
					 | 
				
			||||||
                    type='file',
 | 
					 | 
				
			||||||
                    device='disk',
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                E.interface(
 | 
					 | 
				
			||||||
                    E.source(network='default'),
 | 
					 | 
				
			||||||
                    E.mac(address=mac_addr),
 | 
					 | 
				
			||||||
                    type='network',
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                E.graphics(
 | 
					 | 
				
			||||||
                    E.listen(type='address'),
 | 
					 | 
				
			||||||
                    type='vnc', port='-1', autoport='yes'
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                E.video(
 | 
					 | 
				
			||||||
                    E.model(type='vga', vram='16384', heads='1', primary='yes'),
 | 
					 | 
				
			||||||
                    E.address(type='pci', domain='0x0000', bus='0x00',
 | 
					 | 
				
			||||||
                              slot='0x02', function='0x0'),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            type='kvm',
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        return self.to_string(domain_xml)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def gen_volume_xml(self,
 | 
					 | 
				
			||||||
                       device_name: str,
 | 
					 | 
				
			||||||
                       file: Path,
 | 
					 | 
				
			||||||
                       bus: str = 'virtio',
 | 
					 | 
				
			||||||
                       cache: str = 'writethrough',
 | 
					 | 
				
			||||||
                       disktype: str = 'file'):
 | 
					 | 
				
			||||||
        disk_xml = E.disk(E.driver(name='qemu', type='qcow2', cache=cache),
 | 
					 | 
				
			||||||
                      E.source(file=file),
 | 
					 | 
				
			||||||
                      E.target(dev=device_name, bus=bus),
 | 
					 | 
				
			||||||
                      type=disktype,
 | 
					 | 
				
			||||||
                      device='disk')
 | 
					 | 
				
			||||||
        return self.to_string(disk_xml)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def add_volume(self):
 | 
					 | 
				
			||||||
        raise NotImplementedError()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def add_meta(self, data: dict, namespace: str, nsprefix: str) -> None:
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Add metadata to domain. See:
 | 
					 | 
				
			||||||
        https://libvirt.org/formatdomain.html#general-metadata
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        metadata = metadata_old = self.xml.xpath('/domain/metadata')[0]
 | 
					 | 
				
			||||||
        metadata.append(
 | 
					 | 
				
			||||||
            self.construct_xml(
 | 
					 | 
				
			||||||
                data,
 | 
					 | 
				
			||||||
                namespace=namespace,
 | 
					 | 
				
			||||||
                nsprefix=nsprefix,
 | 
					 | 
				
			||||||
            ))
 | 
					 | 
				
			||||||
        self.xml.replace(metadata_old, metadata)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def remove_meta(self, namespace: str):
 | 
					 | 
				
			||||||
        """Remove metadata by namespace."""
 | 
					 | 
				
			||||||
        raise NotImplementedError()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def construct_xml(self,
 | 
					 | 
				
			||||||
                      tag: dict,
 | 
					 | 
				
			||||||
                      namespace: str | None = None,
 | 
					 | 
				
			||||||
                      nsprefix: str | None = None,
 | 
					 | 
				
			||||||
                      root: Element = None) -> Element:
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Shortly this recursive function transforms dictonary to XML.
 | 
					 | 
				
			||||||
        Return etree.Element built from dict with following structure::
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                'name': 'device',  # tag name
 | 
					 | 
				
			||||||
                'text': '',  # optional key
 | 
					 | 
				
			||||||
                'values': {  # optional key, must be a dict of key-value pairs
 | 
					 | 
				
			||||||
                    'type': 'disk'
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                children: []  # optional key, must be a list of dicts
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Child elements must have the same structure. Infinite `children` nesting
 | 
					 | 
				
			||||||
        is allowed.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        use_ns = False
 | 
					 | 
				
			||||||
        if isinstance(namespace, str) and isinstance(nsprefix, str):
 | 
					 | 
				
			||||||
            use_ns = True
 | 
					 | 
				
			||||||
        # Create element
 | 
					 | 
				
			||||||
        if root is None:
 | 
					 | 
				
			||||||
            if use_ns:
 | 
					 | 
				
			||||||
                element = Element(QName(namespace, tag['name']),
 | 
					 | 
				
			||||||
                                  nsmap={nsprefix: namespace})
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                element = Element(tag['name'])
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            if use_ns:
 | 
					 | 
				
			||||||
                element = SubElement(root, QName(namespace, tag['name']))
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                element = SubElement(root, tag['name'])
 | 
					 | 
				
			||||||
        # Fill up element with content
 | 
					 | 
				
			||||||
        if 'text' in tag.keys():
 | 
					 | 
				
			||||||
            element.text = tag['text']
 | 
					 | 
				
			||||||
        if 'values' in tag.keys():
 | 
					 | 
				
			||||||
            for key in tag['values'].keys():
 | 
					 | 
				
			||||||
                element.set(str(key), str(tag['values'][key]))
 | 
					 | 
				
			||||||
        if 'children' in tag.keys():
 | 
					 | 
				
			||||||
            for child in tag['children']:
 | 
					 | 
				
			||||||
                element.append(
 | 
					 | 
				
			||||||
                    self.construct_xml(child,
 | 
					 | 
				
			||||||
                                       namespace=namespace,
 | 
					 | 
				
			||||||
                                       nsprefix=nsprefix,
 | 
					 | 
				
			||||||
                                       root=element))
 | 
					 | 
				
			||||||
        return element
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def to_string(self):
 | 
					 | 
				
			||||||
        return (tostring(self.xml, pretty_print=True,
 | 
					 | 
				
			||||||
                         encoding='utf-8').decode().strip())
 | 
					 | 
				
			||||||
@@ -8,9 +8,3 @@ def random_mac() -> str:
 | 
				
			|||||||
           random.randint(0x00, 0xff),
 | 
					           random.randint(0x00, 0xff),
 | 
				
			||||||
           random.randint(0x00, 0xff)]
 | 
					           random.randint(0x00, 0xff)]
 | 
				
			||||||
    return ':'.join(map(lambda x: "%02x" % x, mac))
 | 
					    return ':'.join(map(lambda x: "%02x" % x, mac))
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def unique_mac() -> str:
 | 
					 | 
				
			||||||
    """Return non-conflicting MAC address."""
 | 
					 | 
				
			||||||
    # todo: see virtinst.DeviceInterface.generate_mac
 | 
					 | 
				
			||||||
    raise NotImplementedError()
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,5 @@
 | 
				
			|||||||
from pathlib import Path
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from lxml.builder import E
 | 
					from lxml.builder import E
 | 
				
			||||||
from lxml.etree import Element, QName, SubElement, tostring, fromstring
 | 
					from lxml.etree import Element, QName, SubElement, fromstring, tostring
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Constructor:
 | 
					class Constructor:
 | 
				
			||||||
@@ -63,15 +61,85 @@ class Constructor:
 | 
				
			|||||||
        domain.append(devices)
 | 
					        domain.append(devices)
 | 
				
			||||||
        return tostring(domain, encoding='unicode', pretty_print=True).strip()
 | 
					        return tostring(domain, encoding='unicode', pretty_print=True).strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def gen_volume_xml(self, dev: str, mode: str) -> str:
 | 
					    def gen_volume_xml(self, dev: str, mode: str, path: str) -> str:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Todo: No hardcode
 | 
					        Todo: No hardcode
 | 
				
			||||||
        https://libvirt.org/formatdomain.html#hard-drives-floppy-disks-cdroms
 | 
					        https://libvirt.org/formatdomain.html#hard-drives-floppy-disks-cdroms
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        volume = E.disk(type='file', device='disk')
 | 
					        volume = E.disk(type='file', device='disk')
 | 
				
			||||||
        volume.append(E.driver(name='qemu', type='qcow2', cache='writethrough'))
 | 
					        volume.append(
 | 
				
			||||||
 | 
					            E.driver(
 | 
				
			||||||
 | 
					                name='qemu',
 | 
				
			||||||
 | 
					                type='qcow2',
 | 
				
			||||||
 | 
					                cache='writethrough'))
 | 
				
			||||||
        volume.append(E.source(file=path))
 | 
					        volume.append(E.source(file=path))
 | 
				
			||||||
        volume.append(E.target(dev=dev, bus='virtio'))
 | 
					        volume.append(E.target(dev=dev, bus='virtio'))
 | 
				
			||||||
        if mode.lower() == 'ro':
 | 
					        if mode.lower() == 'ro':
 | 
				
			||||||
            volume.append(E.readonly())
 | 
					            volume.append(E.readonly())
 | 
				
			||||||
        return tostring(volume, encoding='unicode', pretty_print=True).strip()
 | 
					        return tostring(volume, encoding='unicode', pretty_print=True).strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def construct_xml(self,
 | 
				
			||||||
 | 
					                      tag: dict,
 | 
				
			||||||
 | 
					                      namespace: str | None = None,
 | 
				
			||||||
 | 
					                      nsprefix: str | None = None,
 | 
				
			||||||
 | 
					                      root: Element = None) -> Element:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Shortly this recursive function transforms dictonary to XML.
 | 
				
			||||||
 | 
					        Return etree.Element built from dict with following structure::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'name': 'device',  # tag name
 | 
				
			||||||
 | 
					                'text': '',  # optional key
 | 
				
			||||||
 | 
					                'values': {  # optional key, must be a dict of key-value pairs
 | 
				
			||||||
 | 
					                    'type': 'disk'
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                children: []  # optional key, must be a list of dicts
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Child elements must have the same structure. Infinite `children` nesting
 | 
				
			||||||
 | 
					        is allowed.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        use_ns = False
 | 
				
			||||||
 | 
					        if isinstance(namespace, str) and isinstance(nsprefix, str):
 | 
				
			||||||
 | 
					            use_ns = True
 | 
				
			||||||
 | 
					        # Create element
 | 
				
			||||||
 | 
					        if root is None:
 | 
				
			||||||
 | 
					            if use_ns:
 | 
				
			||||||
 | 
					                element = Element(QName(namespace, tag['name']),
 | 
				
			||||||
 | 
					                                  nsmap={nsprefix: namespace})
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                element = Element(tag['name'])
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            if use_ns:
 | 
				
			||||||
 | 
					                element = SubElement(root, QName(namespace, tag['name']))
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                element = SubElement(root, tag['name'])
 | 
				
			||||||
 | 
					        # Fill up element with content
 | 
				
			||||||
 | 
					        if 'text' in tag.keys():
 | 
				
			||||||
 | 
					            element.text = tag['text']
 | 
				
			||||||
 | 
					        if 'values' in tag.keys():
 | 
				
			||||||
 | 
					            for key in tag['values'].keys():
 | 
				
			||||||
 | 
					                element.set(str(key), str(tag['values'][key]))
 | 
				
			||||||
 | 
					        if 'children' in tag.keys():
 | 
				
			||||||
 | 
					            for child in tag['children']:
 | 
				
			||||||
 | 
					                element.append(
 | 
				
			||||||
 | 
					                    self.construct_xml(child,
 | 
				
			||||||
 | 
					                                       namespace=namespace,
 | 
				
			||||||
 | 
					                                       nsprefix=nsprefix,
 | 
				
			||||||
 | 
					                                       root=element))
 | 
				
			||||||
 | 
					        return element
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_meta(self, xml: Element, data: dict,
 | 
				
			||||||
 | 
					                 namespace: str, nsprefix: str) -> None:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Add metadata to domain. See:
 | 
				
			||||||
 | 
					        https://libvirt.org/formatdomain.html#general-metadata
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        metadata = metadata_old = xml.xpath('/domain/metadata')[0]
 | 
				
			||||||
 | 
					        metadata.append(
 | 
				
			||||||
 | 
					            self.construct_xml(
 | 
				
			||||||
 | 
					                data,
 | 
				
			||||||
 | 
					                namespace=namespace,
 | 
				
			||||||
 | 
					                nsprefix=nsprefix,
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					        xml.replace(metadata_old, metadata)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
from .exceptions import *
 | 
					from .exceptions import *
 | 
				
			||||||
from .guest_agent import QemuAgent
 | 
					from .guest_agent import GuestAgent
 | 
				
			||||||
 | 
					from .installer import CPUMode, CPUTopology, VirtualMachineInstaller
 | 
				
			||||||
from .virtual_machine import VirtualMachine
 | 
					from .virtual_machine import VirtualMachine
 | 
				
			||||||
from .installer import VirtualMachineInstaller
 | 
					 | 
				
			||||||
from .hardware import vCPUMode, vCPUTopology
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,6 @@ class VirtualMachineBase:
 | 
				
			|||||||
            raise VMError(f'Cannot get domain name: {err}') from err
 | 
					            raise VMError(f'Cannot get domain name: {err}') from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _get_domain_info(self):
 | 
					    def _get_domain_info(self):
 | 
				
			||||||
        # https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo
 | 
					 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            info = self.domain.info()
 | 
					            info = self.domain.info()
 | 
				
			||||||
            return {
 | 
					            return {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
class QemuAgentError(Exception):
 | 
					class GuestAgentError(Exception):
 | 
				
			||||||
    """Mostly QEMU Guest Agent is not responding."""
 | 
					    """Mostly QEMU Guest Agent is not responding."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,17 +7,16 @@ import libvirt
 | 
				
			|||||||
import libvirt_qemu
 | 
					import libvirt_qemu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .base import VirtualMachineBase
 | 
					from .base import VirtualMachineBase
 | 
				
			||||||
from .exceptions import QemuAgentError
 | 
					from .exceptions import GuestAgentError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger(__name__)
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Note that if no QEMU_TIMEOUT libvirt cannot connect to agent
 | 
					 | 
				
			||||||
QEMU_TIMEOUT = 60  # in seconds
 | 
					QEMU_TIMEOUT = 60  # in seconds
 | 
				
			||||||
POLL_INTERVAL = 0.3  # also in seconds
 | 
					POLL_INTERVAL = 0.3  # also in seconds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class QemuAgent(VirtualMachineBase):
 | 
					class GuestAgent(VirtualMachineBase):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Interacting with QEMU guest agent. Methods:
 | 
					    Interacting with QEMU guest agent. Methods:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -47,7 +46,7 @@ class QemuAgent(VirtualMachineBase):
 | 
				
			|||||||
        Execute command on guest and return output if `capture_output` is True.
 | 
					        Execute command on guest and return output if `capture_output` is True.
 | 
				
			||||||
        See https://wiki.qemu.org/Documentation/QMP for QEMU commands reference.
 | 
					        See https://wiki.qemu.org/Documentation/QMP for QEMU commands reference.
 | 
				
			||||||
        If `wait` is True poll guest command output with POLL_INTERVAL. Raise
 | 
					        If `wait` is True poll guest command output with POLL_INTERVAL. Raise
 | 
				
			||||||
        QemuAgentError on `timeout` reached (in seconds).
 | 
					        GuestAgentError on `timeout` reached (in seconds).
 | 
				
			||||||
        Return values:
 | 
					        Return values:
 | 
				
			||||||
            tuple(
 | 
					            tuple(
 | 
				
			||||||
                exited: bool | None,
 | 
					                exited: bool | None,
 | 
				
			||||||
@@ -121,7 +120,7 @@ class QemuAgent(VirtualMachineBase):
 | 
				
			|||||||
                self.flags,
 | 
					                self.flags,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            raise QemuAgentError(
 | 
					            raise GuestAgentError(
 | 
				
			||||||
                f'Cannot execute command on vm={self.domain_name}: {err}'
 | 
					                f'Cannot execute command on vm={self.domain_name}: {err}'
 | 
				
			||||||
            ) from err
 | 
					            ) from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -145,7 +144,7 @@ class QemuAgent(VirtualMachineBase):
 | 
				
			|||||||
            sleep(POLL_INTERVAL)
 | 
					            sleep(POLL_INTERVAL)
 | 
				
			||||||
            now = time()
 | 
					            now = time()
 | 
				
			||||||
            if now - start_time > timeout:
 | 
					            if now - start_time > timeout:
 | 
				
			||||||
                raise QemuAgentError(
 | 
					                raise GuestAgentError(
 | 
				
			||||||
                    f'Polling command pid={pid} on vm={self.domain_name} '
 | 
					                    f'Polling command pid={pid} on vm={self.domain_name} '
 | 
				
			||||||
                    f'took longer than {timeout} seconds.'
 | 
					                    f'took longer than {timeout} seconds.'
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,81 +0,0 @@
 | 
				
			|||||||
import textwrap
 | 
					 | 
				
			||||||
from enum import Enum
 | 
					 | 
				
			||||||
from collections import UserDict
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import libvirt
 | 
					 | 
				
			||||||
from lxml.etree import SubElement, fromstring, tostring
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Boot(Enum):
 | 
					 | 
				
			||||||
    BIOS = 'bios'
 | 
					 | 
				
			||||||
    UEFI = 'uefi'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class vCPUMode(Enum):
 | 
					 | 
				
			||||||
    HOST_MODEL = 'host-model'
 | 
					 | 
				
			||||||
    HOST_PASSTHROUGTH = 'host-passthrougth'
 | 
					 | 
				
			||||||
    CUSTOM = 'custom'
 | 
					 | 
				
			||||||
    MAXIMUM = 'maximum'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class DomainCapabilities:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, session: libvirt.virConnect):
 | 
					 | 
				
			||||||
        self.session = session
 | 
					 | 
				
			||||||
        self.domcaps = fromstring(
 | 
					 | 
				
			||||||
            self.session.getDomainCapabilities())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def arch(self):
 | 
					 | 
				
			||||||
        return self.domcaps.xpath('/domainCapabilities/arch')[0].text
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def virttype(self):
 | 
					 | 
				
			||||||
        return self.domcaps.xpath('/domainCapabilities/domain')[0].text
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def emulator(self):
 | 
					 | 
				
			||||||
        return self.domcaps.xpath('/domainCapabilities/path')[0].text
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def machine(self):
 | 
					 | 
				
			||||||
        return self.domcaps.xpath('/domainCapabilities/machine')[0].text
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def best_cpu(self, mode: vCPUMode) -> str:
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        See https://libvirt.org/html/libvirt-libvirt-host.html
 | 
					 | 
				
			||||||
        #virConnectBaselineHypervisorCPU
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        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.session.baselineHypervisorCPU(self.emulator,
 | 
					 | 
				
			||||||
            self.arch, self.machine, self.virttype, [xmlcpus])
 | 
					 | 
				
			||||||
        return textwrap.indent(xml, ' ' * 2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class vCPUTopology(UserDict):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    CPU topology schema ``{'sockets': 1, 'cores': 4, 'threads': 1}``::
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <topology sockets='1' dies='1' cores='4' threads='1'/>
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, topology: dict):
 | 
					 | 
				
			||||||
        super().__init__(self._validate(topology))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _validate(self, topology: dict):
 | 
					 | 
				
			||||||
        if isinstance(topology, dict):
 | 
					 | 
				
			||||||
            if ['sockets', 'cores', 'threads'] != list(topology.keys()):
 | 
					 | 
				
			||||||
                raise ValueError("Topology must have 'sockets', 'cores' "
 | 
					 | 
				
			||||||
                                 "and 'threads' keys.")
 | 
					 | 
				
			||||||
            for key in topology.keys():
 | 
					 | 
				
			||||||
                if not isinstance(topology[key], int):
 | 
					 | 
				
			||||||
                    raise TypeError(f"Key '{key}' must be 'int'")
 | 
					 | 
				
			||||||
            return topology
 | 
					 | 
				
			||||||
        raise TypeError("Topology must be a 'dict'")
 | 
					 | 
				
			||||||
@@ -1,28 +1,87 @@
 | 
				
			|||||||
import re
 | 
					import re
 | 
				
			||||||
 | 
					import textwrap
 | 
				
			||||||
 | 
					from dataclasses import dataclass
 | 
				
			||||||
 | 
					from enum import Enum
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from uuid import UUID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					from lxml.etree import SubElement, fromstring, tostring
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..utils.xml import Constructor
 | 
					from ..utils import mac, xml
 | 
				
			||||||
from ..utils.mac import random_mac
 | 
					 | 
				
			||||||
from .hardware import DomainCapabilities, vCPUMode, vCPUTopology, Boot
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class vCPUInfo:
 | 
					class CPUMode(Enum):
 | 
				
			||||||
    pass
 | 
					    HOST_MODEL = 'host-model'
 | 
				
			||||||
 | 
					    HOST_PASSTHROUGH = 'host-passthrough'
 | 
				
			||||||
 | 
					    CUSTOM = 'custom'
 | 
				
			||||||
 | 
					    MAXIMUM = 'maximum'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImageVolume:
 | 
					    @classmethod
 | 
				
			||||||
    pass
 | 
					    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:
 | 
					class CloudInitConfig:
 | 
				
			||||||
    pass
 | 
					    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 BootOrder:
 | 
					 | 
				
			||||||
    pass
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VirtualMachineInstaller:
 | 
					class VirtualMachineInstaller:
 | 
				
			||||||
    def __init__(self, session: libvirt.virConnect):
 | 
					
 | 
				
			||||||
        self.session = session
 | 
					    def __init__(self, session: 'LibvirtSession'):
 | 
				
			||||||
        self.info = {}
 | 
					        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(
 | 
					    def install(
 | 
				
			||||||
            self,
 | 
					            self,
 | 
				
			||||||
@@ -30,48 +89,71 @@ class VirtualMachineInstaller:
 | 
				
			|||||||
            title: str | None = None,
 | 
					            title: str | None = None,
 | 
				
			||||||
            description: str = '',
 | 
					            description: str = '',
 | 
				
			||||||
            os: str | None = None,
 | 
					            os: str | None = None,
 | 
				
			||||||
        image: ImageVolume | None = None,
 | 
					            image: UUID | None = None,
 | 
				
			||||||
            volumes: list['VolumeInfo'] | None = None,
 | 
					            volumes: list['VolumeInfo'] | None = None,
 | 
				
			||||||
            vcpus: int = 0,
 | 
					            vcpus: int = 0,
 | 
				
			||||||
        vcpu_info: vCPUInfo | None = None,
 | 
					            vcpu_info: CPUInfo | None = None,
 | 
				
			||||||
        vcpu_mode: vCPUMode | None = None,
 | 
					            vcpu_mode: CPUMode = CPUMode.default(),
 | 
				
			||||||
        vcpu_topology: vCPUTopology | None = None,
 | 
					            vcpu_topology: CPUTopology | None = None,
 | 
				
			||||||
            memory: int = 0,
 | 
					            memory: int = 0,
 | 
				
			||||||
        boot: Boot = Boot.BIOS,
 | 
					            boot: Boot = Boot.default(),
 | 
				
			||||||
        boot_menu: bool = False,
 | 
					            boot_menu: BootMenu = BootMenu(),
 | 
				
			||||||
        boot_order: BootOrder = ('cdrom', 'hd'),
 | 
					            boot_order: tuple[str] = ('cdrom', 'hd'),
 | 
				
			||||||
            cloud_init: CloudInitConfig | None = None):
 | 
					            cloud_init: CloudInitConfig | None = None):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Install virtual machine with passed parameters.
 | 
					        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.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        domcaps = DomainCapabilities(self.session.session)
 | 
					 | 
				
			||||||
        name = self._validate_name(name)
 | 
					        name = self._validate_name(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if vcpu_topology is None:
 | 
					        if vcpu_topology is None:
 | 
				
			||||||
            vcpu_topology = vCPUTopology(
 | 
					            vcpu_topology = CPUTopology(sockets=1, cores=vcpus, threads=1)
 | 
				
			||||||
                {'sockets': 1, 'cores': vcpus, 'threads': 1})
 | 
					        vcpu_topology.validate(vcpus)
 | 
				
			||||||
        self._validate_topology(vcpus, vcpu_topology)
 | 
					
 | 
				
			||||||
        if vcpu_info is None:
 | 
					        if vcpu_info is None:
 | 
				
			||||||
            if not vcpu_mode:
 | 
					            if not vcpu_mode:
 | 
				
			||||||
                vcpu_mode = vCPUMode.CUSTOM.value
 | 
					                vcpu_mode = CPUMode.CUSTOM.value
 | 
				
			||||||
            xml_cpu = domcaps.best_cpu(vcpu_mode)
 | 
					            xml_cpu = self._choose_best_cpu(vcpu_mode)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            raise NotImplementedError('Custom CPU not implemented')
 | 
					            raise NotImplementedError('Custom CPU not implemented')
 | 
				
			||||||
        xml_domain = Constructor().gen_domain_xml(
 | 
					
 | 
				
			||||||
 | 
					        xml_domain = xml.Constructor().gen_domain_xml(
 | 
				
			||||||
            name=name,
 | 
					            name=name,
 | 
				
			||||||
            title=title if title else name,
 | 
					            title=title if title else name,
 | 
				
			||||||
            desc=description if description else '',
 | 
					            desc=description if description else '',
 | 
				
			||||||
            vcpus=vcpus,
 | 
					            vcpus=vcpus,
 | 
				
			||||||
 | 
					            # vcpu_topology=vcpu_topology,
 | 
				
			||||||
 | 
					            # vcpu_info=vcpu_info,
 | 
				
			||||||
            memory=memory,
 | 
					            memory=memory,
 | 
				
			||||||
            domain_type='hvm',
 | 
					            domain_type='hvm',
 | 
				
			||||||
            machine=domcaps.machine,
 | 
					            machine=self.machine,
 | 
				
			||||||
            arch=domcaps.arch,
 | 
					            arch=self.arch,
 | 
				
			||||||
            boot_order=('cdrom', 'hd'),
 | 
					            # boot_menu=boot_menu,
 | 
				
			||||||
 | 
					            boot_order=boot_order,
 | 
				
			||||||
            cpu=xml_cpu,
 | 
					            cpu=xml_cpu,
 | 
				
			||||||
            mac=random_mac()
 | 
					            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
 | 
					        return xml_domain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _validate_name(self, name):
 | 
					    def _validate_name(self, name) -> str:
 | 
				
			||||||
        if name is None:
 | 
					        if name is None:
 | 
				
			||||||
            raise ValueError("'name' cannot be empty")
 | 
					            raise ValueError("'name' cannot be empty")
 | 
				
			||||||
        if isinstance(name, str):
 | 
					        if isinstance(name, str):
 | 
				
			||||||
@@ -82,13 +164,27 @@ class VirtualMachineInstaller:
 | 
				
			|||||||
            return name.lower()
 | 
					            return name.lower()
 | 
				
			||||||
        raise TypeError(f"'name' must be 'str', not {type(name)}")
 | 
					        raise TypeError(f"'name' must be 'str', not {type(name)}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _validate_topology(self, vcpus, topology):
 | 
					    def _choose_best_cpu(self, mode: CPUMode) -> str:
 | 
				
			||||||
        sockets = topology['sockets']
 | 
					        if mode == 'host-passthrough':
 | 
				
			||||||
        cores = topology['cores']
 | 
					            xml = '<cpu mode="host-passthrough" migratable="on"/>'
 | 
				
			||||||
        threads = topology['threads']
 | 
					        elif mode == 'maximum':
 | 
				
			||||||
        if sockets * cores * threads == vcpus:
 | 
					            xml = '<cpu mode="maximum" migratable="on"/>'
 | 
				
			||||||
            return
 | 
					        elif mode in ['host-model', 'custom']:
 | 
				
			||||||
        raise ValueError("CPU topology must match the number of 'vcpus'")
 | 
					            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):
 | 
					    def _define(self, xml: str) -> None:
 | 
				
			||||||
        self.session.defineXML(xml)
 | 
					        self.connection.defineXML(xml)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import logging
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ..volume import VolumeInfo
 | 
				
			||||||
from .base import VirtualMachineBase
 | 
					from .base import VirtualMachineBase
 | 
				
			||||||
from .exceptions import VMError
 | 
					from .exceptions import VMError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -73,7 +74,7 @@ class VirtualMachine(VirtualMachineBase):
 | 
				
			|||||||
            raise VMError(
 | 
					            raise VMError(
 | 
				
			||||||
                f'Cannot start vm={self.domain_name}: {err}') from err
 | 
					                f'Cannot start vm={self.domain_name}: {err}') from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def shutdown(self, mode: str | None = None) -> None:
 | 
					    def shutdown(self, method: str | None = None) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Send signal to guest OS to shutdown. Supports several modes:
 | 
					        Send signal to guest OS to shutdown. Supports several modes:
 | 
				
			||||||
        * GUEST_AGENT - use guest agent
 | 
					        * GUEST_AGENT - use guest agent
 | 
				
			||||||
@@ -82,26 +83,26 @@ class VirtualMachine(VirtualMachineBase):
 | 
				
			|||||||
        * SIGKILL - send SIGKILL to QEMU process. May corrupt guest data!
 | 
					        * SIGKILL - send SIGKILL to QEMU process. May corrupt guest data!
 | 
				
			||||||
        If mode is not passed use 'NORMAL' mode.
 | 
					        If mode is not passed use 'NORMAL' mode.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        MODES = {
 | 
					        METHODS = {
 | 
				
			||||||
            'GUEST_AGENT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
 | 
					            'GUEST_AGENT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
 | 
				
			||||||
            'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT,
 | 
					            'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT,
 | 
				
			||||||
            'SIGTERM': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL,
 | 
					            'SIGTERM': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL,
 | 
				
			||||||
            'SIGKILL': libvirt.VIR_DOMAIN_DESTROY_DEFAULT
 | 
					            'SIGKILL': libvirt.VIR_DOMAIN_DESTROY_DEFAULT
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if mode is None:
 | 
					        if method is None:
 | 
				
			||||||
            mode = 'NORMAL'
 | 
					            method = 'NORMAL'
 | 
				
			||||||
        if not isinstance(mode, str):
 | 
					        if not isinstance(method, str):
 | 
				
			||||||
            raise ValueError(f"Mode must be a 'str', not {type(mode)}")
 | 
					            raise ValueError(f"Mode must be a 'str', not {type(method)}")
 | 
				
			||||||
        if mode.upper() not in MODES:
 | 
					        if method.upper() not in METHODS:
 | 
				
			||||||
            raise ValueError(f"Unsupported mode: '{mode}'")
 | 
					            raise ValueError(f"Unsupported mode: '{method}'")
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            if mode in ['GUEST_AGENT', 'NORMAL']:
 | 
					            if method in ['GUEST_AGENT', 'NORMAL']:
 | 
				
			||||||
                self.domain.shutdownFlags(flags=MODES.get(mode))
 | 
					                self.domain.shutdownFlags(flags=METHODS.get(method))
 | 
				
			||||||
            elif mode in ['SIGTERM', 'SIGKILL']:
 | 
					            elif method in ['SIGTERM', 'SIGKILL']:
 | 
				
			||||||
                self.domain.destroyFlags(flags=MODES.get(mode))
 | 
					                self.domain.destroyFlags(flags=METHODS.get(method))
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            raise VMError(f'Cannot shutdown vm={self.domain_name} with '
 | 
					            raise VMError(f'Cannot shutdown vm={self.domain_name} with '
 | 
				
			||||||
                          f'mode={mode}: {err}') from err
 | 
					                          f'method={method}: {err}') from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reset(self) -> None:
 | 
					    def reset(self) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -186,10 +187,26 @@ class VirtualMachine(VirtualMachineBase):
 | 
				
			|||||||
            raise VMError(
 | 
					            raise VMError(
 | 
				
			||||||
                f'Cannot set memory for vm={self.domain_name}: {err}') from err
 | 
					                f'Cannot set memory for vm={self.domain_name}: {err}') from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def attach_device(self, device: str):
 | 
					    def attach_device(self, dev_xml: str, hotplug: bool = False):
 | 
				
			||||||
        pass
 | 
					        if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
 | 
				
			||||||
 | 
					            flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE +
 | 
				
			||||||
 | 
					                     libvirt.VIR_DOMAIN_AFFECT_CONFIG)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
				
			||||||
 | 
					        self.domain.attachDeviceFlags(dev_xml, flags=flags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def detach_device(self, device: str):
 | 
					    def detach_device(self, dev_xml: str, hotplug: bool = False):
 | 
				
			||||||
 | 
					        if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
 | 
				
			||||||
 | 
					            flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE +
 | 
				
			||||||
 | 
					                     libvirt.VIR_DOMAIN_AFFECT_CONFIG)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
				
			||||||
 | 
					        self.domain.detachDeviceFlags(dev_xml, flags=flags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def resize_volume(self, vol_info: VolumeInfo, online: bool = False):
 | 
				
			||||||
 | 
					        # Этот метод должен принимать описание волюма и в зависимости от
 | 
				
			||||||
 | 
					        # флага online вызывать virStorageVolResize или virDomainBlockResize
 | 
				
			||||||
 | 
					        # https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainBlockResize
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def list_ssh_keys(self, user: str):
 | 
					    def list_ssh_keys(self, user: str):
 | 
				
			||||||
@@ -201,8 +218,8 @@ class VirtualMachine(VirtualMachineBase):
 | 
				
			|||||||
    def remove_ssh_keys(self, user: str):
 | 
					    def remove_ssh_keys(self, user: str):
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set_user_password(self, user: str):
 | 
					    def set_user_password(self, user: str, password: str):
 | 
				
			||||||
        pass
 | 
					        self.domain.setUserPassword(user, password)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dump_xml(self) -> str:
 | 
					    def dump_xml(self) -> str:
 | 
				
			||||||
        return self.domain.XMLDesc()
 | 
					        return self.domain.XMLDesc()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					from .storage_pool import StoragePool
 | 
				
			||||||
 | 
					from .volume import Volume, VolumeInfo
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,37 @@
 | 
				
			|||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .volume import Volume, VolumeInfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StoragePool:
 | 
					class StoragePool:
 | 
				
			||||||
    def __init__(self, pool: libvirt.virStoragePool):
 | 
					    def __init__(self, pool: libvirt.virStoragePool):
 | 
				
			||||||
        self.pool = pool
 | 
					        self.pool = pool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_volume(self):
 | 
					    @property
 | 
				
			||||||
 | 
					    def name(self) -> str:
 | 
				
			||||||
 | 
					        return self.pool.name()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def dump_xml(self) -> str:
 | 
				
			||||||
 | 
					        return self.pool.XMLDesc()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def create(self):
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete(self):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def refresh(self) -> None:
 | 
				
			||||||
 | 
					        self.pool.refresh()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def create_volume(self, vol_info: VolumeInfo) -> None:
 | 
				
			||||||
 | 
					        # todo: return Volume object?
 | 
				
			||||||
 | 
					        self.pool.createXML(
 | 
				
			||||||
 | 
					            vol_info.to_xml(),
 | 
				
			||||||
 | 
					            flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_volume(self, name: str) -> Volume:
 | 
				
			||||||
 | 
					        vol = self.pool.storageVolLookupByName(name)
 | 
				
			||||||
 | 
					        return Volume(self.pool, vol)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def list_volumes(self) -> list[Volume]:
 | 
				
			||||||
 | 
					        return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +1,64 @@
 | 
				
			|||||||
 | 
					from dataclasses import dataclass
 | 
				
			||||||
 | 
					from time import time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
 | 
					from lxml.builder import E
 | 
				
			||||||
 | 
					from lxml.etree import tostring
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
class VolumeInfo:
 | 
					class VolumeInfo:
 | 
				
			||||||
    """
 | 
					    name: str
 | 
				
			||||||
    Volume info schema
 | 
					    path: str
 | 
				
			||||||
    {'type': 'local', 'system': True, 'size': 102400, 'mode': 'rw'}
 | 
					    capacity: int
 | 
				
			||||||
    """
 | 
					
 | 
				
			||||||
    pass
 | 
					    def to_xml(self) -> str:
 | 
				
			||||||
 | 
					        unixtime = str(int(time()))
 | 
				
			||||||
 | 
					        xml = E.volume(type='file')
 | 
				
			||||||
 | 
					        xml.append(E.name(self.name))
 | 
				
			||||||
 | 
					        xml.append(E.key(self.path))
 | 
				
			||||||
 | 
					        xml.append(E.source())
 | 
				
			||||||
 | 
					        xml.append(E.capacity(str(self.capacity * 1024 * 1024), unit='bytes'))
 | 
				
			||||||
 | 
					        xml.append(E.allocation('0'))
 | 
				
			||||||
 | 
					        xml.append(E.target(
 | 
				
			||||||
 | 
					            E.path(self.path),
 | 
				
			||||||
 | 
					            E.format(type='qcow2'),
 | 
				
			||||||
 | 
					            E.timestamps(
 | 
				
			||||||
 | 
					                E.atime(unixtime),
 | 
				
			||||||
 | 
					                E.mtime(unixtime),
 | 
				
			||||||
 | 
					                E.ctime(unixtime)),
 | 
				
			||||||
 | 
					            E.compat('1.1'),
 | 
				
			||||||
 | 
					            E.features(E.lazy_refcounts())
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					        return tostring(xml, encoding='unicode', pretty_print=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Volume:
 | 
					class Volume:
 | 
				
			||||||
    def __init__(self, pool: libvirt.virStorageVol):
 | 
					    def __init__(self, pool: libvirt.virStoragePool,
 | 
				
			||||||
 | 
					                 vol: libvirt.virStorageVol):
 | 
				
			||||||
        self.pool = pool
 | 
					        self.pool = pool
 | 
				
			||||||
 | 
					        self.vol = vol
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def lookup_by_path(self):
 | 
					    @property
 | 
				
			||||||
        pass
 | 
					    def name(self) -> str:
 | 
				
			||||||
 | 
					        return self.vol.name()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def generate_xml(self):
 | 
					    @property
 | 
				
			||||||
        pass
 | 
					    def path(self) -> str:
 | 
				
			||||||
 | 
					        return self.vol.path()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self):
 | 
					    def dump_xml(self) -> str:
 | 
				
			||||||
        pass
 | 
					        return self.vol.XMLDesc()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clone(self, vol_info: VolumeInfo) -> None:
 | 
				
			||||||
 | 
					        self.pool.createXMLFrom(
 | 
				
			||||||
 | 
					            vol_info.to_xml(),
 | 
				
			||||||
 | 
					            self.vol,
 | 
				
			||||||
 | 
					            flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def resize(self, capacity: int):
 | 
				
			||||||
 | 
					        """Resize volume to `capacity`. Unit is mebibyte."""
 | 
				
			||||||
 | 
					        self.vol.resize(capacity * 1024 * 1024)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete(self) -> None:
 | 
				
			||||||
 | 
					        self.vol.delete()
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user