various improvements
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -2,3 +2,5 @@ __pycache__/
 | 
			
		||||
*.pyc
 | 
			
		||||
*~
 | 
			
		||||
domain.xml
 | 
			
		||||
na
 | 
			
		||||
dist/
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										77
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										77
									
								
								README.md
									
									
									
									
									
								
							@@ -6,11 +6,11 @@
 | 
			
		||||
 | 
			
		||||
`node-agent` должен стать обычным DEB-пакетом. Вместе с самим приложением пойдут вспомагательные утилиты:
 | 
			
		||||
 | 
			
		||||
- **na-vmctl** "Своя" версия virsh, которая дёргает код из Node Agent. Базовые операции с VM и также установка и миграция ВМ. Реализована частично.
 | 
			
		||||
- **na-vmexec**. Обёртка для вызова QEMU guest agent на машинах, больше нчего уметь не должна. Реализована целиком.
 | 
			
		||||
- **na-volctl**. Предполагается здесь оставить всю работу с дисками. Не реализовано.
 | 
			
		||||
- `na-vmctl` virsh на минималках, который дёргает код из Node Agent. Выполняет базовые операции с VM, установку и миграцию и т.п. Реализована частично.
 | 
			
		||||
- `na-vmexec`. Обёртка для вызова QEMU guest agent на машинах, больше нчего уметь не должна. Реализована целиком.
 | 
			
		||||
- `na-volctl`. Предполагается здесь оставить всю работу с дисками. Не реализована.
 | 
			
		||||
 | 
			
		||||
Этими утилитами Нет цели заменять virsh, нужно реализовать только специфичные для Node Agent вещи.
 | 
			
		||||
Этими утилитами нет цели заменять virsh, бцдет реализован только специфичный для Node Agent функционал.
 | 
			
		||||
 | 
			
		||||
Зависимости (версии из APT репозитория Debian 12):
 | 
			
		||||
 | 
			
		||||
@@ -18,6 +18,8 @@
 | 
			
		||||
- `python3-docopt` 0.6.2
 | 
			
		||||
- `python3-libvirt` 9.0.0 (актуальная новее)
 | 
			
		||||
 | 
			
		||||
Минимальная поддерживаемая версия Python — `3.11`, потому, что можем.
 | 
			
		||||
 | 
			
		||||
# Классы
 | 
			
		||||
 | 
			
		||||
Весь пакет разбит на модули, а основной функционал на классы.
 | 
			
		||||
@@ -49,25 +51,78 @@ with LibvirtSession() as session:
 | 
			
		||||
 | 
			
		||||
## `VirtualMachine`
 | 
			
		||||
 | 
			
		||||
Класс для базового управления виртуалкой. В конструктор принимает объект LibvirtSession, который в себе содержит объект virConnect и конфиг в виде словаря.
 | 
			
		||||
Класс для базового управления виртуалкой. В конструктор принимает объект LibvirtSession и создаёт объект `virDomain`.
 | 
			
		||||
 | 
			
		||||
## `QemuAgent`
 | 
			
		||||
 | 
			
		||||
Класс для работы с агентом на гостях. Его можно считать законченным. Он умеет:
 | 
			
		||||
Класс для работы с агентом на гостях. Инициализируется аналогично `VirtualMachine`. Его можно считать законченным. Он умеет:
 | 
			
		||||
 | 
			
		||||
- Выполнять шелл команды через метод `shellexec()`.
 | 
			
		||||
- Выполнять команды через `execute()`.
 | 
			
		||||
- Выполнять любые команды QEMU через `execute()`.
 | 
			
		||||
 | 
			
		||||
Внутри также способен:
 | 
			
		||||
Также способен:
 | 
			
		||||
 | 
			
		||||
- Поллить выполнение команды. То есть можно дождаться вывода долгой команды.
 | 
			
		||||
- Декодирует base64 вывод STDERR и STDOUT если надо.
 | 
			
		||||
- Принимать STDIN
 | 
			
		||||
- Декодировать base64 вывод STDERR и STDOUT если надо.
 | 
			
		||||
- Отправлять данные на STDIN.
 | 
			
		||||
 | 
			
		||||
## `XMLConstructor`
 | 
			
		||||
 | 
			
		||||
