various improvements
This commit is contained in:
1 parent
0d2a18d1f3
commit
bd4e575a7c
23 files changed
+453
-37
No files matched your search
@@ -20,13 +20,13 @@ Run `make serve-docs`. See [Development](#development) below.
|
||||
- [x] CPU customization (emulation mode, model, vendor, features)
|
||||
- [ ] BIOS/UEFI settings
|
||||
- [x] Device attaching
|
||||
- [ ] Device detaching
|
||||
- [x] Device detaching
|
||||
- [ ] GPU passthrough
|
||||
- [ ] CPU guarantied resource percent support
|
||||
- [x] QEMU Guest Agent management
|
||||
- [ ] Instance resources usage stats
|
||||
- [ ] SSH-keys management
|
||||
- [x] Setting user passwords in guest [not tested]
|
||||
- [x] Setting user passwords in guest
|
||||
- [x] QCOW2 disks support
|
||||
- [ ] ZVOL support
|
||||
- [ ] Network disks support
|
||||
|
||||
+25
-5
@@ -234,13 +234,20 @@ def main(session: Session, args: argparse.Namespace) -> None:
|
||||
case 'setmem':
|
||||
instance = session.get_instance(args.instance)
|
||||
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
|
||||
"""Parse command line arguments."""
|
||||
"""Return command line arguments parser."""
|
||||
root = argparse.ArgumentParser(
|
||||
prog='compute',
|
||||
description='manage compute instances and storage volumes.',
|
||||
description='manage compute instances',
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
root.add_argument(
|
||||
@@ -383,13 +390,27 @@ def cli() -> None: # noqa: PLR0915
|
||||
setmem.add_argument('instance')
|
||||
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()
|
||||
if args.command is None:
|
||||
root.print_help()
|
||||
sys.exit()
|
||||
|
||||
# Set logging level
|
||||
log_level = args.log_level or os.getenv('CMP_LOG')
|
||||
|
||||
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)
|
||||
# Perform actions
|
||||
try:
|
||||
with Session(args.connect) as session:
|
||||
main(session, args)
|
||||
|
||||
@@ -37,6 +37,22 @@ class StoragePoolError(ComputeServiceError):
|
||||
"""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):
|
||||
"""Something went wrong while interacting with the domain."""
|
||||
|
||||
|
||||
@@ -78,8 +78,8 @@ class GuestAgent:
|
||||
except GuestAgentError:
|
||||
return False
|
||||
|
||||
def available_commands(self) -> set[str]:
|
||||
"""Return set of available guest agent commands."""
|
||||
def get_supported_commands(self) -> set[str]:
|
||||
"""Return set of supported guest agent commands."""
|
||||
output = self.execute({'execute': 'guest-info', 'arguments': {}})
|
||||
return {
|
||||
cmd['name']
|
||||
@@ -94,8 +94,9 @@ class GuestAgent:
|
||||
:param commands: List of required commands
|
||||
:raise: GuestAgentCommandNotSupportedError
|
||||
"""
|
||||
supported = self.get_supported_commands()
|
||||
for command in commands:
|
||||
if command not in self.available_commands():
|
||||
if command not in supported:
|
||||
raise GuestAgentCommandNotSupportedError(command)
|
||||
|
||||
def guest_exec( # noqa: PLR0913
|
||||
|
||||
@@ -13,6 +13,7 @@ from compute.exceptions import (
|
||||
GuestAgentCommandNotSupportedError,
|
||||
InstanceError,
|
||||
)
|
||||
from compute.storage import DiskConfig
|
||||
from compute.utils import units
|
||||
|
||||
from .guest_agent import GuestAgent
|
||||
@@ -181,7 +182,7 @@ class InstanceInfo(NamedTuple):
|
||||
|
||||
|
||||
class DeviceConfig:
|
||||
"""Abstract device description class."""
|
||||
"""Abstract device config class."""
|
||||
|
||||
|
||||
class Instance:
|
||||
@@ -485,6 +486,11 @@ class Instance:
|
||||
msg = f'Cannot set memory for instance={self.name} {memory=}: {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(
|
||||
self, device: 'DeviceConfig', *, live: bool = False
|
||||
) -> None:
|
||||
@@ -501,6 +507,13 @@ class Instance:
|
||||
)
|
||||
else:
|
||||
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)
|
||||
|
||||
def detach_device(
|
||||
@@ -519,8 +532,48 @@ class Instance:
|
||||
)
|
||||
else:
|
||||
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)
|
||||
|
||||
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(
|
||||
self, name: str, capacity: int, unit: units.DataUnit
|
||||
) -> None:
|
||||
@@ -573,7 +626,9 @@ class Instance:
|
||||
"""
|
||||
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.
|
||||
|
||||
@@ -581,8 +636,16 @@ class Instance:
|
||||
|
||||
:param user: Username.
|
||||
: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:
|
||||
"""Return instance XML description."""
|
||||
|
||||
+59
-11
@@ -10,7 +10,11 @@ from uuid import uuid4
|
||||
import libvirt
|
||||
from lxml import etree
|
||||
|
||||
from .exceptions import InstanceNotFoundError, SessionError
|
||||
from .exceptions import (
|
||||
InstanceNotFoundError,
|
||||
SessionError,
|
||||
StoragePoolNotFoundError,
|
||||
)
|
||||
from .instance import Instance, InstanceConfig, InstanceSchema
|
||||
from .storage import DiskConfig, StoragePool, VolumeConfig
|
||||
from .utils import units
|
||||
@@ -23,10 +27,14 @@ class Capabilities(NamedTuple):
|
||||
"""Store domain capabilities info."""
|
||||
|
||||
arch: str
|
||||
virt: str
|
||||
virt_type: str
|
||||
emulator: str
|
||||
machine: str
|
||||
max_vcpus: int
|
||||
cpu_vendor: str
|
||||
cpu_model: str
|
||||
cpu_features: dict
|
||||
usable_cpus: list[dict]
|
||||
|
||||
|
||||
class NodeInfo(NamedTuple):
|
||||
@@ -101,16 +109,47 @@ class Session(AbstractContextManager):
|
||||
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:
|
||||
"""Return capabilities e.g. arch, virt, emulator, etc."""
|
||||
prefix = '/domainCapabilities'
|
||||
hprefix = f'{prefix}/cpu/mode[@name="host-model"]'
|
||||
caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320
|
||||
return Capabilities(
|
||||
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],
|
||||
machine=caps.xpath(f'{prefix}/machine/text()')[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:
|
||||
@@ -169,7 +208,7 @@ class Session(AbstractContextManager):
|
||||
volumes_pool = self.get_storage_pool(self.VOLUMES_POOL)
|
||||
log.info('Building volume configuration...')
|
||||
if not volume.source:
|
||||
vol_name = f'{config.name}-{volume.target}-{uuid4()}.qcow2'
|
||||
vol_name = f'{uuid4()}.qcow2'
|
||||
else:
|
||||
vol_name = volume.source
|
||||
vol_conf = VolumeConfig(
|
||||
@@ -196,7 +235,12 @@ class Session(AbstractContextManager):
|
||||
volumes_pool.create_volume(vol_conf)
|
||||
log.info('Attaching volume to instance...')
|
||||
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
|
||||
|
||||
@@ -204,10 +248,10 @@ class Session(AbstractContextManager):
|
||||
"""Get compute instance by name."""
|
||||
try:
|
||||
return Instance(self.connection.lookupByName(name))
|
||||
except libvirt.libvirtError as err:
|
||||
if err.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
|
||||
raise InstanceNotFoundError(name) from err
|
||||
raise SessionError(err) from err
|
||||
except libvirt.libvirtError as e:
|
||||
if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
|
||||
raise InstanceNotFoundError(name) from e
|
||||
raise SessionError(e) from e
|
||||
|
||||
def list_instances(self) -> list[Instance]:
|
||||
"""List all instances."""
|
||||
@@ -215,8 +259,12 @@ class Session(AbstractContextManager):
|
||||
|
||||
def get_storage_pool(self, name: str) -> StoragePool:
|
||||
"""Get storage pool by name."""
|
||||
# TODO @ge: handle Storage pool not found error
|
||||
return StoragePool(self.connection.storagePoolLookupByName(name))
|
||||
try:
|
||||
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]:
|
||||
"""List all strage pools."""
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import NamedTuple
|
||||
import libvirt
|
||||
from lxml import etree
|
||||
|
||||
from compute.exceptions import StoragePoolError
|
||||
from compute.exceptions import StoragePoolError, VolumeNotFoundError
|
||||
|
||||
from .volume import Volume, VolumeConfig
|
||||
|
||||
@@ -99,13 +99,8 @@ class StoragePool:
|
||||
vol = self.pool.storageVolLookupByName(name)
|
||||
return Volume(self.pool, vol)
|
||||
except libvirt.libvirtError as e:
|
||||
# TODO @ge: Raise VolumeNotFoundError instead
|
||||
if (
|
||||
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
|
||||
if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL:
|
||||
raise VolumeNotFoundError(name) from e
|
||||
log.exception('unexpected error from libvirt')
|
||||
raise StoragePoolError(e) from e
|
||||
|
||||
|
||||
@@ -56,15 +56,17 @@ class DiskConfig:
|
||||
to compute instances.
|
||||
"""
|
||||
|
||||
disk_type: str
|
||||
source: str | Path
|
||||
target: str
|
||||
path: str
|
||||
readonly: bool = False
|
||||
|
||||
def to_xml(self) -> str:
|
||||
"""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.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'))
|
||||
if self.readonly:
|
||||
xml.append(E.readonly())
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Compute
|
||||
=======
|
||||
|
||||
Compute-instance management library.
|
||||
Compute instances management library.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
python-api/index
|
||||
pyapi/index
|
||||
|
||||
Indices and tables
|
||||
------------------
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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>
|
||||
|
||||
Executable
+14
@@ -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
@@ -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'),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user