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-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
|
||||||
|
|
||||||
|
@ -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 *
|
||||||
|
@ -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 = []
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
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
|
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):
|
||||||
|
@ -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
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import libvirt
|
import libvirt
|
||||||
|
|
||||||
from .exceptions import VMError
|
from ..exceptions import VMError
|
||||||
|
|
||||||
|
|
||||||
class VirtualMachineBase:
|
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
|
||||||
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)
|
||||||
|
@ -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__)
|
||||||
|
@ -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()]
|
||||||
|
Loading…
Reference in New Issue
Block a user