Класс для генерации XML конфигов для либвирта и редактирования XML. Пока умеет очень мало и требует перепиливания. Возможно стоит разбить его на несколько классов. Пример работы с ним:
 | 
			
		||||
 | 
			
		||||
```python
 | 
			
		||||
from node_agent.xml import XMLConstructor
 | 
			
		||||
 | 
			
		||||
domain_xml = XMLConstructor()
 | 
			
		||||
domain_xml.gen_domain_xml(
 | 
			
		||||
    name='13',
 | 
			
		||||
    title='',
 | 
			
		||||
    vcpus=2,
 | 
			
		||||
    cpu_vendor='Intel',
 | 
			
		||||
    cpu_model='Broadwell',
 | 
			
		||||
    memory=2048,
 | 
			
		||||
    volume='/srv/vm-volumes/ef0bcd68-02c2-4f31-ae96-14d2bda5a97b.qcow2',
 | 
			
		||||
)
 | 
			
		||||
tags_meta = {
 | 
			
		||||
    'name': 'tags',
 | 
			
		||||
    'children': [
 | 
			
		||||
        {'name': 'god_mode'},
 | 
			
		||||
        {'name': 'service'}
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
domain_xml.add_meta(tags_meta, namespace='http://half-it-stack.org/xmlns/tags-meta', nsprefix='tags')
 | 
			
		||||
print(domain_xml.to_string())
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
В итоге должен получиться какой-то конфиг для ВМ.
 | 
			
		||||
 | 
			
		||||
Имеет метод `construct_xml()`, который позволяет привести словарь Python в XML элемент (обхект `lxml.etree.Element`). Пример:
 | 
			
		||||
 | 
			
		||||
```python
 | 
			
		||||
>>> from lxml.etree import tostring
 | 
			
		||||
>>> from na.xml import XMLConstructor
 | 
			
		||||
>>> xml = XMLConstructor()
 | 
			
		||||
>>> tag = {
 | 
			
		||||
...     'name': 'mytag',
 | 
			
		||||
...     'values': {
 | 
			
		||||
...             'firstname': 'John',
 | 
			
		||||
...             'lastname': 'Doe'
 | 
			
		||||
...     },
 | 
			
		||||
...     'text': 'Hello!',
 | 
			
		||||
...     'children': [{'name': 'okay'}]
 | 
			
		||||
... }
 | 
			
		||||
>>> element = xml.construct_xml(tag)
 | 
			
		||||
>>> print(tostring(element).decode())
 | 
			
		||||
'<mytag firstname="John" lastname="Doe">Hello!<okay/></mytag>'
 | 
			
		||||
>>>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Функция рекурсивная, так что теоретически можно положить бесконечное число вложенных элементов в `children`. С аргументами `namespace` и `nsprefix` будет сгенерирован XML с неймспейсом, Ваш кэп.
 | 
			
		||||
 | 
			
		||||
# TODO
 | 
			
		||||
 | 
			
		||||
- [ ] Установка ВМ
 | 
			
		||||
    - [ ] Конструктор XML
 | 
			
		||||
    - [x] Конструктор XML (базовый)
 | 
			
		||||
- [ ] Управление дисками
 | 
			
		||||
- [ ] Удаление ВМ
 | 
			
		||||
- [ ] Изменение CPU
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								config.toml
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								config.toml
									
									
									
									
									
								
							@@ -1,4 +1,7 @@
 | 
			
		||||
[general]
 | 
			
		||||
# Наверное стоит создавать локи в виде файлов во время операций с ВМ
 | 
			
		||||
# /var/node-agent/locks/vms/{vm}
 | 
			
		||||
locks_dir = '/var/node-agent/locks'
 | 
			
		||||
 | 
			
		||||
[libvirt]
 | 
			
		||||
uri = 'qemu:///system'
 | 
			
		||||
@@ -20,5 +23,13 @@ name = 'hdd'
 | 
			
		||||
enabled = true
 | 
			
		||||
path = '/srv/vm-volumes/hdd'
 | 
			
		||||
 | 
			
		||||
[vm.images]
 | 
			
		||||
[vms.defaults]
 | 
			
		||||
# Какие-то значения по-умолчанию, используемые при создании/работе с ВМ
 | 
			
		||||
