init
This commit is contained in:
commit
b608d88265
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*~
|
8
Makefile
Normal file
8
Makefile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
poetry build
|
||||||
|
|
||||||
|
clean:
|
||||||
|
[ -d dist/ ] && rm -rf dist/ || true
|
||||||
|
find . -type d -name __pycache__ -exec rm -rf {} \; > /dev/null 2>&1 || true
|
27
README.md
Normal file
27
README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Node Agent
|
||||||
|
|
||||||
|
Агент для работы на ворк-нодах.
|
||||||
|
|
||||||
|
Пока взаимодействовать можно только так (через [test.py](test.py)):
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo env NODEAGENT_CONFIG_FILE=$PWD/configuration.toml python test.py
|
||||||
|
```
|
||||||
|
|
||||||
|
# Модули
|
||||||
|
|
||||||
|
Основной класс тут `NodeAgent`. Через него осуществляется доступ ко всем методам.
|
||||||
|
|
||||||
|
- `base` тут базовый класс.
|
||||||
|
- `main` тут объявлен `NodeAgent`.
|
||||||
|
- `exceptions` тут исключения.
|
||||||
|
- `config` тут понятно.
|
||||||
|
- `vm` тут объявлен класс `VirtualMachine` с базовыми методами для виртуалок. Генерацию XML для дефайна ВМ следует сделать в отдельном модуле.
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
Нужно что-то придумать с обработкой ошибок. Сейчас на неожиданности я вызываю исключения, нужно некритичные из них обработать, чтобы приложение не падало при обращении к несуществующему домену или нефатальных ошибок при работе с существующими доменами.
|
||||||
|
|
||||||
|
# Как это должно выглядеть
|
||||||
|
|
||||||
|
`node-agent` должен быть обычным DEB-пакетом. В пакете само приложение, sysyemd-сервис, конфиг. Бонусом можно доложить консольные утилиты (пока не реализованы): `nodeagent-vmctl` (чтобы напрямую дергать методы виртуалок), `guest-cmd` (обёртка над virsh quest-agent-command).
|
10
configuration.toml
Normal file
10
configuration.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[general]
|
||||||
|
connect_uri = 'qemu:///system'
|
||||||
|
|
||||||
|
[blockstorage]
|
||||||
|
local = true
|
||||||
|
local_path = '/srv/vm-disks'
|
||||||
|
|
||||||
|
[imagestorage]
|
||||||
|
local = true
|
||||||
|
local_path = '/srv/vm-images'
|
1
node_agent/__init__.py
Normal file
1
node_agent/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .main import NodeAgent
|
7
node_agent/base.py
Normal file
7
node_agent/base.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import libvirt
|
||||||
|
|
||||||
|
|
||||||
|
class NodeAgentBase:
|
||||||
|
def __init__(self, conn: libvirt.virConnect, config: dict):
|
||||||
|
self.config = config
|
||||||
|
self.conn = conn
|
21
node_agent/config.py
Normal file
21
node_agent/config.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
|
||||||
|
NODEAGENT_CONFIG_FILE = \
|
||||||
|
os.getenv('NODEAGENT_CONFIG_FILE') or '/etc/nodeagent/configuration.toml'
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(config: pathlib.Path):
|
||||||
|
try:
|
||||||
|
with open(config, 'rb') as conf:
|
||||||
|
return tomllib.load(conf)
|
||||||
|
except (OSError, ValueError) as readerr:
|
||||||
|
sys.exit(f'Error: Cannot read configuration file: {readerr}')
|
||||||
|
except tomllib.TOMLDecodeError as tomlerr:
|
||||||
|
sys.exit(f'Error: Bad TOML syntax in configuration file: {tomlerr}')
|
||||||
|
|
||||||
|
|
||||||
|
config = load_config(pathlib.Path(NODEAGENT_CONFIG_FILE))
|
34
node_agent/exceptions.py
Normal file
34
node_agent/exceptions.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
class VMNotFound(Exception):
|
||||||
|
def __init__(self, domain, message='VM not found: {domain}'):
|
||||||
|
self.domain = domain
|
||||||
|
self.message = message.format(domain=domain)
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class VMStartError(Exception):
|
||||||
|
def __init__(self, domain, message='VM start error: {domain}'):
|
||||||
|
self.domain = domain
|
||||||
|
self.message = message.format(domain=domain)
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class VMShutdownError(Exception):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
domain,
|
||||||
|
message="VM '{domain}' cannot shutdown, try with hard=True"
|
||||||
|
):
|
||||||
|
self.domain = domain
|
||||||
|
self.message = message.format(domain=domain)
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class VMRebootError(Exception):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
domain,
|
||||||
|
message="VM '{domain}' reboot, try with hard=True",
|
||||||
|
):
|
||||||
|
self.domain = domain
|
||||||
|
self.message = message.format(domain=domain)
|
||||||
|
super().__init__(self.message)
|
8
node_agent/main.py
Normal file
8
node_agent/main.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import libvirt
|
||||||
|
|
||||||
|
from .vm import VirtualMachine
|
||||||
|
|
||||||
|
|
||||||
|
class NodeAgent:
|
||||||
|
def __init__(self, conn: libvirt.virConnect, config: dict):
|
||||||
|
self.vm = VirtualMachine(conn, config)
|
120
node_agent/vm.py
Normal file
120
node_agent/vm.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import libvirt
|
||||||
|
|
||||||
|
from .base import NodeAgentBase
|
||||||
|
from .exceptions import (
|
||||||
|
VMNotFound,
|
||||||
|
VMStartError,
|
||||||
|
VMRebootError,
|
||||||
|
VMShutdownError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualMachine(NodeAgentBase):
|
||||||
|
|
||||||
|
def _dom(self, domain: str) -> libvirt.virDomain:
|
||||||
|
"""Get virDomain object to manipulate with domain."""
|
||||||
|
try:
|
||||||
|
ret = self.conn.lookupByName(domain)
|
||||||
|
if ret is not None:
|
||||||
|
return ret
|
||||||
|
raise VMNotFound(domain)
|
||||||
|
except libvirt.libvirtError as err:
|
||||||
|
raise VMNotFound(err) from err
|
||||||
|
|
||||||
|
def create(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
volumes: list[dict],
|
||||||
|
vcpus: int,
|
||||||
|
vram: int,
|
||||||
|
image: dict,
|
||||||
|
cdrom: dict | None = None,
|
||||||
|
):
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete(self, name: str, delete_volumes=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def status(self, name: str) -> str:
|
||||||
|
"""
|
||||||
|
Return VM state: 'running', 'shutoff', etc. Ref:
|
||||||
|
https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState
|
||||||
|
"""
|
||||||
|
state = self._dom(name).info()[0]
|
||||||
|
match state:
|
||||||
|
case libvirt.VIR_DOMAIN_NOSTATE:
|
||||||
|
return 'nostate'
|
||||||
|
case libvirt.VIR_DOMAIN_RUNNING:
|
||||||
|
return 'running'
|
||||||
|
case libvirt.VIR_DOMAIN_BLOCKED:
|
||||||
|
return 'blocked'
|
||||||
|
case libvirt.VIR_DOMAIN_PAUSED:
|
||||||
|
return 'paused'
|
||||||
|
case libvirt.VIR_DOMAIN_SHUTDOWN:
|
||||||
|
return 'shutdown'
|
||||||
|
case libvirt.VIR_DOMAIN_SHUTOFF:
|
||||||
|
return 'shutoff'
|
||||||
|
case libvirt.VIR_DOMAIN_CRASHED:
|
||||||
|
return 'crashed'
|
||||||
|
case libvirt.VIR_DOMAIN_PMSUSPENDED:
|
||||||
|
return 'pmsuspended'
|
||||||
|
|
||||||
|
def is_running(self, name: str) -> bool:
|
||||||
|
"""Return True if VM is running, else return False."""
|
||||||
|
if self._dom(name).isActive() != 1:
|
||||||
|
return False # inactive (0) or error (-1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def start(self, name: str) -> None:
|
||||||
|
"""Start VM."""
|
||||||
|
if not self.is_running(name):
|
||||||
|
ret = self._dom(name).create()
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
if ret != 0:
|
||||||
|
raise VMStartError(name)
|
||||||
|
|
||||||
|
def shutdown(self, name: str, hard=False) -> None:
|
||||||
|
"""Shutdown VM. Use hard=True to force shutdown."""
|
||||||
|
if hard:
|
||||||
|
# Destroy VM gracefully (no SIGKILL)
|
||||||
|
ret = self._dom(name).destroyFlags(flags=libvirt.VIR_DOMAIN_DESTROY_GRACEFUL)
|
||||||
|
else:
|
||||||
|
# Normal VM shutdown, OS may ignore this.
|
||||||
|
ret = self._dom(name).shutdown()
|
||||||
|
if ret != 0:
|
||||||
|
raise VMShutdownError(name)
|
||||||
|
|
||||||
|
def reboot(self, name: str, hard=False) -> None:
|
||||||
|
"""
|
||||||
|
Reboot VM. Use hard=True to force reboot. With forced reboot
|
||||||
|
VM will shutdown via self.shutdown() (no forced) and started.
|
||||||
|
"""
|
||||||
|
if hard:
|
||||||
|
# Forced "reboot"
|
||||||
|
self.shutdown(name)
|
||||||
|
self.start(name)
|
||||||
|
else:
|
||||||
|
# Normal reboot.
|
||||||
|
ret = self._dom(name).reboot()
|
||||||
|
if ret != 0:
|
||||||
|
raise VMRebootError(name)
|
||||||
|
|
||||||
|
def vcpu_set(self, name: str, count: int):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def vram_set(self, name: str, count: int):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ssh_keys_list(self, name: str, user: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ssh_keys_add(self, name: str, user: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ssh_keys_remove(self, name: str, user: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_user_password(self, name: str, user: str):
|
||||||
|
pass
|
15
pyproject.toml
Normal file
15
pyproject.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "node-agent"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Node Agent"
|
||||||
|
authors = ["ge <ge@nixhacks.net>"]
|
||||||
|
readme = "README.md"
|
||||||
|
packages = [{include = "node_agent"}]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.11"
|
||||||
|
libvirt-python = "^9.4.0" # 9.0.0 on Debian 12
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
16
test.py
Normal file
16
test.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import libvirt
|
||||||
|
|
||||||
|
from node_agent import NodeAgent
|
||||||
|
from node_agent.config import config
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = libvirt.open(config['general']['connect_uri'])
|
||||||
|
except libvirt.libvirtError as err:
|
||||||
|
sys.exit('Failed to open connection to the hypervisor: %s' % err)
|
||||||
|
|
||||||
|
|
||||||
|
node_agent = NodeAgent(conn, config)
|
||||||
|
s = node_agent.vm.status('debian12')
|
||||||
|
print(s)
|
||||||
|
conn.close()
|
Loading…
Reference in New Issue
Block a user