some updates
This commit is contained in:
		
							
								
								
									
										55
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								README.md
									
									
									
									
									
								
							@@ -1,43 +1,28 @@
 | 
				
			|||||||
# Compute Node Agent
 | 
					# Compute Node Agent library
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Агент для работы на ворк-нодах. В этом репозитории развивается базовая библиотека для взаимодействия с libvirt и выполнения основных операций.
 | 
					В этом репозитории развивается базовая библиотека для взаимодействия с libvirt и выполнения операций с виртуальными машинами. Фокус на QEMU/KVM.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Как это должно выглядеть
 | 
					# Зависимости (версии из APT репозитория Debian 12):
 | 
				
			||||||
 | 
					 | 
				
			||||||
`node-agent` должен стать обычным DEB-пакетом. Вместе с самим приложением пойдут вспомагательные утилиты:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- `na-vmctl` virsh на минималках, который дёргает код из Node Agent. Выполняет базовые операции с VM, установку и миграцию и т.п. Реализована частично.
 | 
					 | 
				
			||||||
- `na-vmexec`. Обёртка для вызова QEMU guest agent на машинах, больше нчего уметь не должна. Реализована целиком.
 | 
					 | 
				
			||||||
- `na-volctl`. Предполагается здесь оставить всю работу с дисками. Не реализована.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Этими утилитами нет цели заменять virsh, бцдет реализован только специфичный для Node Agent функционал.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Зависимости (версии из APT репозитория Debian 12):
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
- `python3-lxml` 4.9.2
 | 
					- `python3-lxml` 4.9.2
 | 
				
			||||||
- `python3-docopt` 0.6.2
 | 
					- `python3-docopt` 0.6.2
 | 
				
			||||||
- `python3-libvirt` 9.0.0
 | 
					- `python3-libvirt` 9.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
`docopt` скорее всего будет выброшен в будущем, так как интерфейс CLI сильно усложнится.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Минимальная поддерживаемая версия Python — `3.11`, потому, что можем.
 | 
					Минимальная поддерживаемая версия Python — `3.11`, потому, что можем.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Утилиты
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `na-vmctl` virsh на минималках. Выполняет базовые операции с VM, установку и миграцию и т.п.
 | 
				
			||||||
 | 
					- `na-vmexec`. Обёртка для вызова QEMU guest agent на машинах, больше нчего уметь не должна.
 | 
				
			||||||
 | 
					- `na-volctl`. Предполагается здесь оставить всю работу с дисками. Не реализована.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# API
 | 
					# API
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Кодовая база растёт, необходимо автоматически генерировать документацию, в README её больше не будет.
 | 
					Кодовая база растёт, необходимо автоматически генерировать документацию, в README её больше не будет.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
В структуре проекта сейчас бардак, многое будет переосмыслено и переделано позже. Основная цель на текущем этапе — получить минимально работающий код, с помощью которого возможно выполнить установку виртуальной машины и как-то управлять ею.
 | 
					В структуре проекта сейчас бардак, многое будет переосмыслено и переделано позже. Основная цель на текущем этапе — получить минимально работающий код, с помощью которого возможно выполнить установку виртуальной машины и как-то управлять ею.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Базовые сущности:
 | 
					Есть набор классов, предоставляющих собой интерфейсы для взаимодействия с виртуальными машинами, стораджами, дисками и т.п. Датаклассы описывают сущности и имеют метод `to_xml()` для получения XML конфига для `libvirt`. Смысл датакласса в том, чтобы иметь один объект, содержащий в себе нормальные легкочитаемые аттрибуты и XML описание сущности одновременно.
 | 
				
			||||||
 | 
					 | 
				
			||||||
- `LivbirtSession` - обёртка над объектом `libvirt.virConnect`.
 | 
					 | 
				
			||||||
- `VirtualMachine` - класс для работы с доменами, через него выполняется большинство действий.
 | 
					 | 
				
			||||||
- `VirtualMachineInstaller` - класс для установки ВМ, выполняет кучу проверок, генерирует XML конфиг и т.п. [В ПРОЦЕССЕ]
 | 
					 | 
				
			||||||
- `StoragePool` - обёртка над `libvirt.virStoragePool`.
 | 
					 | 
				
			||||||
- `Volume` - класс для управления дисками.
 | 
					 | 
				
			||||||
- `VolumeInfo` - датакласс хранящий информацию о диске, с помощью метода `to_xml()` получаем XML описание.
 | 
					 | 
				
			||||||
- `GuestAgent` - понятно что это.
 | 
					 | 
				
			||||||
- `ConfigLoader` - загрузчик TOML-конфига.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO
 | 
					# TODO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -49,12 +34,20 @@
 | 
				
			|||||||
    - [x] Работа со StoragePool
 | 
					    - [x] Работа со StoragePool
 | 
				
			||||||
    - [x] Создание блочных устройств
 | 
					    - [x] Создание блочных устройств
 | 
				
			||||||
    - [x] Подключение/отключение устройств
 | 
					    - [x] Подключение/отключение устройств
 | 
				
			||||||
    - [ ] Метод install()
 | 
					    - [x] Метод install()
 | 
				
			||||||
 | 
					    - [ ] Выбор между SeaBIOS/UEFI
 | 
				
			||||||
 | 
					    - [ ] Выбор модели процессора
 | 
				
			||||||
- [ ] Установка ВМ (нормальный вариант)
 | 
					- [ ] Установка ВМ (нормальный вариант)
 | 
				
			||||||
