various updates

This commit is contained in:
ge 2023-08-24 22:36:12 +03:00
parent a0344b703f
commit 91478b8122
18 changed files with 261 additions and 234 deletions

3
.gitignore vendored
View File

@ -2,5 +2,8 @@ __pycache__/
*.pyc
*~
domain.xml
domgen.py
na
dist/
P@ssw0rd
*.todo

View File

@ -1,3 +1,5 @@
SRC = na/
all: build
build:
@ -6,3 +8,10 @@ build:
clean:
[ -d dist/ ] && rm -rf dist/ || true
find . -type d -name __pycache__ -exec rm -rf {} \; > /dev/null 2>&1 || true
format:
isort --lai 2 $(SRC)
autopep8 -riva --experimental --ignore e255 $(SRC)
lint:
pylint $(SRC)

View File

@ -123,6 +123,8 @@ print(domain_xml.to_string())
- [ ] Установка ВМ
- [x] Конструктор XML (базовый)
- [ ] Метод создания дисков
- [ ] Дефайн, запуск и автостарт ВМ
- [ ] Управление дисками
- [ ] Удаление ВМ
- [ ] Изменение CPU
@ -130,12 +132,12 @@ print(domain_xml.to_string())
- [ ] Миграция ВМ между нодами
- [x] Работа с qemu-ga
- [x] Управление питанием
- [ ] Вкл/выкл автостарт ВМ
- [x] Вкл/выкл автостарт ВМ
- [ ] Статистика потребления ресурсов
- [ ] Получение инфомрации из/о ВМ
- [ ] SSH-ключи
- [ ] Сеть
- [ ] ???
- [ ] Создание снапшотов
# Заметки

View File

@ -1,3 +1,3 @@
from .main import LibvirtSession
from .config import ConfigLoader
from .session import LibvirtSession
from .vm import *

View File

@ -13,14 +13,14 @@ Options:
-9, --sigkill Send SIGKILL to QEMU process. Not affects without --force
"""
import sys
import pathlib
import logging
import pathlib
import sys
import libvirt
from docopt import docopt
from ..main import LibvirtSession
from ..session import LibvirtSession
from ..vm import VirtualMachine, VMError, VMNotFound

View File

@ -10,13 +10,13 @@ Options:
-t, --timeout <sec> QEMU timeout in seconds to stop polling command status [default: 60]
"""
import sys
import pathlib
import logging
import pathlib
import sys
from docopt import docopt
from ..main import LibvirtSession
from ..session import LibvirtSession
from ..vm import QemuAgent, QemuAgentError, VMNotFound
@ -30,6 +30,8 @@ class Color:
YELLOW = '\033[33m'
NONE = '\033[0m'
# TODO: Add STDIN support e.g.: cat something.sh | na-vmexec vmname bash
def cli():
args = docopt(__doc__)
@ -50,44 +52,28 @@ def cli():
try:
ga = QemuAgent(session, machine)
exited, exitcode, stdout, stderr = ga.shellexec(
cmd,
executable=shell,
capture_output=True,
decode_output=True,
timeout=int(args['--timeout']),
)
cmd, executable=shell, capture_output=True, decode_output=True,
timeout=int(args['--timeout']))
except QemuAgentError as qemuerr:
errmsg = f'{Color.RED}{err}{Color.NONE}'
if str(err).startswith('Polling command pid='):
errmsg = (
errmsg + Color.YELLOW
+ '\n[NOTE: command may still running]'
+ Color.NONE
)
errmsg = f'{Color.RED}{qemuerr}{Color.NONE}'
if str(qemuerr).startswith('Polling command pid='):
errmsg = (errmsg + Color.YELLOW +
'\n[NOTE: command may still running]' + Color.NONE)
sys.exit(errmsg)
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:
print(
Color.YELLOW
+'[NOTE: command may still running]'
+ Color.NONE,
file=sys.stderr
)
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(exitcolor + f'[command exited with exit code {exitcode}]' +
Color.NONE,
file=sys.stderr)
if stderr:
print(Color.RED + stderr.strip() + Color.NONE, file=sys.stderr)
@ -95,5 +81,6 @@ def cli():
print(Color.GREEN + stdout.strip() + Color.NONE, file=sys.stdout)
sys.exit(exitcode)
if __name__ == '__main__':
cli()

