python-compute/compute/session.py

348 lines
12 KiB
Python
Raw Permalink Normal View History

2023-11-23 02:34:02 +03:00
# This file is part of Compute
#
# Compute is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
2023-12-03 23:25:34 +03:00
# Compute is distributed in the hope that it will be useful,
2023-11-23 02:34:02 +03:00
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
2023-12-03 23:25:34 +03:00
# along with Compute. If not, see <http://www.gnu.org/licenses/>.
2023-11-23 02:34:02 +03:00
2023-11-06 12:52:19 +03:00
"""Hypervisor session manager."""
import logging
from contextlib import AbstractContextManager
from pathlib import Path
2023-11-06 12:52:19 +03:00
from types import TracebackType
from typing import Any, NamedTuple
from uuid import uuid4
import libvirt
from lxml import etree
2023-12-13 01:42:50 +03:00
from .config import Config
2023-11-11 02:28:46 +03:00
from .exceptions import (
InstanceNotFoundError,
SessionError,
StoragePoolNotFoundError,
)
2023-11-06 12:52:19 +03:00
from .instance import Instance, InstanceConfig, InstanceSchema
2023-12-13 01:42:50 +03:00
from .instance.cloud_init import CloudInit
2023-12-03 23:25:34 +03:00
from .instance.devices import DiskConfig, DiskDriver
from .storage import StoragePool, VolumeConfig
2023-12-13 01:42:50 +03:00
from .utils import diskutils, units
2023-11-06 12:52:19 +03:00
log = logging.getLogger(__name__)
2023-12-13 01:42:50 +03:00
config = Config()
2023-11-06 12:52:19 +03:00
class Capabilities(NamedTuple):
"""Store domain capabilities info."""
arch: str
2023-11-11 02:28:46 +03:00
virt_type: str
2023-11-06 12:52:19 +03:00
emulator: str
machine: str
2023-11-09 01:17:50 +03:00
max_vcpus: int
2023-11-11 02:28:46 +03:00
cpu_vendor: str
cpu_model: str
cpu_features: dict
usable_cpus: list[dict]
2023-11-09 01:17:50 +03:00
class NodeInfo(NamedTuple):
"""
Store compute node info.
See https://libvirt.org/html/libvirt-libvirt-host.html#virNodeInfo
NOTE: memory unit in libvirt docs is wrong! Actual unit is MiB.
"""
arch: str
memory: int
cpus: int
mhz: int
nodes: int
sockets: int
cores: int
threads: int
2023-11-06 12:52:19 +03:00
class Session(AbstractContextManager):
2023-12-13 01:42:50 +03:00
"""Hypervisor session context manager."""
2023-11-06 12:52:19 +03:00
def __init__(self, uri: str | None = None):
"""
Initialise session with hypervisor.
:param uri: libvirt connection URI.
"""
2023-12-13 01:42:50 +03:00
log.debug('Config=%s', config)
self.LIBVIRT_URI = config['libvirt']['uri']
self.IMAGES_POOL = config['storage']['images']
self.VOLUMES_POOL = config['storage']['volumes']
self._uri = uri or self.LIBVIRT_URI
self._connection = libvirt.open(self._uri)
2023-11-06 12:52:19 +03:00
def __enter__(self):
"""Return Session object."""
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
exc_traceback: TracebackType | None,
):
"""Close the connection when leaving the context."""
self.close()
2023-12-13 01:42:50 +03:00
@property
def uri(self) -> str:
"""Libvirt connection URI."""
return self._uri
@property
def connection(self) -> libvirt.virConnect:
"""Libvirt connection object."""
return self._connection
2023-11-06 12:52:19 +03:00
def close(self) -> None:
"""Close connection to libvirt daemon."""
self.connection.close()
2023-11-09 01:17:50 +03:00
def get_node_info(self) -> NodeInfo:
"""Return information about compute node."""
info = self.connection.getInfo()
return NodeInfo(
arch=info[0],
memory=info[1],
cpus=info[2],
mhz=info[3],
nodes=info[4],
sockets=info[5],
cores=info[6],
threads=info[7],
)
2023-11-11 02:28:46 +03:00
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}
2023-11-09 01:17:50 +03:00
def get_capabilities(self) -> Capabilities:
2023-11-06 12:52:19 +03:00
"""Return capabilities e.g. arch, virt, emulator, etc."""
prefix = '/domainCapabilities'
2023-11-11 02:28:46 +03:00
hprefix = f'{prefix}/cpu/mode[@name="host-model"]'
2023-12-01 01:39:26 +03:00
caps = etree.fromstring(self.connection.getDomainCapabilities())
2023-11-06 12:52:19 +03:00
return Capabilities(
arch=caps.xpath(f'{prefix}/arch/text()')[0],
2023-11-11 02:28:46 +03:00
virt_type=caps.xpath(f'{prefix}/domain/text()')[0],
2023-11-06 12:52:19 +03:00
emulator=caps.xpath(f'{prefix}/path/text()')[0],
machine=caps.xpath(f'{prefix}/machine/text()')[0],
2023-11-09 01:17:50 +03:00
max_vcpus=int(caps.xpath(f'{prefix}/vcpu/@max')[0]),
2023-11-11 02:28:46 +03:00
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),
2023-12-03 23:25:34 +03:00
usable_cpus=self._cap_get_usable_cpus(caps),
2023-11-06 12:52:19 +03:00
)
def create_instance(self, **kwargs: Any) -> Instance:
"""
Create and return new compute instance.
2023-11-06 17:47:56 +03:00
:param name: Instance name.
:type name: str
:param title: Instance title for humans.
:type title: str
2023-11-09 01:17:50 +03:00
:param description: Some information about instance.
2023-11-06 17:47:56 +03:00
:type description: str
:param memory: Memory in MiB.
:type memory: int
:param max_memory: Maximum memory in MiB.
:type max_memory: int
2023-11-09 01:17:50 +03:00
:param vcpus: Number of vCPUs.
:type vcpus: int
:param max_vcpus: Maximum vCPUs.
:type max_vcpus: int
:param cpu: CPU configuration. See :class:`CPUSchema` for info.
:type cpu: dict
:param machine: QEMU emulated machine.
:type machine: str
:param emulator: Path to emulator.
:type emulator: str
:param arch: CPU architecture to virtualization.
:type arch: str
:param boot: Boot settings. See :class:`BootOptionsSchema`.
:type boot: dict
:param image: Source disk image name for system disk.
:type image: str
:param volumes: List of storage volume configs. For more info
see :class:`VolumeSchema`.
:type volumes: list[dict]
2024-01-13 00:45:30 +03:00
:param network: List of virtual network interfaces configs.
See :class:`NetworkSchema` for more info.
2023-11-09 01:17:50 +03:00
:type network_interfaces: list[dict]
2023-12-13 21:02:09 +03:00
:param cloud_init: Cloud-init configuration. See
:class:`CloudInitSchema` for info.
:type cloud_init: dict
2023-11-06 12:52:19 +03:00
"""
data = InstanceSchema(**kwargs)
config = InstanceConfig(data)
2023-12-13 01:42:50 +03:00
log.info('Define instance XML')
log.debug(config.to_xml())
2023-12-03 23:25:34 +03:00
try:
self.connection.defineXML(config.to_xml())
except libvirt.libvirtError as e:
raise SessionError(f'Error defining instance: {e}') from e
2023-12-13 01:42:50 +03:00
log.info('Getting instance object...')
2023-11-06 12:52:19 +03:00
instance = self.get_instance(config.name)
2023-12-03 23:25:34 +03:00
log.info('Start processing volumes...')
2023-12-13 01:42:50 +03:00
log.info('Connecting to images pool...')
images_pool = self.get_storage_pool(self.IMAGES_POOL)
images_pool.refresh()
log.info('Connecting to volumes pool...')
volumes_pool = self.get_storage_pool(self.VOLUMES_POOL)
volumes_pool.refresh()
disk_targets = []
2023-11-06 12:52:19 +03:00
for volume in data.volumes:
2023-12-03 23:25:34 +03:00
log.info('Processing volume=%s', volume)
2023-11-06 12:52:19 +03:00
log.info('Building volume configuration...')
2023-12-13 01:42:50 +03:00
capacity = None
disk_targets.append(volume.target)
2023-11-09 01:17:50 +03:00
if not volume.source:
2023-12-13 01:42:50 +03:00
volume_name = f'{uuid4()}.qcow2'
2023-11-09 01:17:50 +03:00
else:
2023-12-13 01:42:50 +03:00
volume_name = volume.source
2023-12-03 23:25:34 +03:00
if volume.device == 'cdrom':
2023-12-13 01:42:50 +03:00
log.info('Volume %s is CDROM device', volume_name)
elif volume.source is not None:
log.info('Using volume %s as source', volume_name)
if volume.capacity:
capacity = units.to_bytes(
volume.capacity.value, volume.capacity.unit
)
log.info('Getting volume %s', volume.source)
vol = volumes_pool.get_volume(Path(volume_name).name)
log.info(
'Resize volume to specified size: %s',
capacity,
)
vol.resize(capacity, unit=units.DataUnit.BYTES)
2023-12-03 23:25:34 +03:00
else:
capacity = units.to_bytes(
volume.capacity.value, volume.capacity.unit
2023-11-06 12:52:19 +03:00
)
2023-12-13 01:42:50 +03:00
volume_config = VolumeConfig(
name=volume_name,
path=str(volumes_pool.path.joinpath(volume_name)),
2023-12-03 23:25:34 +03:00
capacity=capacity,
2023-11-06 12:52:19 +03:00
)
volume.source = volume_config.path
2023-12-13 01:42:50 +03:00
log.debug('Volume config: %s', volume_config)
2023-12-03 23:25:34 +03:00
if volume.is_system is True and data.image:
log.info(
"Volume is marked as 'system', start cloning image..."
)
log.info('Get image %s', data.image)
image = images_pool.get_volume(data.image)
log.info('Cloning image into volumes pool...')
2023-12-13 01:42:50 +03:00
vol = volumes_pool.clone_volume(image, volume_config)
log.info(
'Resize cloned volume to specified size: %s',
capacity,
)
vol.resize(capacity, unit=units.DataUnit.BYTES)
2023-12-03 23:25:34 +03:00
else:
2023-12-13 01:42:50 +03:00
log.info('Create volume %s', volume_config.name)
volumes_pool.create_volume(volume_config)
2023-11-06 12:52:19 +03:00
log.info('Attaching volume to instance...')
instance.attach_device(
2023-11-11 02:28:46 +03:00
DiskConfig(
2023-12-03 23:25:34 +03:00
type=volume.type,
device=volume.device,
source=volume.source,
2023-11-11 02:28:46 +03:00
target=volume.target,
2023-12-03 23:25:34 +03:00
is_readonly=volume.is_readonly,
bus=volume.bus,
driver=DiskDriver(
volume.driver.name,
volume.driver.type,
volume.driver.cache,
),
2023-11-11 02:28:46 +03:00
)
2023-11-06 12:52:19 +03:00
)
2023-12-13 01:42:50 +03:00
if data.cloud_init:
log.info('Crating disk for cloud-init...')
cloud_init = CloudInit()
cloud_init.user_data = data.cloud_init.user_data
cloud_init.vendor_data = data.cloud_init.vendor_data
cloud_init.network_config = data.cloud_init.network_config
cloud_init.meta_data = data.cloud_init.meta_data
cloud_init_disk_path = volumes_pool.path.joinpath(
f'{instance.name}-cloud-init.img'
)
cloud_init.create_disk(cloud_init_disk_path)
log.info('Attaching cloud-init disk to instance...')
volumes_pool.refresh()
cloud_init.attach_disk(
cloud_init_disk_path,
diskutils.get_disk_target(disk_targets, prefix='vd'),
instance,
)
2023-11-06 12:52:19 +03:00
return instance
def get_instance(self, name: str) -> Instance:
"""Get compute instance by name."""
try:
return Instance(self.connection.lookupByName(name))
2023-11-11 02:28:46 +03:00
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
2023-11-06 12:52:19 +03:00
def list_instances(self) -> list[Instance]:
"""List all instances."""
return [Instance(dom) for dom in self.connection.listAllDomains()]
def get_storage_pool(self, name: str) -> StoragePool:
"""Get storage pool by name."""
2023-11-11 02:28:46 +03:00
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
2023-11-06 12:52:19 +03:00
def list_storage_pools(self) -> list[StoragePool]:
"""List all strage pools."""
return [StoragePool(p) for p in self.connection.listStoragePools()]