- [x] Управление дисками (всратый вариант)
 | 
					- [x] Управление дисками
 | 
				
			||||||
 | 
					    - [x] Локальные qcow2
 | 
				
			||||||
 | 
					    - [ ] ZVOL
 | 
				
			||||||
 | 
					    - [ ] Сетевые диски
 | 
				
			||||||
 | 
					    - [ ] Живой ресайз файловой системы (?)
 | 
				
			||||||
- [x] Удаление ВМ
 | 
					- [x] Удаление ВМ
 | 
				
			||||||
- [x] Изменение CPU
 | 
					- [x] Изменение CPU
 | 
				
			||||||
 | 
					    - [ ] Полноценный hotplug
 | 
				
			||||||
- [x] Изменение RAM
 | 
					- [x] Изменение RAM
 | 
				
			||||||
 | 
					    - [ ] Полноценный hotplug
 | 
				
			||||||
- [ ] Миграция ВМ между нодами
 | 
					- [ ] Миграция ВМ между нодами
 | 
				
			||||||
- [x] Работа с qemu-ga
 | 
					- [x] Работа с qemu-ga
 | 
				
			||||||
- [x] Управление питанием
 | 
					- [x] Управление питанием
 | 
				
			||||||
@@ -68,15 +61,15 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Заметки
 | 
					# Заметки
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Что там с LXC?
 | 
					### Что там с LXC?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Можно добавить поддержку LXC, но только после реализации основного функционала для QEMU/KVM.
 | 
					Можно добавить поддержку LXC, но только после реализации основного функционала для QEMU/KVM.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Будущее этой библиотеки
 | 
					### Будущее этой библиотеки
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Нужно ей придумать название.
 | 
					Нужно задействовать билиотеку [libosinfo](https://libosinfo.org/) для получения информации об операционных системах. См. [How to populate Libosinfo DataBase](https://wiki.libvirt.org/HowToPopulateLibosinfoDB.html).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Failover
 | 
					### Failover
 | 
				
			||||||
 | 
					
 | 
				
			||||||
В перспективе для ВМ с сетевыми дисками возможно организовать Failover решение — ВМ будет автоматически запускаться на другой ноде из пула при отключении оригинальной ноды. Технически это можно реализовать как создание ВМ с аналогичными характеристиками на другой ноде с подключением к тому же самому сетевому диску. Сразу нужно отметить, что для реализации:
 | 
					В перспективе для ВМ с сетевыми дисками возможно организовать Failover решение — ВМ будет автоматически запускаться на другой ноде из пула при отключении оригинальной ноды. Технически это можно реализовать как создание ВМ с аналогичными характеристиками на другой ноде с подключением к тому же самому сетевому диску. Сразу нужно отметить, что для реализации:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
from .config import ConfigLoader
 | 
					from .config import ConfigLoader
 | 
				
			||||||
from .session import LibvirtSession
 | 
					 | 
				
			||||||
from .exceptions import *
 | 
					from .exceptions import *
 | 
				
			||||||
from .volume import *
 | 
					from .session import LibvirtSession
 | 
				
			||||||
from .vm import *
 | 
					from .vm import *
 | 
				
			||||||
 | 
					from .volume import *
 | 
				
			||||||
@@ -22,9 +22,9 @@ import sys
 | 
				
			|||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
from docopt import docopt
 | 
					from docopt import docopt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ..exceptions import VMError, VMNotFound
 | 
				
			||||||
from ..session import LibvirtSession
 | 
					from ..session import LibvirtSession
 | 
				
			||||||
from ..vm import VirtualMachine
 | 
					from ..vm import VirtualMachine
 | 
				
			||||||
from ..exceptions import VMError, VMNotFound
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger(__name__)
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
@@ -18,9 +18,9 @@ import sys
 | 
				
			|||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
from docopt import docopt
 | 
					from docopt import docopt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ..exceptions import GuestAgentError, VMNotFound
 | 
				
			||||||
from ..session import LibvirtSession
 | 
					from ..session import LibvirtSession
 | 
				
			||||||
from ..vm import GuestAgent
 | 
					from ..vm import GuestAgent
 | 
				
			||||||
from ..exceptions import GuestAgentError, VMNotFound
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger(__name__)
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
class ConfigLoaderError(Exception):
 | 
					class ConfigLoaderError(Exception):
 | 
				
			||||||
    """Bad config file syntax, unreachable file or bad config schema."""
 | 
					    """Bad config file syntax, unreachable file or bad config schema."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LibvirtSessionError(Exception):
 | 
					class LibvirtSessionError(Exception):
 | 
				
			||||||
    """Something went wrong while connecting to libvirtd."""
 | 
					    """Something went wrong while connecting to libvirtd."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2,15 +2,18 @@ from contextlib import AbstractContextManager
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .exceptions import LibvirtSessionError, VMNotFound
 | 
				
			||||||
from .vm import GuestAgent, VirtualMachine
 | 
					from .vm import GuestAgent, VirtualMachine
 | 
				
			||||||
from .volume import StoragePool
 | 
					from .volume import StoragePool
 | 
				
			||||||
from .exceptions import LibvirtSessionError, VMNotFound
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LibvirtSession(AbstractContextManager):
 | 
					class LibvirtSession(AbstractContextManager):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, uri: str = 'qemu:///system'):
 | 
					    def __init__(self, uri: str = 'qemu:///system'):
 | 
				
			||||||
        self.connection = self._connect(uri)
 | 
					        try:
 | 
				
			||||||
 | 
					            self.connection = libvirt.open(uri)
 | 
				
			||||||
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
 | 
					            raise LibvirtSessionError(err) from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __enter__(self):
 | 
					    def __enter__(self):
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
@@ -18,47 +21,31 @@ 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) -> libvirt.virConnect:
 | 
					    def get_machine(self, name: str) -> VirtualMachine:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            return libvirt.open(connection_uri)
 | 
					            return VirtualMachine(self.connection.lookupByName(name))
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					 | 
				
			||||||
            raise LibvirtSessionError(
 | 
					 | 
				
			||||||
                f'Failed to open connection to the hypervisor: {err}') from err
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _get_domain(self, name: str) -> libvirt.virDomain:
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            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) from err
 | 
					                raise VMNotFound(name) from err
 | 
				
			||||||
            raise LibvirtSessionError(err) from err
 | 
					            raise LibvirtSessionError(err) from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _list_all_domains(self) -> list[libvirt.virDomain]:
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            return self.connection.listAllDomains()
 | 
					 | 
				
			||||||
        except libvirt.libvirtError as 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]:
 | 
					    def list_machines(self) -> list[VirtualMachine]:
 | 
				
			||||||
        return [VirtualMachine(dom) for dom in self._list_all_domains()]
 | 
					        return [VirtualMachine(dom) for dom in
 | 
				
			||||||
 | 
					                self.connection.listAllDomains()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_guest_agent(self, name: str, timeout: int | None = None,
 | 
					    def get_guest_agent(self, name: str,
 | 
				
			||||||
                        flags: int | None = None) -> GuestAgent:
 | 
					                        timeout: int | None = None) -> GuestAgent:
 | 
				
			||||||
        return GuestAgent(self._get_domain(name), timeout, flags)
 | 
					        try:
 | 
				
			||||||
 | 
					            return GuestAgent(self.connection.lookupByName(name), timeout)
 | 
				
			||||||
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
 | 
					            if err.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
 | 
				
			||||||
 | 
					                raise VMNotFound(name) from err
 | 
				
			||||||
 | 
					            raise LibvirtSessionError(err) from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_storage_pool(self, name: str) -> StoragePool:
 | 
					    def get_storage_pool(self, name: str) -> StoragePool:
 | 
				
			||||||
        return StoragePool(self._get_storage_pool(name))
 | 
					        return StoragePool(self.connection.storagePoolLookupByName(name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def list_storage_pools(self):
 | 
					    def list_storage_pools(self) -> list[StoragePool]:
 | 
				
			||||||
        return [StoragePool(p) for p in self.connection.listStoragePools()]
 | 
					        return [StoragePool(p) for p in self.connection.listStoragePools()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def close(self) -> None:
 | 
					    def close(self) -> None:
 | 
				
			||||||
							
								
								
									
										73
									
								
								computelib/utils/xml.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								computelib/utils/xml.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					from lxml.etree import Element, QName, SubElement
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Constructor:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    The XML constructor. This class builds XML configs for libvirt.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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,3 +1,3 @@
 | 
				
			|||||||
from .guest_agent import GuestAgent
 | 
					from .guest_agent import GuestAgent
 | 
				
			||||||
from .installer import CPUMode, CPUTopology, VirtualMachineInstaller
 | 
					from .installer import VirtualMachineInstaller
 | 
				
			||||||
from .virtual_machine import VirtualMachine
 | 
					from .virtual_machine import VirtualMachine
 | 
				
			||||||
@@ -26,6 +26,9 @@ class GuestAgent(VirtualMachineBase):
 | 
				
			|||||||
    shellexec()
 | 
					    shellexec()
 | 
				
			||||||
        High-level method for executing shell commands on guest. Command
 | 
					        High-level method for executing shell commands on guest. Command
 | 
				
			||||||
        must be passed as string. Wraps execute() method.
 | 
					        must be passed as string. Wraps execute() method.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    TODO:
 | 
				
			||||||
 | 
					        check() method. Ping guest agent and check supported commands.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, domain: libvirt.virDomain, timeout: int | None = None,
 | 
					    def __init__(self, domain: libvirt.virDomain, timeout: int | None = None,
 | 
				
			||||||
							
								
								
									
										168
									
								
								computelib/vm/installer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								computelib/vm/installer.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,168 @@
 | 
				
			|||||||
 | 
					import textwrap
 | 
				
			||||||
 | 
					from dataclasses import dataclass
 | 
				
			||||||
 | 
					from enum import Enum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from lxml import etree
 | 
				
			||||||
 | 
					from lxml.builder import E
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ..utils import mac
 | 
				
			||||||
 | 
					from ..volume import DiskInfo, VolumeInfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class VirtualMachineInfo:
 | 
				
			||||||
 | 
					    name: str
 | 
				
			||||||
 | 
					    title: str
 | 
				
			||||||
 | 
					    memory: int
 | 
				
			||||||
 | 
					    vcpus: int
 | 
				
			||||||
 | 
					    machine: str
 | 
				
			||||||
 | 
					    emulator: str
 | 
				
			||||||
 | 
					    arch: str
 | 
				
			||||||
 | 
					    cpu: str  # CPU full XML description
 | 
				
			||||||
 | 
					    mac: str
 | 
				
			||||||
 | 
					    description: str = ''
 | 
				
			||||||
 | 
					    boot_order: tuple = ('cdrom', 'hd')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_xml(self) -> str:
 | 
				
			||||||
 | 
					        xml = E.domain(
 | 
				
			||||||
 | 
					            E.name(self.name),
 | 
				
			||||||
 | 
					            E.title(self.title),
 | 
				
			||||||
 | 
					            E.description(self.description),
 | 
				
			||||||
 | 
					            E.metadata(),
 | 
				
			||||||
 | 
					            E.memory(str(self.memory), unit='MB'),
 | 
				
			||||||
 | 
					            E.currentMemory(str(self.memory), unit='MB'),
 | 
				
			||||||
 | 
					            E.vcpu(str(self.vcpus), placement='static'),
 | 
				
			||||||
 | 
					            type='kvm')
 | 
				
			||||||
 | 
					        os = E.os(E.type('hvm', machine=self.machine, arch=self.arch))
 | 
				
			||||||
 | 
					        for dev in self.boot_order:
 | 
				
			||||||
 | 
					            os.append(E.boot(dev=dev))
 | 
				
			||||||
 | 
					        xml.append(os)
 | 
				
			||||||
 | 
					        xml.append(E.features(E.acpi(), E.apic()))
 | 
				
			||||||
 | 
					        xml.append(etree.fromstring(self.cpu))
 | 
				
			||||||
 | 
					        xml.append(E.on_poweroff('destroy'))
 | 
				
			||||||
 | 
					        xml.append(E.on_reboot('restart'))
 | 
				
			||||||
 | 
					        xml.append(E.on_crash('restart'))
 | 
				
			||||||
 | 
					        xml.append(E.pm(
 | 
				
			||||||
 | 
					            E('suspend-to-mem', enabled='no'),
 | 
				
			||||||
 | 
					            E('suspend-to-disk', enabled='no'))
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        devices = E.devices()
 | 
				
			||||||
 | 
					        devices.append(E.emulator(self.emulator))
 | 
				
			||||||
 | 
					        devices.append(E.interface(
 | 
				
			||||||
 | 
					            E.source(network='default'),
 | 
				
			||||||
 | 
					            E.mac(address=self.mac),
 | 
				
			||||||
 | 
					            type='network'))
 | 
				
			||||||
 | 
					        devices.append(E.graphics(type='vnc', port='-1', autoport='yes'))
 | 
				
			||||||
 | 
					        devices.append(E.input(type='tablet', bus='usb'))
 | 
				
			||||||
 | 
					        devices.append(E.channel(
 | 
				
			||||||
 | 
					            E.source(mode='bind'),
 | 
				
			||||||
 | 
					            E.target(type='virtio', name='org.qemu.guest_agent.0'),
 | 
				
			||||||
 | 
					            E.address(type='virtio-serial', controller='0', bus='0', port='1'),
 | 
				
			||||||
 | 
					            type='unix')
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        devices.append(E.console(
 | 
				
			||||||
 | 
					            E.target(type='serial', port='0'),
 | 
				
			||||||
 | 
					            type='pty')
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        devices.append(E.video(
 | 
				
			||||||
 | 
					            E.model(type='vga', vram='16384', heads='1', primary='yes'))
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        xml.append(devices)
 | 
				
			||||||
 | 
					        return etree.tostring(xml, encoding='unicode', pretty_print=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CPUMode(Enum):
 | 
				
			||||||
 | 
					    HOST_MODEL = 'host-model'
 | 
				
			||||||
 | 
					    HOST_PASSTHROUGH = 'host-passthrough'
 | 
				
			||||||
 | 
					    CUSTOM = 'custom'
 | 
				
			||||||
 | 
					    MAXIMUM = 'maximum'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def default(cls):
 | 
				
			||||||
 | 
					        return cls.HOST_PASSTHROUGH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@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'")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class VirtualMachineInstaller:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, session: 'LibvirtSession'):
 | 
				
			||||||
 | 
					        self.session = session
 | 
				
			||||||
 | 
					        self.connection = session.connection  # libvirt.virConnect object
 | 
				
			||||||
 | 
					        self.domcaps = etree.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, data: 'VirtualMachineSchema'):
 | 
				
			||||||
 | 
					        xml_cpu = self._choose_best_cpu(CPUMode.default())
 | 
				
			||||||
 | 
					        xml_vm = VirtualMachineInfo(
 | 
				
			||||||
 | 
					            name=data['name'],
 | 
				
			||||||
 | 
					            title=data['title'],
 | 
				
			||||||
 | 
					            vcpus=data['vcpus'],
 | 
				
			||||||
 | 
					            memory=data['memory'],
 | 
				
			||||||
 | 
					            machine=self.machine,
 | 
				
			||||||
 | 
					            emulator=self.emulator,
 | 
				
			||||||
 | 
					            arch=self.arch,
 | 
				
			||||||
 | 
					            cpu=xml_cpu,
 | 
				
			||||||
 | 
					            mac=mac.random_mac()
 | 
				
			||||||
 | 
					        ).to_xml()
 | 
				
			||||||
 | 
					        self._define(xml_vm)
 | 
				
			||||||
 | 
					        storage_pool = self.session.get_storage_pool('default')
 | 
				
			||||||
 | 
					        etalon_vol = storage_pool.get_volume('bookworm.qcow2')
 | 
				
			||||||
 | 
					        new_vol = VolumeInfo(
 | 
				
			||||||
 | 
					            name=data['name'] +
 | 
				
			||||||
 | 
					            '_disk_some_pattern.qcow2',
 | 
				
			||||||
 | 
					            path=storage_pool.path +
 | 
				
			||||||
 | 
					            '/' +
 | 
				
			||||||
 | 
					            data['name'] +
 | 
				
			||||||
 | 
					            '_disk_some_pattern.qcow2',
 | 
				
			||||||
 | 
					            capacity=data['volume']['capacity'])
 | 
				
			||||||
 | 
					        etalon_vol.clone(new_vol)
 | 
				
			||||||
 | 
					        vm = self.session.get_machine(data['name'])
 | 
				
			||||||
 | 
					        vm.attach_device(DiskInfo(path=new_vol.path, target='vda'))
 | 
				
			||||||
 | 
					        vm.set_vcpus(data['vcpus'])
 | 
				
			||||||
 | 
					        vm.set_memory(data['memory'])
 | 
				
			||||||
 | 
					        vm.start()
 | 
				
			||||||
 | 
					        vm.set_autostart(enabled=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _choose_best_cpu(self, mode: CPUMode) -> str:
 | 
				
			||||||
 | 
					        if mode == 'host-passthrough':
 | 
				
			||||||
 | 
					            xml = '<cpu mode="host-passthrough" migratable="on"/>'
 | 
				
			||||||
 | 
					        elif mode == 'maximum':
 | 
				
			||||||
 | 
					            xml = '<cpu mode="maximum" migratable="on"/>'
 | 
				
			||||||
 | 
					        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 = etree.SubElement(cpus, 'arch')
 | 
				
			||||||
 | 
					            arch.text = self.arch
 | 
				
			||||||
 | 
					            xmlcpus = etree.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)
 | 
				
			||||||
@@ -156,7 +156,7 @@ class VirtualMachine(VirtualMachineBase):
 | 
				
			|||||||
        if nvcpus == 0:
 | 
					        if nvcpus == 0:
 | 
				
			||||||
            raise VMError(f'Cannot set zero vCPUs vm={self.domain_name}')
 | 
					            raise VMError(f'Cannot set zero vCPUs vm={self.domain_name}')
 | 
				
			||||||
        if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
 | 
					        if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
 | 
				
			||||||
            flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE +
 | 
					            flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE |
 | 
				
			||||||
                     libvirt.VIR_DOMAIN_AFFECT_CONFIG)
 | 
					                     libvirt.VIR_DOMAIN_AFFECT_CONFIG)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
					            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
				
			||||||
@@ -177,31 +177,33 @@ class VirtualMachine(VirtualMachineBase):
 | 
				
			|||||||
        if memory == 0:
 | 
					        if memory == 0:
 | 
				
			||||||
            raise VMError(f'Cannot set zero memory vm={self.domain_name}')
 | 
					            raise VMError(f'Cannot set zero memory vm={self.domain_name}')
 | 
				
			||||||
        if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
 | 
					        if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
 | 
				
			||||||
            flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE +
 | 
					            flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE |
 | 
				
			||||||
                     libvirt.VIR_DOMAIN_AFFECT_CONFIG)
 | 
					                     libvirt.VIR_DOMAIN_AFFECT_CONFIG)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
					            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.domain.setVcpusFlags(memory * 1024, flags=flags)
 | 
					            self.domain.setMemoryFlags(memory * 1024,
 | 
				
			||||||
 | 
					                                       libvirt.VIR_DOMAIN_MEM_MAXIMUM)
 | 
				
			||||||
 | 
					            self.domain.setMemoryFlags(memory * 1024, flags=flags)
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            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} {memory=}: {err}') from err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def attach_device(self, dev_xml: str, hotplug: bool = False):
 | 
					    def attach_device(self, device_info: 'DeviceInfo', hotplug: bool = False):
 | 
				
			||||||
        if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
 | 
					        if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
 | 
				
			||||||
            flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE +
 | 
					            flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE |
 | 
				
			||||||
                     libvirt.VIR_DOMAIN_AFFECT_CONFIG)
 | 
					                     libvirt.VIR_DOMAIN_AFFECT_CONFIG)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
					            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
				
			||||||
        self.domain.attachDeviceFlags(dev_xml, flags=flags)
 | 
					        self.domain.attachDeviceFlags(device_info.to_xml(), flags=flags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def detach_device(self, dev_xml: str, hotplug: bool = False):
 | 
					    def detach_device(self, device_info: 'DeviceInfo', hotplug: bool = False):
 | 
				
			||||||
        if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
 | 
					        if hotplug and self.domain_info['state'] == libvirt.VIR_DOMAIN_RUNNING:
 | 
				
			||||||
            flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE +
 | 
					            flags = (libvirt.VIR_DOMAIN_AFFECT_LIVE |
 | 
				
			||||||
                     libvirt.VIR_DOMAIN_AFFECT_CONFIG)
 | 
					                     libvirt.VIR_DOMAIN_AFFECT_CONFIG)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
					            flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
 | 
				
			||||||
        self.domain.detachDeviceFlags(dev_xml, flags=flags)
 | 
					        self.domain.detachDeviceFlags(device_info.to_xml(), flags=flags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def resize_volume(self, vol_info: VolumeInfo, online: bool = False):
 | 
					    def resize_volume(self, vol_info: VolumeInfo, online: bool = False):
 | 
				
			||||||
        # Этот метод должен принимать описание волюма и в зависимости от
 | 
					        # Этот метод должен принимать описание волюма и в зависимости от
 | 
				
			||||||
@@ -218,13 +220,14 @@ 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, password: str):
 | 
					    def set_user_password(self, user: str, password: str) -> None:
 | 
				
			||||||
        self.domain.setUserPassword(user, password)
 | 
					        self.domain.setUserPassword(user, password)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dump_xml(self) -> str:
 | 
					    def dump_xml(self) -> str:
 | 
				
			||||||
        return self.domain.XMLDesc()
 | 
					        return self.domain.XMLDesc()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self, delete_volumes: bool = False):
 | 
					    def delete(self, delete_volumes: bool = False) -> None:
 | 
				
			||||||
        """Undefine VM."""
 | 
					        """Undefine VM."""
 | 
				
			||||||
        self.shutdown(method='SIGTERM')
 | 
					        self.shutdown(method='SIGTERM')
 | 
				
			||||||
        self.domain.undefine()
 | 
					        self.domain.undefine()
 | 
				
			||||||
 | 
					        # todo: delete local volumes
 | 
				
			||||||
							
								
								
									
										2
									
								
								computelib/volume/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								computelib/volume/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					from .storage_pool import StoragePool
 | 
				
			||||||
 | 
					from .volume import DiskInfo, Volume, VolumeInfo
 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
import logging
 | 
					import logging
 | 
				
			||||||
 | 
					from collections import namedtuple
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
 | 
					from lxml import etree
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..exceptions import StoragePoolError
 | 
					from ..exceptions import StoragePoolError
 | 
				
			||||||
from .volume import Volume, VolumeInfo
 | 
					from .volume import Volume, VolumeInfo
 | 
				
			||||||
@@ -17,15 +19,24 @@ class StoragePool:
 | 
				
			|||||||
    def name(self) -> str:
 | 
					    def name(self) -> str:
 | 
				
			||||||
        return self.pool.name()
 | 
					        return self.pool.name()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def path(self) -> str:
 | 
				
			||||||
 | 
					        xml = etree.fromstring(self.pool.XMLDesc())
 | 
				
			||||||
 | 
					        return xml.xpath('/pool/target/path/text()')[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def usage(self) -> 'StoragePoolUsage':
 | 
				
			||||||
 | 
					        xml = etree.fromstring(self.pool.XMLDesc())
 | 
				
			||||||
 | 
					        StoragePoolUsage = namedtuple('StoagePoolUsage',
 | 
				
			||||||
 | 
					                                      ['capacity', 'allocation', 'available'])
 | 
				
			||||||
 | 
					        return StoragePoolUsage(
 | 
				
			||||||
 | 
					            capacity=int(xml.xpath('/pool/capacity/text()')[0])
 | 
				
			||||||
 | 
					            allocation=int(xml.xpath('/pool/allocation/text()')[0])
 | 
				
			||||||
 | 
					            available=int(xml.xpath('/pool/available/text()')[0]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dump_xml(self) -> str:
 | 
					    def dump_xml(self) -> str:
 | 
				
			||||||
        return self.pool.XMLDesc()
 | 
					        return self.pool.XMLDesc()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self):
 | 
					 | 
				
			||||||
        pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def delete(self):
 | 
					 | 
				
			||||||
        pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def refresh(self) -> None:
 | 
					    def refresh(self) -> None:
 | 
				
			||||||
        self.pool.refresh()
 | 
					        self.pool.refresh()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -33,30 +44,27 @@ class StoragePool:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Create storage volume and return Volume instance.
 | 
					        Create storage volume and return Volume instance.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        logger.info(f'Create storage volume vol={vol_info.name} '
 | 
					        logger.info('Create storage volume vol=%s in pool=%s',
 | 
				
			||||||
                    f'in pool={self.pool}')
 | 
					                    vol_info.name, self.pool)
 | 
				
			||||||
        vol = self.pool.createXML(
 | 
					        vol = self.pool.createXML(
 | 
				
			||||||
            vol_info.to_xml(),
 | 
					            vol_info.to_xml(),
 | 
				
			||||||
            flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA)
 | 
					            flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA)
 | 
				
			||||||
        return Volume(self.pool, vol)
 | 
					        return Volume(self.pool, vol)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_volume(self, name: str) -> Volume | None:
 | 
					    def get_volume(self, name: str) -> Volume | None:
 | 
				
			||||||
        """
 | 
					        """Lookup and return Volume instance or None."""
 | 
				
			||||||
        Lookup and return Volume instance or None.
 | 
					        logger.info('Lookup for storage volume vol=%s in pool=%s',
 | 
				
			||||||
        """
 | 
					                    name, self.pool.name)
 | 
				
			||||||
        logger.info(f'Lookup for storage volume vol={name} '
 | 
					 | 
				
			||||||
                    f'in pool={self.pool.name}')
 | 
					 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            vol = self.pool.storageVolLookupByName(name)
 | 
					            vol = self.pool.storageVolLookupByName(name)
 | 
				
			||||||
            return Volume(self.pool, vol)
 | 
					            return Volume(self.pool, vol)
 | 
				
			||||||
        except libvirt.libvirtError as err:
 | 
					        except libvirt.libvirtError as err:
 | 
				
			||||||
            if (err.get_error_domain() == libvirt.VIR_FROM_STORAGE
 | 
					            if (err.get_error_domain() == libvirt.VIR_FROM_STORAGE or
 | 
				
			||||||
                err.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL):
 | 
					                    err.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL):
 | 
				
			||||||
                logger.error(err.get_error_message())
 | 
					                logger.error(err.get_error_message())
 | 
				
			||||||
                return None
 | 
					                return None
 | 
				
			||||||
            else:
 | 
					            logger.error('libvirt error: %s' err)
 | 
				
			||||||
                logger.error(f'libvirt error: {err}')
 | 
					            raise StoragePoolError(f'libvirt error: {err}') from err
 | 
				
			||||||
                raise StoragePoolError(f'libvirt error: {err}') from err
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def list_volumes(self) -> list[Volume]:
 | 
					    def list_volumes(self) -> list[Volume]:
 | 
				
			||||||
        return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()]
 | 
					        return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()]
 | 
				
			||||||