View File

@ -1,7 +1,7 @@
import os
import tomllib
from pathlib import Path
from collections import UserDict
from pathlib import Path
NODEAGENT_CONFIG_FILE = os.getenv('NODEAGENT_CONFIG_FILE')
@ -9,15 +9,16 @@ NODEAGENT_DEFAULT_CONFIG_FILE = '/etc/node-agent/config.toml'
class ConfigLoadError(Exception):
"""Bad config file syntax, unreachable file or bad data."""
"""Bad config file syntax, unreachable file or bad config schema."""
class ConfigLoader(UserDict):
def __init__(self, file: Path | None = None):
if file is None:
file = NODEAGENT_CONFIG_FILE or NODEAGENT_DEFAULT_CONFIG_FILE
self.file = Path(file)
self.data = self._load()
super().__init__(self._load())
# todo: load deafult configuration
def _load(self):
@ -26,6 +27,12 @@ class ConfigLoader(UserDict):
return tomllib.load(config)
# todo: config schema validation
except tomllib.TOMLDecodeError as tomlerr:
raise ConfigLoadError(f'Bad TOML syntax in config file: {self.file}: {tomlerr}') from tomlerr
raise ConfigLoadError(
f'Bad TOML syntax in config file: {self.file}: {tomlerr}'
) from tomlerr
except (OSError, ValueError) as readerr:
raise ConfigLoadError(f'Cannot read config file: {self.file}: {readerr}') from readerr
raise ConfigLoadError(
f'Cannot read config file: {self.file}: {readerr}') from readerr
def reload(self):
self.data = self._load()

View File

@ -1,5 +1,5 @@
from pathlib import Path
from contextlib import AbstractContextManager
from pathlib import Path
import libvirt
@ -7,10 +7,11 @@ from .config import ConfigLoader
class LibvirtSessionError(Exception):
"""Something went wrong while connecting to libvirt."""
"""Something went wrong while connecting to libvirtd."""
class LibvirtSession(AbstractContextManager):
def __init__(self, config: Path | None = None):
self.config = ConfigLoader(config)
self.session = self._connect(self.config['libvirt']['uri'])
@ -26,8 +27,7 @@ class LibvirtSession(AbstractContextManager):
return libvirt.open(connection_uri)
except libvirt.libvirtError as err:
raise LibvirtSessionError(
f'Failed to open connection to the hypervisor: {err}'
) from err
f'Failed to open connection to the hypervisor: {err}') from err
def close(self) -> None:
self.session.close()

View File

@ -0,0 +1 @@
from .mac import *

16
node_agent/utils/mac.py Normal file
View File

@ -0,0 +1,16 @@
import random
def random_mac() -> str:
"""Retrun random MAC address."""
mac = [0x00, 0x16, 0x3e,
random.randint(0x00, 0x7f),
random.randint(0x00, 0xff),
random.randint(0x00, 0xff)]
return ':'.join(map(lambda x: "%02x" % x, mac))
def unique_mac() -> str:
"""Return non-conflicting MAC address."""
# todo: see virtinst.DeviceInterface.generate_mac
raise NotImplementedError()

View File

