diff --git a/.gitignore b/.gitignore index ca06d0d..7672fdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__/ *.pyc *~ +domain.xml diff --git a/README.md b/README.md index 6292649..d2226b6 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,90 @@ Агент для работы на ворк-нодах. -Пока взаимодействовать можно только так (через [test.py](test.py)): +# Как это должно выглядеть -``` -sudo env NODEAGENT_CONFIG_FILE=$PWD/configuration.toml python test.py +`node-agent` должен стать обычным DEB-пакетом. Вместе с самим приложением пойдут вспомагательные утилиты: + +- **na-vmctl** "Своя" версия virsh, которая дёргает код из Node Agent. Базовые операции с VM и также установка и миграция ВМ. Реализована частично. +- **na-vmexec**. Обёртка для вызова QEMU guest agent на машинах, больше нчего уметь не должна. Реализована целиком. +- **na-volctl**. Предполагается здесь оставить всю работу с дисками. Не реализовано. + +Этими утилитами Нет цели заменять virsh, нужно реализовать только специфичные для Node Agent вещи. + +Зависимости (версии из APT репозитория Debian 12): + +- `python3-lxml` 4.9.2 +- `python3-docopt` 0.6.2 +- `python3-libvirt` 9.0.0 (актуальная новее) + +# Классы + +Весь пакет разбит на модули, а основной функционал на классы. + +## `ConfigLoader` + +Наследуется от `UserDict`. Принимает в конструктор путь до файла, после чего экземпляром `ConfigLoader` можно пользоваться как обычным словарём. Вызывается внутри `LibvirtSession` при инициализации. + +## `LibvirtSession` + +Устанавливает сессию с libvirtd и создаёт объект virConnect. Класс умеет принимать в конструктор один аргумент — путь до файла конфигурации, но его можно опустить. + +```python +from node_agent import LibvirtSession + +session = LibvirtSession() +session.close() ``` -# Модули +Также этот класс является контекстным менеджером и его можно использвоать так: -Основной класс тут `NodeAgent`. Через него осуществляется доступ ко всем методам. +```python +from node_agent import LibvirtSession, VirtualMachine -- `base` тут базовый класс. -- `main` тут объявлен `NodeAgent`. -- `exceptions` тут исключения. -- `config` тут понятно. -- `vm` тут объявлен класс `VirtualMachine` с базовыми методами для виртуалок. Генерацию XML для дефайна ВМ следует сделать в отдельном модуле. +with LibvirtSession() as session: + vm = VirtualMachine(session, 'имя_вм') + vm.status +``` + +## `VirtualMachine` + +Класс для базового управления виртуалкой. В конструктор принимает объект LibvirtSession, который в себе содержит объект virConnect и конфиг в виде словаря. + +## `QemuAgent` + +Класс для работы с агентом на гостях. Его можно считать законченным. Он умеет: + +- Выполнять шелл команды через метод `shellexec()`. +- Выполнять команды через `execute()`. + +Внутри также способен: + +- Поллить выполнение команды. То есть можно дождаться вывода долгой команды. +- Декодирует base64 вывод STDERR и STDOUT если надо. +- Принимать STDIN # TODO -Нужно что-то придумать с обработкой ошибок. Сейчас на неожиданности я вызываю исключения, нужно некритичные из них обработать, чтобы приложение не падало при обращении к несуществующему домену или нефатальных ошибок при работе с существующими доменами. +- [ ] Установка ВМ + - [ ] Конструктор XML +- [ ] Управление дисками +- [ ] Удаление ВМ +- [ ] Изменение CPU +- [ ] Изменение RAM +- [ ] Миграция ВМ между нодами +- [x] Работа с qemu-ga +- [x] Управление питанием +- [ ] Вкл/выкл автостарт ВМ +- [ ] Статистика потребления ресурсов +- [ ] Получение инфомрации из/о ВМ +- [ ] SSH-ключи +- [ ] Сеть +- [ ] ??? -# Как это должно выглядеть +# Заметки -`node-agent` должен быть обычным DEB-пакетом. В пакете само приложение, sysyemd-сервис, конфиг. Бонусом можно доложить консольные утилиты (пока не реализованы): `nodeagent-vmctl` (чтобы напрямую дергать методы виртуалок), `guest-cmd` (обёртка над virsh quest-agent-command). +xml.py наверное лучше реализовать через lxml.objectify: https://stackoverflow.com/questions/47304314/adding-child-element-to-xml-in-python + +???: https://www.geeksforgeeks.org/reading-and-writing-xml-files-in-python/ + +Минимальный рабочий XML: https://access.redhat.com/documentation/ru-ru/red_hat_enterprise_linux/6/html/virtualization_administration_guide/section-libvirt-dom-xml-example diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..b6755fb --- /dev/null +++ b/config.toml @@ -0,0 +1,24 @@ +[general] + +[libvirt] +uri = 'qemu:///system' + +[logging] +level = 'INFO' +file = '/var/log/node-agent.log' + +[volumes] + +[[volumes.pools]] +name = 'ssd-nvme' +enabled = true +default = true +path = '/srv/vm-volumes/ssd-nvme' + +[[volumes.pools]] +name = 'hdd' +enabled = true +path = '/srv/vm-volumes/hdd' + +[vm.images] +path = '/srv/vm-images' diff --git a/configuration.toml b/configuration.toml deleted file mode 100644 index e1bac53..0000000 --- a/configuration.toml +++ /dev/null @@ -1,10 +0,0 @@ -[general] -connect_uri = 'qemu:///system' - -[blockstorage] -local = true -local_path = '/srv/vm-disks' - -[imagestorage] -local = true -local_path = '/srv/vm-images' diff --git a/node_agent/__init__.py b/node_agent/__init__.py index 1d833fd..c8d207e 100644 --- a/node_agent/__init__.py +++ b/node_agent/__init__.py @@ -1 +1,4 @@ -from .main import NodeAgent +from .main import LibvirtSession +from .vm import VirtualMachine, QemuAgent +from .config import ConfigLoader +from .exceptions import * diff --git a/node_agent/base.py b/node_agent/base.py deleted file mode 100644 index eafd489..0000000 --- a/node_agent/base.py +++ /dev/null @@ -1,7 +0,0 @@ -import libvirt - - -class NodeAgentBase: - def __init__(self, conn: libvirt.virConnect, config: dict): - self.config = config - self.conn = conn diff --git a/node_agent/config.py b/node_agent/config.py index 7dab597..68e1faf 100644 --- a/node_agent/config.py +++ b/node_agent/config.py @@ -1,21 +1,29 @@ import os import sys -import pathlib import tomllib +from pathlib import Path +from collections import UserDict + +from .exceptions import ConfigLoadError -NODEAGENT_CONFIG_FILE = \ - os.getenv('NODEAGENT_CONFIG_FILE') or '/etc/nodeagent/configuration.toml' +NODEAGENT_CONFIG_FILE = os.getenv('NODEAGENT_CONFIG_FILE') +NODEAGENT_DEFAULT_CONFIG_FILE = '/etc/node-agent/config.toml' -def load_config(config: pathlib.Path): - try: - with open(config, 'rb') as conf: - return tomllib.load(conf) - except (OSError, ValueError) as readerr: - sys.exit(f'Error: Cannot read configuration file: {readerr}') - except tomllib.TOMLDecodeError as tomlerr: - sys.exit(f'Error: Bad TOML syntax in configuration file: {tomlerr}') +class ConfigLoader(UserDict): + def __init__(self, file: Path | None = None): + if file is None: + file = NODEAGENT_CONFIG_FILE or NODEAGENT_DEFAULT_CONFIG_FILE + self.file = Path(file) + self.data = self._load() - -config = load_config(pathlib.Path(NODEAGENT_CONFIG_FILE)) + def _load(self): + try: + with open(self.file, 'rb') as config: + return tomllib.load(config) + # todo: schema validation + except (OSError, ValueError) as readerr: + raise ConfigLoadError('Cannot read config file: %s: %s', (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 diff --git a/node_agent/exceptions.py b/node_agent/exceptions.py index 2fa30fb..e50b223 100644 --- a/node_agent/exceptions.py +++ b/node_agent/exceptions.py @@ -1,3 +1,15 @@ +class ConfigLoadError(Exception): + """Bad config file syntax, unreachable file or bad data.""" + + +class LibvirtSessionError(Exception): + """Something went wrong while connecting to libvirt.""" + + +class VMError(Exception): + """Something went wrong while interacting with the domain.""" + + class VMNotFound(Exception): def __init__(self, domain, message='VM not found: {domain}'): self.domain = domain @@ -5,30 +17,5 @@ class VMNotFound(Exception): super().__init__(self.message) -class VMStartError(Exception): - def __init__(self, domain, message='VM start error: {domain}'): - self.domain = domain - self.message = message.format(domain=domain) - super().__init__(self.message) - - -class VMShutdownError(Exception): - def __init__( - self, - domain, - message="VM '{domain}' cannot shutdown, try with hard=True" - ): - self.domain = domain - self.message = message.format(domain=domain) - super().__init__(self.message) - - -class VMRebootError(Exception): - def __init__( - self, - domain, - message="VM '{domain}' reboot, try with hard=True", - ): - self.domain = domain - self.message = message.format(domain=domain) - super().__init__(self.message) +class QemuAgentError(Exception): + """Mostly QEMU Guest Agent is not responding.""" diff --git a/node_agent/main.py b/node_agent/main.py index 7a069fe..ba4adc0 100644 --- a/node_agent/main.py +++ b/node_agent/main.py @@ -1,8 +1,30 @@ +from pathlib import Path +from contextlib import AbstractContextManager + import libvirt -from .vm import VirtualMachine +from .config import ConfigLoader +from .exceptions import LibvirtSessionError -class NodeAgent: - def __init__(self, conn: libvirt.virConnect, config: dict): - self.vm = VirtualMachine(conn, config) +class LibvirtSession(AbstractContextManager): + def __init__(self, config: Path | None = None): + self.config = ConfigLoader(config) + self.session = self._connect(self.config['libvirt']['uri']) + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, exception_traceback): + self.close() + + def _connect(self, connection_uri: str): + try: + return libvirt.open(connection_uri) + except libvirt.libvirtError as err: + raise LibvirtSessionError( + 'Failed to open connection to the hypervisor: %s' % err + ) + + def close(self) -> None: + self.session.close() diff --git a/node_agent/utils/vmctl.py b/node_agent/utils/vmctl.py new file mode 100644 index 0000000..7db4529 --- /dev/null +++ b/node_agent/utils/vmctl.py @@ -0,0 +1,67 @@ +""" +Manage virtual machines. + +Usage: na-vmctl [options] status + na-vmctl [options] is-running + na-vmctl [options] start + na-vmctl [options] shutdown [-f|--force] [-9|--sigkill] + +Options: + -c, --config Config file [default: /etc/node-agent/config.yaml] + -l, --loglvl Logging level [default: INFO] + -f, --force Force action. On shutdown calls graceful destroy() + -9, --sigkill Send SIGKILL to QEMU process. Not affects without --force +""" + +import sys +import pathlib +import logging + +from docopt import docopt + +sys.path.append('/home/ge/Code/node-agent') +from node_agent import LibvirtSession, VirtualMachine, VMError, VMNotFound + + +logger = logging.getLogger(__name__) +levels = logging.getLevelNamesMapping() + + +class Color: + RED = '\033[31m' + GREEN = '\033[32m' + YELLOW = '\033[33m' + NONE = '\033[0m' + + +def cli(): + args = docopt(__doc__) + config = pathlib.Path(args['--config']) or None + loglvl = args['--loglvl'].upper() + + if loglvl in levels: + logging.basicConfig(level=levels[loglvl]) + + with LibvirtSession(config) as session: + try: + vm = VirtualMachine(session, args['']) + if args['status']: + print(vm.status) + if args['is-running']: + if vm.is_running: + print('running') + else: + sys.exit(vm.status) + if args['start']: + vm.start() + print(f'{vm.name} started') + if args['shutdown']: + vm.shutdown(force=args['--force'], sigkill=args['sigkill']) + except VMNotFound as nferr: + sys.exit(f'{Color.RED}VM {args[""]} not found.{Color.NONE}') + except VMError as vmerr: + sys.exit(f'{Color.RED}{vmerr}{Color.NONE}') + + +if __name__ == '__main__': + cli() diff --git a/node_agent/utils/vmexec.py b/node_agent/utils/vmexec.py new file mode 100644 index 0000000..c4c4a88 --- /dev/null +++ b/node_agent/utils/vmexec.py @@ -0,0 +1,92 @@ +""" +Execute shell commands on guest via guest agent. + +Usage: na-vmexec [options] + +Options: + -c, --config Config file [default: /etc/node-agent/config.yaml] + -l, --loglvl Logging level [default: INFO] + -s, --shell Guest shell [default: /bin/sh] + -t, --timeout QEMU timeout in seconds to stop polling command status [default: 60] +""" + +import sys +import pathlib +import logging + +from docopt import docopt + +sys.path.append('/home/ge/Code/node-agent') +from node_agent import LibvirtSession, VMNotFound, QemuAgent, QemuAgentError + + +logger = logging.getLogger(__name__) +levels = logging.getLevelNamesMapping() + + +class Color: + RED = '\033[31m' + GREEN = '\033[32m' + YELLOW = '\033[33m' + NONE = '\033[0m' + + +def cli(): + args = docopt(__doc__) + config = pathlib.Path(args['--config']) or None + loglvl = args['--loglvl'].upper() + + if loglvl in levels: + logging.basicConfig(level=levels[loglvl]) + + with LibvirtSession(config) as session: + shell = args['--shell'] + cmd = args[''] + + try: + ga = QemuAgent(session, args['']) + exited, exitcode, stdout, stderr = ga.shellexec( + cmd, + executable=shell, + capture_output=True, + decode_output=True, + timeout=int(args['--timeout']), + ) + except QemuAgentError as qemuerr: + errmsg = f'{Color.RED}{err}{Color.NONE}' + if str(err).startswith('Polling command pid='): + errmsg = ( + errmsg + Color.YELLOW + + '\n[NOTE: command may still running]' + + Color.NONE + ) + sys.exit(errmsg) + except VMNotFound as err: + sys.exit( + f'{Color.RED}VM {args[""]} not found.{Color.NONE}' + ) + + if not exited: + print( + Color.YELLOW + +'[NOTE: command may still running]' + + Color.NONE + ) + else: + if exitcode == 0: + exitcolor = Color.GREEN + else: + exitcolor = Color.RED + print( + exitcolor + + f'[command exited with exit code {exitcode}]' + + Color.NONE + ) + + if stderr: + print(Color.RED + stderr.strip() + Color.NONE) + if stdout: + print(Color.GREEN + stdout.strip() + Color.NONE) + +if __name__ == '__main__': + cli() diff --git a/node_agent/utils/volctl.py b/node_agent/utils/volctl.py new file mode 100644 index 0000000..e69de29 diff --git a/node_agent/vm.py b/node_agent/vm.py deleted file mode 100644 index d125428..0000000 --- a/node_agent/vm.py +++ /dev/null @@ -1,120 +0,0 @@ -import libvirt - -from .base import NodeAgentBase -from .exceptions import ( - VMNotFound, - VMStartError, - VMRebootError, - VMShutdownError, -) - - -class VirtualMachine(NodeAgentBase): - - def _dom(self, domain: str) -> libvirt.virDomain: - """Get virDomain object to manipulate with domain.""" - try: - ret = self.conn.lookupByName(domain) - if ret is not None: - return ret - raise VMNotFound(domain) - except libvirt.libvirtError as err: - raise VMNotFound(err) from err - - def create( - self, - name: str, - volumes: list[dict], - vcpus: int, - vram: int, - image: dict, - cdrom: dict | None = None, - ): - # TODO - pass - - def delete(self, name: str, delete_volumes=False): - pass - - def status(self, name: str) -> str: - """ - Return VM state: 'running', 'shutoff', etc. Ref: - https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState - """ - state = self._dom(name).info()[0] - match state: - case libvirt.VIR_DOMAIN_NOSTATE: - return 'nostate' - case libvirt.VIR_DOMAIN_RUNNING: - return 'running' - case libvirt.VIR_DOMAIN_BLOCKED: - return 'blocked' - case libvirt.VIR_DOMAIN_PAUSED: - return 'paused' - case libvirt.VIR_DOMAIN_SHUTDOWN: - return 'shutdown' - case libvirt.VIR_DOMAIN_SHUTOFF: - return 'shutoff' - case libvirt.VIR_DOMAIN_CRASHED: - return 'crashed' - case libvirt.VIR_DOMAIN_PMSUSPENDED: - return 'pmsuspended' - - def is_running(self, name: str) -> bool: - """Return True if VM is running, else return False.""" - if self._dom(name).isActive() != 1: - return False # inactive (0) or error (-1) - return True - - def start(self, name: str) -> None: - """Start VM.""" - if not self.is_running(name): - ret = self._dom(name).create() - else: - return - if ret != 0: - raise VMStartError(name) - - def shutdown(self, name: str, hard=False) -> None: - """Shutdown VM. Use hard=True to force shutdown.""" - if hard: - # Destroy VM gracefully (no SIGKILL) - ret = self._dom(name).destroyFlags(flags=libvirt.VIR_DOMAIN_DESTROY_GRACEFUL) - else: - # Normal VM shutdown, OS may ignore this. - ret = self._dom(name).shutdown() - if ret != 0: - raise VMShutdownError(name) - - def reboot(self, name: str, hard=False) -> None: - """ - Reboot VM. Use hard=True to force reboot. With forced reboot - VM will shutdown via self.shutdown() (no forced) and started. - """ - if hard: - # Forced "reboot" - self.shutdown(name) - self.start(name) - else: - # Normal reboot. - ret = self._dom(name).reboot() - if ret != 0: - raise VMRebootError(name) - - def vcpu_set(self, name: str, count: int): - pass - - def vram_set(self, name: str, count: int): - pass - - def ssh_keys_list(self, name: str, user: str): - pass - - def ssh_keys_add(self, name: str, user: str): - pass - - def ssh_keys_remove(self, name: str, user: str): - pass - - def set_user_password(self, name: str, user: str): - pass diff --git a/node_agent/vm/__init__.py b/node_agent/vm/__init__.py new file mode 100644 index 0000000..3feeb85 --- /dev/null +++ b/node_agent/vm/__init__.py @@ -0,0 +1,2 @@ +from .main import VirtualMachine +from .ga import QemuAgent diff --git a/node_agent/vm/base.py b/node_agent/vm/base.py new file mode 100644 index 0000000..693c403 --- /dev/null +++ b/node_agent/vm/base.py @@ -0,0 +1,22 @@ +import libvirt + +from ..main import LibvirtSession +from ..exceptions import VMNotFound + + +class VMBase: + def __init__(self, session: LibvirtSession, name: str): + self.domname = name + self.session = session.session # virConnect object + self.config = session.config # ConfigLoader object + self.domain = self._get_domain(name) + + def _get_domain(self, name: str) -> libvirt.virDomain: + """Get virDomain object by name to manipulate with domain.""" + try: + domain = self.session.lookupByName(name) + if domain is not None: + return domain + raise VMNotFound(name) + except libvirt.libvirtError as err: + raise VMNotFound(err) from err diff --git a/node_agent/vm/ga.py b/node_agent/vm/ga.py new file mode 100644 index 0000000..6c3f65d --- /dev/null +++ b/node_agent/vm/ga.py @@ -0,0 +1,192 @@ +import json +import logging +from time import time, sleep +from base64 import standard_b64encode, b64decode, b64encode + +import libvirt +import libvirt_qemu + +from ..main import LibvirtSession +from ..exceptions import QemuAgentError +from .base import VMBase + + +logger = logging.getLogger(__name__) + + +DEFAULT_WAIT_TIMEOUT = 60 # seconds +POLL_INTERVAL = 0.3 + + +class QemuAgent(VMBase): + """ + Interacting with QEMU guest agent. Methods: + + execute() + Low-level method for executing QEMU command as dict. Command dict + internally converts to JSON. See method docstring for more info. + shellexec() + High-level method for executing shell commands on guest. Command + must be passed as string. Wraps execute() method. + _execute() + Just executes QEMU command. Wraps libvirt_qemu.qemuAgentCommand() + _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. + _return_tuple() + This method transforms JSON command output to tuple and decode + base64 encoded strings optionally. + """ + + def __init__(self, + session: LibvirtSession, + name: str, + timeout: int | None = None, + flags: int | None = None + ): + super().__init__(session, name) + self.timeout = timeout or DEFAULT_WAIT_TIMEOUT # timeout for guest agent + self.flags = flags or libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT + + def execute( + self, + command: dict, + stdin: str | None = None, + capture_output: bool = False, + decode_output: bool = False, + wait: bool = True, + timeout: int = DEFAULT_WAIT_TIMEOUT, + ): + """ + Execute command on guest and return output if capture_output is True. + See https://wiki.qemu.org/Documentation/QMP for QEMU commands reference. + Return values: + tuple( + exited: bool | None, + exitcode: int | None, + stdout: str | None, + stderr: str | None + ) + stdout and stderr are base64 encoded strings or None. + """ + # todo command dict schema validation + if capture_output: + command['arguments']['capture-output'] = True + if isinstance(stdin, str): + command['arguments']['input-data'] = standard_b64encode( + stdin.encode('utf-8') + ).decode('utf-8') + + # Execute command on guest + cmd_out = self._execute(command) + + if capture_output: + cmd_pid = json.loads(cmd_out)['return']['pid'] + return self._get_cmd_result( + cmd_pid, + decode_output=decode_output, + wait=wait, + timeout=timeout, + ) + return None, None, None, None + + def shellexec( + self, + command: str, + stdin: str | None = None, + executable: str = '/bin/sh', + capture_output: bool = False, + decode_output: bool = False, + wait: bool = True, + timeout: int = DEFAULT_WAIT_TIMEOUT, + ): + """ + Execute command on guest with selected shell. /bin/sh by default. + Otherwise of execute() this function brings command as string. + """ + cmd = { + 'execute': 'guest-exec', + 'arguments': { + 'path': executable, + 'arg': ['-c', command], + } + } + return self.execute( + cmd, + stdin=stdin, + capture_output=capture_output, + decode_output=decode_output, + wait=wait, + timeout=timeout, + ) + + + def _execute(self, command: dict): + logging.debug('Execute command: vm=%s cmd=%s', self.domname, command) + try: + return libvirt_qemu.qemuAgentCommand( + self.domain, # virDomain object + json.dumps(command), + self.timeout, + self.flags, + ) + except libvirt.libvirtError as err: + raise QemuAgentError(err) from err + + def _get_cmd_result( + self, + pid: int, + decode_output: bool = False, + wait: bool = True, + timeout: int = DEFAULT_WAIT_TIMEOUT, + ): + """Get executed command result. See GuestAgent.execute() for info.""" + exited = exitcode = stdout = stderr = None + + cmd = { + 'execute': 'guest-exec-status', + 'arguments': {'pid': pid}, + } + + if not wait: + output = json.loads(self._execute(cmd)) + return self._return_tuple(output, decode=decode_output) + + logger.debug('Start polling command pid=%s', pid) + start_time = time() + while True: + output = json.loads(self._execute(cmd)) + if output['return']['exited']: + break + sleep(POLL_INTERVAL) + now = time() + if now - start_time > timeout: + raise QemuAgentError( + f'Polling command pid={pid} took longer than {timeout} seconds.' + ) + logger.debug( + 'Polling command pid=%s finished, time taken: %s seconds', + pid, int(time()-start_time) + ) + return self._return_tuple(output, decode=decode_output) + + def _return_tuple(self, cmd_output: dict, decode: bool = False): + exited = cmd_output['return']['exited'] + exitcode = cmd_output['return']['exitcode'] + + try: + stdout = cmd_output['return']['out-data'] + if decode and stdout: + stdout = b64decode(stdout).decode('utf-8') + except KeyError: + stdout = None + + try: + stderr = cmd_output['return']['err-data'] + if decode and stderr: + stderr = b64decode(stderr).decode('utf-8') + except KeyError: + stderr = None + + return exited, exitcode, stdout, stderr diff --git a/node_agent/vm/main.py b/node_agent/vm/main.py new file mode 100644 index 0000000..4a1d119 --- /dev/null +++ b/node_agent/vm/main.py @@ -0,0 +1,126 @@ +import logging + +import libvirt + +from ..exceptions import VMError +from .base import VMBase + + +logger = logging.getLogger(__name__) + + +class VirtualMachine(VMBase): + + @property + def name(self): + return self.domain.name() + + @property + def status(self) -> str: + """ + Return VM state: 'running', 'shutoff', etc. Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState + """ + state = self.domain.info()[0] + match state: + case libvirt.VIR_DOMAIN_NOSTATE: + return 'nostate' + case libvirt.VIR_DOMAIN_RUNNING: + return 'running' + case libvirt.VIR_DOMAIN_BLOCKED: + return 'blocked' + case libvirt.VIR_DOMAIN_PAUSED: + return 'paused' + case libvirt.VIR_DOMAIN_SHUTDOWN: + return 'shutdown' + case libvirt.VIR_DOMAIN_SHUTOFF: + return 'shutoff' + case libvirt.VIR_DOMAIN_CRASHED: + return 'crashed' + case libvirt.VIR_DOMAIN_PMSUSPENDED: + return 'pmsuspended' + + @property + def is_running(self) -> bool: + """Return True if VM is running, else return False.""" + if self.domain.isActive() != 1: + # inactive (0) or error (-1) + return False + return True + + def start(self) -> None: + """Start defined VM.""" + logger.info('Starting VM: vm=%s', self.domname) + if self.is_running: + logger.debug('VM vm=%s is already started, nothing to do', self.domname) + return + 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) + + def shutdown(self, force=False, sigkill=False) -> None: + """ + Send ACPI signal to guest OS to shutdown. OS may ignore this. + Use `force=True` for graceful VM destroy. Add `sigkill=True` + to hard shutdown (may corrupt guest data!). + """ + if sigkill: + 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: + raise VMError( + f'Cannot shutdown VM, try force or sigkill: %s', self.domname + ) + + def reset(self): + """ + 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. + + 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) + + 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) + + def set_autostart(self) -> None: + ret = self.domain.autostart() + if ret != 0: + raise VMError('Cannot set : %s', self.domname) + + def vcpu_set(self, count: int): + pass + + def vram_set(self, count: int): + pass + + def ssh_keys_list(self, user: str): + pass + + def ssh_keys_add(self, user: str): + pass + + def ssh_keys_remove(self, user: str): + pass + + def set_user_password(self, user: str): + pass diff --git a/node_agent/volume/__init__.py b/node_agent/volume/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/node_agent/xml.py b/node_agent/xml.py new file mode 100644 index 0000000..bf354e1 --- /dev/null +++ b/node_agent/xml.py @@ -0,0 +1,115 @@ +import pathlib + +from lxml import etree +from lxml.builder import E + + +class NewXML: + def __init__( + 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, + ): + """ + Initialise basic XML using lxml E-Factory. Ref: + + - https://lxml.de/tutorial.html#the-e-factory + - https://libvirt.org/formatdomain.html + """ + 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'), + ), + FEATURES( + ACPI(), + APIC(), + ), + CPU( + CPU_VENDOR(cpu_vendor), + CPU_MODEL(cpu_model, fallback='forbid'), + 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'), + type='file', + device='disk', + ), + ), + 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 + +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', +) + +# x.add_volume() +# print(x.domain) +print(etree.tostring(x.domain, pretty_print=True).decode().strip()) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..34965bd --- /dev/null +++ b/poetry.lock @@ -0,0 +1,136 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] + +[[package]] +name = "libvirt-python" +version = "9.0.0" +description = "The libvirt virtualization API python binding" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "libvirt-python-9.0.0.tar.gz", hash = "sha256:49702d33fa8cbcae19fa727467a69f7ae2241b3091324085ca1cc752b2b414ce"}, +] + +[[package]] +name = "lxml" +version = "4.9.3" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +files = [ + {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"}, + {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"}, + {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"}, + {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, + {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, + {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, + {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, + {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, + {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, + {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, + {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"}, + {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"}, + {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"}, + {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"}, + {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"}, + {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"}, + {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"}, + {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, + {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, + {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, + {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, + {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, + {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, + {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, + {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=0.29.35)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "8e62a9e51f66c5a3a124d0e631ca68803f2c8d933a75faf2783dc4ddf118e7ab" diff --git a/pyproject.toml b/pyproject.toml index 12010b3..6cbbe64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,16 @@ version = "0.1.0" description = "Node Agent" authors = ["ge "] readme = "README.md" -packages = [{include = "node_agent"}] [tool.poetry.dependencies] python = "^3.11" -libvirt-python = "^9.4.0" # 9.0.0 on Debian 12 +libvirt-python = "9.0.0" # 9.0.0 on Debian 12 +lxml = "^4.9.2" # 4.9.2 on Debian 12 +docopt = "^0.6.2" # 0.6.2 on Debian 12 + +[tool.poetry.scripts] +na-vmctl = "node_agent.utils.vmctl:cli" +na-vmexec = "node_agent.utils.vmexec:cli" [build-system] requires = ["poetry-core"] diff --git a/test.py b/test.py deleted file mode 100644 index fb4e086..0000000 --- a/test.py +++ /dev/null @@ -1,16 +0,0 @@ -import libvirt - -from node_agent import NodeAgent -from node_agent.config import config - - -try: - conn = libvirt.open(config['general']['connect_uri']) -except libvirt.libvirtError as err: - sys.exit('Failed to open connection to the hypervisor: %s' % err) - - -node_agent = NodeAgent(conn, config) -s = node_agent.vm.status('debian12') -print(s) -conn.close()