@@ -2,8 +2,8 @@ from dataclasses import dataclass
 | 
				
			|||||||
from time import time
 | 
					from time import time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
 | 
					from lxml import etree
 | 
				
			||||||
from lxml.builder import E
 | 
					from lxml.builder import E
 | 
				
			||||||
from lxml.etree import tostring
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass
 | 
				
			||||||
@@ -30,7 +30,23 @@ class VolumeInfo:
 | 
				
			|||||||
            E.compat('1.1'),
 | 
					            E.compat('1.1'),
 | 
				
			||||||
            E.features(E.lazy_refcounts())
 | 
					            E.features(E.lazy_refcounts())
 | 
				
			||||||
        ))
 | 
					        ))
 | 
				
			||||||
        return tostring(xml, encoding='unicode', pretty_print=True)
 | 
					        return etree.tostring(xml, encoding='unicode', pretty_print=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class DiskInfo:
 | 
				
			||||||
 | 
					    target: str
 | 
				
			||||||
 | 
					    path: str
 | 
				
			||||||
 | 
					    readonly: bool = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_xml(self) -> str:
 | 
				
			||||||
 | 
					        xml = E.disk(type='file', device='disk')
 | 
				
			||||||
 | 
					        xml.append(E.driver(name='qemu', type='qcow2', cache='writethrough'))
 | 
				
			||||||
 | 
					        xml.append(E.source(file=self.path))
 | 
				
			||||||
 | 
					        xml.append(E.target(dev=self.target, bus='virtio'))
 | 
				
			||||||
 | 
					        if self.readonly:
 | 
				
			||||||
 | 
					            xml.append(E.readonly())
 | 
				
			||||||
 | 
					        return etree.tostring(xml, encoding='unicode', pretty_print=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Volume:
 | 
					class Volume:
 | 
				
			||||||
							
								
								
									
										10
									
								
								config.toml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								config.toml
									
									
									
									
									
								
							@@ -1,26 +1,26 @@
 | 
				
			|||||||
[libvirt]
 | 
					[libvirt]
 | 
				
			||||||
uri = 'qemu:///session'
 | 
					uri = 'qemu:///system'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[logging]
 | 
					[logging]
 | 
				
			||||||
level = 'INFO'
 | 
					level = 'INFO'
 | 
				
			||||||
driver = 'file'
 | 
					driver = 'file'
 | 
				
			||||||
file = '/var/log/node-agent.log'
 | 
					file = '/var/log/compute/compute.log'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[storages.pools]]
 | 
					[[storages.pools]]
 | 
				
			||||||
name = 'ssd-nvme'
 | 
					name = 'ssd-nvme'
 | 
				
			||||||
enabled = true
 | 
					enabled = true
 | 
				
			||||||
default = true
 | 
					default = true
 | 
				
			||||||
path = '/srv/vm-volumes/ssd-nvme'
 | 
					path = '/vm-volumes/ssd-nvme'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[storages.pools]]
 | 
					[[storages.pools]]
 | 
				
			||||||