@ -1,7 +1,20 @@
from pathlib import Path
from lxml.etree import Element, SubElement, QName, tostring
from lxml.builder import E
from lxml.etree import Element, QName, SubElement, tostring
from .mac import random_mac
XPATH_DOMAIN_NAME = '/domain/name'
XPATH_DOMAIN_TITLE = '/domain/title'
XPATH_DOMAIN_DESCRIPTION = '/domain/description'
XPATH_DOMAIN_METADATA = '/domain/metadata'
XPATH_DOMAIN_MEMORY = '/domain/memory'
XPATH_DOMAIN_CURRENT_MEMORY = '/domain/currentMemory'
XPATH_DOMAIN_VCPU = '/domain/vcpu'
XPATH_DOMAIN_OS = '/domian/os'
XPATH_DOMAIN_CPU = '/domain/cpu'
class XMLConstructor:
@ -21,17 +34,16 @@ class XMLConstructor:
def domain_xml(self):
return self.xml
def gen_domain_xml(
self,
def gen_domain_xml(self,
name: str,
title: str,
vcpus: int,
cpu_vendor: str,
cpu_model: str,
vcpu_vendor: str,
vcpu_model: str,
memory: int,
volume: Path,
desc: str = ""
) -> None:
vcpu_features: dict | None = None,
desc: str = "") -> None:
"""
Generate default domain XML configuration for virtual machines.
See https://lxml.de/tutorial.html#the-e-factory for details.
@ -54,9 +66,10 @@ class XMLConstructor:
E.apic(),
),
E.cpu(
E.vendor(cpu_vendor),
E.model(cpu_model, fallback='forbid'),
E.topology(sockets='1', dies='1', cores=str(vcpus), threads='1'),
E.vendor(vcpu_vendor),
E.model(vcpu_model, fallback='forbid'),
E.topology(sockets='1', dies='1', cores=str(vcpus),
threads='1'),
mode='custom',
match='exact',
check='partial',
@ -77,25 +90,35 @@ class XMLConstructor:
type='file',
device='disk',
),
E.interface(
E.source(network='default'),
E.mac(address=random_mac()),
type='network',
),
E.graphics(
E.listen(type='address'),
type='vnc', port='-1', autoport='yes'
),
E.video(
E.model(type='vga', vram='16384', heads='1', primary='yes'),
E.address(type='pci', domain='0x0000', bus='0x00',
slot='0x02', function='0x0'),
),
),
type='kvm',
)
def gen_volume_xml(
self,
def gen_volume_xml(self,
device_name: str,
file: Path,
bus: str = 'virtio',
cache: str = 'writethrough',
disktype: str = 'file',
):
return E.disk(
E.driver(name='qemu', type='qcow2', cache=cache),
disktype: str = 'file'):
return E.disk(E.driver(name='qemu', type='qcow2', cache=cache),
E.source(file=file),
E.target(dev=device_name, bus=bus),
type=disktype,
device='disk'
)
device='disk')
def add_volume(self):
raise NotImplementedError()
@ -111,21 +134,18 @@ class XMLConstructor:
data,
namespace=namespace,
nsprefix=nsprefix,
)
)
))
self.xml.replace(metadata_old, metadata)
def remove_meta(self, namespace: str):
"""Remove metadata by namespace."""
raise NotImplementedError()
def construct_xml(
self,
def construct_xml(self,
tag: dict,
namespace: str | None = None,
nsprefix: str | None = None,
root: Element = None,
) -> Element:
root: Element = None) -> Element:
"""
Shortly this recursive function transforms dictonary to XML.
Return etree.Element built from dict with following structure::
@ -148,18 +168,13 @@ class XMLConstructor:
# Create element
if root is None:
if use_ns:
element = Element(
QName(namespace, tag['name']),
nsmap={nsprefix: namespace},
)
element = Element(QName(namespace, tag['name']),
nsmap={nsprefix: namespace})
else:
element = Element(tag['name'])
else:
if use_ns:
element = SubElement(
root,
QName(namespace, tag['name']),
)
element = SubElement(root, QName(namespace, tag['name']))
else:
element = SubElement(root, tag['name'])
# Fill up element with content
@ -171,16 +186,12 @@ class XMLConstructor:
if 'children' in tag.keys():
for child in tag['children']:
element.append(
self.construct_xml(
child,
self.construct_xml(child,
namespace=namespace,
nsprefix=nsprefix,
root=element,
)
)
root=element))
return element
def to_string(self):
return tostring(
self.xml, pretty_print=True, encoding='utf-8'
).decode().strip()
return (tostring(self.xml, pretty_print=True,
encoding='utf-8').decode().strip())

View File

@ -1,3 +1,3 @@
from .main import VirtualMachine
from .ga import QemuAgent, QemuAgentError
from .exceptions import *
from .ga import QemuAgent
from .main import VirtualMachine

View File

