upd
This commit is contained in:
parent
62388e8b67
commit
43033b5a0d
52
README.md
52
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
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
from .config import ConfigLoader
|
||||
from .session import LibvirtSession
|
||||
from .exceptions import *
|
||||
from .volume import *
|
||||
from .vm import *
|
||||
|
@ -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 = []
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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
21
node_agent/exceptions.py
Normal 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."""
|
@ -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):
|
||||
|
@ -1,4 +1,3 @@
|
||||
from .exceptions import *
|
||||
from .guest_agent import GuestAgent
|
||||
from .installer import CPUMode, CPUTopology, VirtualMachineInstaller
|
||||
from .virtual_machine import VirtualMachine
|
||||
|
@ -1,6 +1,6 @@
|
||||
import libvirt
|
||||
|
||||
from .exceptions import VMError
|
||||
from ..exceptions import VMError
|
||||
|
||||
|
||||
class VirtualMachineBase:
|
||||
|
@ -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)
|
@ -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)
|
||||
|
@ -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__)
|
||||
|
@ -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)
|
||||
return Volume(self.pool, vol)
|
||||
|
||||
def get_volume(self, name: str) -> Volume:
|
||||
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()]
|
||||
|
Loading…
Reference in New Issue
Block a user