various improvements

This commit is contained in:
ge 2023-11-09 01:17:50 +03:00
parent 8e7f185fc6
commit 1dd3e9a720
28 changed files with 557 additions and 219 deletions

View File

@ -11,7 +11,7 @@ build: format lint
poetry build poetry build
format: format:
poetry run isort --lai 2 $(SRC) poetry run isort $(SRC)
poetry run ruff format $(SRC) poetry run ruff format $(SRC)
lint: lint:
@ -23,7 +23,7 @@ docs:
docs-versions: docs-versions:
poetry run sphinx-multiversion $(DOCS_SRC) $(DOCS_BUILD) poetry run sphinx-multiversion $(DOCS_SRC) $(DOCS_BUILD)
serve-docs: docs-versions serve-docs:
poetry run sphinx-autobuild $(DOCS_SRC) $(DOCS_BUILD) poetry run sphinx-autobuild $(DOCS_SRC) $(DOCS_BUILD)
clean: clean:

View File

@ -6,7 +6,7 @@ Currently supports only QEMU/KVM based virtual machines.
## Docs ## Docs
Run `make serve-docs`. Run `make serve-docs`. See [Development](#development) below.
## Roadmap ## Roadmap
@ -39,3 +39,11 @@ Run `make serve-docs`.
- [ ] Instance migrations - [ ] Instance migrations
- [ ] HTTP API - [ ] HTTP API
- [ ] Full functional CLI [in progress] - [ ] Full functional CLI [in progress]
## Development
Install [poetry](https://python-poetry.org/), clone this repository and run:
```
poetry install --with dev --with docs
```

View File

@ -1,4 +1,4 @@
"""Compute Service library.""" """Compute instances management library."""
__version__ = '0.1.0' __version__ = '0.1.0'

View File

@ -1,26 +0,0 @@
import argparse
from compute import Session
from compute.utils import identifiers
def _create_instance(session: Session, args: argparse.Namespace) -> None:
"""
Умолчания (достать информацию из либвирта):
- arch
- machine
- emulator
- CPU
- cpu_vendor
- cpu_model
- фичи
- max_memory
- max_vcpus
(сегнерировать):
- MAC адрес
- boot_order = ('cdrom', 'hd')
- title = ''
- name = uuid.uuid4().hex
"""
print(args)

View File

@ -1,12 +1,18 @@
"""Command line interface.""" """Command line interface."""
import argparse import argparse
import io
import logging import logging
import os import os
import shlex import shlex
import sys import sys
from collections import UserDict
from typing import Any
from uuid import uuid4
import libvirt import libvirt
import yaml
from pydantic import ValidationError
from compute import __version__ from compute import __version__
from compute.exceptions import ( from compute.exceptions import (
@ -15,8 +21,7 @@ from compute.exceptions import (
) )
from compute.instance import GuestAgent from compute.instance import GuestAgent
from compute.session import Session from compute.session import Session
from compute.utils import ids
from ._create import _create_instance
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -37,41 +42,41 @@ class Table:
"""Initialise Table.""" """Initialise Table."""
self.whitespace = whitespace or '\t' self.whitespace = whitespace or '\t'
self.header = [] self.header = []
self._rows = [] self.rows = []
self._table = '' self.table = ''
def row(self, row: list) -> None: def add_row(self, row: list) -> None:
"""Add table row.""" """Add table row."""
self._rows.append([str(col) for col in row]) self.rows.append([str(col) for col in row])
def rows(self, rows: list[list]) -> None: def add_rows(self, rows: list[list]) -> None:
"""Add multiple rows.""" """Add multiple rows."""
for row in rows: for row in rows:
self.row(row) self.add_row(row)
def __str__(self) -> str: def __str__(self) -> str:
"""Build table and return.""" """Build table and return."""
widths = [max(map(len, col)) for col in zip(*self._rows, strict=True)] widths = [max(map(len, col)) for col in zip(*self.rows, strict=True)]
self._rows.insert(0, [str(h).upper() for h in self.header]) self.rows.insert(0, [str(h).upper() for h in self.header])
for row in self._rows: for row in self.rows:
self._table += self.whitespace.join( self.table += self.whitespace.join(
( (
val.ljust(width) val.ljust(width)
for val, width in zip(row, widths, strict=True) for val, width in zip(row, widths, strict=True)
) )
) )
self._table += '\n' self.table += '\n'
return self._table.strip() return self.table.strip()
def _list_instances(session: Session) -> None: def _list_instances(session: Session) -> None:
table = Table() table = Table()
table.header = ['NAME', 'STATE'] table.header = ['NAME', 'STATE']
for instance in session.list_instances(): for instance in session.list_instances():
table.row( table.add_row(
[ [
instance.name, instance.name,
instance.status, instance.get_status(),
] ]
) )
print(table) print(table)
@ -113,11 +118,93 @@ def _exec_guest_agent_command(
sys.exit(output.exitcode) sys.exit(output.exitcode)
class _NotPresent:
"""
Type for representing non-existent dictionary keys.
See :class:`_FillableDict`.
"""
class _FillableDict(UserDict):
"""Use :method:`fill` to add key if not present."""
def __init__(self, data: dict):
self.data = data
def fill(self, key: str, value: Any) -> None: # noqa: ANN401
if self.data.get(key, _NotPresent) is _NotPresent:
self.data[key] = value
def _merge_dicts(a: dict, b: dict, path: list[str] | None = None) -> dict:
"""Merge `b` into `a`. Return modified `a`."""
if path is None:
path = []
for key in b:
if key in a:
if isinstance(a[key], dict) and isinstance(b[key], dict):
_merge_dicts(a[key], b[key], [path + str(key)])
elif a[key] == b[key]:
pass # same leaf value
else:
a[key] = b[key] # replace existing key's values
else:
a[key] = b[key]
return a
def _create_instance(session: Session, file: io.TextIOWrapper) -> None:
try:
data = _FillableDict(yaml.load(file.read(), Loader=yaml.SafeLoader))
log.debug('Read from file: %s', data)
except yaml.YAMLError as e:
sys.exit(f'error: cannot parse YAML: {e}')
capabilities = session.get_capabilities()
node_info = session.get_node_info()
data.fill('name', uuid4().hex)
data.fill('title', None)
data.fill('description', None)
data.fill('arch', capabilities.arch)
data.fill('machine', capabilities.machine)
data.fill('emulator', capabilities.emulator)
data.fill('max_vcpus', node_info.cpus)
data.fill('max_memory', node_info.memory)
data.fill('cpu', {})
cpu = {
'emulation_mode': 'host-passthrough',
'model': None,
'vendor': None,
'topology': None,
'features': None,
}
data['cpu'] = _merge_dicts(data['cpu'], cpu)
data.fill(
'network_interfaces',
[{'source': 'default', 'mac': ids.random_mac()}],
)
data.fill('boot', {'order': ['cdrom', 'hd']})
try:
log.debug('Input data: %s', data)
session.create_instance(**data)
except ValidationError as e:
for error in e.errors():
fields = '.'.join([str(lc) for lc in error['loc']])
print(
f"validation error: {fields}: {error['msg']}",
file=sys.stderr,
)
sys.exit()
def main(session: Session, args: argparse.Namespace) -> None: def main(session: Session, args: argparse.Namespace) -> None:
"""Perform actions.""" """Perform actions."""
match args.command: match args.command:
case 'create': case 'create':
_create_instance(session, args) _create_instance(session, args.file)
case 'exec': case 'exec':
_exec_guest_agent_command(session, args) _exec_guest_agent_command(session, args)
case 'ls': case 'ls':
@ -179,30 +266,13 @@ def cli() -> None: # noqa: PLR0915
subparsers = root.add_subparsers(dest='command', metavar='COMMAND') subparsers = root.add_subparsers(dest='command', metavar='COMMAND')
# create command # create command
create = subparsers.add_parser('create', help='create compute instance') create = subparsers.add_parser(
create.add_argument('image', nargs='?') 'create', help='create new instance from YAML config file'
create.add_argument('--name', help='instance name, used as ID') )
create.add_argument('--title', help='human-understandable instance title') create.add_argument(
create.add_argument('--desc', default='', help='instance description') 'file',
create.add_argument('--memory', type=int, help='memory in MiB') type=argparse.FileType('r', encoding='UTF-8'),
create.add_argument('--max-memory', type=int, help='max memory in MiB')
create.add_argument('--vcpus', type=int)
create.add_argument('--max-vcpus', type=int)
create.add_argument('--cpu-vendor')
create.add_argument('--cpu-model')
create.add_argument(
'--cpu-emulation-mode',
choices=['host-passthrough', 'host-model', 'custom'],
default='host-passthrough',
) )
create.add_argument('--cpu-features')
create.add_argument('--cpu-topology')
create.add_argument('--mahine')
create.add_argument('--emulator')
create.add_argument('--arch')
create.add_argument('--boot-order')
create.add_argument('--volume')
create.add_argument('-f', '--file', help='create instance from YAML')
# exec subcommand # exec subcommand
execute = subparsers.add_parser( execute = subparsers.add_parser(
@ -303,14 +373,17 @@ def cli() -> None: # noqa: PLR0915
if log_level in log_levels: if log_level in log_levels:
logging.basicConfig(level=log_levels[log_level]) logging.basicConfig(level=log_levels[log_level])
log.debug('CLI started with args: %s', args)
# Perform actions # Perform actions
try: try:
with Session(args.connect) as session: with Session(args.connect) as session:
main(session, args) main(session, args)
except ComputeServiceError as e: except ComputeServiceError as e:
sys.exit(f'error: {e}') sys.exit(f'error: {e}')
except (KeyboardInterrupt, SystemExit): except KeyboardInterrupt:
sys.exit() sys.exit()
except SystemExit as e:
sys.exit(e)
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
sys.exit(f'unexpected error {type(e)}: {e}') sys.exit(f'unexpected error {type(e)}: {e}')

View File

@ -1,4 +1,4 @@
"""Compute Service exceptions.""" """Exceptions."""
class ComputeServiceError(Exception): class ComputeServiceError(Exception):

View File

@ -1,4 +1,4 @@
"""Manage QEMU guest agent.""" """Interacting with the QEMU Guest Agent."""
import json import json
import logging import logging
@ -19,9 +19,6 @@ from compute.exceptions import (
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
QEMU_TIMEOUT = 60
POLL_INTERVAL = 0.3
class GuestExecOutput(NamedTuple): class GuestExecOutput(NamedTuple):
"""QEMU guest-exec command output.""" """QEMU guest-exec command output."""
@ -35,7 +32,7 @@ class GuestExecOutput(NamedTuple):
class GuestAgent: class GuestAgent:
"""Class for interacting with QEMU guest agent.""" """Class for interacting with QEMU guest agent."""
def __init__(self, domain: libvirt.virDomain, timeout: int | None = None): def __init__(self, domain: libvirt.virDomain, timeout: int = 60):
""" """
Initialise GuestAgent. Initialise GuestAgent.
@ -43,7 +40,7 @@ class GuestAgent:
:param timeout: QEMU timeout :param timeout: QEMU timeout
""" """
self.domain = domain self.domain = domain
self.timeout = timeout or QEMU_TIMEOUT self.timeout = timeout
self.flags = libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT self.flags = libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT
self.last_pid = None self.last_pid = None
@ -65,9 +62,6 @@ class GuestAgent:
return json.loads(output) return json.loads(output)
except libvirt.libvirtError as e: except libvirt.libvirtError as e:
if e.get_error_code() == libvirt.VIR_ERR_AGENT_UNRESPONSIVE: if e.get_error_code() == libvirt.VIR_ERR_AGENT_UNRESPONSIVE:
log.exception(
'Guest agent is unavailable on instanse=%s', self.name
)
raise GuestAgentUnavailableError(e) from e raise GuestAgentUnavailableError(e) from e
raise GuestAgentError(e) from e raise GuestAgentError(e) from e
@ -95,9 +89,7 @@ class GuestAgent:
def raise_for_commands(self, commands: list[str]) -> None: def raise_for_commands(self, commands: list[str]) -> None:
""" """
Check QEMU guest agent command availability. Raise exception if QEMU GA command is not available.
Raise exception if command is not available.
:param commands: List of required commands :param commands: List of required commands
:raise: GuestAgentCommandNotSupportedError :raise: GuestAgentCommandNotSupportedError
@ -164,12 +156,15 @@ class GuestAgent:
stderr = b64decode(stderr or '').decode('utf-8') stderr = b64decode(stderr or '').decode('utf-8')
return GuestExecOutput(exited, exitcode, stdout, stderr) return GuestExecOutput(exited, exitcode, stdout, stderr)
def guest_exec_status(self, pid: int, *, poll: bool = False) -> dict: def guest_exec_status(
self, pid: int, *, poll: bool = False, poll_interval: float = 0.3
) -> dict:
""" """
Execute guest-exec-status and return output. Execute guest-exec-status and return output.
:param pid: PID in guest :param pid: PID in guest.
:param poll: If True poll command status with POLL_INTERVAL :param poll: If True poll command status.
:param poll_interval: Time between attempts to obtain command status.
:return: Command output :return: Command output
:rtype: dict :rtype: dict
""" """
@ -185,7 +180,7 @@ class GuestAgent:
command_status = self.execute(command) command_status = self.execute(command)
if command_status['return']['exited']: if command_status['return']['exited']:
break break
sleep(POLL_INTERVAL) sleep(poll_interval)
now = time() now = time()
if now - start_time > self.timeout: if now - start_time > self.timeout:
raise GuestAgentTimeoutExceededError(self.timeout) raise GuestAgentTimeoutExceededError(self.timeout)

View File

@ -1,9 +1,9 @@
"""Manage compute instances.""" """Manage compute instances."""
__all__ = ['Instance', 'InstanceConfig'] __all__ = ['Instance', 'InstanceConfig', 'InstanceInfo']
import logging import logging
from dataclasses import dataclass from typing import NamedTuple
import libvirt import libvirt
from lxml import etree from lxml import etree
@ -16,14 +16,19 @@ from compute.exceptions import (
from compute.utils import units from compute.utils import units
from .guest_agent import GuestAgent from .guest_agent import GuestAgent
from .schemas import CPUSchema, InstanceSchema, NetworkInterfaceSchema from .schemas import (
CPUEmulationMode,
CPUSchema,
InstanceSchema,
NetworkInterfaceSchema,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class InstanceConfig: class InstanceConfig:
"""Compute instance description for libvirt.""" """Compute instance config builder."""
def __init__(self, schema: InstanceSchema): def __init__(self, schema: InstanceSchema):
""" """
@ -46,21 +51,33 @@ class InstanceConfig:
self.network_interfaces = schema.network_interfaces self.network_interfaces = schema.network_interfaces
def _gen_cpu_xml(self, cpu: CPUSchema) -> etree.Element: def _gen_cpu_xml(self, cpu: CPUSchema) -> etree.Element:
xml = E.cpu(match='exact', mode=cpu.emulation_mode) options = {
xml.append(E.model(cpu.model, fallback='forbid')) 'mode': cpu.emulation_mode,
xml.append(E.vendor(cpu.vendor)) 'match': 'exact',
xml.append( 'check': 'partial',
E.topology( }
sockets=str(cpu.topology.sockets), if cpu.emulation_mode == CPUEmulationMode.HOST_PASSTHROUGH:
dies=str(cpu.topology.dies), options['check'] = 'none'
cores=str(cpu.topology.cores), options['migratable'] = 'on'
threads=str(cpu.topology.threads), xml = E.cpu(**options)
if cpu.model:
xml.append(E.model(cpu.model, fallback='forbid'))
if cpu.vendor:
xml.append(E.vendor(cpu.vendor))
if cpu.topology:
xml.append(
E.topology(
sockets=str(cpu.topology.sockets),
dies=str(cpu.topology.dies),
cores=str(cpu.topology.cores),
threads=str(cpu.topology.threads),
)
) )
) if cpu.features:
for feature in cpu.features.require: for feature in cpu.features.require:
xml.append(E.feature(policy='require', name=feature)) xml.append(E.feature(policy='require', name=feature))
for feature in cpu.features.disable: for feature in cpu.features.disable:
xml.append(E.feature(policy='disable', name=feature)) xml.append(E.feature(policy='disable', name=feature))
return xml return xml
def _gen_vcpus_xml(self, vcpus: int, max_vcpus: int) -> etree.Element: def _gen_vcpus_xml(self, vcpus: int, max_vcpus: int) -> etree.Element:
@ -89,15 +106,15 @@ class InstanceConfig:
def to_xml(self) -> str: def to_xml(self) -> str:
"""Return XML config for libvirt.""" """Return XML config for libvirt."""
xml = E.domain( xml = E.domain(type='kvm')
E.name(self.name), xml.append(E.name(self.name))
E.title(self.title), if self.title:
E.description(self.description), xml.append(E.title(self.title))
E.metadata(), if self.description:
E.memory(str(self.memory * 1024), unit='KiB'), xml.append(E.description(self.description))
E.currentMemory(str(self.memory * 1024), unit='KiB'), xml.append(E.metadata())
type='kvm', xml.append(E.memory(str(self.max_memory * 1024), unit='KiB'))
) xml.append(E.currentMemory(str(self.memory * 1024), unit='KiB'))
xml.append( xml.append(
E.vcpu( E.vcpu(
str(self.max_vcpus), str(self.max_vcpus),
@ -148,8 +165,14 @@ class InstanceConfig:
return etree.tostring(xml, encoding='unicode', pretty_print=True) return etree.tostring(xml, encoding='unicode', pretty_print=True)
@dataclass class InstanceInfo(NamedTuple):
class InstanceInfo: """
Store compute instance info.
Reference:
https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo
"""
state: str state: str
max_memory: int max_memory: int
memory: int memory: int
@ -193,13 +216,8 @@ class Instance:
} }
return states[state] return states[state]
@property def get_info(self) -> InstanceInfo:
def info(self) -> InstanceInfo: """Return instance info."""
"""
Return instance info.
https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo
"""
_info = self.domain.info() _info = self.domain.info()
return InstanceInfo( return InstanceInfo(
state=self._expand_instance_state(_info[0]), state=self._expand_instance_state(_info[0]),
@ -209,8 +227,7 @@ class Instance:
cputime=_info[4], cputime=_info[4],
) )
@property def get_status(self) -> str:
def status(self) -> str:
""" """
Return instance state: 'running', 'shutoff', etc. Return instance state: 'running', 'shutoff', etc.
@ -225,7 +242,6 @@ class Instance:
) from e ) from e
return self._expand_instance_state(state) return self._expand_instance_state(state)
@property
def is_running(self) -> bool: def is_running(self) -> bool:
"""Return True if instance is running, else return False.""" """Return True if instance is running, else return False."""
if self.domain.isActive() != 1: if self.domain.isActive() != 1:
@ -233,7 +249,6 @@ class Instance:
return False return False
return True return True
@property
def is_autostart(self) -> bool: def is_autostart(self) -> bool:
"""Return True if instance autostart is enabled, else return False.""" """Return True if instance autostart is enabled, else return False."""
try: try:
@ -244,10 +259,18 @@ class Instance:
f'instance={self.name}: {e}' f'instance={self.name}: {e}'
) from e ) from e
def get_max_memory(self) -> int:
"""Maximum memory value for domain in KiB."""
return self.domain.maxMemory()
def get_max_vcpus(self) -> int:
"""Maximum vCPUs number for domain."""
return self.domain.maxVcpus()
def start(self) -> None: def start(self) -> None:
"""Start defined instance.""" """Start defined instance."""
log.info('Starting instnce=%s', self.name) log.info('Starting instnce=%s', self.name)
if self.is_running: if self.is_running():
log.warning( log.warning(
'Already started, nothing to do instance=%s', self.name 'Already started, nothing to do instance=%s', self.name
) )
@ -311,6 +334,15 @@ class Instance:
f'Cannot shutdown instance={self.name} ' f'{method=}: {e}' f'Cannot shutdown instance={self.name} ' f'{method=}: {e}'
) from e ) from e
def reboot(self) -> None:
"""Send ACPI signal to guest OS to reboot. OS may ignore this."""
try:
self.domain.reboot()
except libvirt.libvirtError as e:
raise InstanceError(
f'Cannot reboot instance={self.name}: {e}'
) from e
def reset(self) -> None: def reset(self) -> None:
""" """
Reset instance. Reset instance.
@ -331,14 +363,19 @@ class Instance:
f'Cannot reset instance={self.name}: {e}' f'Cannot reset instance={self.name}: {e}'
) from e ) from e
def reboot(self) -> None: def power_reset(self) -> None:
"""Send ACPI signal to guest OS to reboot. OS may ignore this.""" """
try: Shutdown instance and start.
self.domain.reboot()
except libvirt.libvirtError as e: By analogy with real hardware, this is a normal server shutdown,
raise InstanceError( and then turning off from the power supply and turning it on again.
f'Cannot reboot instance={self.name}: {e}'
) from e This method is applicable in cases where there has been a
configuration change in libvirt and you need to restart the
instance to apply the new configuration.
"""
self.shutdown(method='NORMAL')
self.start()
def set_autostart(self, *, enabled: bool) -> None: def set_autostart(self, *, enabled: bool) -> None:
""" """
@ -383,7 +420,7 @@ class Instance:
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
self.domain.setVcpusFlags(nvcpus, flags=flags) self.domain.setVcpusFlags(nvcpus, flags=flags)
if live is True: if live is True:
if not self.is_running: if not self.is_running():
log.warning( log.warning(
'Instance is not running, changes applied in ' 'Instance is not running, changes applied in '
'instance config.' 'instance config.'
@ -453,7 +490,7 @@ class Instance:
:param device: Object with device description e.g. DiskConfig :param device: Object with device description e.g. DiskConfig
:param live: Affect a running instance :param live: Affect a running instance
""" """
if live and self.is_running: if live and self.is_running():
flags = ( flags = (
libvirt.VIR_DOMAIN_AFFECT_LIVE libvirt.VIR_DOMAIN_AFFECT_LIVE
| libvirt.VIR_DOMAIN_AFFECT_CONFIG | libvirt.VIR_DOMAIN_AFFECT_CONFIG
@ -471,7 +508,7 @@ class Instance:
:param device: Object with device description e.g. DiskConfig :param device: Object with device description e.g. DiskConfig
:param live: Affect a running instance :param live: Affect a running instance
""" """
if live and self.is_running: if live and self.is_running():
flags = ( flags = (
libvirt.VIR_DOMAIN_AFFECT_LIVE libvirt.VIR_DOMAIN_AFFECT_LIVE
| libvirt.VIR_DOMAIN_AFFECT_CONFIG | libvirt.VIR_DOMAIN_AFFECT_CONFIG

View File

@ -4,11 +4,20 @@ import re
from enum import StrEnum from enum import StrEnum
from pathlib import Path from pathlib import Path
from pydantic import BaseModel, validator from pydantic import BaseModel, Extra, validator
from compute.utils.units import DataUnit from compute.utils.units import DataUnit
class EntityModel(BaseModel):
"""Basic entity model."""
class Config:
"""Do not allow extra fields."""
extra = Extra.forbid
class CPUEmulationMode(StrEnum): class CPUEmulationMode(StrEnum):
"""CPU emulation mode enumerated.""" """CPU emulation mode enumerated."""
@ -18,7 +27,7 @@ class CPUEmulationMode(StrEnum):
MAXIMUM = 'maximum' MAXIMUM = 'maximum'
class CPUTopologySchema(BaseModel): class CPUTopologySchema(EntityModel):
"""CPU topology model.""" """CPU topology model."""
sockets: int sockets: int
@ -27,67 +36,66 @@ class CPUTopologySchema(BaseModel):
dies: int = 1 dies: int = 1
class CPUFeaturesSchema(BaseModel): class CPUFeaturesSchema(EntityModel):
"""CPU features model.""" """CPU features model."""
require: list[str] require: list[str]
disable: list[str] disable: list[str]
class CPUSchema(BaseModel): class CPUSchema(EntityModel):
"""CPU model.""" """CPU model."""
emulation_mode: CPUEmulationMode emulation_mode: CPUEmulationMode
model: str model: str | None
vendor: str vendor: str | None
topology: CPUTopologySchema topology: CPUTopologySchema | None
features: CPUFeaturesSchema features: CPUFeaturesSchema | None
class VolumeType(StrEnum): class VolumeType(StrEnum):
"""Storage volume types enumeration.""" """Storage volume types enumeration."""
FILE = 'file' FILE = 'file'
NETWORK = 'network'
class VolumeCapacitySchema(BaseModel): class VolumeCapacitySchema(EntityModel):
"""Storage volume capacity field model.""" """Storage volume capacity field model."""
value: int value: int
unit: DataUnit unit: DataUnit
class VolumeSchema(BaseModel): class VolumeSchema(EntityModel):
"""Storage volume model.""" """Storage volume model."""
type: VolumeType # noqa: A003 type: VolumeType # noqa: A003
source: Path
target: str target: str
capacity: VolumeCapacitySchema capacity: VolumeCapacitySchema
readonly: bool = False source: str | None = None
is_readonly: bool = False
is_system: bool = False is_system: bool = False
class NetworkInterfaceSchema(BaseModel): class NetworkInterfaceSchema(EntityModel):
"""Network inerface model.""" """Network inerface model."""
source: str source: str
mac: str mac: str
class BootOptionsSchema(BaseModel): class BootOptionsSchema(EntityModel):
"""Instance boot settings.""" """Instance boot settings."""
order: tuple order: tuple
class InstanceSchema(BaseModel): class InstanceSchema(EntityModel):
"""Compute instance model.""" """Compute instance model."""
name: str name: str
title: str title: str | None
description: str description: str | None
memory: int memory: int
max_memory: int max_memory: int
vcpus: int vcpus: int
@ -96,10 +104,10 @@ class InstanceSchema(BaseModel):
machine: str machine: str
emulator: Path emulator: Path
arch: str arch: str
image: str
boot: BootOptionsSchema boot: BootOptionsSchema
volumes: list[VolumeSchema] volumes: list[VolumeSchema]
network_interfaces: list[NetworkInterfaceSchema] network_interfaces: list[NetworkInterfaceSchema]
image: str | None = None
@validator('name') @validator('name')
def _check_name(cls, value: str) -> str: # noqa: N805 def _check_name(cls, value: str) -> str: # noqa: N805
@ -111,12 +119,28 @@ class InstanceSchema(BaseModel):
raise ValueError(msg) raise ValueError(msg)
return value return value
@validator('volumes') @validator('cpu')
def _check_volumes(cls, value: list) -> list: # noqa: N805 def _check_topology(cls, cpu: int, values: dict) -> CPUSchema: # noqa: N805
if len([v for v in value if v.is_system is True]) != 1: topo = cpu.topology
msg = 'Volumes list must contain one system volume' max_vcpus = values['max_vcpus']
if topo and topo.sockets * topo.cores * topo.threads != max_vcpus:
msg = f'CPU topology does not match with {max_vcpus=}'
raise ValueError(msg) raise ValueError(msg)
return value return cpu
@validator('volumes')
def _check_volumes(cls, volumes: list) -> list: # noqa: N805
if len([v for v in volumes if v.is_system is True]) != 1:
msg = 'volumes list must contain one system volume'
raise ValueError(msg)
vol_with_source = 0
for vol in volumes:
if vol.is_system is True and vol.is_readonly is True:
msg = 'volume marked as system cannot be readonly'
raise ValueError(msg)
if vol.source is not None:
vol_with_source += 1
return volumes
@validator('network_interfaces') @validator('network_interfaces')
def _check_network_interfaces(cls, value: list) -> list: # noqa: N805 def _check_network_interfaces(cls, value: list) -> list: # noqa: N805

View File

@ -26,6 +26,25 @@ class Capabilities(NamedTuple):
virt: str virt: str
emulator: str emulator: str
machine: str machine: str
max_vcpus: int
class NodeInfo(NamedTuple):
"""
Store compute node info.
See https://libvirt.org/html/libvirt-libvirt-host.html#virNodeInfo
NOTE: memory unit in libvirt docs is wrong! Actual unit is MiB.
"""
arch: str
memory: int
cpus: int
mhz: int
nodes: int
sockets: int
cores: int
threads: int
class Session(AbstractContextManager): class Session(AbstractContextManager):
@ -68,7 +87,21 @@ class Session(AbstractContextManager):
"""Close connection to libvirt daemon.""" """Close connection to libvirt daemon."""
self.connection.close() self.connection.close()
def capabilities(self) -> Capabilities: def get_node_info(self) -> NodeInfo:
"""Return information about compute node."""
info = self.connection.getInfo()
return NodeInfo(
arch=info[0],
memory=info[1],
cpus=info[2],
mhz=info[3],
nodes=info[4],
sockets=info[5],
cores=info[6],
threads=info[7],
)
def get_capabilities(self) -> Capabilities:
"""Return capabilities e.g. arch, virt, emulator, etc.""" """Return capabilities e.g. arch, virt, emulator, etc."""
prefix = '/domainCapabilities' prefix = '/domainCapabilities'
caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320 caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320
@ -77,6 +110,7 @@ class Session(AbstractContextManager):
virt=caps.xpath(f'{prefix}/domain/text()')[0], virt=caps.xpath(f'{prefix}/domain/text()')[0],
emulator=caps.xpath(f'{prefix}/path/text()')[0], emulator=caps.xpath(f'{prefix}/path/text()')[0],
machine=caps.xpath(f'{prefix}/machine/text()')[0], machine=caps.xpath(f'{prefix}/machine/text()')[0],
max_vcpus=int(caps.xpath(f'{prefix}/vcpu/@max')[0]),
) )
def create_instance(self, **kwargs: Any) -> Instance: def create_instance(self, **kwargs: Any) -> Instance:
@ -87,14 +121,35 @@ class Session(AbstractContextManager):
:type name: str :type name: str
:param title: Instance title for humans. :param title: Instance title for humans.
:type title: str :type title: str
:param description: Some information about instance :param description: Some information about instance.
:type description: str :type description: str
:param memory: Memory in MiB. :param memory: Memory in MiB.
:type memory: int :type memory: int
:param max_memory: Maximum memory in MiB. :param max_memory: Maximum memory in MiB.
:type max_memory: int :type max_memory: int
:param vcpus: Number of vCPUs.
:type vcpus: int
:param max_vcpus: Maximum vCPUs.
:type max_vcpus: int
:param cpu: CPU configuration. See :class:`CPUSchema` for info.
:type cpu: dict
:param machine: QEMU emulated machine.
:type machine: str
:param emulator: Path to emulator.
:type emulator: str
:param arch: CPU architecture to virtualization.
:type arch: str
:param boot: Boot settings. See :class:`BootOptionsSchema`.
:type boot: dict
:param image: Source disk image name for system disk.
:type image: str
:param volumes: List of storage volume configs. For more info
see :class:`VolumeSchema`.
:type volumes: list[dict]
:param network_interfaces: List of virtual network interfaces
configs. See :class:`NetworkInterfaceSchema` for more info.
:type network_interfaces: list[dict]
""" """
# TODO @ge: create instances in transaction
data = InstanceSchema(**kwargs) data = InstanceSchema(**kwargs)
config = InstanceConfig(data) config = InstanceConfig(data)
log.info('Define XML...') log.info('Define XML...')
@ -113,19 +168,17 @@ class Session(AbstractContextManager):
log.info('Connecting to volumes pool...') log.info('Connecting to volumes pool...')
volumes_pool = self.get_storage_pool(self.VOLUMES_POOL) volumes_pool = self.get_storage_pool(self.VOLUMES_POOL)
log.info('Building volume configuration...') log.info('Building volume configuration...')
# if not volume.source: if not volume.source:
# В случае если пользователь передаёт source для волюма, следует vol_name = f'{config.name}-{volume.target}-{uuid4()}.qcow2'
# в либвирте делать поиск волюма по пути, а не по имени else:
# gen_vol_name vol_name = volume.source
# TODO @ge: come up with something else
vol_name = f'{config.name}-{volume.target}-{uuid4()}.qcow2'
vol_conf = VolumeConfig( vol_conf = VolumeConfig(
name=vol_name, name=vol_name,
path=str(volumes_pool.path.joinpath(vol_name)), path=str(volumes_pool.path.joinpath(vol_name)),
capacity=capacity, capacity=capacity,
) )
log.info('Volume configuration is:\n %s', vol_conf.to_xml()) log.info('Volume configuration is:\n %s', vol_conf.to_xml())
if volume.is_system is True: if volume.is_system is True and data.image:
log.info( log.info(
"Volume is marked as 'system', start cloning image..." "Volume is marked as 'system', start cloning image..."
) )

View File

@ -15,8 +15,8 @@ from .volume import Volume, VolumeConfig
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class StoragePoolUsage(NamedTuple): class StoragePoolUsageInfo(NamedTuple):
"""Storage pool usage info schema.""" """Storage pool usage info."""
capacity: int capacity: int
allocation: int allocation: int
@ -30,17 +30,17 @@ class StoragePool:
"""Initislise StoragePool.""" """Initislise StoragePool."""
self.pool = pool self.pool = pool
self.name = pool.name() self.name = pool.name()
self.path = self._get_path()
@property def _get_path(self) -> Path:
def path(self) -> Path:
"""Return storage pool path.""" """Return storage pool path."""
xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320
return Path(xml.xpath('/pool/target/path/text()')[0]) return Path(xml.xpath('/pool/target/path/text()')[0])
def usage(self) -> StoragePoolUsage: def get_usage_info(self) -> StoragePoolUsageInfo:
"""Return info about storage pool usage.""" """Return info about storage pool usage."""
xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320
return StoragePoolUsage( return StoragePoolUsageInfo(
capacity=int(xml.xpath('/pool/capacity/text()')[0]), capacity=int(xml.xpath('/pool/capacity/text()')[0]),
allocation=int(xml.xpath('/pool/allocation/text()')[0]), allocation=int(xml.xpath('/pool/allocation/text()')[0]),
available=int(xml.xpath('/pool/available/text()')[0]), available=int(xml.xpath('/pool/available/text()')[0]),
@ -58,7 +58,7 @@ class StoragePool:
def create_volume(self, vol_conf: VolumeConfig) -> Volume: def create_volume(self, vol_conf: VolumeConfig) -> Volume:
"""Create storage volume and return Volume instance.""" """Create storage volume and return Volume instance."""
log.info( log.info(
'Create storage volume vol=%s in pool=%s', vol_conf.name, self.pool 'Create storage volume vol=%s in pool=%s', vol_conf.name, self.name
) )
vol = self.pool.createXML( vol = self.pool.createXML(
vol_conf.to_xml(), vol_conf.to_xml(),

View File

@ -87,11 +87,7 @@ class Volume:
self.pool_name = pool.name() self.pool_name = pool.name()
self.vol = vol self.vol = vol
self.name = vol.name() self.name = vol.name()
self.path = Path(vol.path())
@property
def path(self) -> Path:
"""Return path to volume."""
return Path(self.vol.path())
def dump_xml(self) -> str: def dump_xml(self) -> str:
"""Return volume XML description as string.""" """Return volume XML description as string."""

View File

@ -1,3 +1,4 @@
# Add ../../compute to path for autodoc
import os import os
import sys import sys
sys.path.insert(0, os.path.abspath('../../compute')) sys.path.insert(0, os.path.abspath('../../compute'))

View File

@ -0,0 +1,5 @@
``exceptions``
==============
.. automodule:: compute.exceptions
:members:

View File

@ -45,3 +45,5 @@ Modules documentation
session session
instance/index instance/index
storage/index storage/index
utils
exceptions

View File

@ -3,3 +3,4 @@
.. automodule:: compute.instance.guest_agent .. automodule:: compute.instance.guest_agent
:members: :members:
:special-members: __init__

View File

@ -3,4 +3,4 @@
.. automodule:: compute.instance.instance .. automodule:: compute.instance.instance
:members: :members:
:special-members: :special-members: __init__

View File

@ -1,9 +1,6 @@
``session`` ``session``
=========== ===========
.. autoclass:: compute.Session .. automodule:: compute.session
:members:
:special-members:
.. autoclass:: compute.session.Capabilities
:members: :members:
:special-members: __init__

View File

@ -3,3 +3,4 @@
.. automodule:: compute.storage.pool .. automodule:: compute.storage.pool
:members: :members:
:special-members: __init__

View File

@ -3,3 +3,4 @@
.. automodule:: compute.storage.volume .. automodule:: compute.storage.volume
:members: :members:
:special-members: __init__

View File

@ -0,0 +1,14 @@
``utils``
=========
``utils.units``
---------------
.. automodule:: compute.utils.units
:members:
``utils.ids``
-------------
.. automodule:: compute.utils.ids
:members:

25
fdict.py Normal file
View File

@ -0,0 +1,25 @@
from collections import UserDict
from typing import Any
class _NotPresent:
"""
Type for representing non-existent dictionary keys.
See :class:`_FillableDict`
"""
class _FillableDict(UserDict):
"""Use :method:`fill` to add key if not present."""
def __init__(self, data: dict):
self.data = data
def fill(self, key: str, value: Any) -> None:
if self.data.get(key, _NotPresent) is _NotPresent:
self.data[key] = value
d = _FillableDict({'a': None, 'b': 'BBBB'})
d.fill('c', 'CCCCCCCCC')
d.fill('a', 'CCCCCCCCC')
d['a'].fill('gg', 'AAAAAAAA')
print(d)

10
instance.yaml Normal file
View File

@ -0,0 +1,10 @@
title: dev-1
vcpus: 4
memory: 4096
volumes:
- is_system: true
type: file
target: vda
capacity:
value: 5
unit: GiB

34
pars.py Normal file
View File

@ -0,0 +1,34 @@
import re
def _split_unit(val: str) -> dict | None:
match = re.match(r'([0-9]+)([a-z]+)', val, re.I)
if match:
return {
'value': match.groups()[0],
'unit': match.groups()[1],
}
return None
def _parse_complex_arg(arg: str) -> dict:
# key=value --> {'key': 'value'}
if re.match(r'.+=.+', arg):
key, val = arg.split('=')
# system --> {'is_system': True}
# ro --> {'is_readonly': True}
elif re.match(r'^[a-z0-9_\.\-]+$', arg, re.I):
key = 'is_' + arg.replace('ro', 'readonly')
val = True
else:
raise ValueError('Invalid argument pattern')
# key=15GiB --> {'key': {'value': 15, 'unit': 'GiB'}}
if not isinstance(val, bool):
val = _split_unit(val) or val
return {key: val}
print(_parse_complex_arg('source=/volumes/50c4410b-2ef0-4ffd-a2e5-04f0212772d4.qcow2'))
print(_parse_complex_arg('capacity=15GiB'))
print(_parse_complex_arg('system'))
print(_parse_complex_arg('cpu.cores=8'))

20
pd.py Normal file
View File

@ -0,0 +1,20 @@
from pydantic import BaseModel, Extra
class EntityModel(BaseModel):
"""Basic entity model."""
aaa: int
bbb: int
class Config:
extra = Extra.forbid
class Some(EntityModel):
ooo: str
www: str
a = Some(ooo='dsda', www='wcd', sds=1)
print(a)

64
poetry.lock generated
View File

@ -511,6 +511,66 @@ files = [
[package.extras] [package.extras]
plugins = ["importlib-metadata"] plugins = ["importlib-metadata"]
[[package]]
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
{file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
{file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
{file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
{file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
{file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
{file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.31.0" version = "2.31.0"
@ -834,5 +894,5 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = '^3.11'
content-hash = "413ca8b2e0d37bf9e2835dd9050a3cc98e4a37186c78b780a65d62d05adce8c1" content-hash = "e5c07eebe683b92360ec12cada14fc5ccbe4e4add52549bf978f580e551abfb0"

View File

@ -1,53 +1,60 @@
[tool.poetry] [tool.poetry]
name = "compute" name = 'compute'
version = "0.1.0" version = '0.1.0'
description = "Library built on top of libvirt for Compute Service" description = 'Compute instances management library'
authors = ["ge <ge@nixhacks.net>"] authors = ['ge <ge@nixhacks.net>']
readme = "README.md" readme = 'README.md'
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = '^3.11'
libvirt-python = "9.0.0" libvirt-python = '9.0.0'
lxml = "^4.9.2" lxml = '^4.9.2'
pydantic = "1.10.4" pydantic = '1.10.4'
pyyaml = "^6.0.1"
[tool.poetry.scripts] [tool.poetry.scripts]
compute = "compute.cli.control:cli" compute = 'compute.cli.control:cli'
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
ruff = "^0.1.3" ruff = '^0.1.3'
isort = "^5.12.0" isort = '^5.12.0'
[tool.poetry.group.docs.dependencies] [tool.poetry.group.docs.dependencies]
sphinx = "^7.2.6" sphinx = '^7.2.6'
sphinx-autobuild = "^2021.3.14" sphinx-autobuild = '^2021.3.14'
sphinx-multiversion = "^0.2.4" sphinx-multiversion = '^0.2.4'
[build-system] [build-system]
requires = ["poetry-core"] requires = ['poetry-core']
build-backend = "poetry.core.masonry.api" build-backend = 'poetry.core.masonry.api'
[tool.isort]
skip = ['.gitignore']
lines_after_imports = 2
include_trailing_comma = true
split_on_trailing_comma = true
[tool.ruff] [tool.ruff]
line-length = 79 line-length = 79
indent-width = 4 indent-width = 4
target-version = "py311" target-version = 'py311'
[tool.ruff.lint] [tool.ruff.lint]
select = ["ALL"] select = ['ALL']
ignore = [ ignore = [
"Q000", "Q003", "D211", "D212", "ANN101", "ISC001", "COM812", 'Q000', 'Q003', 'D211', 'D212', 'ANN101', 'ISC001', 'COM812',
"D203", "ANN204", "T201", 'D203', 'ANN204', 'T201',
"EM102", "TRY003", # maybe not ignore? 'EM102', 'TRY003', # maybe not ignore?
"TD003", "TD006", "FIX002", # todo strings linting 'TD003', 'TD006', 'FIX002', # todo strings linting
] ]
exclude = ["__init__.py"] exclude = ['__init__.py']
[tool.ruff.lint.flake8-annotations] [tool.ruff.lint.flake8-annotations]
mypy-init-return = true mypy-init-return = true
allow-star-arg-any = true allow-star-arg-any = true
[tool.ruff.format] [tool.ruff.format]
quote-style = "single" quote-style = 'single'
[tool.ruff.isort] [tool.ruff.isort]
lines-after-imports = 2 lines-after-imports = 2