various improvements
This commit is contained in:
parent
0d2a18d1f3
commit
bd4e575a7c
@ -20,13 +20,13 @@ Run `make serve-docs`. See [Development](#development) below.
|
|||||||
- [x] CPU customization (emulation mode, model, vendor, features)
|
- [x] CPU customization (emulation mode, model, vendor, features)
|
||||||
- [ ] BIOS/UEFI settings
|
- [ ] BIOS/UEFI settings
|
||||||
- [x] Device attaching
|
- [x] Device attaching
|
||||||
- [ ] Device detaching
|
- [x] Device detaching
|
||||||
- [ ] GPU passthrough
|
- [ ] GPU passthrough
|
||||||
- [ ] CPU guarantied resource percent support
|
- [ ] CPU guarantied resource percent support
|
||||||
- [x] QEMU Guest Agent management
|
- [x] QEMU Guest Agent management
|
||||||
- [ ] Instance resources usage stats
|
- [ ] Instance resources usage stats
|
||||||
- [ ] SSH-keys management
|
- [ ] SSH-keys management
|
||||||
- [x] Setting user passwords in guest [not tested]
|
- [x] Setting user passwords in guest
|
||||||
- [x] QCOW2 disks support
|
- [x] QCOW2 disks support
|
||||||
- [ ] ZVOL support
|
- [ ] ZVOL support
|
||||||
- [ ] Network disks support
|
- [ ] Network disks support
|
||||||
|
@ -234,13 +234,20 @@ def main(session: Session, args: argparse.Namespace) -> None:
|
|||||||
case 'setmem':
|
case 'setmem':
|
||||||
instance = session.get_instance(args.instance)
|
instance = session.get_instance(args.instance)
|
||||||
instance.set_memory(args.memory, live=True)
|
instance.set_memory(args.memory, live=True)
|
||||||
|
case 'setpasswd':
|
||||||
|
instance = session.get_instance(args.instance)
|
||||||
|
instance.set_user_password(
|
||||||
|
args.username,
|
||||||
|
args.password,
|
||||||
|
encrypted=args.encrypted,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def cli() -> None: # noqa: PLR0915
|
def cli() -> None: # noqa: PLR0915
|
||||||
"""Parse command line arguments."""
|
"""Return command line arguments parser."""
|
||||||
root = argparse.ArgumentParser(
|
root = argparse.ArgumentParser(
|
||||||
prog='compute',
|
prog='compute',
|
||||||
description='manage compute instances and storage volumes.',
|
description='manage compute instances',
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
formatter_class=argparse.RawTextHelpFormatter,
|
||||||
)
|
)
|
||||||
root.add_argument(
|
root.add_argument(
|
||||||
@ -383,13 +390,27 @@ def cli() -> None: # noqa: PLR0915
|
|||||||
setmem.add_argument('instance')
|
setmem.add_argument('instance')
|
||||||
setmem.add_argument('memory', type=int, help='memory in MiB')
|
setmem.add_argument('memory', type=int, help='memory in MiB')
|
||||||
|
|
||||||
# Run parser
|
# setpasswd subcommand
|
||||||
|
setpasswd = subparsers.add_parser(
|
||||||
|
'setpasswd',
|
||||||
|
help='set user password in guest',
|
||||||
|
)
|
||||||
|
setpasswd.add_argument('instance')
|
||||||
|
setpasswd.add_argument('username')
|
||||||
|
setpasswd.add_argument('password')
|
||||||
|
setpasswd.add_argument(
|
||||||
|
'-e',
|
||||||
|
'--encrypted',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='set it if password is already encrypted',
|
||||||
|
)
|
||||||
|
|
||||||
args = root.parse_args()
|
args = root.parse_args()
|
||||||
if args.command is None:
|
if args.command is None:
|
||||||
root.print_help()
|
root.print_help()
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
# Set logging level
|
|
||||||
log_level = args.log_level or os.getenv('CMP_LOG')
|
log_level = args.log_level or os.getenv('CMP_LOG')
|
||||||
|
|
||||||
if isinstance(log_level, str) and log_level.lower() in log_levels:
|
if isinstance(log_level, str) and log_level.lower() in log_levels:
|
||||||
@ -398,7 +419,6 @@ def cli() -> None: # noqa: PLR0915
|
|||||||
)
|
)
|
||||||
|
|
||||||
log.debug('CLI started with args: %s', args)
|
log.debug('CLI started with args: %s', args)
|
||||||
# Perform actions
|
|
||||||
try:
|
try:
|
||||||
with Session(args.connect) as session:
|
with Session(args.connect) as session:
|
||||||
main(session, args)
|
main(session, args)
|
||||||
|
@ -37,6 +37,22 @@ class StoragePoolError(ComputeServiceError):
|
|||||||
"""Something went wrong when operating with storage pool."""
|
"""Something went wrong when operating with storage pool."""
|
||||||
|
|
||||||
|
|
||||||
|
class StoragePoolNotFoundError(StoragePoolError):
|
||||||
|
"""Storage pool not found."""
|
||||||
|
|
||||||
|
def __init__(self, msg: str):
|
||||||
|
"""Initialise StoragePoolNotFoundError."""
|
||||||
|
super().__init__(f"storage pool named '{msg}' not found")
|
||||||
|
|
||||||
|
|
||||||
|
class VolumeNotFoundError(StoragePoolError):
|
||||||
|
"""Storage volume not found."""
|
||||||
|
|
||||||
|
def __init__(self, msg: str):
|
||||||
|
"""Initialise VolumeNotFoundError."""
|
||||||
|
super().__init__(f"storage volume '{msg}' not found")
|
||||||
|
|
||||||
|
|
||||||
class InstanceError(ComputeServiceError):
|
class InstanceError(ComputeServiceError):
|
||||||
"""Something went wrong while interacting with the domain."""
|
"""Something went wrong while interacting with the domain."""
|
||||||
|
|
||||||
|
@ -78,8 +78,8 @@ class GuestAgent:
|
|||||||
except GuestAgentError:
|
except GuestAgentError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def available_commands(self) -> set[str]:
|
def get_supported_commands(self) -> set[str]:
|
||||||
"""Return set of available guest agent commands."""
|
"""Return set of supported guest agent commands."""
|
||||||
output = self.execute({'execute': 'guest-info', 'arguments': {}})
|
output = self.execute({'execute': 'guest-info', 'arguments': {}})
|
||||||
return {
|
return {
|
||||||
cmd['name']
|
cmd['name']
|
||||||
@ -94,8 +94,9 @@ class GuestAgent:
|
|||||||
:param commands: List of required commands
|
:param commands: List of required commands
|
||||||
:raise: GuestAgentCommandNotSupportedError
|
:raise: GuestAgentCommandNotSupportedError
|
||||||
"""
|
"""
|
||||||
|
supported = self.get_supported_commands()
|
||||||
for command in commands:
|
for command in commands:
|
||||||
if command not in self.available_commands():
|
if command not in supported:
|
||||||
raise GuestAgentCommandNotSupportedError(command)
|
raise GuestAgentCommandNotSupportedError(command)
|
||||||
|
|
||||||
def guest_exec( # noqa: PLR0913
|
def guest_exec( # noqa: PLR0913
|
||||||
|
@ -13,6 +13,7 @@ from compute.exceptions import (
|
|||||||
GuestAgentCommandNotSupportedError,
|
GuestAgentCommandNotSupportedError,
|
||||||
InstanceError,
|
InstanceError,
|
||||||
)
|
)
|
||||||
|
from compute.storage import DiskConfig
|
||||||
from compute.utils import units
|
from compute.utils import units
|
||||||
|
|
||||||
from .guest_agent import GuestAgent
|
from .guest_agent import GuestAgent
|
||||||
@ -181,7 +182,7 @@ class InstanceInfo(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceConfig:
|
class DeviceConfig:
|
||||||
"""Abstract device description class."""
|
"""Abstract device config class."""
|
||||||
|
|
||||||
|
|
||||||
class Instance:
|
class Instance:
|
||||||
@ -485,6 +486,11 @@ 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:
|
||||||
@ -501,6 +507,13 @@ class Instance:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
||||||
|
if isinstance(device, DiskConfig): # noqa: SIM102
|
||||||
|
if self._get_disk_by_target(device.target):
|
||||||
|
log.warning(
|
||||||
|
"Volume with target '%s' is already attached",
|
||||||
|
device.target,
|
||||||
|
)
|
||||||
|
return
|
||||||
self.domain.attachDeviceFlags(device.to_xml(), flags=flags)
|
self.domain.attachDeviceFlags(device.to_xml(), flags=flags)
|
||||||
|
|
||||||
def detach_device(
|
def detach_device(
|
||||||
@ -519,8 +532,48 @@ class Instance:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG
|
||||||
|
if isinstance(device, DiskConfig): # noqa: SIM102
|
||||||
|
if self._get_disk_by_target(device.target) is None:
|
||||||
|
log.warning(
|
||||||
|
"Volume with target '%s' is already detached",
|
||||||
|
device.target,
|
||||||
|
)
|
||||||
|
return
|
||||||
self.domain.detachDeviceFlags(device.to_xml(), flags=flags)
|
self.domain.detachDeviceFlags(device.to_xml(), flags=flags)
|
||||||
|
|
||||||
|
def detach_disk(self, name: str) -> None:
|
||||||
|
"""
|
||||||
|
Detach disk device by target name.
|
||||||
|
|
||||||
|
There is no ``attach_disk()`` method. Use :method:`attach_device`
|
||||||
|
with :class:`DiskConfig` as parameter.
|
||||||
|
|
||||||
|
:param name: Disk name e.g. 'vda', 'sda', etc. This name may
|
||||||
|
not match the name of the disk inside the guest OS.
|
||||||
|
"""
|
||||||
|
xml = self._get_disk_by_target(name)
|
||||||
|
if xml is None:
|
||||||
|
log.warning(
|
||||||
|
"Volume with target '%s' is already detached",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
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"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_volume(
|
def resize_volume(
|
||||||
self, name: str, capacity: int, unit: units.DataUnit
|
self, name: str, capacity: int, unit: units.DataUnit
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -573,7 +626,9 @@ class Instance:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def set_user_password(self, user: str, password: str) -> None:
|
def set_user_password(
|
||||||
|
self, user: str, password: str, *, encrypted: bool = False
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Set new user password in guest OS.
|
Set new user password in guest OS.
|
||||||
|
|
||||||
@ -581,8 +636,16 @@ class Instance:
|
|||||||
|
|
||||||
:param user: Username.
|
:param user: Username.
|
||||||
:param password: Password.
|
:param password: Password.
|
||||||
|
:param encrypted: Set it to True if password is already encrypted.
|
||||||
|
Right encryption method depends on guest OS.
|
||||||
"""
|
"""
|
||||||
self.domain.setUserPassword(user, password)
|
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'])
|
||||||
|
flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0
|
||||||
|
self.domain.setUserPassword(user, password, flags=flags)
|
||||||
|
|
||||||
def dump_xml(self, *, inactive: bool = False) -> str:
|
def dump_xml(self, *, inactive: bool = False) -> str:
|
||||||
"""Return instance XML description."""
|
"""Return instance XML description."""
|
||||||
|
@ -10,7 +10,11 @@ from uuid import uuid4
|
|||||||
import libvirt
|
import libvirt
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from .exceptions import InstanceNotFoundError, SessionError
|
from .exceptions import (
|
||||||
|
InstanceNotFoundError,
|
||||||
|
SessionError,
|
||||||
|
StoragePoolNotFoundError,
|
||||||
|
)
|
||||||
from .instance import Instance, InstanceConfig, InstanceSchema
|
from .instance import Instance, InstanceConfig, InstanceSchema
|
||||||
from .storage import DiskConfig, StoragePool, VolumeConfig
|
from .storage import DiskConfig, StoragePool, VolumeConfig
|
||||||
from .utils import units
|
from .utils import units
|
||||||
@ -23,10 +27,14 @@ class Capabilities(NamedTuple):
|
|||||||
"""Store domain capabilities info."""
|
"""Store domain capabilities info."""
|
||||||
|
|
||||||
arch: str
|
arch: str
|
||||||
virt: str
|
virt_type: str
|
||||||
emulator: str
|
emulator: str
|
||||||
machine: str
|
machine: str
|
||||||
max_vcpus: int
|
max_vcpus: int
|
||||||
|
cpu_vendor: str
|
||||||
|
cpu_model: str
|
||||||
|
cpu_features: dict
|
||||||
|
usable_cpus: list[dict]
|
||||||
|
|
||||||
|
|
||||||
class NodeInfo(NamedTuple):
|
class NodeInfo(NamedTuple):
|
||||||
@ -101,16 +109,47 @@ class Session(AbstractContextManager):
|
|||||||
threads=info[7],
|
threads=info[7],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _cap_get_usable_cpus(self, xml: etree.Element) -> list[dict]:
|
||||||
|
x = xml.xpath('/domainCapabilities/cpu/mode[@name="custom"]')[0]
|
||||||
|
cpus = []
|
||||||
|
for cpu in x.findall('model'):
|
||||||
|
if cpu.get('usable') == 'yes':
|
||||||
|
cpus.append( # noqa: PERF401
|
||||||
|
{
|
||||||
|
'vendor': cpu.get('vendor'),
|
||||||
|
'model': cpu.text,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return cpus
|
||||||
|
|
||||||
|
def _cap_get_cpu_features(self, xml: etree.Element) -> dict:
|
||||||
|
x = xml.xpath('/domainCapabilities/cpu/mode[@name="host-model"]')[0]
|
||||||
|
require = []
|
||||||
|
disable = []
|
||||||
|
for feature in x.findall('feature'):
|
||||||
|
policy = feature.get('policy')
|
||||||
|
name = feature.get('name')
|
||||||
|
if policy == 'require':
|
||||||
|
require.append(name)
|
||||||
|
if policy == 'disable':
|
||||||
|
disable.append(name)
|
||||||
|
return {'require': require, 'disable': disable}
|
||||||
|
|
||||||
def get_capabilities(self) -> Capabilities:
|
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'
|
||||||
|
hprefix = f'{prefix}/cpu/mode[@name="host-model"]'
|
||||||
caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320
|
caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320
|
||||||
return Capabilities(
|
return Capabilities(
|
||||||
arch=caps.xpath(f'{prefix}/arch/text()')[0],
|
arch=caps.xpath(f'{prefix}/arch/text()')[0],
|
||||||
virt=caps.xpath(f'{prefix}/domain/text()')[0],
|
virt_type=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]),
|
max_vcpus=int(caps.xpath(f'{prefix}/vcpu/@max')[0]),
|
||||||
|
cpu_vendor=caps.xpath(f'{hprefix}/vendor/text()')[0],
|
||||||
|
cpu_model=caps.xpath(f'{hprefix}/model/text()')[0],
|
||||||
|
cpu_features=self._cap_get_cpu_features(caps),
|
||||||
|
usable_cpus=self._cap_get_cpus(caps),
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_instance(self, **kwargs: Any) -> Instance:
|
def create_instance(self, **kwargs: Any) -> Instance:
|
||||||
@ -169,7 +208,7 @@ class Session(AbstractContextManager):
|
|||||||
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:
|
||||||
vol_name = f'{config.name}-{volume.target}-{uuid4()}.qcow2'
|
vol_name = f'{uuid4()}.qcow2'
|
||||||
else:
|
else:
|
||||||
vol_name = volume.source
|
vol_name = volume.source
|
||||||
vol_conf = VolumeConfig(
|
vol_conf = VolumeConfig(
|
||||||
@ -196,7 +235,12 @@ class Session(AbstractContextManager):
|
|||||||
volumes_pool.create_volume(vol_conf)
|
volumes_pool.create_volume(vol_conf)
|
||||||
log.info('Attaching volume to instance...')
|
log.info('Attaching volume to instance...')
|
||||||
instance.attach_device(
|
instance.attach_device(
|
||||||
DiskConfig(path=vol_conf.path, target=volume.target)
|
DiskConfig(
|
||||||
|
disk_type=volume.type,
|
||||||
|
source=vol_conf.path,
|
||||||
|
target=volume.target,
|
||||||
|
readonly=volume.is_readonly,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
@ -204,10 +248,10 @@ class Session(AbstractContextManager):
|
|||||||
"""Get compute instance by name."""
|
"""Get compute instance by name."""
|
||||||
try:
|
try:
|
||||||
return Instance(self.connection.lookupByName(name))
|
return Instance(self.connection.lookupByName(name))
|
||||||
except libvirt.libvirtError as err:
|
except libvirt.libvirtError as e:
|
||||||
if err.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
|
if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
|
||||||
raise InstanceNotFoundError(name) from err
|
raise InstanceNotFoundError(name) from e
|
||||||
raise SessionError(err) from err
|
raise SessionError(e) from e
|
||||||
|
|
||||||
def list_instances(self) -> list[Instance]:
|
def list_instances(self) -> list[Instance]:
|
||||||
"""List all instances."""
|
"""List all instances."""
|
||||||
@ -215,8 +259,12 @@ class Session(AbstractContextManager):
|
|||||||
|
|
||||||
def get_storage_pool(self, name: str) -> StoragePool:
|
def get_storage_pool(self, name: str) -> StoragePool:
|
||||||
"""Get storage pool by name."""
|
"""Get storage pool by name."""
|
||||||
# TODO @ge: handle Storage pool not found error
|
try:
|
||||||
return StoragePool(self.connection.storagePoolLookupByName(name))
|
return StoragePool(self.connection.storagePoolLookupByName(name))
|
||||||
|
except libvirt.libvirtError as e:
|
||||||
|
if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_POOL:
|
||||||
|
raise StoragePoolNotFoundError(name) from e
|
||||||
|
raise SessionError(e) from e
|
||||||
|
|
||||||
def list_storage_pools(self) -> list[StoragePool]:
|
def list_storage_pools(self) -> list[StoragePool]:
|
||||||
"""List all strage pools."""
|
"""List all strage pools."""
|
||||||
|
@ -7,7 +7,7 @@ from typing import NamedTuple
|
|||||||
import libvirt
|
import libvirt
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from compute.exceptions import StoragePoolError
|
from compute.exceptions import StoragePoolError, VolumeNotFoundError
|
||||||
|
|
||||||
from .volume import Volume, VolumeConfig
|
from .volume import Volume, VolumeConfig
|
||||||
|
|
||||||
@ -99,13 +99,8 @@ class StoragePool:
|
|||||||
vol = self.pool.storageVolLookupByName(name)
|
vol = self.pool.storageVolLookupByName(name)
|
||||||
return Volume(self.pool, vol)
|
return Volume(self.pool, vol)
|
||||||
except libvirt.libvirtError as e:
|
except libvirt.libvirtError as e:
|
||||||
# TODO @ge: Raise VolumeNotFoundError instead
|
if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL:
|
||||||
if (
|
raise VolumeNotFoundError(name) from e
|
||||||
e.get_error_domain() == libvirt.VIR_FROM_STORAGE
|
|
||||||
or e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL
|
|
||||||
):
|
|
||||||
log.exception(e.get_error_message())
|
|
||||||
return None
|
|
||||||
log.exception('unexpected error from libvirt')
|
log.exception('unexpected error from libvirt')
|
||||||
raise StoragePoolError(e) from e
|
raise StoragePoolError(e) from e
|
||||||
|
|
||||||
|
@ -56,15 +56,17 @@ class DiskConfig:
|
|||||||
to compute instances.
|
to compute instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
disk_type: str
|
||||||
|
source: str | Path
|
||||||
target: str
|
target: str
|
||||||
path: str
|
|
||||||
readonly: bool = False
|
readonly: bool = False
|
||||||
|
|
||||||
def to_xml(self) -> str:
|
def to_xml(self) -> str:
|
||||||
"""Return XML config for libvirt."""
|
"""Return XML config for libvirt."""
|
||||||
xml = E.disk(type='file', device='disk')
|
xml = E.disk(type=self.disk_type, device='disk')
|
||||||
xml.append(E.driver(name='qemu', type='qcow2', cache='writethrough'))
|
xml.append(E.driver(name='qemu', type='qcow2', cache='writethrough'))
|
||||||
xml.append(E.source(file=self.path))
|
if self.disk_type == 'file':
|
||||||
|
xml.append(E.source(file=str(self.source)))
|
||||||
xml.append(E.target(dev=self.target, bus='virtio'))
|
xml.append(E.target(dev=self.target, bus='virtio'))
|
||||||
if self.readonly:
|
if self.readonly:
|
||||||
xml.append(E.readonly())
|
xml.append(E.readonly())
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
Compute
|
Compute
|
||||||
=======
|
=======
|
||||||
|
|
||||||
Compute-instance management library.
|
Compute instances management library.
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
python-api/index
|
pyapi/index
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
------------------
|
------------------
|
||||||
|
227
dom.xml
Normal file
227
dom.xml
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
<domain type='kvm' id='1'>
|
||||||
|
<name>debian12</name>
|
||||||
|
<uuid>721cf06b-879a-4f4e-875b-b3d0382328ef</uuid>
|
||||||
|
<metadata>
|
||||||
|
<libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
|
||||||
|
<libosinfo:os id="http://debian.org/debian/12"/>
|
||||||
|
</libosinfo:libosinfo>
|
||||||
|
</metadata>
|
||||||
|
<memory unit='KiB'>4194304</memory>
|
||||||
|
<currentMemory unit='KiB'>4194304</currentMemory>
|
||||||
|
<vcpu placement='static'>4</vcpu>
|
||||||
|
<resource>
|
||||||
|
<partition>/machine</partition>
|
||||||
|
</resource>
|
||||||
|
<os>
|
||||||
|
<type arch='x86_64' machine='pc-q35-8.0'>hvm</type>
|
||||||
|
<boot dev='hd'/>
|
||||||
|
</os>
|
||||||
|
<features>
|
||||||
|
<acpi/>
|
||||||
|
<apic/>
|
||||||
|
<vmport state='off'/>
|
||||||
|
</features>
|
||||||
|
<cpu mode='host-passthrough' check='none' migratable='on'/>
|
||||||
|
<clock offset='utc'>
|
||||||
|
<timer name='rtc' tickpolicy='catchup'/>
|
||||||
|
<timer name='pit' tickpolicy='delay'/>
|
||||||
|
<timer name='hpet' present='no'/>
|
||||||
|
</clock>
|
||||||
|
<on_poweroff>destroy</on_poweroff>
|
||||||
|
<on_reboot>restart</on_reboot>
|
||||||
|
<on_crash>destroy</on_crash>
|
||||||
|
<pm>
|
||||||
|
<suspend-to-mem enabled='no'/>
|
||||||
|
<suspend-to-disk enabled='no'/>
|
||||||
|
</pm>
|
||||||
|
<devices>
|
||||||
|
<emulator>/usr/bin/qemu-system-x86_64</emulator>
|
||||||
|
<disk type='file' device='disk'>
|
||||||
|
<driver name='qemu' type='qcow2' discard='unmap'/>
|
||||||
|
<source file='/var/lib/libvirt/images/debian12.qcow2' index='1'/>
|
||||||
|
<backingStore/>
|
||||||
|
<target dev='vda' bus='virtio'/>
|
||||||
|
<alias name='virtio-disk0'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x04' slot='0x00' function='0x0'/>
|
||||||
|
</disk>
|
||||||
|
<controller type='usb' index='0' model='qemu-xhci' ports='15'>
|
||||||
|
<alias name='usb'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x02' slot='0x00' function='0x0'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='0' model='pcie-root'>
|
||||||
|
<alias name='pcie.0'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='1' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='1' port='0x10'/>
|
||||||
|
<alias name='pci.1'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0' multifunction='on'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='2' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='2' port='0x11'/>
|
||||||
|
<alias name='pci.2'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x1'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='3' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='3' port='0x12'/>
|
||||||
|
<alias name='pci.3'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x2'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='4' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='4' port='0x13'/>
|
||||||
|
<alias name='pci.4'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x3'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='5' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='5' port='0x14'/>
|
||||||
|
<alias name='pci.5'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x4'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='6' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='6' port='0x15'/>
|
||||||
|
<alias name='pci.6'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x5'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='7' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='7' port='0x16'/>
|
||||||
|
<alias name='pci.7'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x6'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='8' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='8' port='0x17'/>
|
||||||
|
<alias name='pci.8'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x7'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='9' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='9' port='0x18'/>
|
||||||
|
<alias name='pci.9'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0' multifunction='on'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='10' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='10' port='0x19'/>
|
||||||
|
<alias name='pci.10'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x1'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='11' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='11' port='0x1a'/>
|
||||||
|
<alias name='pci.11'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x2'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='12' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='12' port='0x1b'/>
|
||||||
|
<alias name='pci.12'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x3'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='13' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='13' port='0x1c'/>
|
||||||
|
<alias name='pci.13'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x4'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='pci' index='14' model='pcie-root-port'>
|
||||||
|
<model name='pcie-root-port'/>
|
||||||
|
<target chassis='14' port='0x1d'/>
|
||||||
|
<alias name='pci.14'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x5'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='sata' index='0'>
|
||||||
|
<alias name='ide'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x1f' function='0x2'/>
|
||||||
|
</controller>
|
||||||
|
<controller type='virtio-serial' index='0'>
|
||||||
|
<alias name='virtio-serial0'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x03' slot='0x00' function='0x0'/>
|
||||||
|
</controller>
|
||||||
|
<interface type='network'>
|
||||||
|
<mac address='52:54:00:9b:d7:98'/>
|
||||||
|
<source network='default' portid='98463b3d-9ab1-4d7f-9bf4-4ce3df078509' bridge='virbr0'/>
|
||||||
|
<target dev='vnet0'/>
|
||||||
|
<model type='virtio'/>
|
||||||
|
<alias name='net0'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
|
||||||
|
</interface>
|
||||||
|
<serial type='pty'>
|
||||||
|
<source path='/dev/pts/2'/>
|
||||||
|
<target type='isa-serial' port='0'>
|
||||||
|
<model name='isa-serial'/>
|
||||||
|
</target>
|
||||||
|
<alias name='serial0'/>
|
||||||
|
</serial>
|
||||||
|
<console type='pty' tty='/dev/pts/2'>
|
||||||
|
<source path='/dev/pts/2'/>
|
||||||
|
<target type='serial' port='0'/>
|
||||||
|
<alias name='serial0'/>
|
||||||
|
</console>
|
||||||
|
<channel type='unix'>
|
||||||
|
<source mode='bind' path='/run/libvirt/qemu/channel/1-debian12/org.qemu.guest_agent.0'/>
|
||||||
|
<target type='virtio' name='org.qemu.guest_agent.0' state='connected'/>
|
||||||
|
<alias name='channel0'/>
|
||||||
|
<address type='virtio-serial' controller='0' bus='0' port='1'/>
|
||||||
|
</channel>
|
||||||
|
<channel type='spicevmc'>
|
||||||
|
<target type='virtio' name='com.redhat.spice.0' state='disconnected'/>
|
||||||
|
<alias name='channel1'/>
|
||||||
|
<address type='virtio-serial' controller='0' bus='0' port='2'/>
|
||||||
|
</channel>
|
||||||
|
<input type='tablet' bus='usb'>
|
||||||
|
<alias name='input0'/>
|
||||||
|
<address type='usb' bus='0' port='1'/>
|
||||||
|
</input>
|
||||||
|
<input type='mouse' bus='ps2'>
|
||||||
|
<alias name='input1'/>
|
||||||
|
</input>
|
||||||
|
<input type='keyboard' bus='ps2'>
|
||||||
|
<alias name='input2'/>
|
||||||
|
</input>
|
||||||
|
<graphics type='spice' port='5900' autoport='yes' listen='127.0.0.1'>
|
||||||
|
<listen type='address' address='127.0.0.1'/>
|
||||||
|
<image compression='off'/>
|
||||||
|
</graphics>
|
||||||
|
<sound model='ich9'>
|
||||||
|
<alias name='sound0'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x1b' function='0x0'/>
|
||||||
|
</sound>
|
||||||
|
<audio id='1' type='spice'/>
|
||||||
|
<video>
|
||||||
|
<model type='virtio' heads='1' primary='yes'/>
|
||||||
|
<alias name='video0'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x0'/>
|
||||||
|
</video>
|
||||||
|
<redirdev bus='usb' type='spicevmc'>
|
||||||
|
<alias name='redir0'/>
|
||||||
|
<address type='usb' bus='0' port='2'/>
|
||||||
|
</redirdev>
|
||||||
|
<redirdev bus='usb' type='spicevmc'>
|
||||||
|
<alias name='redir1'/>
|
||||||
|
<address type='usb' bus='0' port='3'/>
|
||||||
|
</redirdev>
|
||||||
|
<watchdog model='itco' action='reset'>
|
||||||
|
<alias name='watchdog0'/>
|
||||||
|
</watchdog>
|
||||||
|
<memballoon model='virtio'>
|
||||||
|
<alias name='balloon0'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x05' slot='0x00' function='0x0'/>
|
||||||
|
</memballoon>
|
||||||
|
<rng model='virtio'>
|
||||||
|
<backend model='random'>/dev/urandom</backend>
|
||||||
|
<alias name='rng0'/>
|
||||||
|
<address type='pci' domain='0x0000' bus='0x06' slot='0x00' function='0x0'/>
|
||||||
|
</rng>
|
||||||
|
</devices>
|
||||||
|
<seclabel type='dynamic' model='dac' relabel='yes'>
|
||||||
|
<label>+961:+961</label>
|
||||||
|
<imagelabel>+961:+961</imagelabel>
|
||||||
|
</seclabel>
|
||||||
|
</domain>
|
||||||
|
|
14
packaging/completion.bash
Executable file
14
packaging/completion.bash
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
_cmp_get_domains()
|
||||||
|
{
|
||||||
|
for file in /etc/libvirt/qemu/*.xml; do
|
||||||
|
nodir="${file##*/}"
|
||||||
|
printf '%s\n' "${nodir//\.xml}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
_compute()
|
||||||
|
{
|
||||||
|
:
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -o filenames -F _compute compute
|
30
xmltool.py
Normal file
30
xmltool.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from compute.storage import DiskConfig
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
with Path('./dom.xml').open('r') as f:
|
||||||
|
xml = etree.fromstring(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_disk_by_target(name):
|
||||||
|
disk_tgt = xml.xpath('/domain/devices/disk/target[@dev="vda"]')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_disks(xml: etree.Element) -> list[etree.Element]:
|
||||||
|
xmldisks = xml.findall('devices/disk')
|
||||||
|
for xmldisk in xmldisks:
|
||||||
|
disk_config = DiskConfig(
|
||||||
|
type=xmldisk.get('type'),
|
||||||
|
device=xmldisk.get('device'),
|
||||||
|
target=xmldisk.find('target').get('dev'),
|
||||||
|
path=xmldisk.find('source').get('file'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user