@ -1,11 +1,11 @@
import libvirt
from ..main import LibvirtSession
from .exceptions import VMNotFound
from .exceptions import VMError, VMNotFound
class VirtualMachineBase:
def __init__(self, session: LibvirtSession, name: str):
def __init__(self, session: 'LibvirtSession', name: str):
self.domname = name
self.session = session.session # virConnect object
self.config = session.config # ConfigLoader object
@ -19,4 +19,4 @@ class VirtualMachineBase:
return domain
raise VMNotFound(name)
except libvirt.libvirtError as err:
raise VMNotFound(err) from err
raise VMError(err) from err

View File

@ -7,6 +7,7 @@ class VMError(Exception):
class VMNotFound(Exception):
def __init__(self, domain, message='VM not found: {domain}'):
self.domain = domain
self.message = message.format(domain=domain)

View File

@ -1,19 +1,17 @@
import json
import logging
from time import time, sleep
from base64 import standard_b64encode, b64decode
from base64 import b64decode, standard_b64encode
from time import sleep, time
import libvirt
import libvirt_qemu
from ..main import LibvirtSession
from .base import VirtualMachineBase
from .exceptions import QemuAgentError
logger = logging.getLogger(__name__)
QEMU_TIMEOUT = 60 # seconds
POLL_INTERVAL = 0.3 # also seconds
@ -28,39 +26,30 @@ class QemuAgent(VirtualMachineBase):
shellexec()
High-level method for executing shell commands on guest. Command
must be passed as string. Wraps execute() method.
_execute()
Just executes QEMU command. Wraps libvirt_qemu.qemuAgentCommand()
_get_cmd_result()
Intended for long-time commands. This function loops and every
POLL_INTERVAL calls 'guest-exec-status' for specified guest PID.
Polling ends if command exited or on timeout.
_return_tuple()
This method transforms JSON command output to tuple and decode
base64 encoded strings optionally.
"""
def __init__(self,
session: LibvirtSession,
session: 'LibvirtSession',
name: str,
timeout: int | None = None,
flags: int | None = None
):
flags: int | None = None):
super().__init__(session, name)
self.timeout = timeout or QEMU_TIMEOUT # timeout for guest agent
self.flags = flags or libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT
def execute(
self,
def execute(self,
command: dict,
stdin: str | None = None,
capture_output: bool = False,
decode_output: bool = False,
wait: bool = True,
timeout: int = QEMU_TIMEOUT,
):
timeout: int = QEMU_TIMEOUT
) -> tuple[bool | None, int | None, str | None, str | None]:
"""
Execute command on guest and return output if capture_output is True.
Execute command on guest and return output if `capture_output` is True.
See https://wiki.qemu.org/Documentation/QMP for QEMU commands reference.
If `wait` is True poll guest command output with POLL_INTERVAL. Raise
QemuAgentError on `timeout` reached (in seconds).
Return values:
tuple(
exited: bool | None,
@ -68,15 +57,15 @@ class QemuAgent(VirtualMachineBase):
stdout: str | None,
stderr: str | None
)
stdout and stderr are base64 encoded strings or None.
stdout and stderr are base64 encoded strings or None. stderr and stdout
will be decoded if `decode_output` is True.
"""
# todo command dict schema validation
if capture_output:
command['arguments']['capture-output'] = True
if isinstance(stdin, str):
command['arguments']['input-data'] = standard_b64encode(
stdin.encode('utf-8')
).decode('utf-8')
stdin.encode('utf-8')).decode('utf-8')
# Execute command on guest
cmd_out = self._execute(command)
@ -91,19 +80,18 @@ class QemuAgent(VirtualMachineBase):
)
return None, None, None, None
def shellexec(
self,
def shellexec(self,
command: str,
stdin: str | None = None,
executable: str = '/bin/sh',
capture_output: bool = False,
decode_output: bool = False,
wait: bool = True,
timeout: int = QEMU_TIMEOUT,
):
timeout: int = QEMU_TIMEOUT
) -> tuple[bool | None, int | None, str | None, str | None]:
"""
Execute command on guest with selected shell. /bin/sh by default.
Otherwise of execute() this function brings command as string.
Otherwise of execute() this function brings shell command as string.
"""
cmd = {
'execute': 'guest-exec',
@ -121,7 +109,6 @@ class QemuAgent(VirtualMachineBase):
timeout=timeout,
)
def _execute(self, command: dict):
logging.debug('Execute command: vm=%s cmd=%s', self.domname, command)
try:
@ -135,19 +122,10 @@ class QemuAgent(VirtualMachineBase):
raise QemuAgentError(err) from err
def _get_cmd_result(
self,
pid: int,
decode_output: bool = False,
wait: bool = True,
timeout: int = QEMU_TIMEOUT,
):
self, pid: int, decode_output: bool = False, wait: bool = True,
timeout: int = QEMU_TIMEOUT):
"""Get executed command result. See GuestAgent.execute() for info."""
exited = exitcode = stdout = stderr = None
cmd = {
'execute': 'guest-exec-status',
'arguments': {'pid': pid},
}
cmd = {'execute': 'guest-exec-status', 'arguments': {'pid': pid}}
if not wait:
output = json.loads(self._execute(cmd))
@ -165,28 +143,23 @@ class QemuAgent(VirtualMachineBase):
raise QemuAgentError(
f'Polling command pid={pid} took longer than {timeout} seconds.'
)
logger.debug(
'Polling command pid=%s finished, time taken: %s seconds',
pid, int(time()-start_time)
)
logger.debug('Polling command pid=%s finished, time taken: %s seconds',
pid, int(time() - start_time))
return self._return_tuple(output, decode=decode_output)
def _return_tuple(self, cmd_output: dict, decode: bool = False):
exited = cmd_output['return']['exited']
exitcode = cmd_output['return']['exitcode']
def _return_tuple(self, output: dict, decode: bool = False):
output = output['return']
exited = output['exited']
exitcode = output['exitcode']
stdout = stderr = None
try:
stdout = cmd_output['return']['out-data']
if decode and stdout:
stdout = b64decode(stdout).decode('utf-8')
except KeyError:
stdout = None
if 'out-data' in output.keys():
stdout = output['out-data']
if 'err-data' in output.keys():
stderr = output['err-data']
try:
stderr = cmd_output['return']['err-data']
if decode and stderr:
stderr = b64decode(stderr).decode('utf-8')
except KeyError:
stderr = None
if decode:
stdout = b64decode(stdout).decode('utf-8') if stdout else None
stderr = b64decode(stderr).decode('utf-8') if stderr else None
return exited, exitcode, stdout, stderr

View File

@ -26,24 +26,19 @@ class VirtualMachine(VirtualMachineBase):
# https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainGetState
state = self.domain.state()[0]
except libvirt.libvirtError as err:
raise VMError(f'Cannot fetch VM status vm={self.domname}: {err}') from err
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'
raise VMError(
f'Cannot fetch VM status vm={self.domname}: {err}') from err
STATES = {
libvirt.VIR_DOMAIN_NOSTATE: 'nostate',
libvirt.VIR_DOMAIN_RUNNING: 'running',
libvirt.VIR_DOMAIN_BLOCKED: 'blocked',
libvirt.VIR_DOMAIN_PAUSED: 'paused',
libvirt.VIR_DOMAIN_SHUTDOWN: 'shutdown',
libvirt.VIR_DOMAIN_SHUTOFF: 'shutoff',
libvirt.VIR_DOMAIN_CRASHED: 'crashed',
libvirt.VIR_DOMAIN_PMSUSPENDED: 'pmsuspended',
}
return STATES.get(state)
@property
def is_running(self) -> bool:
@ -61,42 +56,53 @@ class VirtualMachine(VirtualMachineBase):
return True
return False
except libvirt.libvirtError as err:
raise VMError(f'Cannot get autostart status vm={self.domname}: {err}') from err
raise VMError(
f'Cannot get autostart status vm={self.domname}: {err}'
) from err
def start(self) -> None:
"""Start defined VM."""
logger.info('Starting VM: vm=%s', self.domname)
if self.is_running:
logger.debug('VM vm=%s is already started, nothing to do', self.domname)
logger.debug('VM vm=%s is already started, nothing to do',
self.domname)
return
try:
self.domain.create()
except libvirt.libvirtError as err:
raise VMError(f'Cannot start vm={self.domname}: {err}') from err
def shutdown(self, force=False, sigkill=False) -> None:
def shutdown(self, mode: str | None = None) -> None:
"""
Send ACPI signal to guest OS to shutdown. OS may ignore this.
Use `force=True` for graceful VM destroy. Add `sigkill=True`
to hard shutdown (may corrupt guest data!).
Send signal to guest OS to shutdown. Supports several modes:
* GUEST_AGENT - use guest agent
* NORMAL - use method choosen by hypervisor to shutdown machine
* SIGTERM - send SIGTERM to QEMU process, destroy machine gracefully
* SIGKILL - send SIGKILL, this option may corrupt guest data!
If mode is not passed use 'NORMAL' mode.
"""
if sigkill:
flags = libvirt.VIR_DOMAIN_DESTROY_DEFAULT
else:
flags = libvirt.VIR_DOMAIN_DESTROY_GRACEFUL
MODES = {
'GUEST_AGENT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT,
'SIGTERM': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL,
'SIGKILL': libvirt.VIR_DOMAIN_DESTROY_DEFAULT
}
if mode is None:
mode = 'NORMAL'
if not isinstance(mode, str):
raise ValueError(f'Mode must be a string, not {type(mode)}')
if mode.upper() not in MODES:
raise ValueError(f"Unsupported mode: '{mode}'")
try:
if force:
self.domain.destroyFlags(flags=flags)
else:
# Normal VM shutdown via ACPI signal, OS may ignore this.
self.domain.shutdown()
if mode in ['GUEST_AGENT', 'NORMAL']:
self.domain.shutdownFlags(flags=MODES.get(mode))
elif mode in ['SIGTERM', 'SIGKILL']:
self.domain.destroyFlags(flags=MODES.get(mode))
except libvirt.libvirtError as err:
raise VMError(
f'Cannot shutdown vm={self.domname} '
f'force={force} sigkill={sigkill}: {err}'
) from err
raise VMError(f'Cannot shutdown vm={self.domname} with '
f'mode={mode}: {err}') from err
def reset(self):
def reset(self) -> None:
"""
Copypaste from libvirt doc:
@ -119,35 +125,33 @@ class VirtualMachine(VirtualMachineBase):
except libvirt.libvirtError as err:
raise VMError(f'Cannot reboot vm={self.domname}: {err}') from err
def autostart(self, enabled: bool) -> None:
def autostart(self, enable: bool) -> None:
"""
Configure VM to be automatically started when the host machine boots.
"""
if enabled:
if enable:
autostart_flag = 1
else:
autostart_flag = 0
try:
self.domain.setAutostart(autostart_flag)
except libvirt.libvirtError as err:
raise VMError(
f'Cannot set autostart vm={self.domname} '
f'autostart={autostart_flag}: {err}'
) from err
raise VMError(f'Cannot set autostart vm={self.domname} '
f'autostart={autostart_flag}: {err}') from err
def vcpu_set(self, count: int):
def set_vcpus(self, count: int):
pass
def vram_set(self, count: int):
def set_ram(self, count: int):
pass
def ssh_keys_list(self, user: str):
def list_ssh_keys(self, user: str):
pass
def ssh_keys_add(self, user: str):
def set_ssh_keys(self, user: str):
pass
def ssh_keys_remove(self, user: str):
def remove_ssh_keys(self, user: str):
pass
def set_user_password(self, user: str):

View File

@ -12,9 +12,22 @@ lxml = "^4.9.2" # 4.9.2 on Debian 12
docopt = "^0.6.2" # 0.6.2 on Debian 12
[tool.poetry.scripts]
na-vmctl = "node_agent.utils.vmctl:cli"
na-vmexec = "node_agent.utils.vmexec:cli"
na-vmctl = "node_agent.cli.vmctl:cli"
na-vmexec = "node_agent.cli.vmexec:cli"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.yapf]
[tool.pylint."MESSAGES CONTROL"]
disable = [
"invalid-name",
"missing-module-docstring",
"missing-class-docstring",
"missing-function-docstring",
"import-error",
"too-many-arguments",
]