various improvements

This commit is contained in:
ge 2023-11-11 02:28:46 +03:00
parent 0d2a18d1f3
commit bd4e575a7c
23 changed files with 453 additions and 37 deletions

View File

@ -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

View File

@ -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)

View File

@ -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."""

View File

@ -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

View File

@ -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."""

View File

@ -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."""

View File

@ -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

View File

@ -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())

View File

@ -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
View 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
View 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
View 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'),
)