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