# Эти параметры также будут аффектить на CLI утилиты
 | 
			
		||||
autostart = true  # ставить виртуалки в автостарт после установки
 | 
			
		||||
start = true  # запускать ВМ после установки
 | 
			
		||||
cpu_vendor = 'Intel'
 | 
			
		||||
cpu_model = 'Broadwell'
 | 
			
		||||
 | 
			
		||||
[vms.images]
 | 
			
		||||
path = '/srv/vm-images'
 | 
			
		||||
 
 | 
			
		||||
@@ -17,13 +17,14 @@ class ConfigLoader(UserDict):
 | 
			
		||||
            file = NODEAGENT_CONFIG_FILE or NODEAGENT_DEFAULT_CONFIG_FILE
 | 
			
		||||
        self.file = Path(file)
 | 
			
		||||
        self.data = self._load()
 | 
			
		||||
        # todo: load deafult configuration
 | 
			
		||||
 | 
			
		||||
    def _load(self):
 | 
			
		||||
        try:
 | 
			
		||||
            with open(self.file, 'rb') as config:
 | 
			
		||||
                return tomllib.load(config)
 | 
			
		||||
                # todo: schema validation
 | 
			
		||||
                # todo: config schema validation
 | 
			
		||||
        except (OSError, ValueError) as readerr:
 | 
			
		||||
            raise ConfigLoadError('Cannot read config file: %s: %s', (self.file, readerr)) from readerr
 | 
			
		||||
            raise ConfigLoadError(f'Cannot read config file: {self.file}: {readerr}') from readerr
 | 
			
		||||
        except tomllib.TOMLDecodeError as tomlerr:
 | 
			
		||||
            raise ConfigLoadError('Bad TOML syntax in config file: %s: %s', (self.file, tomlerr)) from tomlerr
 | 
			
		||||
            raise ConfigLoadError(f'Bad TOML syntax in config file: {self.file}: {tomlerr}') from tomlerr
 | 
			
		||||
 
 | 
			
		||||
@@ -23,8 +23,8 @@ class LibvirtSession(AbstractContextManager):
 | 
			
		||||
            return libvirt.open(connection_uri)
 | 
			
		||||
        except libvirt.libvirtError as err:
 | 
			
		||||
            raise LibvirtSessionError(
 | 
			
		||||
                'Failed to open connection to the hypervisor: %s' % err
 | 
			
		||||
            )
 | 
			
		||||
                f'Failed to open connection to the hypervisor: {err}'
 | 
			
		||||
            ) from err
 | 
			
		||||
 | 
			
		||||
    def close(self) -> None:
 | 
			
		||||
        self.session.close()
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import sys
 | 
			
		||||
import pathlib
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import libvirt
 | 
			
		||||
from docopt import docopt
 | 
			
		||||
 | 
			
		||||
sys.path.append('/home/ge/Code/node-agent')
 | 
			
		||||
@@ -38,13 +39,14 @@ def cli():
 | 
			
		||||
    args = docopt(__doc__)
 | 
			
		||||
    config = pathlib.Path(args['--config']) or None
 | 
			
		||||
    loglvl = args['--loglvl'].upper()
 | 
			
		||||
    machine = args['<machine>']
 | 
			
		||||
 | 
			
		||||
    if loglvl in levels:
 | 
			
		||||
        logging.basicConfig(level=levels[loglvl])
 | 
			
		||||
 | 
			
		||||
    with LibvirtSession(config) as session:
 | 
			
		||||
        try:
 | 
			
		||||
            vm = VirtualMachine(session, args['<machine>'])
 | 
			
		||||
            vm = VirtualMachine(session, machine)
 | 
			
		||||
            if args['status']:
 | 
			
		||||
                print(vm.status)
 | 
			
		||||
            if args['is-running']:
 | 
			
		||||
