various improvemets
This commit is contained in:
parent
e00979dbb8
commit
dab71df3d0
2
Makefile
2
Makefile
@ -5,7 +5,7 @@ DOCS_BUILDDIR = docs/build
|
|||||||
|
|
||||||
.PHONY: docs
|
.PHONY: docs
|
||||||
|
|
||||||
all: build
|
all: docs build-deb
|
||||||
|
|
||||||
requirements.txt:
|
requirements.txt:
|
||||||
poetry export -f requirements.txt -o requirements.txt
|
poetry export -f requirements.txt -o requirements.txt
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
"""Command line interface for compute module."""
|
"""Command line interface for compute module."""
|
||||||
|
|
||||||
from compute.cli import main
|
from compute.cli import control
|
||||||
|
|
||||||
|
|
||||||
main.cli()
|
control.cli()
|
||||||
|
@ -30,7 +30,7 @@ import yaml
|
|||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from compute import __version__
|
from compute import __version__
|
||||||
from compute.exceptions import ComputeError, GuestAgentTimeoutExceededError
|
from compute.exceptions import ComputeError, GuestAgentTimeoutError
|
||||||
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 compute.utils import ids
|
||||||
@ -116,7 +116,7 @@ def _exec_guest_agent_command(
|
|||||||
decode_output=True,
|
decode_output=True,
|
||||||
poll=True,
|
poll=True,
|
||||||
)
|
)
|
||||||
except GuestAgentTimeoutExceededError as e:
|
except GuestAgentTimeoutError as e:
|
||||||
sys.exit(
|
sys.exit(
|
||||||
f'{e}. NOTE: command may still running in guest, '
|
f'{e}. NOTE: command may still running in guest, '
|
||||||
f'PID={ga.last_pid}'
|
f'PID={ga.last_pid}'
|
||||||
|
@ -17,14 +17,26 @@
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Extra
|
||||||
|
|
||||||
|
|
||||||
|
class EntityModel(BaseModel):
|
||||||
|
"""Basic entity model."""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Do not allow extra fields."""
|
||||||
|
|
||||||
|
extra = Extra.forbid
|
||||||
|
|
||||||
|
|
||||||
class EntityConfig(ABC):
|
class EntityConfig(ABC):
|
||||||
"""An abstract entity XML config builder class."""
|
"""An abstract entity XML config builder class."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def to_xml(self) -> str:
|
def to_xml(self) -> str:
|
||||||
"""Return device XML config."""
|
"""Return entity XML config."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
DeviceConfig = EntityConfig
|
class DeviceConfig(EntityConfig):
|
||||||
|
"""An abstract device XML config."""
|
||||||
|
@ -36,12 +36,12 @@ class GuestAgentUnavailableError(GuestAgentError):
|
|||||||
"""Guest agent is not connected or is unavailable."""
|
"""Guest agent is not connected or is unavailable."""
|
||||||
|
|
||||||
|
|
||||||
class GuestAgentTimeoutExceededError(GuestAgentError):
|
class GuestAgentTimeoutError(GuestAgentError):
|
||||||
"""QEMU timeout exceeded."""
|
"""QEMU timeout exceeded."""
|
||||||
|
|
||||||
def __init__(self, msg: int):
|
def __init__(self, seconds: int):
|
||||||
"""Initialise GuestAgentTimeoutExceededError."""
|
"""Initialise GuestAgentTimeoutExceededError."""
|
||||||
super().__init__(f'QEMU timeout ({msg} sec) exceeded')
|
super().__init__(f'QEMU timeout ({seconds} sec) exceeded')
|
||||||
|
|
||||||
|
|
||||||
class GuestAgentCommandNotSupportedError(GuestAgentError):
|
class GuestAgentCommandNotSupportedError(GuestAgentError):
|
||||||
@ -78,3 +78,11 @@ class InstanceNotFoundError(InstanceError):
|
|||||||
def __init__(self, msg: str):
|
def __init__(self, msg: str):
|
||||||
"""Initialise InstanceNotFoundError."""
|
"""Initialise InstanceNotFoundError."""
|
||||||
super().__init__(f"compute instance '{msg}' not found")
|
super().__init__(f"compute instance '{msg}' not found")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidDataUnitError(ValueError, ComputeError):
|
||||||
|
"""Data unit is not valid."""
|
||||||
|
|
||||||
|
def __init__(self, msg: str, units: list):
|
||||||
|
"""Initialise InvalidDataUnitError."""
|
||||||
|
super().__init__(f'{msg}, valid units are: {", ".join(units)}')
|
||||||
|
@ -27,7 +27,7 @@ import libvirt_qemu
|
|||||||
from compute.exceptions import (
|
from compute.exceptions import (
|
||||||
GuestAgentCommandNotSupportedError,
|
GuestAgentCommandNotSupportedError,
|
||||||
GuestAgentError,
|
GuestAgentError,
|
||||||
GuestAgentTimeoutExceededError,
|
GuestAgentTimeoutError,
|
||||||
GuestAgentUnavailableError,
|
GuestAgentUnavailableError,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ class GuestAgent:
|
|||||||
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 GuestAgentTimeoutError(self.timeout)
|
||||||
log.debug(
|
log.debug(
|
||||||
'Polling command pid=%s finished, time taken: %s seconds',
|
'Polling command pid=%s finished, time taken: %s seconds',
|
||||||
pid,
|
pid,
|
||||||
|
@ -322,6 +322,8 @@ class Instance:
|
|||||||
|
|
||||||
:param method: Method used to shutdown instance
|
:param method: Method used to shutdown instance
|
||||||
"""
|
"""
|
||||||
|
if not self.is_running():
|
||||||
|
return
|
||||||
methods = {
|
methods = {
|
||||||
'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
|
'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
|
||||||
'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT,
|
'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT,
|
||||||
@ -498,11 +500,6 @@ class Instance:
|
|||||||
msg = f'Cannot set memory for instance={self.name} {memory=}: {e}'
|
msg = f'Cannot set memory for instance={self.name} {memory=}: {e}'
|
||||||
raise InstanceError(msg) from e
|
raise InstanceError(msg) from e
|
||||||
|
|
||||||
def _get_disk_by_target(self, target: str) -> etree.Element:
|
|
||||||
xml = etree.fromstring(self.dump_xml()) # noqa: S320
|
|
||||||
child = xml.xpath(f'/domain/devices/disk/target[@dev="{target}"]')
|
|
||||||
return child[0].getparent() if child else None
|
|
||||||
|
|
||||||
def attach_device(
|
def attach_device(
|
||||||
self, device: DeviceConfig, *, live: bool = False
|
self, device: DeviceConfig, *, live: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -520,7 +517,7 @@ class Instance:
|
|||||||
else:
|
else:
|
||||||
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
||||||
if isinstance(device, DiskConfig): # noqa: SIM102
|
if isinstance(device, DiskConfig): # noqa: SIM102
|
||||||
if self._get_disk_by_target(device.target):
|
if self.get_disk(device.target):
|
||||||
log.warning(
|
log.warning(
|
||||||
"Volume with target '%s' is already attached",
|
"Volume with target '%s' is already attached",
|
||||||
device.target,
|
device.target,
|
||||||
@ -545,7 +542,7 @@ class Instance:
|
|||||||
else:
|
else:
|
||||||
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
||||||
if isinstance(device, DiskConfig): # noqa: SIM102
|
if isinstance(device, DiskConfig): # noqa: SIM102
|
||||||
if self._get_disk_by_target(device.target) is None:
|
if self.get_disk(device.target) is None:
|
||||||
log.warning(
|
log.warning(
|
||||||
"Volume with target '%s' is already detached",
|
"Volume with target '%s' is already detached",
|
||||||
device.target,
|
device.target,
|
||||||
@ -553,6 +550,27 @@ class Instance:
|
|||||||
return
|
return
|
||||||
self.domain.detachDeviceFlags(device.to_xml(), flags=flags)
|
self.domain.detachDeviceFlags(device.to_xml(), flags=flags)
|
||||||
|
|
||||||
|
def get_disk(self, name: str) -> DiskConfig | None:
|
||||||
|
"""
|
||||||
|
Return :class:`DiskConfig` by disk target name.
|
||||||
|
|
||||||
|
Return None if disk with specified target not found.
|
||||||
|
|
||||||
|
:param name: Disk name e.g. `vda`, `sda`, etc. This name may
|
||||||
|
not match the name of the disk inside the guest OS.
|
||||||
|
"""
|
||||||
|
xml = etree.fromstring(self.dump_xml())
|
||||||
|
child = xml.xpath(f'/domain/devices/disk/target[@dev="{name}"]')
|
||||||
|
if len(child) == 0:
|
||||||
|
return None
|
||||||
|
return DiskConfig.from_xml(child[0].getparent())
|
||||||
|
|
||||||
|
def list_disks(self) -> list[DiskConfig]:
|
||||||
|
"""Return list of attached disk devices."""
|
||||||
|
xml = etree.fromstring(self.dump_xml())
|
||||||
|
disks = xml.xpath('/domain/devices/disk')
|
||||||
|
return [DiskConfig.from_xml(disk) for disk in disks]
|
||||||
|
|
||||||
def detach_disk(self, name: str) -> None:
|
def detach_disk(self, name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Detach disk device by target name.
|
Detach disk device by target name.
|
||||||
@ -560,31 +578,17 @@ class Instance:
|
|||||||
There is no ``attach_disk()`` method. Use :func:`attach_device`
|
There is no ``attach_disk()`` method. Use :func:`attach_device`
|
||||||
with :class:`DiskConfig` as argument.
|
with :class:`DiskConfig` as argument.
|
||||||
|
|
||||||
:param name: Disk name e.g. 'vda', 'sda', etc. This name may
|
:param name: Disk name e.g. `vda`, `sda`, etc. This name may
|
||||||
not match the name of the disk inside the guest OS.
|
not match the name of the disk inside the guest OS.
|
||||||
"""
|
"""
|
||||||
xml = self._get_disk_by_target(name)
|
disk = self.get_disk(name)
|
||||||
if xml is None:
|
if disk is None:
|
||||||
log.warning(
|
log.warning(
|
||||||
"Volume with target '%s' is already detached",
|
"Volume with target '%s' is already detached",
|
||||||
name,
|
name,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
disk_params = {
|
self.detach_device(disk, live=True)
|
||||||
'disk_type': xml.get('type'),
|
|
||||||
'source': xml.find('source').get('file'),
|
|
||||||
'target': xml.find('target').get('dev'),
|
|
||||||
'readonly': False if xml.find('readonly') is None else True, # noqa: SIM211
|
|
||||||
}
|
|
||||||
for param in disk_params:
|
|
||||||
if disk_params[param] is None:
|
|
||||||
msg = (
|
|
||||||
f"Cannot detach volume with target '{name}': "
|
|
||||||
f"parameter '{param}' is not defined in libvirt XML "
|
|
||||||
'config on host.'
|
|
||||||
)
|
|
||||||
raise InstanceError(msg)
|
|
||||||
self.detach_device(DiskConfig(**disk_params), live=True)
|
|
||||||
|
|
||||||
def resize_disk(
|
def resize_disk(
|
||||||
self, name: str, capacity: int, unit: units.DataUnit
|
self, name: str, capacity: int, unit: units.DataUnit
|
||||||
@ -592,7 +596,8 @@ class Instance:
|
|||||||
"""
|
"""
|
||||||
Resize attached block device.
|
Resize attached block device.
|
||||||
|
|
||||||
:param name: Disk device name e.g. `vda`, `sda`, etc.
|
:param name: Disk name e.g. `vda`, `sda`, etc. This name may
|
||||||
|
not match the name of the disk inside the guest OS.
|
||||||
:param capacity: New capacity.
|
:param capacity: New capacity.
|
||||||
:param unit: Capacity unit.
|
:param unit: Capacity unit.
|
||||||
"""
|
"""
|
||||||
@ -602,10 +607,6 @@ class Instance:
|
|||||||
flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES,
|
flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_disks(self) -> list[DiskConfig]:
|
|
||||||
"""Return list of attached disks."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def pause(self) -> None:
|
def pause(self) -> None:
|
||||||
"""Pause instance."""
|
"""Pause instance."""
|
||||||
if not self.is_running():
|
if not self.is_running():
|
||||||
@ -616,31 +617,75 @@ class Instance:
|
|||||||
"""Resume paused instance."""
|
"""Resume paused instance."""
|
||||||
self.domain.resume()
|
self.domain.resume()
|
||||||
|
|
||||||
def get_ssh_keys(self, user: str) -> list[str]:
|
def list_ssh_keys(self, user: str) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Return list of SSH keys on guest for specific user.
|
Return list of SSH keys on guest for specific user.
|
||||||
|
|
||||||
:param user: Username.
|
:param user: Username.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
self.guest_agent.raise_for_commands(['guest-ssh-get-authorized-keys'])
|
||||||
|
exc = self.guest_agent.guest_exec(
|
||||||
|
path='/bin/sh',
|
||||||
|
args=[
|
||||||
|
'-c',
|
||||||
|
(
|
||||||
|
'su -c "'
|
||||||
|
'if ! [ -f ~/.ssh/authorized_keys ]; then '
|
||||||
|
'mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys; '
|
||||||
|
'fi" '
|
||||||
|
f'{user}'
|
||||||
|
),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
decode_output=True,
|
||||||
|
poll=True,
|
||||||
|
)
|
||||||
|
log.debug(exc)
|
||||||
|
try:
|
||||||
|
return self.domain.authorizedSSHKeysGet(user)
|
||||||
|
except libvirt.libvirtError as e:
|
||||||
|
raise InstanceError(e) from e
|
||||||
|
|
||||||
def set_ssh_keys(self, user: str, ssh_keys: list[str]) -> None:
|
def set_ssh_keys(
|
||||||
|
self,
|
||||||
|
user: str,
|
||||||
|
keys: list[str],
|
||||||
|
*,
|
||||||
|
remove: bool = False,
|
||||||
|
append: bool = False,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add SSH keys to guest for specific user.
|
Add SSH keys to guest for specific user.
|
||||||
|
|
||||||
:param user: Username.
|
:param user: Username.
|
||||||
:param ssh_keys: List of public SSH keys.
|
:param keys: List of authorized SSH keys.
|
||||||
|
:param append: Append keys to authorized SSH keys instead of
|
||||||
|
overriding authorized_keys file.
|
||||||
|
:param remove: Remove authorized keys listed in `keys` parameter.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
qemu_ga_commands = ['guest-ssh-add-authorized-keys']
|
||||||
|
if remove and append:
|
||||||
def delete_ssh_keys(self, user: str, ssh_keys: list[str]) -> None:
|
raise InstanceError(
|
||||||
"""
|
"'append' and 'remove' parameters is mutually exclusive"
|
||||||
Remove SSH keys from guest for specific user.
|
)
|
||||||
|
if not self.is_running():
|
||||||
:param user: Username.
|
raise InstanceError(
|
||||||
:param ssh_keys: List of public SSH keys.
|
'Cannot add authorized SSH keys to inactive instance'
|
||||||
"""
|
)
|
||||||
raise NotImplementedError
|
if append:
|
||||||
|
flags = libvirt.VIR_DOMAIN_AUTHORIZED_SSH_KEYS_SET_APPEND
|
||||||
|
elif remove:
|
||||||
|
flags = libvirt.VIR_DOMAIN_AUTHORIZED_SSH_KEYS_SET_REMOVE
|
||||||
|
qemu_ga_commands = ['guest-ssh-remove-authorized-keys']
|
||||||
|
else:
|
||||||
|
flags = 0
|
||||||
|
if keys.sort() == self.list_ssh_keys().sort():
|
||||||
|
return
|
||||||
|
self.guest_agent.raise_for_commands(qemu_ga_commands)
|
||||||
|
try:
|
||||||
|
self.domain.authorizedSSHKeysSet(user, keys, flags=flags)
|
||||||
|
except libvirt.libvirtError as e:
|
||||||
|
raise InstanceError(e) from e
|
||||||
|
|
||||||
def set_user_password(
|
def set_user_password(
|
||||||
self, user: str, password: str, *, encrypted: bool = False
|
self, user: str, password: str, *, encrypted: bool = False
|
||||||
@ -655,10 +700,6 @@ class Instance:
|
|||||||
:param encrypted: Set it to True if password is already encrypted.
|
:param encrypted: Set it to True if password is already encrypted.
|
||||||
Right encryption method depends on guest OS.
|
Right encryption method depends on guest OS.
|
||||||
"""
|
"""
|
||||||
if not self.guest_agent.is_available():
|
|
||||||
raise InstanceError(
|
|
||||||
'Cannot change password: guest agent is unavailable'
|
|
||||||
)
|
|
||||||
self.guest_agent.raise_for_commands(['guest-set-user-password'])
|
self.guest_agent.raise_for_commands(['guest-set-user-password'])
|
||||||
flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0
|
flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0
|
||||||
self.domain.setUserPassword(user, password, flags=flags)
|
self.domain.setUserPassword(user, password, flags=flags)
|
||||||
@ -669,7 +710,10 @@ class Instance:
|
|||||||
return self.domain.XMLDesc(flags)
|
return self.domain.XMLDesc(flags)
|
||||||
|
|
||||||
def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
"""Undefine instance."""
|
"""Delete instance with local disks."""
|
||||||
# TODO @ge: delete local disks
|
|
||||||
self.shutdown(method='HARD')
|
self.shutdown(method='HARD')
|
||||||
|
for disk in self.list_disks():
|
||||||
|
if disk.disk_type == 'file':
|
||||||
|
volume = self.connection.storageVolLookupByPath(disk.source)
|
||||||
|
volume.delete()
|
||||||
self.domain.undefine()
|
self.domain.undefine()
|
||||||
|
@ -19,20 +19,12 @@ import re
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from pydantic import BaseModel, Extra, validator
|
from pydantic import validator
|
||||||
|
|
||||||
|
from compute.common import EntityModel
|
||||||
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."""
|
||||||
|
|
||||||
|
@ -154,7 +154,7 @@ class Session(AbstractContextManager):
|
|||||||
"""Return capabilities e.g. arch, virt, emulator, etc."""
|
"""Return capabilities e.g. arch, virt, emulator, etc."""
|
||||||
prefix = '/domainCapabilities'
|
prefix = '/domainCapabilities'
|
||||||
hprefix = f'{prefix}/cpu/mode[@name="host-model"]'
|
hprefix = f'{prefix}/cpu/mode[@name="host-model"]'
|
||||||
caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320
|
caps = etree.fromstring(self.connection.getDomainCapabilities())
|
||||||
return Capabilities(
|
return Capabilities(
|
||||||
arch=caps.xpath(f'{prefix}/arch/text()')[0],
|
arch=caps.xpath(f'{prefix}/arch/text()')[0],
|
||||||
virt_type=caps.xpath(f'{prefix}/domain/text()')[0],
|
virt_type=caps.xpath(f'{prefix}/domain/text()')[0],
|
||||||
|
@ -49,12 +49,12 @@ class StoragePool:
|
|||||||
|
|
||||||
def _get_path(self) -> Path:
|
def _get_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())
|
||||||
return Path(xml.xpath('/pool/target/path/text()')[0])
|
return Path(xml.xpath('/pool/target/path/text()')[0])
|
||||||
|
|
||||||
def get_usage_info(self) -> StoragePoolUsageInfo:
|
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())
|
||||||
return StoragePoolUsageInfo(
|
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]),
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import time
|
from time import time
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import libvirt
|
import libvirt
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@ -88,6 +89,28 @@ class DiskConfig(DeviceConfig):
|
|||||||
xml.append(E.readonly())
|
xml.append(E.readonly())
|
||||||
return etree.tostring(xml, encoding='unicode', pretty_print=True)
|
return etree.tostring(xml, encoding='unicode', pretty_print=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_xml(cls, xml: Union[str, etree.Element]) -> 'DiskConfig': # noqa: UP007
|
||||||
|
"""
|
||||||
|
Return :class:`DiskConfig` instance using existing XML config.
|
||||||
|
|
||||||
|
:param xml: Disk device XML configuration as :class:`str` or lxml
|
||||||
|
:class:`etree.Element` object.
|
||||||
|
"""
|
||||||
|
if isinstance(xml, str):
|
||||||
|
xml = etree.fromstring(xml)
|
||||||
|
disk_params = {
|
||||||
|
'disk_type': xml.get('type'),
|
||||||
|
'source': xml.find('source').get('file'),
|
||||||
|
'target': xml.find('target').get('dev'),
|
||||||
|
'readonly': False if xml.find('readonly') is None else True, # noqa: SIM211
|
||||||
|
}
|
||||||
|
for param in disk_params:
|
||||||
|
if disk_params[param] is None:
|
||||||
|
msg = f"Bad XML config: parameter '{param}' is not defined"
|
||||||
|
raise ValueError(msg)
|
||||||
|
return cls(**disk_params)
|
||||||
|
|
||||||
|
|
||||||
class Volume:
|
class Volume:
|
||||||
"""Storage volume manipulating class."""
|
"""Storage volume manipulating class."""
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from compute.exceptions import InvalidDataUnitError
|
||||||
|
|
||||||
|
|
||||||
class DataUnit(StrEnum):
|
class DataUnit(StrEnum):
|
||||||
"""Data units enumerated."""
|
"""Data units enumerated."""
|
||||||
@ -28,22 +30,12 @@ class DataUnit(StrEnum):
|
|||||||
TIB = 'TiB'
|
TIB = 'TiB'
|
||||||
|
|
||||||
|
|
||||||
class InvalidDataUnitError(ValueError):
|
|
||||||
"""Data unit is not valid."""
|
|
||||||
|
|
||||||
def __init__(self, msg: str):
|
|
||||||
"""Initialise InvalidDataUnitError."""
|
|
||||||
super().__init__(
|
|
||||||
f'{msg}, valid units are: {", ".join(list(DataUnit))}'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) -> int:
|
def to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) -> int:
|
||||||
"""Convert value to bytes. See :class:`DataUnit`."""
|
"""Convert value to bytes. See :class:`DataUnit`."""
|
||||||
try:
|
try:
|
||||||
_ = DataUnit(unit)
|
_ = DataUnit(unit)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise InvalidDataUnitError(e) from e
|
raise InvalidDataUnitError(e, list(DataUnit)) from e
|
||||||
powers = {
|
powers = {
|
||||||
DataUnit.BYTES: 0,
|
DataUnit.BYTES: 0,
|
||||||
DataUnit.KIB: 1,
|
DataUnit.KIB: 1,
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
Python API
|
Python API
|
||||||
==========
|
==========
|
||||||
|
|
||||||
The API allows you to perform actions on instances programmatically. Below is
|
The API allows you to perform actions on instances programmatically.
|
||||||
an example of changing parameters and launching the `myinstance` instance.
|
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
import compute
|
||||||
|
|
||||||
from compute import Session
|
with compute.Session() as session:
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
with Session() as session:
|
|
||||||
instance = session.get_instance('myinstance')
|
instance = session.get_instance('myinstance')
|
||||||
instance.set_vcpus(4)
|
info = instance.get_info()
|
||||||
instance.start()
|
|
||||||
instance.set_autostart(enabled=True)
|
print(info)
|
||||||
|
|
||||||
|
|
||||||
:class:`Session` context manager provides an abstraction over :class:`libvirt.virConnect`
|
:class:`Session` context manager provides an abstraction over :class:`libvirt.virConnect`
|
||||||
|
@ -18,16 +18,12 @@ _compute_root_cmd="
|
|||||||
status
|
status
|
||||||
setvcpus
|
setvcpus
|
||||||
setmem
|
setmem
|
||||||
setpasswd"
|
setpass"
|
||||||
_compute_init_opts=""
|
_compute_init_opts=""
|
||||||
_compute_exec_opts="
|
_compute_exec_opts="--timeout --executable --env --no-join-args"
|
||||||
--timeout
|
|
||||||
--executable
|
|
||||||
--env
|
|
||||||
--no-join-args"
|
|
||||||
_compute_ls_opts=""
|
_compute_ls_opts=""
|
||||||
_compute_start_opts=""
|
_compute_start_opts=""
|
||||||
_compute_shutdown_opts="--method"
|
_compute_shutdown_opts="--soft --normal --hard --unsafe"
|
||||||
_compute_reboot_opts=""
|
_compute_reboot_opts=""
|
||||||
_compute_reset_opts=""
|
_compute_reset_opts=""
|
||||||
_compute_powrst_opts=""
|
_compute_powrst_opts=""
|
||||||
@ -36,13 +32,14 @@ _compute_resume_opts=""
|
|||||||
_compute_status_opts=""
|
_compute_status_opts=""
|
||||||
_compute_setvcpus_opts=""
|
_compute_setvcpus_opts=""
|
||||||
_compute_setmem_opts=""
|
_compute_setmem_opts=""
|
||||||
_compute_setpasswd_opts="--encrypted"
|
_compute_setpass_opts="--encrypted"
|
||||||
|
|
||||||
_compute_complete_instances()
|
_compute_complete_instances()
|
||||||
{
|
{
|
||||||
|
local base_name
|
||||||
for file in /etc/libvirt/qemu/*.xml; do
|
for file in /etc/libvirt/qemu/*.xml; do
|
||||||
nodir="${file##*/}"
|
base_name="${file##*/}"
|
||||||
printf '%s ' "${nodir//\.xml}"
|
printf '%s ' "${base_name//\.xml}"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +77,7 @@ _compute_complete()
|
|||||||
status) _compute_compreply "$_compute_status_opts";;
|
status) _compute_compreply "$_compute_status_opts";;
|
||||||
setvcpus) _compute_compreply "$_compute_setvcpus_opts";;
|
setvcpus) _compute_compreply "$_compute_setvcpus_opts";;
|
||||||
setmem) _compute_compreply "$_compute_setmem_opts";;
|
setmem) _compute_compreply "$_compute_setmem_opts";;
|
||||||
setpasswd) _compute_compreply "$_compute_setpasswd_opts";;
|
setpass) _compute_compreply "$_compute_setpass_opts";;
|
||||||
*) COMPREPLY=()
|
*) COMPREPLY=()
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = 'compute'
|
name = 'compute'
|
||||||
version = '0.1.0-dev1'
|
version = '0.1.0-dev2'
|
||||||
description = 'Compute instances management library and tools'
|
description = 'Compute instances management library and tools'
|
||||||
authors = ['ge <ge@nixhacks.net>']
|
authors = ['ge <ge@nixhacks.net>']
|
||||||
readme = 'README.md'
|
readme = 'README.md'
|
||||||
@ -42,11 +42,19 @@ target-version = 'py311'
|
|||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ['ALL']
|
select = ['ALL']
|
||||||
ignore = [
|
ignore = [
|
||||||
'Q000', 'Q003', 'D211', 'D212',
|
'Q000', 'Q003',
|
||||||
'ANN101', 'ISC001', 'COM812',
|
'D211', 'D212',
|
||||||
'D203', 'ANN204', 'T201',
|
'ANN101', 'ANN102', 'ANN204',
|
||||||
'EM102', 'TRY003', 'EM101',
|
'ISC001',
|
||||||
'TD003', 'TD006', 'FIX002', # 'todo' strings linting
|
'COM812',
|
||||||
|
'D203',
|
||||||
|
'T201',
|
||||||
|
'S320',
|
||||||
|
'EM102',
|
||||||
|
'TRY003',
|
||||||
|
'EM101',
|
||||||
|
'TD003', 'TD006',
|
||||||
|
'FIX002',
|
||||||
]
|
]
|
||||||
exclude = ['__init__.py']
|
exclude = ['__init__.py']
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user