This commit is contained in:
ge 2023-09-02 00:52:28 +03:00
parent 62388e8b67
commit 43033b5a0d
13 changed files with 110 additions and 110 deletions

View File

@ -16,7 +16,7 @@
- `python3-lxml` 4.9.2 - `python3-lxml` 4.9.2
- `python3-docopt` 0.6.2 - `python3-docopt` 0.6.2
- `python3-libvirt` 9.0.0 (актуальная новее) - `python3-libvirt` 9.0.0
`docopt` скорее всего будет выброшен в будущем, так как интерфейс CLI сильно усложнится. `docopt` скорее всего будет выброшен в будущем, так как интерфейс CLI сильно усложнится.
@ -24,7 +24,7 @@
# API # API
Кодовая база растёт, необходимо автоматически генерировать документацию в README её больше небудет. Кодовая база растёт, необходимо автоматически генерировать документацию, в README её больше не будет.
В структуре проекта сейчас бардак, многое будет переосмыслено и переделано позже. Основная цель на текущем этапе — получить минимально работающий код, с помощью которого возможно выполнить установку виртуальной машины и как-то управлять ею. В структуре проекта сейчас бардак, многое будет переосмыслено и переделано позже. Основная цель на текущем этапе — получить минимально работающий код, с помощью которого возможно выполнить установку виртуальной машины и как-то управлять ею.
@ -32,41 +32,27 @@
- `LivbirtSession` - обёртка над объектом `libvirt.virConnect`. - `LivbirtSession` - обёртка над объектом `libvirt.virConnect`.
- `VirtualMachine` - класс для работы с доменами, через него выполняется большинство действий. - `VirtualMachine` - класс для работы с доменами, через него выполняется большинство действий.
- `VirtualMachineInstaller` - класс для установки ВМ, выполняет кучу проверок, генерирует XML конфиг и т.п. - `VirtualMachineInstaller` - класс для установки ВМ, выполняет кучу проверок, генерирует XML конфиг и т.п. [В ПРОЦЕССЕ]
- `StoragePool` - обёртка для `libvirt.virStoragePool`. - `StoragePool` - обёртка над `libvirt.virStoragePool`.
- `Volume` - объект для управления дисками. - `Volume` - класс для управления дисками.
- `VolumeInfo` - датакласс хранящий информацию о диске, может собрать XML. - `VolumeInfo` - датакласс хранящий информацию о диске, с помощью метода `to_xml()` получаем XML описание.
- `GuestAgent` - понятно что это. - `GuestAgent` - понятно что это.
- `ConfigLoader` - загрузчик TOML-конфига, возможно будет выброшен на мороз. - `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()
```
# TODO # TODO
- [ ] Установка ВМ - [x] Установка ВМ (всратый вариант)
- [x] Конструктор XML (базовый) - [x] Конструктор XML (базовый)
- [x] Автоматический выбор модели процессора - [x] Автоматический выбор модели процессора
- [ ] Метод создания дисков - [x] Метод создания дисков
- [x] Дефайн, запуск и автостарт ВМ - [x] Дефайн, запуск и автостарт ВМ
- [ ] Работа со StoragePool - [x] Работа со StoragePool
- [ ] Создание блочных устройств - [x] Создание блочных устройств
- [ ] Подключение/отключение устройств - [x] Подключение/отключение устройств
- [ ] Управление дисками - [ ] Метод install()
- [ ] Удаление ВМ - [ ] Установка ВМ (нормальный вариант)
- [x] Управление дисками (всратый вариант)
- [x] Удаление ВМ
- [x] Изменение CPU - [x] Изменение CPU
- [x] Изменение RAM - [x] Изменение RAM
- [ ] Миграция ВМ между нодами - [ ] Миграция ВМ между нодами
@ -82,9 +68,13 @@ session.close()
# Заметки # Заметки
## Что там с LXC?
Можно добавить поддержку LXC, но только после реализации основного функционала для QEMU/KVM.
## Будущее этой библиотеки ## Будущее этой библиотеки
Либа Нужно ей придумать название.
## Failover ## Failover

View File

@ -1,3 +1,5 @@
from .config import ConfigLoader from .config import ConfigLoader
from .session import LibvirtSession from .session import LibvirtSession
from .exceptions import *
from .volume import *
from .vm import * from .vm import *

View File