@@ -58,7 +60,7 @@ def cli():
 | 
			
		||||
            if args['shutdown']:
 | 
			
		||||
                vm.shutdown(force=args['--force'], sigkill=args['sigkill'])
 | 
			
		||||
        except VMNotFound as nferr:
 | 
			
		||||
            sys.exit(f'{Color.RED}VM {args["<machine>"]} not found.{Color.NONE}')
 | 
			
		||||
            sys.exit(f'{Color.RED}VM {machine} not found.{Color.NONE}')
 | 
			
		||||
        except VMError as vmerr:
 | 
			
		||||
            sys.exit(f'{Color.RED}{vmerr}{Color.NONE}')
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,7 @@ def cli():
 | 
			
		||||
    args = docopt(__doc__)
 | 
			
		||||
    config = pathlib.Path(args['--config']) or None
 | 
			
		||||
    loglvl = args['--loglvl'].upper()
 | 
			
		||||
    machine = args['<machine>']
 | 
			
		||||
 | 
			
		||||
    if loglvl in levels:
 | 
			
		||||
        logging.basicConfig(level=levels[loglvl])
 | 
			
		||||
@@ -44,7 +45,7 @@ def cli():
 | 
			
		||||
        cmd = args['<command>']
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            ga = QemuAgent(session, args['<machine>'])
 | 
			
		||||
            ga = QemuAgent(session, machine)
 | 
			
		||||
            exited, exitcode, stdout, stderr = ga.shellexec(
 | 
			
		||||
                cmd,
 | 
			
		||||
                executable=shell,
 | 
			
		||||
@@ -63,14 +64,15 @@ def cli():
 | 
			
		||||
            sys.exit(errmsg)
 | 
			
		||||
        except VMNotFound as err:
 | 
			
		||||
            sys.exit(
 | 
			
		||||
                f'{Color.RED}VM {args["<machine>"]} not found.{Color.NONE}'
 | 
			
		||||
                f'{Color.RED}VM {machine} not found.{Color.NONE}'
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    if not exited:
 | 
			
		||||
        print(
 | 
			
		||||
            Color.YELLOW
 | 
			
		||||
            +'[NOTE: command may still running]'
 | 
			
		||||
            + Color.NONE
 | 
			
		||||
            + Color.NONE,
 | 
			
		||||
            file=sys.stderr
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        if exitcode == 0:
 | 
			
		||||
@@ -80,13 +82,14 @@ def cli():
 | 
			
		||||
        print(
 | 
			
		||||
            exitcolor
 | 
			
		||||
            + f'[command exited with exit code {exitcode}]'
 | 
			
		||||
            + Color.NONE
 | 
			
		||||
            + Color.NONE,
 | 
			
		||||
            file=sys.stderr
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if stderr:
 | 
			
		||||
        print(Color.RED + stderr.strip() + Color.NONE)
 | 
			
		||||
        print(Color.RED + stderr.strip() + Color.NONE, file=sys.stderr)
 | 
			
		||||
    if stdout:
 | 
			
		||||
        print(Color.GREEN + stdout.strip() + Color.NONE)
 | 
			
		||||
        print(Color.GREEN + stdout.strip() + Color.NONE, file=sys.stdout)
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    cli()
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,8 @@ from .base import VMBase
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DEFAULT_WAIT_TIMEOUT = 60  # seconds
 | 
			
		||||
POLL_INTERVAL = 0.3
 | 
			
		||||
QEMU_TIMEOUT = 60  # seconds
 | 
			
		||||
POLL_INTERVAL = 0.3  # also seconds
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class QemuAgent(VMBase):
 | 
			
		||||
@@ -33,7 +33,7 @@ class QemuAgent(VMBase):
 | 
			
		||||
    _get_cmd_result()
 | 
			
		||||
        Intended for long-time commands. This function loops and every
 | 
			
		||||
        POLL_INTERVAL calls 'guest-exec-status' for specified guest PID.
 | 
			
		||||
        Polling ends on command exited or on timeout.
 | 
			
		||||
        Polling ends if command exited or on timeout.
 | 
			
		||||
    _return_tuple()
 | 
			
		||||
        This method transforms JSON command output to tuple and decode
 | 
			
		||||
        base64 encoded strings optionally.
 | 
			
		||||
@@ -46,7 +46,7 @@ class QemuAgent(VMBase):
 | 
			
		||||
        flags: int | None = None
 | 
			
		||||
    ):
 | 
			
		||||
        super().__init__(session, name)
 | 
			
		||||
        self.timeout = timeout or DEFAULT_WAIT_TIMEOUT  # timeout for guest agent
 | 
			
		||||
        self.timeout = timeout or QEMU_TIMEOUT  # timeout for guest agent
 | 
			
		||||
        self.flags = flags or libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT
 | 
			
		||||
 | 
			
		||||
    def execute(
 | 
			
		||||
@@ -56,7 +56,7 @@ class QemuAgent(VMBase):
 | 
			
		||||
        capture_output: bool = False,
 | 
			
		||||
        decode_output: bool = False,
 | 
			
		||||
        wait: bool = True,
 | 
			
		||||
        timeout: int = DEFAULT_WAIT_TIMEOUT,
 | 
			
		||||
        timeout: int = QEMU_TIMEOUT,
 | 
			
		||||
    ):
 | 
			
		||||
        """
 | 
			
		||||
        Execute command on guest and return output if capture_output is True.
 | 
			
		||||
@@ -99,7 +99,7 @@ class QemuAgent(VMBase):
 | 
			
		||||
        capture_output: bool = False,
 | 
			
		||||
        decode_output: bool = False,
 | 
			
		||||
        wait: bool = True,
 | 
			
		||||
        timeout: int = DEFAULT_WAIT_TIMEOUT,
 | 
			
		||||
        timeout: int = QEMU_TIMEOUT,
 | 
			
		||||
    ):
 | 
			
		||||
        """
 | 
			
		||||
        Execute command on guest with selected shell. /bin/sh by default.
 | 
			
		||||
@@ -139,7 +139,7 @@ class QemuAgent(VMBase):
 | 
			
		||||
        pid: int,
 | 
			
		||||
        decode_output: bool = False,
 | 
			
		||||
        wait: bool = True,
 | 
			
		||||
        timeout: int = DEFAULT_WAIT_TIMEOUT,
 | 
			
		||||
        timeout: int = QEMU_TIMEOUT,
 | 
			
		||||
    ):
 | 
			
		||||
        """Get executed command result. See GuestAgent.execute() for info."""
 | 
			
		||||
        exited = exitcode = stdout = stderr = None
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ class VirtualMachine(VMBase):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def name(self):
 | 
			
		||||
        return self.domain.name()
 | 
			
		||||
        return self.domname
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def status(self) -> str:
 | 
			
		||||
@@ -21,7 +21,12 @@ class VirtualMachine(VMBase):
 | 
			
		||||
        Return VM state: 'running', 'shutoff', etc. Reference:
 | 
			
		||||
        https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState
 | 
			
		||||
        """
 | 
			
		||||
        state = self.domain.info()[0]
 | 
			
		||||
        try:
 | 
			
		||||
            # libvirt returns list [state: int, reason: int]
 | 
			
		||||
            # https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainGetState
 | 
			
		||||
            state = self.domain.state()[0]
 | 
			
		||||
        except libvirt.libvirtError as err:
 | 
			
		||||
            raise VMError(f'Cannot fetch VM status vm={self.domname}: {err}') from err
 | 
			
		||||
        match state:
 | 
			
		||||
            case libvirt.VIR_DOMAIN_NOSTATE:
 | 
			
		||||
                return 'nostate'
 | 
			
		||||
@@ -48,6 +53,16 @@ class VirtualMachine(VMBase):
 | 
			
		||||
            return False
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_autostart(self) -> bool:
 | 
			
		||||
        """Return True if VM autostart is enabled, else return False."""
 | 
			
		||||
        try:
 | 
			
		||||
            if self.domain.autostart() == 1:
 | 
			
		||||
                return True
 | 
			
		||||
            return False
 | 
			
		||||
        except libvirt.libvirtError as err:
 | 
			
		||||
            raise VMError(f'Cannot get autostart status vm={self.domname}: {err}') from err
 | 
			
		||||
 | 
			
		||||
    def start(self) -> None:
 | 
			
		||||
        """Start defined VM."""
 | 
			
		||||
        logger.info('Starting VM: vm=%s', self.domname)
 | 
			
		||||
@@ -57,9 +72,7 @@ class VirtualMachine(VMBase):
 | 
			
		||||
        try:
 | 
			
		||||
            ret = self.domain.create()
 | 
			
		||||
        except libvirt.libvirtError as err:
 | 
			
		||||
            raise VMError(err) from err
 | 
			
		||||
        if ret != 0:
 | 
			
		||||
            raise VMError('Cannot start VM: vm=%s exit_code=%s', self.domname, ret)
 | 
			
		||||
            raise VMError(f'Cannot start vm={self.domname} return_code={ret}: {err}') from err
 | 
			
		||||
 | 
			
		||||
    def shutdown(self, force=False, sigkill=False) -> None:
 | 
			
		||||
        """
 | 
			
		||||
@@ -71,41 +84,56 @@ class VirtualMachine(VMBase):
 | 
			
		||||
            flags = libvirt.VIR_DOMAIN_DESTROY_DEFAULT
 | 
			
		||||
        else:
 | 
			
		||||
            flags = libvirt.VIR_DOMAIN_DESTROY_GRACEFUL
 | 
			
		||||
        if force:
 | 
			
		||||
            ret = self.domain.destroyFlags(flags=flags)
 | 
			
		||||
        else:
 | 
			
		||||
            # Normal VM shutdown via ACPI signal, OS may ignore this.
 | 
			
		||||
            ret = self.domain.shutdown()
 | 
			
		||||
        if ret != 0:
 | 
			
		||||
        try:
 | 
			
		||||
            if force:
 | 
			
		||||
                self.domain.destroyFlags(flags=flags)
 | 
			
		||||
            else:
 | 
			
		||||
                # Normal VM shutdown via ACPI signal, OS may ignore this.
 | 
			
		||||
                self.domain.shutdown()
 | 
			
		||||
        except libvirt.libvirtError as err:
 | 
			
		||||
            raise VMError(
 | 
			
		||||
                f'Cannot shutdown VM, try force or sigkill: %s', self.domname
 | 
			
		||||
            )
 | 
			
		||||
                f'Cannot shutdown vm={self.domname} '
 | 
			
		||||
                f'force={force} sigkill={sigkill}: {err}'
 | 
			
		||||
            ) from err
 | 
			
		||||
 | 
			
		||||
    def reset(self):
 | 
			
		||||
        """
 | 
			
		||||
        Copypaste from libvirt doc::
 | 
			
		||||
        Copypaste from libvirt doc:
 | 
			
		||||
 | 
			
		||||
            Reset a domain immediately without any guest OS shutdown.
 | 
			
		||||
            Reset emulates the power reset button on a machine, where all
 | 
			
		||||
            hardware sees the RST line set and reinitializes internal state.
 | 
			
		||||
        Reset a domain immediately without any guest OS shutdown.
 | 
			
		||||
        Reset emulates the power reset button on a machine, where all
 | 
			
		||||
        hardware sees the RST line set and reinitializes internal state.
 | 
			
		||||
 | 
			
		||||
            Note that there is a risk of data loss caused by reset without any
 | 
			
		||||
            guest OS shutdown.
 | 
			
		||||
        Note that there is a risk of data loss caused by reset without any
 | 
			
		||||
        guest OS shutdown.
 | 
			
		||||
        """
 | 
			
		||||
        ret = self.domian.reset()
 | 
			
		||||
        if ret != 0:
 | 
			
		||||
            raise VMError('Cannot reset VM: %s', self.domname)
 | 
			
		||||
        try:
 | 
			
		||||
            self.domian.reset()
 | 
			
		||||
        except libvirt.libvirtError as err:
 | 
			
		||||
            raise VMError(f'Cannot reset vm={self.domname}: {err}') from err
 | 
			
		||||
 | 
			
		||||
    def reboot(self) -> None:
 | 
			
		||||
        """Send ACPI signal to guest OS to reboot. OS may ignore this."""
 | 
			
		||||
        ret = self.domain.reboot()
 | 
			
		||||
        if ret != 0:
 | 
			
		||||
            raise VMError('Cannot reboot: %s', self.domname)
 | 
			
		||||
        try:
 | 
			
		||||
            self.domain.reboot()
 | 
			
		||||
        except libvirt.libvirtError as err:
 | 
			
		||||
            raise VMError(f'Cannot reboot vm={self.domname}: {err}') from err
 | 
			
		||||
 | 
			
		||||
    def set_autostart(self) -> None:
 | 
			
		||||
        ret = self.domain.autostart()
 | 
			
		||||
        if ret != 0:
 | 
			
		||||
            raise VMError('Cannot set : %s', self.domname)
 | 
			
		||||
    def autostart(self, enabled: bool) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Configure VM to be automatically started when the host machine boots.
 | 
			
		||||
        """
 | 
			
		||||
        if enabled:
 | 
			
		||||
            autostart_flag = 1
 | 
			
		||||
        else:
 | 
			
		||||
            autostart_flag = 0
 | 
			
		||||
        try:
 | 
			
		||||
            self.domain.setAutostart(autostart_flag)
 | 
			
		||||
        except libvirt.libvirtError as err:
 | 
			
		||||
            raise VMError(
 | 
			
		||||
                f'Cannot set autostart vm={self.domname} '
 | 
			
		||||
                f'autostart={autostart_flag}: {err}'
 | 
			
		||||
            ) from err
 | 
			
		||||
 | 
			
		||||
    def vcpu_set(self, count: int):
 | 
			
		||||
        pass
 | 
			
		||||
 
 | 
			
		||||
@@ -1,91 +1,79 @@
 | 
			
		||||
import pathlib
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from lxml import etree
 | 
			
		||||
from lxml.etree import Element, SubElement, QName, tostring
 | 
			
		||||
from lxml.builder import E
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NewXML:
 | 
			
		||||
    def __init__(
 | 
			
		||||
class XMLConstructor:
 | 
			
		||||
    """
 | 
			
		||||
    The XML constructor. This class builds XML configs for libvirtd.
 | 
			
		||||
    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,
 | 
			
		||||
        memory: int,
 | 
			
		||||
        vcpus: int,
 | 
			
		||||
        cpu_vendor: str,
 | 
			
		||||
        cpu_model: str,
 | 
			
		||||
        volume_path: str,
 | 
			
		||||
 | 
			
		||||
        desc: str | None = None,
 | 
			
		||||
        show_boot_menu: bool = False,
 | 
			
		||||
    ):
 | 
			
		||||
        memory: int,
 | 
			
		||||
        volume: Path,
 | 
			
		||||
        desc: str = ""
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Initialise basic XML using lxml E-Factory. Ref:
 | 
			
		||||
 | 
			
		||||
            - https://lxml.de/tutorial.html#the-e-factory
 | 
			
		||||
            - https://libvirt.org/formatdomain.html
 | 
			
		||||
        Generate default domain XML configuration for virtual machines.
 | 
			
		||||
        See https://lxml.de/tutorial.html#the-e-factory for details.
 | 
			
		||||
        """
 | 
			
		||||
        DOMAIN = E.domain
 | 
			
		||||
        NAME = E.name
 | 
			
		||||
        TITLE = E.title
 | 
			
		||||
        DESCRIPTION = E.description
 | 
			
		||||
        METADATA = E.metadata
 | 
			
		||||
        MEMORY = E.memory
 | 
			
		||||
        CURRENTMEMORY = E.currentMemory
 | 
			
		||||
        VCPU = E.vcpu
 | 
			
		||||
        OS = E.os
 | 
			
		||||
        OS_TYPE = E.type
 | 
			
		||||
        OS_BOOT = E.boot
 | 
			
		||||
        FEATURES = E.features
 | 
			
		||||
        ACPI = E.acpi
 | 
			
		||||
        APIC = E.apic
 | 
			
		||||
        CPU = E.cpu
 | 
			
		||||
        CPU_VENDOR = E.vendor
 | 
			
		||||
        CPU_MODEL = E.model
 | 
			
		||||
        ON_POWEROFF = E.on_poweroff
 | 
			
		||||
        ON_REBOOT = E.on_reboot
 | 
			
		||||
        ON_CRASH = E.on_crash
 | 
			
		||||
        DEVICES = E.devices
 | 
			
		||||
        EMULATOR = E.emulator
 | 
			
		||||
        DISK = E.disk
 | 
			
		||||
        DISK_DRIVER = E.driver
 | 
			
		||||
        DISK_SOURCE = E.source
 | 
			
		||||
        DISK_TARGET = E.target
 | 
			
		||||
        INTERFACE = E.interface
 | 
			
		||||
        GRAPHICS = E.graphics
 | 
			
		||||
 | 
			
		||||
        self.domain = DOMAIN(
 | 
			
		||||
            NAME(name),
 | 
			
		||||
            TITLE(title),
 | 
			
		||||
            DESCRIPTION(desc or ""),
 | 
			
		||||
            METADATA(),
 | 
			
		||||
            MEMORY(str(memory), unit='MB'),
 | 
			
		||||
            CURRENTMEMORY(str(memory), unit='MB'),
 | 
			
		||||
            VCPU(str(vcpus), placement='static'),
 | 
			
		||||
            OS(
 | 
			
		||||
                OS_TYPE('hvm', arch='x86_64'),
 | 
			
		||||
                OS_BOOT(dev='cdrom'),
 | 
			
		||||
                OS_BOOT(dev='hd'),
 | 
			
		||||
        self.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'),
 | 
			
		||||
            ),
 | 
			
		||||
            FEATURES(
 | 
			
		||||
                ACPI(),
 | 
			
		||||
                APIC(),
 | 
			
		||||
            E.features(
 | 
			
		||||
                E.acpi(),
 | 
			
		||||
                E.apic(),
 | 
			
		||||
            ),
 | 
			
		||||
            CPU(
 | 
			
		||||
                CPU_VENDOR(cpu_vendor),
 | 
			
		||||
                CPU_MODEL(cpu_model, fallback='forbid'),
 | 
			
		||||
            E.cpu(
 | 
			
		||||
                E.vendor(cpu_vendor),
 | 
			
		||||
                E.model(cpu_model, fallback='forbid'),
 | 
			
		||||
                E.topology(sockets='1', dies='1', cores=str(vcpus), threads='1'),
 | 
			
		||||
                mode='custom',
 | 
			
		||||
                match='exact',
 | 
			
		||||
                check='partial',
 | 
			
		||||
            ),
 | 
			
		||||
            ON_POWEROFF('destroy'),
 | 
			
		||||
            ON_REBOOT('restart'),
 | 
			
		||||
            ON_CRASH('restart'),
 | 
			
		||||
            DEVICES(
 | 
			
		||||
                EMULATOR('/usr/bin/qemu-system-x86_64'),
 | 
			
		||||
                DISK(
 | 
			
		||||
                    DISK_DRIVER(name='qemu', type='qcow2', cache='writethrough'),
 | 
			
		||||
                    DISK_SOURCE(file=volume_path),
 | 
			
		||||
                    DISK_TARGET(dev='vda', bus='virtio'),
 | 
			
		||||
            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',
 | 
			
		||||
                ),
 | 
			
		||||
@@ -93,23 +81,106 @@ class NewXML:
 | 
			
		||||
            type='kvm',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def add_volume(self, options: dict, params: dict):
 | 
			
		||||
        """Add disk device to domain."""
 | 
			
		||||
        DISK = E.disk
 | 
			
		||||
        DISK_DRIVER = E.driver
 | 
			
		||||
        DISK_SOURCE = E.source
 | 
			
		||||
        DISK_TARGET = E.target
 | 
			
		||||
    def gen_volume_xml(
 | 
			
		||||
        self,
 | 
			
		||||
        device_name: str,
 | 
			
		||||
        file: Path,
 | 
			
		||||
        bus: str = 'virtio',
 | 
			
		||||
        cache: str = 'writethrough',
 | 
			
		||||
        disktype: str = 'file',
 | 
			
		||||
    ):
 | 
			
		||||
        return 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'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
x = NewXML(
 | 
			
		||||
    name='1',
 | 
			
		||||
    title='first',
 | 
			
		||||
    memory=2048,
 | 
			
		||||
    vcpus=4,
 | 
			
		||||
    cpu_vendor='Intel',
 | 
			
		||||
    cpu_model='Broadwell',
 | 
			
		||||
    volume_path='/srv/vm-volumes/5031077f-f9ea-410b-8d84-ae6e79f8cde0.qcow2',
 | 
			
		||||
)
 | 
			
		||||
    def add_volume(self):
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
# x.add_volume()
 | 
			
		||||
# print(x.domain)
 | 
			
		||||
print(etree.tostring(x.domain, pretty_print=True).decode().strip())
 | 
			
		||||
    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': 'devices',  # 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()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user