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