@ -10,11 +10,9 @@ Usage: na-vmctl [options] status <machine>
na-vmctl [options] list [-a|--all] na-vmctl [options] list [-a|--all]
Options: Options:
-c, --config <file> Config file [default: /etc/node-agent/config.yaml] -c, --config <file> config file [default: /etc/node-agent/config.yaml]
-l, --loglvl <lvl> Logging level -l, --loglvl <lvl> logging level
-a, --all List all machines including inactive -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
""" """
import logging import logging
@ -25,13 +23,13 @@ import libvirt
from docopt import docopt from docopt import docopt
from ..session import LibvirtSession from ..session import LibvirtSession
from ..vm import VirtualMachine, VMError, VMNotFound from ..vm import VirtualMachine
from ..exceptions import VMError, VMNotFound
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
levels = logging.getLevelNamesMapping() levels = logging.getLevelNamesMapping()
# Supress libvirt errors
libvirt.registerErrorHandler(lambda userdata, err: None, ctx=None) libvirt.registerErrorHandler(lambda userdata, err: None, ctx=None)
@ -43,21 +41,6 @@ class Color:
class Table: 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'): def __init__(self, whitespace: str = '\t'):
self.__rows = [] self.__rows = []

View File

@ -4,10 +4,11 @@ Execute shell commands on guest via guest agent.
Usage: na-vmexec [options] <machine> <command> Usage: na-vmexec [options] <machine> <command>
Options: Options:
-c, --config <file> Config file [default: /etc/node-agent/config.yaml] -c, --config <file> config file [default: /etc/node-agent/config.yaml]
-l, --loglvl <lvl> Logging level -l, --loglvl <lvl> logging level
-s, --shell <shell> Guest shell [default: /bin/sh] -s, --shell <shell> guest shell [default: /bin/sh]
-t, --timeout <sec> QEMU timeout in seconds to stop polling command status [default: 60] -t, --timeout <sec> QEMU timeout in seconds to stop polling command status [default: 60]
-p, --pid <PID> PID on guest to poll output
""" """
import logging import logging
@ -18,13 +19,13 @@ import libvirt
from docopt import docopt from docopt import docopt
from ..session import LibvirtSession from ..session import LibvirtSession
from ..vm import GuestAgent, GuestAgentError, VMNotFound from ..vm import GuestAgent
from ..exceptions import GuestAgentError, VMNotFound
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
levels = logging.getLevelNamesMapping() levels = logging.getLevelNamesMapping()
# Supress libvirt errors
libvirt.registerErrorHandler(lambda userdata, err: None, ctx=None) libvirt.registerErrorHandler(lambda userdata, err: None, ctx=None)
@ -58,31 +59,24 @@ def cli():
exited, exitcode, stdout, stderr = ga.shellexec( exited, exitcode, stdout, stderr = ga.shellexec(
cmd, executable=shell, capture_output=True, decode_output=True, cmd, executable=shell, capture_output=True, decode_output=True,
timeout=int(args['--timeout'])) timeout=int(args['--timeout']))
except GuestAgentError as qemuerr: except GuestAgentError as gaerr:
errmsg = f'{Color.RED}{qemuerr}{Color.NONE}' errmsg = f'{Color.RED}{gaerr}{Color.NONE}'
if str(qemuerr).startswith('Polling command pid='): if str(gaerr).startswith('Polling command pid='):
errmsg = (errmsg + Color.YELLOW + 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) sys.exit(errmsg)
except VMNotFound as err: except VMNotFound as err:
sys.exit(f'{Color.RED}VM {machine} not found{Color.NONE}') sys.exit(f'{Color.RED}VM {machine} not found{Color.NONE}')
if not exited: if not exited:
print(Color.YELLOW + '[NOTE: command may still running]' + Color.NONE, print(Color.YELLOW +
file=sys.stderr) '[NOTE: command may still running on guest pid={ga.last_pid}]' +
else: Color.NONE, file=sys.stderr)
if exitcode == 0:
exitcolor = Color.GREEN
else:
exitcolor = Color.RED
print(exitcolor + f'[command exited with exit code {exitcode}]' +
Color.NONE,
file=sys.stderr)
if stderr: if stderr:
print(Color.RED + stderr.strip() + Color.NONE, file=sys.stderr) print(stderr.strip(), file=sys.stderr)
if stdout: if stdout:
print(Color.GREEN + stdout.strip() + Color.NONE, file=sys.stdout) print(stdout.strip(), file=sys.stdout)
sys.exit(exitcode) sys.exit(exitcode)

View File

