cool updates
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,3 +1,4 @@
 | 
				
			|||||||
__pycache__/
 | 
					__pycache__/
 | 
				
			||||||
*.pyc
 | 
					*.pyc
 | 
				
			||||||
*~
 | 
					*~
 | 
				
			||||||
 | 
					domain.xml
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										90
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										90
									
								
								README.md
									
									
									
									
									
								
							@@ -2,26 +2,90 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Агент для работы на ворк-нодах.
 | 
					Агент для работы на ворк-нодах.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Пока взаимодействовать можно только так (через [test.py](test.py)):
 | 
					# Как это должно выглядеть
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					`node-agent` должен стать обычным DEB-пакетом. Вместе с самим приложением пойдут вспомагательные утилиты:
 | 
				
			||||||
sudo env NODEAGENT_CONFIG_FILE=$PWD/configuration.toml python test.py
 | 
					
 | 
				
			||||||
 | 
					- **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` тут базовый класс.
 | 
					with LibvirtSession() as session:
 | 
				
			||||||
- `main` тут объявлен `NodeAgent`.
 | 
					    vm = VirtualMachine(session, 'имя_вм')
 | 
				
			||||||
- `exceptions` тут исключения.
 | 
					    vm.status
 | 
				
			||||||
- `config` тут понятно.
 | 
					```
 | 
				
			||||||