name = 'hdd'
 | 
					name = 'hdd'
 | 
				
			||||||
enabled = true
 | 
					enabled = true
 | 
				
			||||||
path = '/srv/vm-volumes/hdd'
 | 
					path = '/vm-volumes/hdd'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[storages.pools]]
 | 
					[[storages.pools]]
 | 
				
			||||||
name = 'images'
 | 
					name = 'images'
 | 
				
			||||||
enabled = true
 | 
					enabled = true
 | 
				
			||||||
path = '/srv/vm-images/vendor'
 | 
					path = '/vm-images/vendor'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[virtual_machine.defaults]
 | 
					[virtual_machine.defaults]
 | 
				
			||||||
autostart = true
 | 
					autostart = true
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,145 +0,0 @@
 | 
				
			|||||||
from lxml.builder import E
 | 
					 | 
				
			||||||
from lxml.etree import Element, QName, SubElement, fromstring, tostring
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Constructor:
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    The XML constructor. This class builds XML configs for libvirt.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def gen_domain_xml(self, name: str, title: str, desc: str, memory: int,
 | 
					 | 
				
			||||||
                       vcpus: int, domain_type: str, machine: str, arch: str,
 | 
					 | 
				
			||||||
                       boot_order: tuple, cpu: str, mac: str) -> str:
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Return basic libvirt domain configuration.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        domain = 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'),
 | 
					 | 
				
			||||||
            type='kvm'
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        os = E.os(E.type(domain_type, machine=machine, arch=arch))
 | 
					 | 
				
			||||||
        for dev in boot_order:
 | 
					 | 
				
			||||||
            os.append(E.boot(dev=dev))
 | 
					 | 
				
			||||||
        domain.append(os)
 | 
					 | 
				
			||||||
        domain.append(E.features(E.acpi(), E.apic()))
 | 
					 | 
				
			||||||
        domain.append(fromstring(cpu))
 | 
					 | 
				
			||||||
        domain.append(E.on_poweroff('destroy'))
 | 
					 | 
				
			||||||
        domain.append(E.on_reboot('restart'))
 | 
					 | 
				
			||||||
        domain.append(E.on_crash('restart'))
 | 
					 | 
				
			||||||
        domain.append(E.pm(
 | 
					 | 
				
			||||||
            E('suspend-to-mem', enabled='no'),
 | 
					 | 
				
			||||||
            E('suspend-to-disk', enabled='no'))
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        devices = E.devices()
 | 
					 | 
				
			||||||
        devices.append(E.emulator('/usr/bin/qemu-system-x86_64'))
 | 
					 | 
				
			||||||
        devices.append(E.interface(
 | 
					 | 
				
			||||||
            E.source(network='default'),
 | 
					 | 
				
			||||||
            E.mac(address=mac),
 | 
					 | 
				
			||||||
            type='network')
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        devices.append(E.graphics(type='vnc', port='-1', autoport='yes'))
 | 
					 | 
				
			||||||
        devices.append(E.input(type='tablet', bus='usb'))
 | 
					 | 
				
			||||||
        devices.append(E.channel(
 | 
					 | 
				
			||||||
            E.source(mode='bind'),
 | 
					 | 
				
			||||||
            E.target(type='virtio', name='org.qemu.guest_agent.0'),
 | 
					 | 
				
			||||||
            E.address(type='virtio-serial', controller='0', bus='0', port='1'),
 | 
					 | 
				
			||||||
            type='unix')
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        devices.append(E.console(
 | 
					 | 
				
			||||||
            E.target(type='serial', port='0'),
 | 
					 | 
				
			||||||
            type='pty')
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        devices.append(E.video(
 | 
					 | 
				
			||||||
            E.model(type='vga', vram='16384', heads='1', primary='yes'))
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        domain.append(devices)
 | 
					 | 
				
			||||||
        return tostring(domain, encoding='unicode', pretty_print=True).strip()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def gen_volume_xml(self, dev: str, mode: str, path: str) -> str:
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Todo: No hardcode
 | 
					 | 
				
			||||||
        https://libvirt.org/formatdomain.html#hard-drives-floppy-disks-cdroms
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        volume = E.disk(type='file', device='disk')
 | 
					 | 
				
			||||||
        volume.append(
 | 
					 | 
				
			||||||
            E.driver(
 | 
					 | 
				
			||||||
                name='qemu',
 | 
					 | 
				
			||||||
                type='qcow2',
 | 
					 | 
				
			||||||
                cache='writethrough'))
 | 
					 | 
				
			||||||
        volume.append(E.source(file=path))
 | 
					 | 
				
			||||||
        volume.append(E.target(dev=dev, bus='virtio'))
 | 
					 | 
				
			||||||
        if mode.lower() == 'ro':
 | 
					 | 
				
			||||||
            volume.append(E.readonly())
 | 
					 | 
				
			||||||
        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,190 +0,0 @@
 | 
				
			|||||||
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 = '<cpu mode="host-passthrough" migratable="on"/>'
 | 
					 | 
				
			||||||
        elif mode == 'maximum':
 | 
					 | 
				
			||||||
            xml = '<cpu mode="maximum" migratable="on"/>'
 | 
					 | 
				
			||||||
        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)
 | 
					 | 
				
			||||||
@@ -1,2 +0,0 @@
 | 
				
			|||||||
from .storage_pool import StoragePool
 | 
					 | 
				
			||||||
from .volume import Volume, VolumeInfo
 | 
					 | 
				
			||||||
@@ -1,26 +1,24 @@
 | 
				
			|||||||
[tool.poetry]
 | 
					[tool.poetry]
 | 
				
			||||||
name = "node-agent"
 | 
					name = "computelib"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
description = "Node Agent"
 | 
					description = "Compute Node Agent library"
 | 
				
			||||||
authors = ["ge <ge@nixhacks.net>"]
 | 
					authors = ["ge <ge@nixhacks.net>"]
 | 
				
			||||||
readme = "README.md"
 | 
					readme = "README.md"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[tool.poetry.dependencies]
 | 
					[tool.poetry.dependencies]
 | 
				
			||||||
python = "^3.11"
 | 
					python = "^3.11"
 | 
				
			||||||
libvirt-python = "9.0.0"  # 9.0.0 on Debian 12
 | 
					libvirt-python = "9.0.0"
 | 
				
			||||||
lxml = "^4.9.2"  # 4.9.2 on Debian 12
 | 
					lxml = "^4.9.2"
 | 
				
			||||||
docopt = "^0.6.2"  # 0.6.2 on Debian 12
 | 
					docopt = "^0.6.2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[tool.poetry.scripts]
 | 
					[tool.poetry.scripts]
 | 
				
			||||||
na-vmctl = "node_agent.cli.vmctl:cli"
 | 
					na-vmctl = "computelib.cli.vmctl:cli"
 | 
				
			||||||
na-vmexec = "node_agent.cli.vmexec:cli"
 | 
					na-vmexec = "computelib.cli.vmexec:cli"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[build-system]
 | 
					[build-system]
 | 
				
			||||||
requires = ["poetry-core"]
 | 
					requires = ["poetry-core"]
 | 
				
			||||||
build-backend = "poetry.core.masonry.api"
 | 
					build-backend = "poetry.core.masonry.api"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[tool.yapf]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[tool.pylint."MESSAGES CONTROL"]
 | 
					[tool.pylint."MESSAGES CONTROL"]
 | 
				
			||||||
disable = [
 | 
					disable = [
 | 
				
			||||||
    "invalid-name",
 | 
					    "invalid-name",
 | 
				
			||||||
@@ -30,4 +28,3 @@ disable = [
 | 
				
			|||||||
    "import-error",
 | 
					    "import-error",
 | 
				
			||||||
    "too-many-arguments",
 | 
					    "too-many-arguments",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user