@ -3,15 +3,13 @@ import tomllib
from collections import UserDict from collections import UserDict
from pathlib import Path from pathlib import Path
from .exceptions import ConfigLoaderError
NODEAGENT_CONFIG_FILE = os.getenv('NODEAGENT_CONFIG_FILE') NODEAGENT_CONFIG_FILE = os.getenv('NODEAGENT_CONFIG_FILE')
NODEAGENT_DEFAULT_CONFIG_FILE = '/etc/node-agent/config.toml' 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): class ConfigLoader(UserDict):
def __init__(self, file: Path | None = None): def __init__(self, file: Path | None = None):

21
node_agent/exceptions.py Normal file
View File

@ -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."""

View File

@ -2,12 +2,9 @@ from contextlib import AbstractContextManager
import libvirt import libvirt
from .vm import GuestAgent, VirtualMachine, VMNotFound from .vm import GuestAgent, VirtualMachine
from .volume import StoragePool from .volume import StoragePool
from .exceptions import LibvirtSessionError, VMNotFound
class LibvirtSessionError(Exception):
"""Something went wrong while connecting to libvirtd."""
class LibvirtSession(AbstractContextManager): class LibvirtSession(AbstractContextManager):

View File

@ -1,4 +1,3 @@
from .exceptions import *
from .guest_agent import GuestAgent from .guest_agent import GuestAgent
from .installer import CPUMode, CPUTopology, VirtualMachineInstaller from .installer import CPUMode, CPUTopology, VirtualMachineInstaller
from .virtual_machine import VirtualMachine from .virtual_machine import VirtualMachine

View File

@ -1,6 +1,6 @@
import libvirt import libvirt
from .exceptions import VMError from ..exceptions import VMError
class VirtualMachineBase: class VirtualMachineBase:

View File

@ -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)

View File

@ -6,8 +6,8 @@ from time import sleep, time
import libvirt import libvirt
import libvirt_qemu import libvirt_qemu
from ..exceptions import GuestAgentError
from .base import VirtualMachineBase from .base import VirtualMachineBase
from .exceptions import GuestAgentError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,6 +33,7 @@ class GuestAgent(VirtualMachineBase):
super().__init__(domain) super().__init__(domain)
self.timeout = timeout or QEMU_TIMEOUT # timeout for guest agent self.timeout = timeout or QEMU_TIMEOUT # timeout for guest agent
self.flags = flags or libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT self.flags = flags or libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT
self.last_pid = None
def execute(self, def execute(self,
command: dict, command: dict,
@ -68,9 +69,9 @@ class GuestAgent(VirtualMachineBase):
cmd_out = self._execute(command) cmd_out = self._execute(command)
if capture_output: 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( return self._get_cmd_result(
cmd_pid, self.last_pid,
decode_output=decode_output, decode_output=decode_output,
wait=wait, wait=wait,
timeout=timeout, timeout=timeout,
@ -106,6 +107,10 @@ class GuestAgent(VirtualMachineBase):
timeout=timeout, timeout=timeout,
) )
def poll_pid(self, pid: int):
# Нужно цепляться к PID и вывести результат
pass
def _execute(self, command: dict): def _execute(self, command: dict):
logging.debug('Execute command: vm=%s cmd=%s', self.domain_name, logging.debug('Execute command: vm=%s cmd=%s', self.domain_name,
command) command)

View File

@ -2,9 +2,9 @@ import logging
import libvirt import libvirt
from ..exceptions import VMError
from ..volume import VolumeInfo from ..volume import VolumeInfo
from .base import VirtualMachineBase from .base import VirtualMachineBase
from .exceptions import VMError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,8 +1,14 @@
import logging
import libvirt import libvirt
from ..exceptions import StoragePoolError
from .volume import Volume, VolumeInfo from .volume import Volume, VolumeInfo
logger = logging.getLogger(__name__)
class StoragePool: class StoragePool:
def __init__(self, pool: libvirt.virStoragePool): def __init__(self, pool: libvirt.virStoragePool):
self.pool = pool self.pool = pool
@ -23,15 +29,34 @@ class StoragePool:
def refresh(self) -> None: def refresh(self) -> None:
self.pool.refresh() self.pool.refresh()
def create_volume(self, vol_info: VolumeInfo) -> None: def create_volume(self, vol_info: VolumeInfo) -> Volume:
# todo: return Volume object? """
self.pool.createXML( 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(), vol_info.to_xml(),
flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA) 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) 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]: def list_volumes(self) -> list[Volume]:
return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()] return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()]