- `vm` тут объявлен класс `VirtualMachine` с базовыми методами для виртуалок. Генерацию XML для дефайна ВМ следует сделать в отдельном модуле.
 | 
					
 | 
				
			||||||
 | 
					## `VirtualMachine`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Класс для базового управления виртуалкой. В конструктор принимает объект LibvirtSession, который в себе содержит объект virConnect и конфиг в виде словаря.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## `QemuAgent`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Класс для работы с агентом на гостях. Его можно считать законченным. Он умеет:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Выполнять шелл команды через метод `shellexec()`.
 | 
				
			||||||
 | 
					- Выполнять команды через `execute()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Внутри также способен:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Поллить выполнение команды. То есть можно дождаться вывода долгой команды.
 | 
				
			||||||
 | 
					- Декодирует base64 вывод STDERR и STDOUT если надо.
 | 
				
			||||||
 | 
					- Принимать STDIN
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO
 | 
					# 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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								config.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								config.toml
									
									
									
									
									
										Normal file
									
								
							@@ -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'
 | 
				
			||||||
@@ -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'
 | 
					 | 
				
			||||||
@@ -1 +1,4 @@
 | 
				
			|||||||
from .main import NodeAgent
 | 
					from .main import LibvirtSession
 | 
				
			||||||
 | 
					from .vm import VirtualMachine, QemuAgent
 | 
				
			||||||
 | 
					from .config import ConfigLoader
 | 
				
			||||||
 | 
					from .exceptions import *
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +0,0 @@
 | 
				
			|||||||
import libvirt
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NodeAgentBase:
 | 
					 | 
				
			||||||
    def __init__(self, conn: libvirt.virConnect, config: dict):
 | 
					 | 
				
			||||||
        self.config = config
 | 
					 | 
				
			||||||
        self.conn = conn
 | 
					 | 
				
			||||||
@@ -1,21 +1,29 @@
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
import pathlib
 | 
					 | 
				
			||||||
import tomllib
 | 
					import tomllib
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from collections import UserDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .exceptions import ConfigLoadError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
NODEAGENT_CONFIG_FILE = \
 | 
					NODEAGENT_CONFIG_FILE = os.getenv('NODEAGENT_CONFIG_FILE')
 | 
				
			||||||
    os.getenv('NODEAGENT_CONFIG_FILE') or '/etc/nodeagent/configuration.toml'
 | 
					NODEAGENT_DEFAULT_CONFIG_FILE = '/etc/node-agent/config.toml'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def load_config(config: pathlib.Path):
 | 
					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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _load(self):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
        with open(config, 'rb') as conf:
 | 
					            with open(self.file, 'rb') as config:
 | 
				
			||||||
            return tomllib.load(conf)
 | 
					                return tomllib.load(config)
 | 
				
			||||||
 | 
					                # todo: schema validation
 | 
				
			||||||
        except (OSError, ValueError) as readerr:
 | 
					        except (OSError, ValueError) as readerr:
 | 
				
			||||||
        sys.exit(f'Error: Cannot read configuration file: {readerr}')
 | 
					            raise ConfigLoadError('Cannot read config file: %s: %s', (self.file, readerr)) from readerr
 | 
				
			||||||
        except tomllib.TOMLDecodeError as tomlerr:
 | 
					        except tomllib.TOMLDecodeError as tomlerr:
 | 
				
			||||||
        sys.exit(f'Error: Bad TOML syntax in configuration file: {tomlerr}')
 | 
					            raise ConfigLoadError('Bad TOML syntax in config file: %s: %s', (self.file, tomlerr)) from tomlerr
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
config = load_config(pathlib.Path(NODEAGENT_CONFIG_FILE))
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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):
 | 
					class VMNotFound(Exception):
 | 
				
			||||||
    def __init__(self, domain, message='VM not found: {domain}'):
 | 
					    def __init__(self, domain, message='VM not found: {domain}'):
 | 
				
			||||||
        self.domain = domain
 | 
					        self.domain = domain
 | 
				
			||||||
@@ -5,30 +17,5 @@ class VMNotFound(Exception):
 | 
				
			|||||||
        super().__init__(self.message)
 | 
					        super().__init__(self.message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VMStartError(Exception):
 | 
					class QemuAgentError(Exception):
 | 
				
			||||||
    def __init__(self, domain, message='VM start error: {domain}'):
 | 
					    """Mostly QEMU Guest Agent is not responding."""
 | 
				
			||||||
        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)
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,30 @@
 | 
				
			|||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from contextlib import AbstractContextManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import libvirt
 | 
					import libvirt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .vm import VirtualMachine
 | 
					from .config import ConfigLoader
 | 
				
			||||||
 | 
					from .exceptions import LibvirtSessionError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NodeAgent:
 | 
					class LibvirtSession(AbstractContextManager):
 | 
				
			||||||
    def __init__(self, conn: libvirt.virConnect, config: dict):
 | 
					    def __init__(self, config: Path | None = None):
 | 
				
			||||||
        self.vm = VirtualMachine(conn, config)
 | 
					        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()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										67
									
								
								node_agent/utils/vmctl.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								node_agent/utils/vmctl.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					Manage virtual machines.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Usage:  na-vmctl [options] status <machine>
 | 
				
			||||||
 | 
					        na-vmctl [options] is-running <machine>
 | 
				
			||||||
 | 
					        na-vmctl [options] start <machine>
 | 
				
			||||||
 | 
					        na-vmctl [options] shutdown <machine> [-f|--force] [-9|--sigkill]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Options:
 | 
				
			||||||
 | 
					    -c, --config <file>  Config file [default: /etc/node-agent/config.yaml]
 | 
				
			||||||
 | 
					    -l, --loglvl <lvl>   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['<machine>'])
 | 
				
			||||||
 | 
					            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["<machine>"]} not found.{Color.NONE}')
 | 
				
			||||||
 | 
					        except VMError as vmerr:
 | 
				
			||||||
 | 
					            sys.exit(f'{Color.RED}{vmerr}{Color.NONE}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    cli()
 | 
				
			||||||
							
								
								
									
										92
									
								
								node_agent/utils/vmexec.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								node_agent/utils/vmexec.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					Execute shell commands on guest via guest agent.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Usage:  na-vmexec [options] <machine> <command>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Options:
 | 
				
			||||||
 | 
					    -c, --config <file>  Config file [default: /etc/node-agent/config.yaml]
 | 
				
			||||||
 | 
					    -l, --loglvl <lvl>   Logging level [default: INFO]
 | 
				
			||||||
 | 
					    -s, --shell <shell>  Guest shell [default: /bin/sh]
 | 
				
			||||||
 | 
					    -t, --timeout <sec>  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['<command>']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            ga = QemuAgent(session, args['<machine>'])
 | 
				
			||||||
 | 
					            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["<machine>"]} 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()
 | 
				
			||||||
							
								
								
									
										0
									
								
								node_agent/utils/volctl.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								node_agent/utils/volctl.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										120
									
								
								node_agent/vm.py
									
									
									
									
									
								
							
							
						
						
									
										120
									
								
								node_agent/vm.py
									
									
									
									
									
								
							@@ -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
 | 
					 | 
				
			||||||
							
								
								
									
										2
									
								
								node_agent/vm/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								node_agent/vm/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					from .main import VirtualMachine
 | 
				
			||||||
 | 
					from .ga import QemuAgent
 | 
				
			||||||
							
								
								
									
										22
									
								
								node_agent/vm/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								node_agent/vm/base.py
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
							
								
								
									
										192
									
								
								node_agent/vm/ga.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								node_agent/vm/ga.py
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
							
								
								
									
										126
									
								
								node_agent/vm/main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								node_agent/vm/main.py
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
							
								
								
									
										0
									
								
								node_agent/volume/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								node_agent/volume/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										115
									
								
								node_agent/xml.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								node_agent/xml.py
									
									
									
									
									
										Normal file
									
								
							@@ -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())
 | 
				
			||||||
							
								
								
									
										136
									
								
								poetry.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								poetry.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -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"
 | 
				
			||||||
@@ -4,11 +4,16 @@ version = "0.1.0"
 | 
				
			|||||||
description = "Node Agent"
 | 
					description = "Node Agent"
 | 
				
			||||||
authors = ["ge <ge@nixhacks.net>"]
 | 
					authors = ["ge <ge@nixhacks.net>"]
 | 
				
			||||||
readme = "README.md"
 | 
					readme = "README.md"
 | 
				
			||||||
packages = [{include = "node_agent"}]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[tool.poetry.dependencies]
 | 
					[tool.poetry.dependencies]
 | 
				
			||||||
python = "^3.11"
 | 
					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]
 | 
					[build-system]
 | 
				
			||||||
requires = ["poetry-core"]
 | 
					requires = ["poetry-core"]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										16
									
								
								test.py
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								test.py
									
									
									
									
									
								
							@@ -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()
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user