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

View File

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

View File

@ -10,11 +10,9 @@ Usage: na-vmctl [options] status <machine>
na-vmctl [options] list [-a|--all]
Options:
-c, --config <file> Config file [default: /etc/node-agent/config.yaml]
-l, --loglvl <lvl> 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 <file> config file [default: /etc/node-agent/config.yaml]
-l, --loglvl <lvl> 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 = []

View File

@ -4,10 +4,11 @@ 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
-s, --shell <shell> Guest shell [default: /bin/sh]
-c, --config <file> config file [default: /etc/node-agent/config.yaml]
-l, --loglvl <lvl> logging level
-s, --shell <shell> guest shell [default: /bin/sh]
-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
@ -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)

View File

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

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

View File

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

View File

@ -1,6 +1,6 @@
import libvirt
from .exceptions import VMError
from ..exceptions import VMError
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_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)

View File

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

View File

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