From 43033b5a0d297caff80254ef3f71f31f7e9568a9 Mon Sep 17 00:00:00 2001 From: ge Date: Sat, 2 Sep 2023 00:52:28 +0300 Subject: [PATCH] upd --- README.md | 52 +++++++++++++------------------ node_agent/__init__.py | 2 ++ node_agent/cli/vmctl.py | 27 +++------------- node_agent/cli/vmexec.py | 38 ++++++++++------------ node_agent/config.py | 6 ++-- node_agent/exceptions.py | 21 +++++++++++++ node_agent/session.py | 7 ++--- node_agent/vm/__init__.py | 1 - node_agent/vm/base.py | 2 +- node_agent/vm/exceptions.py | 14 --------- node_agent/vm/guest_agent.py | 11 +++++-- node_agent/vm/virtual_machine.py | 2 +- node_agent/volume/storage_pool.py | 37 ++++++++++++++++++---- 13 files changed, 110 insertions(+), 110 deletions(-) create mode 100644 node_agent/exceptions.py delete mode 100644 node_agent/vm/exceptions.py diff --git a/README.md b/README.md index 73e9cec..a07cab5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - `python3-lxml` 4.9.2 - `python3-docopt` 0.6.2 -- `python3-libvirt` 9.0.0 (актуальная новее) +- `python3-libvirt` 9.0.0 `docopt` скорее всего будет выброшен в будущем, так как интерфейс CLI сильно усложнится. @@ -24,7 +24,7 @@ # API -Кодовая база растёт, необходимо автоматически генерировать документацию в README её больше небудет. +Кодовая база растёт, необходимо автоматически генерировать документацию, в README её больше не будет. В структуре проекта сейчас бардак, многое будет переосмыслено и переделано позже. Основная цель на текущем этапе — получить минимально работающий код, с помощью которого возможно выполнить установку виртуальной машины и как-то управлять ею. @@ -32,41 +32,27 @@ - `LivbirtSession` - обёртка над объектом `libvirt.virConnect`. - `VirtualMachine` - класс для работы с доменами, через него выполняется большинство действий. -- `VirtualMachineInstaller` - класс для установки ВМ, выполняет кучу проверок, генерирует XML конфиг и т.п. -- `StoragePool` - обёртка для `libvirt.virStoragePool`. -- `Volume` - объект для управления дисками. -- `VolumeInfo` - датакласс хранящий информацию о диске, может собрать XML. +- `VirtualMachineInstaller` - класс для установки ВМ, выполняет кучу проверок, генерирует XML конфиг и т.п. [В ПРОЦЕССЕ] +- `StoragePool` - обёртка над `libvirt.virStoragePool`. +- `Volume` - класс для управления дисками. +- `VolumeInfo` - датакласс хранящий информацию о диске, с помощью метода `to_xml()` получаем XML описание. - `GuestAgent` - понятно что это. -- `ConfigLoader` - загрузчик TOML-конфига, возможно будет выброшен на мороз. - -```python -from na import LibvirtSession -from na.vm import VirtualMachineInstaller - - -session = LibvirtSession('config.toml') -compute = VirtualMachineInstaller(session).install( - name='devuan', - vcpus=4, - vcpu_mode='host-model', - memory=2048, -) -print(compute) -session.close() -``` +- `ConfigLoader` - загрузчик TOML-конфига. # TODO -- [ ] Установка ВМ +- [x] Установка ВМ (всратый вариант) - [x] Конструктор XML (базовый) - [x] Автоматический выбор модели процессора - - [ ] Метод создания дисков + - [x] Метод создания дисков - [x] Дефайн, запуск и автостарт ВМ - - [ ] Работа со StoragePool - - [ ] Создание блочных устройств - - [ ] Подключение/отключение устройств -- [ ] Управление дисками -- [ ] Удаление ВМ + - [x] Работа со StoragePool + - [x] Создание блочных устройств + - [x] Подключение/отключение устройств + - [ ] Метод install() +- [ ] Установка ВМ (нормальный вариант) +- [x] Управление дисками (всратый вариант) +- [x] Удаление ВМ - [x] Изменение CPU - [x] Изменение RAM - [ ] Миграция ВМ между нодами @@ -82,9 +68,13 @@ session.close() # Заметки +## Что там с LXC? + +Можно добавить поддержку LXC, но только после реализации основного функционала для QEMU/KVM. + ## Будущее этой библиотеки -Либа +Нужно ей придумать название. ## Failover diff --git a/node_agent/__init__.py b/node_agent/__init__.py index c20e163..78d357c 100644 --- a/node_agent/__init__.py +++ b/node_agent/__init__.py @@ -1,3 +1,5 @@ from .config import ConfigLoader from .session import LibvirtSession +from .exceptions import * +from .volume import * from .vm import * diff --git a/node_agent/cli/vmctl.py b/node_agent/cli/vmctl.py index f95c476..21c447c 100644 --- a/node_agent/cli/vmctl.py +++ b/node_agent/cli/vmctl.py @@ -10,11 +10,9 @@ Usage: na-vmctl [options] status na-vmctl [options] list [-a|--all] Options: - -c, --config Config file [default: /etc/node-agent/config.yaml] - -l, --loglvl Logging level - -a, --all List all machines including inactive - -f, --force Force action. On shutdown calls graceful destroy() - -9, --sigkill Send SIGKILL to QEMU process. Not affects without --force + -c, --config config file [default: /etc/node-agent/config.yaml] + -l, --loglvl logging level + -a, --all list all machines including inactive """ import logging @@ -25,13 +23,13 @@ import libvirt from docopt import docopt from ..session import LibvirtSession -from ..vm import VirtualMachine, VMError, VMNotFound +from ..vm import VirtualMachine +from ..exceptions import VMError, VMNotFound logger = logging.getLogger(__name__) levels = logging.getLevelNamesMapping() -# Supress libvirt errors libvirt.registerErrorHandler(lambda userdata, err: None, ctx=None) @@ -43,21 +41,6 @@ class Color: class Table: - """Print table. Example:: - - t = Table() - t.header(['KEY', 'VALUE']) # header is optional - t.row(['key 1', 'value 1']) - t.row(['key 2', 'value 2']) - t.rows( - [ - ['key 3', 'value 3'], - ['key 4', 'value 4'] - ] - ) - t.print() - - """ def __init__(self, whitespace: str = '\t'): self.__rows = [] diff --git a/node_agent/cli/vmexec.py b/node_agent/cli/vmexec.py index ec4e7f7..f23c13a 100644 --- a/node_agent/cli/vmexec.py +++ b/node_agent/cli/vmexec.py @@ -4,10 +4,11 @@ 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 - -s, --shell Guest shell [default: /bin/sh] + -c, --config config file [default: /etc/node-agent/config.yaml] + -l, --loglvl logging level + -s, --shell guest shell [default: /bin/sh] -t, --timeout QEMU timeout in seconds to stop polling command status [default: 60] + -p, --pid PID on guest to poll output """ import logging @@ -18,13 +19,13 @@ import libvirt from docopt import docopt from ..session import LibvirtSession -from ..vm import GuestAgent, GuestAgentError, VMNotFound +from ..vm import GuestAgent +from ..exceptions import GuestAgentError, VMNotFound logger = logging.getLogger(__name__) levels = logging.getLevelNamesMapping() -# Supress libvirt errors libvirt.registerErrorHandler(lambda userdata, err: None, ctx=None) @@ -58,31 +59,24 @@ def cli(): exited, exitcode, stdout, stderr = ga.shellexec( cmd, executable=shell, capture_output=True, decode_output=True, timeout=int(args['--timeout'])) - except GuestAgentError as qemuerr: - errmsg = f'{Color.RED}{qemuerr}{Color.NONE}' - if str(qemuerr).startswith('Polling command pid='): + except GuestAgentError as gaerr: + errmsg = f'{Color.RED}{gaerr}{Color.NONE}' + if str(gaerr).startswith('Polling command pid='): errmsg = (errmsg + Color.YELLOW + - '\n[NOTE: command may still running]' + Color.NONE) + '\n[NOTE: command may still running on guest ' + 'pid={ga.last_pid}]' + Color.NONE) sys.exit(errmsg) except VMNotFound as err: sys.exit(f'{Color.RED}VM {machine} not found{Color.NONE}') if not exited: - print(Color.YELLOW + '[NOTE: command may still running]' + Color.NONE, - file=sys.stderr) - else: - if exitcode == 0: - exitcolor = Color.GREEN - else: - exitcolor = Color.RED - print(exitcolor + f'[command exited with exit code {exitcode}]' + - Color.NONE, - file=sys.stderr) - + print(Color.YELLOW + + '[NOTE: command may still running on guest pid={ga.last_pid}]' + + Color.NONE, file=sys.stderr) if stderr: - print(Color.RED + stderr.strip() + Color.NONE, file=sys.stderr) + print(stderr.strip(), file=sys.stderr) if stdout: - print(Color.GREEN + stdout.strip() + Color.NONE, file=sys.stdout) + print(stdout.strip(), file=sys.stdout) sys.exit(exitcode) diff --git a/node_agent/config.py b/node_agent/config.py index ca05dd5..300d324 100644 --- a/node_agent/config.py +++ b/node_agent/config.py @@ -3,15 +3,13 @@ import tomllib from collections import UserDict from pathlib import Path +from .exceptions import ConfigLoaderError + NODEAGENT_CONFIG_FILE = os.getenv('NODEAGENT_CONFIG_FILE') NODEAGENT_DEFAULT_CONFIG_FILE = '/etc/node-agent/config.toml' -class ConfigLoaderError(Exception): - """Bad config file syntax, unreachable file or bad config schema.""" - - class ConfigLoader(UserDict): def __init__(self, file: Path | None = None): diff --git a/node_agent/exceptions.py b/node_agent/exceptions.py new file mode 100644 index 0000000..6d38011 --- /dev/null +++ b/node_agent/exceptions.py @@ -0,0 +1,21 @@ +class ConfigLoaderError(Exception): + """Bad config file syntax, unreachable file or bad config schema.""" + +class LibvirtSessionError(Exception): + """Something went wrong while connecting to libvirtd.""" + + +class VMError(Exception): + """Something went wrong while interacting with the domain.""" + + +class VMNotFound(VMError): + """Virtual machine not found on node.""" + + +class GuestAgentError(Exception): + """Mostly QEMU Guest Agent is not responding.""" + + +class StoragePoolError(Exception): + """Something went wrong when operating with storage pool.""" diff --git a/node_agent/session.py b/node_agent/session.py index eda384d..d01a428 100644 --- a/node_agent/session.py +++ b/node_agent/session.py @@ -2,12 +2,9 @@ from contextlib import AbstractContextManager import libvirt -from .vm import GuestAgent, VirtualMachine, VMNotFound +from .vm import GuestAgent, VirtualMachine from .volume import StoragePool - - -class LibvirtSessionError(Exception): - """Something went wrong while connecting to libvirtd.""" +from .exceptions import LibvirtSessionError, VMNotFound class LibvirtSession(AbstractContextManager): diff --git a/node_agent/vm/__init__.py b/node_agent/vm/__init__.py index 5c23220..9c2cb30 100644 --- a/node_agent/vm/__init__.py +++ b/node_agent/vm/__init__.py @@ -1,4 +1,3 @@ -from .exceptions import * from .guest_agent import GuestAgent from .installer import CPUMode, CPUTopology, VirtualMachineInstaller from .virtual_machine import VirtualMachine diff --git a/node_agent/vm/base.py b/node_agent/vm/base.py index 97b5368..052c198 100644 --- a/node_agent/vm/base.py +++ b/node_agent/vm/base.py @@ -1,6 +1,6 @@ import libvirt -from .exceptions import VMError +from ..exceptions import VMError class VirtualMachineBase: diff --git a/node_agent/vm/exceptions.py b/node_agent/vm/exceptions.py deleted file mode 100644 index c37392a..0000000 --- a/node_agent/vm/exceptions.py +++ /dev/null @@ -1,14 +0,0 @@ -class GuestAgentError(Exception): - """Mostly QEMU Guest Agent is not responding.""" - - -class VMError(Exception): - """Something went wrong while interacting with the domain.""" - - -class VMNotFound(Exception): - - def __init__(self, domain, message='VM not found vm={domain}'): - self.domain = domain - self.message = message.format(domain=domain) - super().__init__(self.message) diff --git a/node_agent/vm/guest_agent.py b/node_agent/vm/guest_agent.py index 3d233f1..39a5f29 100644 --- a/node_agent/vm/guest_agent.py +++ b/node_agent/vm/guest_agent.py @@ -6,8 +6,8 @@ from time import sleep, time import libvirt import libvirt_qemu +from ..exceptions import GuestAgentError from .base import VirtualMachineBase -from .exceptions import GuestAgentError logger = logging.getLogger(__name__) @@ -33,6 +33,7 @@ class GuestAgent(VirtualMachineBase): super().__init__(domain) self.timeout = timeout or QEMU_TIMEOUT # timeout for guest agent self.flags = flags or libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT + self.last_pid = None def execute(self, command: dict, @@ -68,9 +69,9 @@ class GuestAgent(VirtualMachineBase): cmd_out = self._execute(command) if capture_output: - cmd_pid = json.loads(cmd_out)['return']['pid'] + self.last_pid = json.loads(cmd_out)['return']['pid'] return self._get_cmd_result( - cmd_pid, + self.last_pid, decode_output=decode_output, wait=wait, timeout=timeout, @@ -106,6 +107,10 @@ class GuestAgent(VirtualMachineBase): timeout=timeout, ) + def poll_pid(self, pid: int): + # Нужно цепляться к PID и вывести результат + pass + def _execute(self, command: dict): logging.debug('Execute command: vm=%s cmd=%s', self.domain_name, command) diff --git a/node_agent/vm/virtual_machine.py b/node_agent/vm/virtual_machine.py index d508bf9..c097ac3 100644 --- a/node_agent/vm/virtual_machine.py +++ b/node_agent/vm/virtual_machine.py @@ -2,9 +2,9 @@ import logging import libvirt +from ..exceptions import VMError from ..volume import VolumeInfo from .base import VirtualMachineBase -from .exceptions import VMError logger = logging.getLogger(__name__) diff --git a/node_agent/volume/storage_pool.py b/node_agent/volume/storage_pool.py index 0c669ad..b0246c2 100644 --- a/node_agent/volume/storage_pool.py +++ b/node_agent/volume/storage_pool.py @@ -1,8 +1,14 @@ +import logging + import libvirt +from ..exceptions import StoragePoolError from .volume import Volume, VolumeInfo +logger = logging.getLogger(__name__) + + class StoragePool: def __init__(self, pool: libvirt.virStoragePool): self.pool = pool @@ -23,15 +29,34 @@ class StoragePool: def refresh(self) -> None: self.pool.refresh() - def create_volume(self, vol_info: VolumeInfo) -> None: - # todo: return Volume object? - self.pool.createXML( + def create_volume(self, vol_info: VolumeInfo) -> Volume: + """ + Create storage volume and return Volume instance. + """ + logger.info(f'Create storage volume vol={vol_info.name} ' + f'in pool={self.pool}') + vol = self.pool.createXML( vol_info.to_xml(), flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA) - - def get_volume(self, name: str) -> Volume: - vol = self.pool.storageVolLookupByName(name) return Volume(self.pool, vol) + def get_volume(self, name: str) -> Volume | None: + """ + Lookup and return Volume instance or None. + """ + logger.info(f'Lookup for storage volume vol={name} ' + f'in pool={self.pool.name}') + try: + vol = self.pool.storageVolLookupByName(name) + return Volume(self.pool, vol) + except libvirt.libvirtError as err: + if (err.get_error_domain() == libvirt.VIR_FROM_STORAGE + err.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL): + logger.error(err.get_error_message()) + return None + else: + logger.error(f'libvirt error: {err}') + raise StoragePoolError(f'libvirt error: {err}') from err + def list_volumes(self) -> list[Volume]: return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()]