various improvements
This commit is contained in:
parent
754c608826
commit
42ad91fa83
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()
|
||||
|
Loading…
Reference in New Issue
Block a user