various improvements
This commit is contained in:
2
compute/storage/__init__.py
Normal file
2
compute/storage/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .pool import StoragePool
|
||||
from .volume import DiskConfig, Volume, VolumeConfig
|
114
compute/storage/pool.py
Normal file
114
compute/storage/pool.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""Manage storage pools."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
import libvirt
|
||||
from lxml import etree
|
||||
|
||||
from compute.exceptions import StoragePoolError
|
||||
|
||||
from .volume import Volume, VolumeConfig
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StoragePoolUsage(NamedTuple):
|
||||
"""Storage pool usage info schema."""
|
||||
|
||||
capacity: int
|
||||
allocation: int
|
||||
available: int
|
||||
|
||||
|
||||
class StoragePool:
|
||||
"""Storage pool manipulating class."""
|
||||
|
||||
def __init__(self, pool: libvirt.virStoragePool):
|
||||
"""Initislise StoragePool."""
|
||||
self.pool = pool
|
||||
self.name = pool.name()
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
"""Return storage pool path."""
|
||||
xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320
|
||||
return Path(xml.xpath('/pool/target/path/text()')[0])
|
||||
|
||||
def usage(self) -> StoragePoolUsage:
|
||||
"""Return info about storage pool usage."""
|
||||
xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320
|
||||
return StoragePoolUsage(
|
||||
capacity=int(xml.xpath('/pool/capacity/text()')[0]),
|
||||
allocation=int(xml.xpath('/pool/allocation/text()')[0]),
|
||||
available=int(xml.xpath('/pool/available/text()')[0]),
|
||||
)
|
||||
|
||||
def dump_xml(self) -> str:
|
||||
"""Return storage pool XML description as string."""
|
||||
return self.pool.XMLDesc()
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Refresh storage pool."""
|
||||
# TODO @ge: handle libvirt asynchronous job related exceptions
|
||||
self.pool.refresh()
|
||||
|
||||
def create_volume(self, vol_conf: VolumeConfig) -> Volume:
|
||||
"""Create storage volume and return Volume instance."""
|
||||
log.info(
|
||||
'Create storage volume vol=%s in pool=%s', vol_conf.name, self.pool
|
||||
)
|
||||
vol = self.pool.createXML(
|
||||
vol_conf.to_xml(),
|
||||
flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA,
|
||||
)
|
||||
return Volume(self.pool, vol)
|
||||
|
||||
def clone_volume(self, src: Volume, dst: VolumeConfig) -> Volume:
|
||||
"""
|
||||
Make storage volume copy.
|
||||
|
||||
:param src: Input volume
|
||||
:param dst: Output volume config
|
||||
"""
|
||||
log.info(
|
||||
'Start volume cloning '
|
||||
'src_pool=%s src_vol=%s dst_pool=%s dst_vol=%s',
|
||||
src.pool_name,
|
||||
src.name,
|
||||
self.pool.name,
|
||||
dst.name,
|
||||
)
|
||||
vol = self.pool.createXMLFrom(
|
||||
dst.to_xml(), # new volume XML description
|
||||
src.vol, # source volume virStorageVol object
|
||||
flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA,
|
||||
)
|
||||
if vol is None:
|
||||
raise StoragePoolError
|
||||
return Volume(self.pool, vol)
|
||||
|
||||
def get_volume(self, name: str) -> Volume | None:
|
||||
"""Lookup and return Volume instance or None."""
|
||||
log.info(
|
||||
'Lookup for storage volume vol=%s in pool=%s', name, self.pool.name
|
||||
)
|
||||
try:
|
||||
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
|
||||
log.exception('unexpected error from libvirt')
|
||||
raise StoragePoolError(e) from e
|
||||
|
||||
def list_volumes(self) -> list[Volume]:
|
||||
"""Return list of volumes in storage pool."""
|
||||
return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()]
|
124
compute/storage/volume.py
Normal file
124
compute/storage/volume.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""Manage storage volumes."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
|
||||
import libvirt
|
||||
from lxml import etree
|
||||
from lxml.builder import E
|
||||
|
||||
from compute.utils import units
|
||||
|
||||
|
||||
@dataclass
|
||||
class VolumeConfig:
|
||||
"""
|
||||
Storage volume config builder.
|
||||
|
||||
Generate XML config for creating a volume in a libvirt
|
||||
storage pool.
|
||||
"""
|
||||
|
||||
name: str
|
||||
path: str
|
||||
capacity: int
|
||||
|
||||
def to_xml(self) -> str:
|
||||
"""Return XML config for libvirt."""
|
||||
unixtime = str(int(time()))
|
||||
xml = E.volume(type='file')
|
||||
xml.append(E.name(self.name))
|
||||
xml.append(E.key(self.path))
|
||||
xml.append(E.source())
|
||||
xml.append(E.capacity(str(self.capacity), unit='bytes'))
|
||||
xml.append(E.allocation('0'))
|
||||
xml.append(
|
||||
E.target(
|
||||
E.path(self.path),
|
||||
E.format(type='qcow2'),
|
||||
E.timestamps(
|
||||
E.atime(unixtime), E.mtime(unixtime), E.ctime(unixtime)
|
||||
),
|
||||
E.compat('1.1'),
|
||||
E.features(E.lazy_refcounts()),
|
||||
)
|
||||
)
|
||||
return etree.tostring(xml, encoding='unicode', pretty_print=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiskConfig:
|
||||
"""
|
||||
Disk config builder.
|
||||
|
||||
Generate XML config for attaching or detaching storage volumes
|
||||
to compute instances.
|
||||
"""
|
||||
|
||||
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.append(E.driver(name='qemu', type='qcow2', cache='writethrough'))
|
||||
xml.append(E.source(file=self.path))
|
||||
xml.append(E.target(dev=self.target, bus='virtio'))
|
||||
if self.readonly:
|
||||
xml.append(E.readonly())
|
||||
return etree.tostring(xml, encoding='unicode', pretty_print=True)
|
||||
|
||||
|
||||
class Volume:
|
||||
"""Storage volume manipulating class."""
|
||||
|
||||
def __init__(
|
||||
self, pool: libvirt.virStoragePool, vol: libvirt.virStorageVol
|
||||
):
|
||||
"""
|
||||
Initialise Volume.
|
||||
|
||||
:param pool: libvirt virStoragePool object
|
||||
:param vol: libvirt virStorageVol object
|
||||
"""
|
||||
self.pool = pool
|
||||
self.pool_name = pool.name()
|
||||
self.vol = vol
|
||||
self.name = vol.name()
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
"""Return path to volume."""
|
||||
return Path(self.vol.path())
|
||||
|
||||
def dump_xml(self) -> str:
|
||||
"""Return volume XML description as string."""
|
||||
return self.vol.XMLDesc()
|
||||
|
||||
def clone(self, vol_conf: VolumeConfig) -> None:
|
||||
"""
|
||||
Make a copy of volume to the same storage pool.
|
||||
|
||||
:param vol_info VolumeInfo: New storage volume dataclass object
|
||||
"""
|
||||
self.pool.createXMLFrom(
|
||||
vol_conf.to_xml(),
|
||||
self.vol,
|
||||
flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA,
|
||||
)
|
||||
|
||||
def resize(self, capacity: int, unit: units.DataUnit) -> None:
|
||||
"""
|
||||
Resize volume.
|
||||
|
||||
:param capacity int: Volume new capacity.
|
||||
:param unit DataUnit: Data unit. Internally converts into bytes.
|
||||
"""
|
||||
# TODO @ge: Check actual volume size before resize
|
||||
self.vol.resize(units.to_bytes(capacity, unit=unit))
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete volume from storage pool."""
|
||||
self.vol.delete()
|
Reference in New Issue
Block a user