diff --git a/virttest/vt_agent/managers/__init__.py b/virttest/vt_agent/managers/__init__.py new file mode 100644 index 0000000000..4c8a593d86 --- /dev/null +++ b/virttest/vt_agent/managers/__init__.py @@ -0,0 +1,2 @@ +from .image_mgr import imgr +from .resbacking_mgr import backing_mgr diff --git a/virttest/vt_agent/managers/image_mgr.py b/virttest/vt_agent/managers/image_mgr.py new file mode 100644 index 0000000000..3fc21cae2e --- /dev/null +++ b/virttest/vt_agent/managers/image_mgr.py @@ -0,0 +1,16 @@ +from .images import get_image_handler + + +class _ImageManager(object): + + def __init__(self): + self._images = dict() + + def update_image(self, image_config, update_cmd_config): + cmd, argument = list(update_cmd_config.items())[0] + image_type = image_config["meta"]["type"] + handler = get_image_handler(image_type, cmd) + handler.execute(image_config, argument) + + +imgr = _ImageManager() diff --git a/virttest/vt_agent/managers/images/__init__.py b/virttest/vt_agent/managers/images/__init__.py new file mode 100644 index 0000000000..f84b23b21d --- /dev/null +++ b/virttest/vt_agent/managers/images/__init__.py @@ -0,0 +1,21 @@ +from .qemu.qemu_image_handlers import QEMU_IMAGE_TYPE, QemuImgCreate, QemuImgDestroy +#from .xen.xen_image_handlers import XEN_IMAGE_TYPE, XenImgCreate, XenImgDestroy + + +_image_handlers = dict() + +_image_handlers[QEMU_IMAGE_TYPE] = dict() +_image_handlers[QEMU_IMAGE_TYPE][QemuImgCreate.cmd] = QemuImgCreate +_image_handlers[QEMU_IMAGE_TYPE][QemuImgDestroy.cmd] = QemuImgDestroy + +#_image_handlers[XEN_IMAGE_TYPE] = dict() +#_image_handlers[XEN_IMAGE_TYPE][XenImgCreate.cmd] = XenImgCreate +#_image_handlers[XEN_IMAGE_TYPE][XenImgDestroy.cmd] = XenImgDestroy + + +def get_image_handler(image_type, cmd): + handlers = _image_handlers.get(image_type, dict()) + return handlers.get(cmd) + + +__all__ = ["get_image_handler"] diff --git a/virttest/vt_agent/managers/images/image_handlers.py b/virttest/vt_agent/managers/images/image_handlers.py new file mode 100644 index 0000000000..8ae95f81ef --- /dev/null +++ b/virttest/vt_agent/managers/images/image_handlers.py @@ -0,0 +1,14 @@ +from abc import ABC, abstractmethod + + +class _ImageHandler(ABC): + _COMMAND = None + + @staticmethod + @abstractmethod + def execute(image_config, arguments): + raise NotImplemented + + @classmethod + def command(cls): + return cls._COMMAND diff --git a/virttest/vt_agent/managers/images/qemu/__init__.py b/virttest/vt_agent/managers/images/qemu/__init__.py new file mode 100644 index 0000000000..651ea32f42 --- /dev/null +++ b/virttest/vt_agent/managers/images/qemu/__init__.py @@ -0,0 +1 @@ +import qemu_image_handlers diff --git a/virttest/vt_agent/managers/images/qemu/qemu_image_handlers.py b/virttest/vt_agent/managers/images/qemu/qemu_image_handlers.py new file mode 100644 index 0000000000..3065e72baf --- /dev/null +++ b/virttest/vt_agent/managers/images/qemu/qemu_image_handlers.py @@ -0,0 +1,65 @@ +from ..image_handlers import _ImageHandler + + +QEMU_IMAGE_TYPE = "qemu" + + +class QemuImgCreate(_ImageHandler): + _COMMAND = "create" + create_cmd = ( + "create {secret_object} {tls_creds_object} {image_format} " + "{backing_file} {backing_format} {unsafe!b} {options} " + "{image_filename} {image_size}" + ) + + @staticmethod + def execute(image_config, arguments): + pass + + +class QemuImgDestroy(_ImageHandler): + _COMMAND = "destroy" + + @staticmethod + def execute(image_config, arguments): + pass + + +class QemuImgRebase(_ImageHandler): + _COMMAND = "rebase" + + @staticmethod + def execute(image_config, arguments): + pass + + +class QemuImgConvert(_ImageHandler): + _COMMAND = "convert" + + @staticmethod + def execute(image_config, arguments): + pass + + +class QemuImgCommit(_ImageHandler): + _COMMAND = "commit" + + @staticmethod + def execute(image_config, arguments): + pass + + +class QemuImgSnapshot(_ImageHandler): + _COMMAND = "snapshot" + + @staticmethod + def execute(image_config, arguments): + pass + + @staticmethod + def list(image_config, arguments): + pass + + @staticmethod + def snap(image_config, arguments): + pass diff --git a/virttest/vt_agent/managers/resbacking_mgr.py b/virttest/vt_agent/managers/resbacking_mgr.py new file mode 100644 index 0000000000..e57fc3b8aa --- /dev/null +++ b/virttest/vt_agent/managers/resbacking_mgr.py @@ -0,0 +1,53 @@ +from .resbackings import get_resource_backing_class +from .resbackings import get_pool_connection_class + + +class _ResourceBackingManager(object): + + def __init__(self): + self._pool_connections = dict() + self._backings = dict() + + def create_pool_connection(self, pool_id, pool_conn_config): + pool_type = pool_conn_config["type"] + pool_conn_class = get_pool_connection_class(pool_type) + pool_conn = pool_conn_class(pool_conn_config) + pool_conn.startup() + self._pool_connections[pool_id] = pool_conn + + def destroy_pool_connection(self, pool_id): + pool_conn = self._pool_connections[pool_id] + pool_conn.shutdown() + del self._pool_connections[pool_id] + + def create_backing(self, config, need_allocate=False): + resource_id = config["uuid"] + pool_id = config["pool"] + pool_conn = self._pool_connections[pool_id] + res_type = config["type"] + backing_class = get_resource_backing_class(res_type) + backing = backing_class(config) + self._backings[backing.backing_id] = backing + if need_allocate: + backing.allocate(pool_conn) + return backing.backing_id + + def destroy_backing(self, backing_id, need_release=False): + backing = self._backings[backing_id] + pool_conn = self._pool_connections[backing.source_pool] + if need_release: + backing.release(pool_conn) + del self._backings[backing_id] + + def update_backing(self, backing_id, new_config): + backing = self._backings[backing_id] + pool_conn = self._pool_connections[backing.source_pool] + backing.update(pool_conn, new_config) + + def info_backing(self, backing_id, request, verbose=False): + backing = self._backings[backing_id] + pool_conn = self._pool_connections[backing.source_pool] + return backing.query(pool_conn, request, verbose) + + +backing_mgr = _ResourceBackingManager() diff --git a/virttest/vt_agent/managers/resbackings/__init__.py b/virttest/vt_agent/managers/resbackings/__init__.py new file mode 100644 index 0000000000..abc21385de --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/__init__.py @@ -0,0 +1,17 @@ +from .storage import _DirPoolConnection, _DirVolumeBacking + + +_pool_conn_classes = dict() +_backing_classes = dict() +#_pool_conn_classes[_CephPoolConnection.pool_type] = _CephPoolConnection +_pool_conn_classes[_DirPoolConnection.pool_type] = _DirPoolConnection +#_backing_classes[_CephVolumeBacking.resource_type] = _CephVolumeBacking +_backing_classes[_DirVolumeBacking.resource_type] = _DirVolumeBacking + + +def get_resource_backing_class(resource_type): + return _backing_classes.get(resource_type) + + +def get_pool_connection_class(pool_type): + return _pool_conn_classes.get(pool_type) diff --git a/virttest/vt_agent/managers/resbackings/backing.py b/virttest/vt_agent/managers/resbackings/backing.py new file mode 100644 index 0000000000..4a04ea8cf0 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/backing.py @@ -0,0 +1,40 @@ +import uuid +from abc import ABC, abstractmethod + + +class _ResourceBacking(ABC): + _RESOURCE_TYPE = None + + def __init__(self, config): + self._uuid = uuid.uuid4() + self._config = config + self._source_pool = config["pool"] + self._resource_id = config["uuid"] + + @property + def backing_id(self): + return self._uuid + + @abstractmethod + def allocate(self, pool_connection): + pass + + @abstractmethod + def release(self, pool_connection): + pass + + @abstractmethod + def update(self, pool_connection, new_spec): + pass + + @abstractmethod + def info(self, pool_connection): + pass + + @classmethod + def resource_type(cls): + return cls._RESOURCE_TYPE + + @property + def source_pool(self): + return self._source_pool diff --git a/virttest/vt_agent/managers/resbackings/backing_mgr.py b/virttest/vt_agent/managers/resbackings/backing_mgr.py new file mode 100644 index 0000000000..e6b34fcbbd --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/backing_mgr.py @@ -0,0 +1,46 @@ +from abc import ABC, abstractmethod +from .resbackings import get_backing_class +from .resbackings import get_pool_connection_class + + +class _ResourceBackingManager(ABC): + _ATTACHED_POOL_TYPE = None + + def __init__(self): + self._pool_connections = dict() + self._backings = dict() + + @abstractmethod + def create_pool_connection(self, pool_config): + pool_type = pool_config["type"] + pool_conn_class = get_pool_connection_class(pool_type) + pool_conn = pool_conn_class(pool_config) + pool_conn.startup() + self._pool_connections[pool_id] = pool_conn + + def destroy_pool_connection(self, pool_id): + pool_conn = self._pool_connections[pool_id] + pool_conn.shutdown() + del self._pool_connections[pool_id] + + @abstractmethod + def create_backing(self, config, need_allocate=False): + pass + + def destroy_backing(self, backing_id, need_release=False): + backing = self._backings[backing_id] + pool_conn = self._pool_connections[backing.source_pool] + if need_release: + backing.release_resource(pool_conn) + del self._backings[backing_id] + + def update_backing(self, backing_id, config): + backing = self._backings[backing_id] + pool_conn = self._pool_connections[backing.source_pool] + backing.update(pool_conn, config) + + def get_backing(self, backing_id): + return self._backings.get(backing_id) + + def info_backing(self, backing_id): + return self._backings[backing_id].to_specs() diff --git a/virttest/vt_agent/managers/resbackings/cvm/__init__.py b/virttest/vt_agent/managers/resbackings/cvm/__init__.py new file mode 100644 index 0000000000..d31c959cc0 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/cvm/__init__.py @@ -0,0 +1,27 @@ +from .. import _CVMResBackingMgr +from .. import _all_subclasses + + +class LaunchSecurity(object): + _instance = None + + @classmethod + def dispatch(cls): + return cls._instance + + @classmethod + def startup(cls, config): + if cls._instance is not None: + return cls._instance + + for mgr_cls in _all_subclasses(_CVMResBackingMgr): + if mgr_cls.get_platform_flags() is not None: + cls._instance = mgr_cls(config) + cls._instance.startup() + return cls._instance + + raise + + @classmethod + def teardown(cls): + cls._instance.teardown() diff --git a/virttest/vt_agent/managers/resbackings/cvm/_sev_resmgr.py b/virttest/vt_agent/managers/resbackings/cvm/_sev_resmgr.py new file mode 100644 index 0000000000..fcb2982cd8 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/cvm/_sev_resmgr.py @@ -0,0 +1,98 @@ +from .. import _ResBacking +from .. import _ResBackingCaps +from .. import _CVMResBackingMgr + + +class _SEVCommResBacking(_ResBacking): + + def __init__(self, requests): + super().__init__(requests) + self._cbitpos = None + self._reduced_phys_bits = None + # self._sev_device = '/dev/sev' + # self._kernel_hashes = None + + def to_specs(self): + return {"cbitpos": self._cbitpos, "reduced-phys-bits": self._reduced_phys_bits} + + +class _SEVResBacking(_SEVCommResBacking): + RESOURCE_TYPE = "sev" + + def __init__(self): + super().__init__() + self._dh_cert = None + self._session = None + + def allocate(self, requests): + pass + + def free(self): + pass + + def to_specs(self): + pass + + +class _SNPResBacking(_SEVCommResBacking): + RESOURCE_TYPE = "snp" + + def __init__(self): + super().__init__() + + def allocate(self, requests): + pass + + def free(self): + pass + + def to_specs(self): + pass + + +class _SEVResBackingCaps(_ResBackingCaps): + + def __init__(self, params): + self._cbitpos = None + self._reduced_phys_bits = None + self._sev_device = None + self._max_sev_guests = None + self._max_snp_guests = None + self._pdh = None + self._cert_chain = None + self._cpu0_id = None + + def load(self): + pass + + def is_capable(self, requests): + pass + + def increase(self, backing): + pass + + def decrease(self, backing): + pass + + @property + def max_sev_guests(self): + return self._max_sev_guests + + @property + def max_snp_guests(self): + return self._max_snp_guests + + +class _SEVResBackingMgr(_CVMResBackingMgr): + + def __init__(self, config): + super().__init__(config) + self._caps = _SEVResBackingCaps(config) + _SEVResBackingMgr._platform_flags = config + + def startup(self): + reset_sev_platform() + super().startup() + + def teardown(self): + reset_sev_platform() diff --git a/virttest/vt_agent/managers/resbackings/cvm_platform_mgr/_sev_platform_mgr.py b/virttest/vt_agent/managers/resbackings/cvm_platform_mgr/_sev_platform_mgr.py new file mode 100644 index 0000000000..ad162be551 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/cvm_platform_mgr/_sev_platform_mgr.py @@ -0,0 +1,6 @@ +def reset_platform(): + pass + + +def rotate_pdh(): + pass diff --git a/virttest/vt_agent/managers/resbackings/cvm_platform_mgr/_tdx_platform_mgr.py b/virttest/vt_agent/managers/resbackings/cvm_platform_mgr/_tdx_platform_mgr.py new file mode 100644 index 0000000000..ad162be551 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/cvm_platform_mgr/_tdx_platform_mgr.py @@ -0,0 +1,6 @@ +def reset_platform(): + pass + + +def rotate_pdh(): + pass diff --git a/virttest/vt_agent/managers/resbackings/cvm_platform_mgr/cvm_platform_mgr.py b/virttest/vt_agent/managers/resbackings/cvm_platform_mgr/cvm_platform_mgr.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/virttest/vt_agent/managers/resbackings/dispatcher.py b/virttest/vt_agent/managers/resbackings/dispatcher.py new file mode 100644 index 0000000000..801240f063 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/dispatcher.py @@ -0,0 +1,48 @@ +from .storage import _dir_backing_mgr +#from .storage import _nfs_backing_mgr +# from .storage import _ceph_backing_mgr +# from .cvm import _sev_backing_mgr +# from .cvm import _tdx_backing_mgr + + +class _BackingMgrDispatcher(object): + + def __init__(self): + self._managers_mapping = dict() + self._backings_mapping = dict() + self._pools_mapping = dict() + + def dispatch_by_pool(self, pool_id): + return self._pools_mapping.get(pool_id, None) + + def dispatch_by_backing(self, backing_id): + return self._backings_mapping.get(backing_id, None) + + @classmethod + def register(cls, mgr): + self._managers_mapping[mgr.attached_pool_type] = mgr + + def map_pool(self, pool_id, pool_type): + backing_mgr = self._managers_mapping[pool_type] + self._pools_mapping[pool_id] = backing_mgr + + def unmap_pool(self, pool_id): + del self._pools_mapping[pool_id] + + def map_backing(self, backing_id, backing_mgr): + self._backings_mapping[backing_id] = backing_mgr + + def unmap_backing(self, backing_id): + del self._backings_mapping[backing_id] + + +_backing_mgr_dispatcher = _BackingMgrDispatcher() + +# Register storage backing managers +_backing_mgr_dispatcher.register(_dir_backing_mgr) +#_backing_mgr_dispatcher.register(_nfs_backing_mgr) +# _backing_mgr_dispatcher.register(_ceph_backing_mgr) + +# Register cvm backing managers +# _backing_mgr_dispatcher.register(_sev_backing_mgr) +# _backing_mgr_dispatcher.register(_tdx_backing_mgr) diff --git a/virttest/vt_agent/managers/resbackings/pool_connection.py b/virttest/vt_agent/managers/resbackings/pool_connection.py new file mode 100644 index 0000000000..8a5517631e --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/pool_connection.py @@ -0,0 +1,38 @@ +from abc import ABC, abstractmethod + + +class _ResourcePoolAccess(ABC): + + @abstractmethod + def __init__(self, pool_access_config): + pass + + +class _ResourcePoolConnection(ABC): + + def __init__(self, pool_config): + self._config = pool_config + + @property + def config(self): + return self._config + + @property + def spec(self): + return self.config["spec"] + + @property + def meta(self): + return self.config["meta"] + + @abstractmethod + def startup(self): + pass + + @abstractmethod + def shutdown(self): + pass + + @abstractmethod + def connected(self): + return False diff --git a/virttest/vt_agent/managers/resbackings/storage/__init__.py b/virttest/vt_agent/managers/resbackings/storage/__init__.py new file mode 100644 index 0000000000..46e0228ab4 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/storage/__init__.py @@ -0,0 +1,2 @@ +from .dir import _DirPoolConnection, _DirVolumeBacking +#from .nfs import _NfsPoolConnection diff --git a/virttest/vt_agent/managers/resbackings/storage/dir/__init__.py b/virttest/vt_agent/managers/resbackings/storage/dir/__init__.py new file mode 100644 index 0000000000..dd0d5847c5 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/storage/dir/__init__.py @@ -0,0 +1,2 @@ +from .dir_backing import _DirVolumeBacking +from .dir_pool_connection import _DirPoolConnection diff --git a/virttest/vt_agent/managers/resbackings/storage/dir/dir_backing.py b/virttest/vt_agent/managers/resbackings/storage/dir/dir_backing.py new file mode 100644 index 0000000000..18f5e92422 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/storage/dir/dir_backing.py @@ -0,0 +1,26 @@ +import os +from virttest import utils_io +from ...backing import _ResourceBacking + + +class _DirVolumeBacking(_ResourceBacking): + _RESOURCE_TYPE = "volume" + + def __init__(self, config): + super().__init__(config) + self._size = config["size"] + self._filename = config["filename"] + + @property + def allocate(self, pool_connection): + path = os.path.join(pool_connection.root_dir, self._filename) + utils_io.dd(path, self._size) + + def release(self, pool_connection): + path = os.path.join(pool_connection.root_dir, self._filename) + os.unlink(path) + + def info(self, pool_connection): + path = os.path.join(pool_connection.root_dir, self._filename) + s = os.stat(path) + return {"path": path, "allocation": s.st_size} diff --git a/virttest/vt_agent/managers/resbackings/storage/dir/dir_backing_mgr.py b/virttest/vt_agent/managers/resbackings/storage/dir/dir_backing_mgr.py new file mode 100644 index 0000000000..825ca60fee --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/storage/dir/dir_backing_mgr.py @@ -0,0 +1,46 @@ +from ...backing_mgr import _ResourceBackingManager +from .dir_backing import _get_backing_class +from .dir_pool_connection import _DirPoolConnection + + +class _DirBackingManager(_ResourceBackingManager): + _ATTACHED_POOL_TYPE = "filesystem" + + def __init__(self): + super().__init__() + + def create_pool_connection(self, pool_config): + pool_conn = _DirPoolConnection(pool_config) + pool_conn.startup() + self._pool_connections[pool_id] = pool_conn + + def destroy_pool_connection(self, pool_id): + pool_conn = self._pool_connections[pool_id] + pool_conn.shutdown() + del self._pool_connections[pool_id] + + def create_backing(self, config, need_allocate=False): + pool_id = config["pool_id"] + pool_conn = self._pool_connections[pool_id] + backing_class = _get_backing_class(config["resource_type"]) + backing = backing_class(config) + self._backings[backing.uuid] = backing + if need_allocate: + backing.allocate(pool_conn) + + def destroy_backing(self, backing_id, need_release=False): + backing = self._backings[backing_id] + pool_conn = self._pool_connections[backing.source_pool] + if need_release: + backing.release(pool_conn) + del self._backings[backing_id] + + def update_backing(self, backing_id, new_backing_spec): + backing = self._allocated_backings[backing_id] + pool_conn = self._pool_connections[backing.source_pool] + backing.update(pool_conn, new_backing_spec) + + def info_backing(self, backing_id): + backing = self._allocated_backings[backing_id] + pool_conn = self._pool_connections[backing.source_pool] + return backing.info(pool_conn) diff --git a/virttest/vt_agent/managers/resbackings/storage/dir/dir_pool_connection.py b/virttest/vt_agent/managers/resbackings/storage/dir/dir_pool_connection.py new file mode 100644 index 0000000000..48412a7a79 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/storage/dir/dir_pool_connection.py @@ -0,0 +1,24 @@ +import os + +from ...pool_connection import _ResourcePoolConnection + + +class _DirPoolConnection(_ResourcePoolConnection): + + def __init__(self, pool_conn_config): + super().__init__(pool_conn_config) + self._root_dir = pool_conn_config["root_dir"] + + def startup(self): + if not os.path.exists(self.root_dir): + os.mkdir(self.root_dir) + + def shutdown(self): + pass + + def connected(self): + return os.path.exists(self.root_dir) + + @property + def root_dir(self): + return self._root_dir diff --git a/virttest/vt_agent/managers/resbackings/storage/nfs/__init__.py b/virttest/vt_agent/managers/resbackings/storage/nfs/__init__.py new file mode 100644 index 0000000000..0eb8062f6e --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/storage/nfs/__init__.py @@ -0,0 +1 @@ +from .nfs_backing_mgr import _nfs_backing_mgr diff --git a/virttest/vt_agent/managers/resbackings/storage/nfs/nfs_backing.py b/virttest/vt_agent/managers/resbackings/storage/nfs/nfs_backing.py new file mode 100644 index 0000000000..e5f864b6b3 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/storage/nfs/nfs_backing.py @@ -0,0 +1,33 @@ +import os +from virttest import utils_io +from ...backing import _ResourceBacking + + +class _NfsVolumeBacking(_ResourceBacking): + + def __init__(self, config): + super().__init__(config) + self._size = config["size"] + self._name = config["name"] + + @property + def allocate(self, pool_connection): + path = os.path.join(pool_connection.mnt, self._name) + utils_io.dd(path, self._size) + + def release(self, pool_connection): + path = os.path.join(pool_connection.mnt, self._name) + os.unlink(path) + + def info(self, pool_connection): + path = os.path.join(pool_connection.mnt, self._name) + s = os.stat(path) + return {"path": path, "allocation": s.st_size} + + +def _get_backing_class(resource_type): + """ + Get the backing class for a given resource type in case there are + more than one resources are supported by a nfs pool + """ + return _NfsVolumeBacking diff --git a/virttest/vt_agent/managers/resbackings/storage/nfs/nfs_backing_mgr.py b/virttest/vt_agent/managers/resbackings/storage/nfs/nfs_backing_mgr.py new file mode 100644 index 0000000000..91abd364a9 --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/storage/nfs/nfs_backing_mgr.py @@ -0,0 +1,46 @@ +from ...backing_mgr import _ResourceBackingManager +from .nfs_backing import _get_backing_class +from .nfs_pool_connection import _NfsPoolConnection + + +class _NfsBackingManager(_ResourceBackingManager): + _ATTACHED_POOL_TYPE = "nfs" + + def __init__(self): + super().__init__() + + def create_pool_connection(self, pool_config, pool_access_config): + pool_conn = _NfsPoolConnection(pool_config, pool_access_config) + pool_conn.startup() + self._pool_connections[pool_id] = pool_conn + + def destroy_pool_connection(self, pool_id): + pool_conn = self._pool_connections[pool_id] + pool_conn.shutdown() + del self._pool_connections[pool_id] + + def create_backing(self, config, need_allocate=False): + pool_id = config["pool_id"] + pool_conn = self._pool_connections[pool_id] + backing_class = _get_backing_class(config["resource_type"]) + backing = backing_class(config) + self._backings[backing.uuid] = backing + if need_allocate: + backing.allocate(pool_conn) + + def destroy_backing(self, backing_id, need_release=False): + backing = self._backings[backing_id] + pool_conn = self._pool_connections[backing.source_pool] + if need_release: + backing.release(pool_conn) + del self._backings[backing_id] + + def update_backing(self, backing_id, new_backing_spec): + backing = self._allocated_backings[backing_id] + pool_conn = self._pool_connections[backing.source_pool] + backing.update(pool_conn, new_backing_spec) + + def info_backing(self, backing_id): + backing = self._allocated_backings[backing_id] + pool_conn = self._pool_connections[backing.source_pool] + return backing.info(pool_conn) diff --git a/virttest/vt_agent/managers/resbackings/storage/nfs/nfs_pool_connection.py b/virttest/vt_agent/managers/resbackings/storage/nfs/nfs_pool_connection.py new file mode 100644 index 0000000000..56258930ea --- /dev/null +++ b/virttest/vt_agent/managers/resbackings/storage/nfs/nfs_pool_connection.py @@ -0,0 +1,49 @@ +import utils_disk + +from ...pool_connection import _ResourcePoolAccess +from ...pool_connection import _ResourcePoolConnection + + +class _NfsPoolAccess(_ResourcePoolAccess): + """ + Mount options + """ + + def __init__(self, pool_access_config): + self._options = pool_access_config["nfs_options"] + + def __str__(self): + return self._options + + +class _NfsPoolConnection(_ResourcePoolConnection): + + def __init__(self, pool_config, pool_access_config): + super().__init__(pool_config, pool_access_config) + self._connected_pool = pool_config["pool_id"] + self._nfs_server = pool_config["nfs_server"] + self._export_dir = pool_config["export_dir"] + self._nfs_access = _NfsPoolAccess(pool_access_config) + self._mnt = pool_config.get(nfs_mnt_dir) + if self._mnt is None: + self._create_default_mnt() + + def startup(self): + src = "{host}:{export}".format(self._nfs_server, self._export_dir) + dst = self._mnt + options = str(self._nfs_access) + utils_disk.mount(src, dst, fstype="nfs", options=options) + + def shutdown(self): + src = "{host}:{export}".format(self._nfs_server, self._export_dir) + dst = self._mnt + utils_disk.umount(src, dst, fstype="nfs") + + def connected(self): + src = "{host}:{export}".format(self._nfs_server, self._export_dir) + dst = self._mnt + return utils_disk.is_mount(src, dst, fstype="nfs") + + @property + def mnt(self): + return self._mnt diff --git a/virttest/vt_agent/services/__init__.py b/virttest/vt_agent/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/virttest/vt_agent/services/virt/image.py b/virttest/vt_agent/services/virt/image.py new file mode 100644 index 0000000000..8a2a520009 --- /dev/null +++ b/virttest/vt_agent/services/virt/image.py @@ -0,0 +1,12 @@ +from ..managers import imgr +from .resbacking import info_backing + + +def update_image(image_config, update_cmd_config): + # Get the volumes info + for virt_image_tag, virt_image_config in image_config["spec"]["images"]: + backing_id = virt_image_config["meta"]["bindings"] + volume_config = virt_image_config["spec"]["volume"] + config = info_backing(backing_id) + + imgr.update_image(image_config, update_cmd_config) diff --git a/virttest/vt_agent/services/virt/resbacking.py b/virttest/vt_agent/services/virt/resbacking.py new file mode 100644 index 0000000000..b3c62ebc0c --- /dev/null +++ b/virttest/vt_agent/services/virt/resbacking.py @@ -0,0 +1,80 @@ +from ...managers import backing_mgr + + +def connect_pool(pool_id, pool_conn_config): + backing_mgr.create_pool_connection(pool_id, pool_conn_config) + + +def disconnect_pool(pool_id): + backing_mgr.destroy_pool_connection(pool_id) + + +def create_backing(config, need_allocate=False): + """ + Create a resource backing on the worker node, which is bound to one + one resource only, VT can access the specific resource allocation + with the backing when starting VM on the worker node + + :param config: The resource backing configuration, usually, it's a + snippet of the required resource configuration, used + for allocating the resource + need its size and filename + :type config: dict + :param need_allocate: True: Allocate the resource + False: Don't allocate resource + For a shared resource pool, e.g. a nfs pool, + do the allocation once + :type need_allocate: boolean + :return: The resource backing id + :rtype: string + """ + return backing_mgr.create_backing(config, need_allocate) + + +def destroy_backing(backing_id, need_release=False): + """ + Destroy the backing, all resources allocated on worker nodes will be + released. + + :param backing_id: The cluster resource id + :type backing_id: string + :param need_release: True: Release the resource + False: Don't release resource + For a shared resource pool, e.g. a nfs pool, + release the resource once + :type need_release: boolean + """ + backing_mgr.destroy_backing(backing_id, need_release) + + +def info_backing(backing_id, verbose=False): + """ + Get the information of a resource with a specified backing + + We need not get all the information of the resource, because the + static can be got by the resource object, e.g. size, here we only + get the information which is dynamic, such as path and allocation + + :param resource_id: The backing id + :type resource_id: string + :return: The information of a resource, e.g. + { + 'spec':{ + 'allocation': 12, + 'path': [{'node1': '/p1/f1'},{'node2': '/p2/f1'}], + } + } + :rtype: dict + """ + return backing_mgr.info_backing(backing_id) + + +def update_backing(backing_id, config): + """ + :param backing_id: The resource backing id + :type backing_id: string + :param config: The specified action and the snippet of + the resource's spec and meta info used for update + :type config: dict + """ + backing_mgr.update_backing(backing_id, config) diff --git a/virttest/vt_imgr/.api.py b/virttest/vt_imgr/.api.py new file mode 100644 index 0000000000..948768e7ae --- /dev/null +++ b/virttest/vt_imgr/.api.py @@ -0,0 +1,106 @@ +from .vt_resmgr import vt_resmgr + + +class ImageNotFound(Exception): + def __init__(self, image_id): + self._id = image_id + + def __str__(self): + return 'Cannot find the pool(id="%s)"' % self._id + + +class UnknownImageType(Exception): + def __init__(self, image_type): + self._type = image_type + + def __str__(self): + return 'Unknown image type "%s"' % self._type + + +def create_image(config): + """ + Create a logical image without any specific storage allocation, + + :param config: The image's meta and spec data + :type config: dict + :return: The image id + :rtype: string + """ + pass + + +def destroy_image(image_id): + """ + Destroy the logical image, the specific storage allocation + will be released, note the image's backing image will not be + touched + + :param image_id: The resource id + :type image_id: string + """ + pass + + +def get_image(image_id): + """ + Get all information for a specified image + + :param image_id: The image id + :type image_id: string + :return: All the information of an image, e.g. + { + 'meta': { + 'id': 'image1', + 'backing': 'image2' + }, + 'spec': { + 'name': 'stg', + 'format': 'qcow2', + 'backing': { + The backing's information here + }, + 'volume': { + 'meta': { + 'id': 'nfs_vol1' + }, + 'spec': { + 'pool': 'nfs_pool1', + 'type': 'volume', + 'size': 65536, + 'name': 'stg.qcow2', + 'path': [{'node1': '/mnt1/stg.qcow2'}, + {'node2': '/mnt2/stg.qcow2'}], + } + } + } + } + :rtype: dict + """ + pass + + +def update_image(image_id, config): + """ + Update an image, the command format: + {'action': arguments}, in which + the 'action' can be the following for a qemu image: + 'create': qemu-img create + 'destroy': Remove the allocated resource + 'convert': qemu-img convert + 'snapshot': qemu-img snapshot + 'resize': qemu-img resize + arguments is a dict object which contains all related settings for a + specific action + + Examples: + qemu-img create + {'create': } + qemu-img convert + {'convert': } + + :param image_id: The image id + :type image_id: string + :param config: The specified action and its arguments + :type config: dict + """ + pass diff --git a/virttest/vt_imgr/__init__.py b/virttest/vt_imgr/__init__.py new file mode 100644 index 0000000000..73fcb2e24a --- /dev/null +++ b/virttest/vt_imgr/__init__.py @@ -0,0 +1 @@ +from vt_imgr import vt_imgr diff --git a/virttest/vt_imgr/images/__init__.py b/virttest/vt_imgr/images/__init__.py new file mode 100644 index 0000000000..eee9605c4e --- /dev/null +++ b/virttest/vt_imgr/images/__init__.py @@ -0,0 +1,15 @@ +from .qemu import _QemuImage + + +_image_classes = dict() +_image_classes[_QemuImage.image_type] = _QemuImage + + +def get_image_class(image_type): + for t, cls in _image_classes.items(): + if t == image_type: + return cls + return None + + +__all__ = ["get_image_class"] diff --git a/virttest/vt_imgr/images/image.py b/virttest/vt_imgr/images/image.py new file mode 100644 index 0000000000..b61cf61bc9 --- /dev/null +++ b/virttest/vt_imgr/images/image.py @@ -0,0 +1,76 @@ +import uuid +from abc import ABC, abstractmethod + + +class _Image(ABC): + """ + This is the upper-level image, in the context of a VM, it's mapping + to the VM's disk. It can have one or more lower-level images, + e.g. A qemu image can have a lower-level image chain: + base ---> sn + in which "sn" is the top lower-level image name while "base" is the + backing lower-level image name of "sn" + """ + + _IMAGE_TYPE = None + + # The upper-level image configuration template + _IMAGE_CONFIG_TEMP = { + "meta": { + "uuid": None, + "name": None, + "type": None, + "topology": None, + }, + "spec": {"images": {}}, + } + + def __init__(self, image_config): + self._uuid = uuid.uuid4() + self._config = image_config + self._virt_images = None + + @classmethod + def image_type(cls): + return cls._IMAGE_TYPE + + @property + def image_id(self): + return self._uuid + + @property + def image_config(self): + return self._config + + @property + def image_meta(self): + return self.image_config.get("meta") + + @property + def image_spec(self): + return self.image_config.get("spec") + + @classmethod + @abstractmethod + def define_config(cls, image_name, params): + raise NotImplemented + + @abstractmethod + def create(self): + raise NotImplemented + + @abstractmethod + def destroy(self): + raise NotImplemented + + @abstractmethod + def update(self): + raise NotImplemented + + @abstractmethod + def query(self): + raise NotImplemented + + @abstractmethod + def backup(self): + raise NotImplemented diff --git a/virttest/vt_imgr/images/image_handlers.py b/virttest/vt_imgr/images/image_handlers.py new file mode 100644 index 0000000000..f1e5557b57 --- /dev/null +++ b/virttest/vt_imgr/images/image_handlers.py @@ -0,0 +1,14 @@ +from abc import ABC, abstractmethod + + +class _ImageUpdateCommand(ABC): + _UPDATE_ACTION = None + + @staticmethod + @abstractmethod + def execute(image, arguments): + raise NotImplemented + + @classmethod + def action(cls): + return cls._UPDATE_ACTION diff --git a/virttest/vt_imgr/images/qemu/__init__.py b/virttest/vt_imgr/images/qemu/__init__.py new file mode 100644 index 0000000000..a9808db67e --- /dev/null +++ b/virttest/vt_imgr/images/qemu/__init__.py @@ -0,0 +1 @@ +from .qemu_image import _QemuImage diff --git a/virttest/vt_imgr/images/qemu/qemu_image.py b/virttest/vt_imgr/images/qemu/qemu_image.py new file mode 100644 index 0000000000..52bf36b40b --- /dev/null +++ b/virttest/vt_imgr/images/qemu/qemu_image.py @@ -0,0 +1,94 @@ +from collections import OrderedDict +from copy import deepcopy + +from ..image import _Image +from .qemu_virt_image import get_virt_image_class +from .qemu_image_handlers import get_image_handler + + +class _QemuImage(_Image): + + # The upper-level image type + _IMAGE_TYPE = "qemu" + + # The qemu upper-level image configuration template + # The topology of the lower-level images, which construct + # the upper-level qemu image + _IMAGE_CONFIG_TEMP = deepcopy(_Image._IMAGE_CONFIG_TEMP) + _IMAGE_CONFIG_TEMP["meta"]["type"] = _IMAGE_TYPE + + def __init__(self, image_config): + super().__init__(image_config) + # Store images with the same order as tags defined in image_chain + self._virt_images = OrderedDict() + + @classmethod + def _define_virt_image_config(cls, image_tag, image_params): + image_format = image_params.get("image_format", "qcow2") + virt_image_class = get_virt_image_class(image_format) + return virt_image_class.define_config(image_tag, image_params) + + @classmethod + def _define_topo_chain_config(cls, image_name, params): + image_chain = params.object_params(image_name).objects("image_chain") + + config = deepcopy(cls._IMAGE_CONFIG_TEMP) + config["meta"]["name"] = image_name + config["meta"]["topology"] = {"chain": image_chain} + images = config["spec"]["images"] + + for image_tag in image_chain: + image_params = params.object_params(image_tag) + images[image_tag] = cls._define_virt_image_config(image_tag, + image_params) + return config + + @classmethod + def _define_topo_none_config(cls, image_name, params): + config = deepcopy(cls._IMAGE_CONFIG_TEMP) + config["meta"]["name"] = image_name + config["meta"]["topology"] = None + images = config["spec"]["images"] + images[image_tag] = cls._define_virt_image_config(image_name, image_params) + return config + + @classmethod + def define_config(cls, image_name, params): + """ + Define the image configuration by its cartesian params + """ + image_params = params.object_params(image_name) + image_chain = image_params.get("image_chain") + if image_chain: + return cls._define_topo_chain_config(image_name, params) + else: + return cls._define_topo_none_config(image_name, params) + + def _create_virt_images(self): + for image_tag, config in self.spec["images"]: + image_format = config["spec"]["format"] + virt_image_class = get_virt_image_class(image_format) + virt_image = virt_image_class(config) + virt_image.create() + self._virt_images[image_tag] = virt_image + + def create(self): + self._create_virt_images() + + def _destroy_virt_images(self): + for virt_image in self._virt_images.values(): + if not virt_image.keep(): + virt_image.destroy() + del(self._virt_images[image_tag]) + + def destroy(self): + self._destroy_virt_images() + + def update(self, config): + update_image() + cmd, args = list(config.items())[0] + handler = get_image_handler(cmd) + handler.execute(self, args) + + def query(self, request, verbose): + pass diff --git a/virttest/vt_imgr/images/qemu/qemu_image_handlers.py b/virttest/vt_imgr/images/qemu/qemu_image_handlers.py new file mode 100644 index 0000000000..0dda4797a2 --- /dev/null +++ b/virttest/vt_imgr/images/qemu/qemu_image_handlers.py @@ -0,0 +1,117 @@ +from ..image_handlers import _ImageUpdateCommand + + +class _QemuImageCreate(_ImageUpdateCommand): + """ + qemu-img create [--object OBJECTDEF] [-q] [-f FMT] [-b BACKING_FILE [-F BACKING_FMT]] [-u] [-o OPTIONS] FILENAME [SIZE] + {"create": {}} + {"create": {"images": [stg]}} + + """ + + _UPDATE_ACTION = "create" + + @staticmethod + def execute(image, arguments): + image.spec["topology"] + if params.get("create_with_dd") == "yes" and self.image_format == "raw": + # maps K,M,G,T => (count, bs) + human = { + "K": (1, 1), + "M": (1, 1024), + "G": (1024, 1024), + "T": (1024, 1048576), + } + if self.size[-1] in human: + block_size = human[self.size[-1]][1] + size = int(self.size[:-1]) * human[self.size[-1]][0] + qemu_img_cmd = "dd if=/dev/zero of=%s count=%s bs=%sK" % ( + self.image_filename, + size, + block_size, + ) + else: + cmd_dict = {} + cmd_dict["image_format"] = self.image_format + if self.base_tag: + # if base image has secret, use json representation + base_key_secrets = self.encryption_config.base_key_secrets + if self.base_tag in [ + s.image_id for s in base_key_secrets + ] or self._need_auth_info(self.base_tag): + base_params = params.object_params(self.base_tag) + cmd_dict["backing_file"] = "'%s'" % get_image_json( + self.base_tag, base_params, self.root_dir + ) + else: + cmd_dict["backing_file"] = self.base_image_filename + cmd_dict["backing_format"] = self.base_format + + # secret objects of the backing images + secret_objects = self._backing_access_secret_objects + + # secret object of the image itself + if self._image_access_secret_object: + secret_objects.extend(self._image_access_secret_object) + + image_secret_objects = self._secret_objects + if image_secret_objects: + secret_objects.extend(image_secret_objects) + if secret_objects: + cmd_dict["secret_object"] = " ".join(secret_objects) + + # tls creds objects of the backing images of the source + tls_creds_objects = self._backing_access_tls_creds_objects + + # tls creds object of the source image itself + if self._image_access_tls_creds_object: + tls_creds_objects.append(self._image_access_tls_creds_object) + + if tls_creds_objects: + cmd_dict["tls_creds_object"] = " ".join(tls_creds_objects) + + cmd_dict["image_filename"] = self.image_filename + cmd_dict["image_size"] = self.size + options = self._parse_options(params) + if options: + cmd_dict["options"] = ",".join(options) + qemu_img_cmd = ( + self.image_cmd + + " " + + self._cmd_formatter.format(self.create_cmd, **cmd_dict) + ) + + if params.get("image_backend", "filesystem") == "filesystem": + image_dirname = os.path.dirname(self.image_filename) + if image_dirname and not os.path.isdir(image_dirname): + e_msg = ( + "Parent directory of the image file %s does " + "not exist" % self.image_filename + ) + LOG.error(e_msg) + LOG.error("This usually means a serious setup exceptions.") + LOG.error( + "Please verify if your data dir contains the " + "expected directory structure" + ) + LOG.error("Backing data dir: %s", data_dir.get_backing_data_dir()) + LOG.error("Directory structure:") + for root, _, _ in os.walk(data_dir.get_backing_data_dir()): + LOG.error(root) + + LOG.warning( + "We'll try to proceed by creating the dir. " + "Other errors may ensue" + ) + os.makedirs(image_dirname) + + msg = "Create image by command: %s" % qemu_img_cmd + error_context.context(msg, LOG.info) + cmd_result = process.run( + qemu_img_cmd, shell=True, verbose=False, ignore_status=True + ) + if cmd_result.exit_status != 0 and not ignore_errors: + raise exceptions.TestError( + "Failed to create image %s\n%s" % (self.image_filename, cmd_result) + ) + pass diff --git a/virttest/vt_imgr/images/qemu/qemu_virt_image/__init__.py b/virttest/vt_imgr/images/qemu/qemu_virt_image/__init__.py new file mode 100644 index 0000000000..3d353afb0a --- /dev/null +++ b/virttest/vt_imgr/images/qemu/qemu_virt_image/__init__.py @@ -0,0 +1,16 @@ +from .raw_qemu_virt_image import _RawQemuVirtImage +from .qcow2_qemu_virt_image import _Qcow2QemuVirtImage +from .luks_qemu_virt_image import _LuksQemuVirtImage + + +_image_classes= dict() +_image_classes[_RawQemuVirtImage.image_format] = _RawQemuVirtImage +_image_classes[_Qcow2QemuVirtImage.image_format] = _Qcow2QemuVirtImage +_image_classes[_LuksQemuVirtImage.image_format] = _LuksQemuVirtImage + + +def get_virt_image_class(image_format): + return _image_classes.get(image_format) + + +__all__ = ['get_virt_image_class'] diff --git a/virttest/vt_imgr/images/qemu/qemu_virt_image/luks_qemu_virt_image.py b/virttest/vt_imgr/images/qemu/qemu_virt_image/luks_qemu_virt_image.py new file mode 100644 index 0000000000..60f3606308 --- /dev/null +++ b/virttest/vt_imgr/images/qemu/qemu_virt_image/luks_qemu_virt_image.py @@ -0,0 +1,34 @@ +from copy import deepcopy + +from .qemu_virt_image import _QemuVirtImage +from virttest.vt_resmgr import * + + +class _LuksQemuVirtImage(_QemuVirtImage): + _IMAGE_FORMAT = "luks" + + _VIRT_IMAGE_CONFIG_TEMP = deepcopy(_QemuVirtImage._VIRT_IMAGE_CONFIG_TEMP) + _VIRT_IMAGE_CONFIG_TEMP["spec"].update({"format": None, + "preallocation": None, + "extent_size_hint": None, + "encryption": {"data": None, + "file": None}}) + + @classmethod + def _define_config_legacy(cls, image_name, image_params): + def _set_value_by_param(d): + for k, v in d.items(): + if isinstance(v, str): + d[k] = image_params.get(v) + elif isinstance(v, dict) and v: + _set_value_by_param(v) + + config = deepcopy(cls._VIRT_IMAGE_CONFIG_TEMP) + config["spec"]["name"] = image_name + + spec = config["spec"] + spec.update({"preallocation": "preallocated", + "extent_size_hint": "image_extent_size_hint", + "format": "image_format"}) + spec["encryption"].update({"data": "image_secret"}) + _set_value_by_param(spec) diff --git a/virttest/vt_imgr/images/qemu/qemu_virt_image/qcow2_qemu_virt_image.py b/virttest/vt_imgr/images/qemu/qemu_virt_image/qcow2_qemu_virt_image.py new file mode 100644 index 0000000000..69bcba0a60 --- /dev/null +++ b/virttest/vt_imgr/images/qemu/qemu_virt_image/qcow2_qemu_virt_image.py @@ -0,0 +1,45 @@ +from copy import deepcopy + +from .qemu_virt_image import _QemuVirtImage +from virttest.vt_resmgr import * + + +class _Qcow2QemuVirtImage(_QemuVirtImage): + + _IMAGE_FORMAT = "qcow2" + _VIRT_IMAGE_CONFIG_TEMP = deepcopy(_QemuVirtImage._VIRT_IMAGE_CONFIG_TEMP) + _VIRT_IMAGE_CONFIG_TEMP["spec"].update({"format": None, + "preallocation": None, + "extent_size_hint": None, + "cluster_size": None, + "lazy_refcounts": None, + "compat": None, + "encryption": {"format": None, + "data": None, + "file": None}}) + + @classmethod + def _define_config_legacy(cls, image_name, image_params): + def _set_value_by_param(d): + for k, v in d.items(): + if isinstance(v, str): + d[k] = image_params.get(v) + elif isinstance(v, dict) and v: + _set_value_by_param(v) + + config = deepcopy(cls._VIRT_IMAGE_CONFIG_TEMP) + config["spec"]["name"] = image_name + + spec = config["spec"] + spec.update({"cluster_size": "image_cluster_size", + "lazy_refcounts": "lazy_refcounts", + "compat": "qcow2_compatible", + "preallocation": "preallocated", + "extent_size_hint": "image_extent_size_hint", + "format": "image_format"}) + spec["encryption"].update({"format": "image_encryption", + "data": "image_secret"}) + _set_value_by_param(spec) + spec["volume"] = define_resource_config("volume", image_params) + + return config diff --git a/virttest/vt_imgr/images/qemu/qemu_virt_image/qemu_virt_image.py b/virttest/vt_imgr/images/qemu/qemu_virt_image/qemu_virt_image.py new file mode 100644 index 0000000000..7f6070c2a0 --- /dev/null +++ b/virttest/vt_imgr/images/qemu/qemu_virt_image/qemu_virt_image.py @@ -0,0 +1,50 @@ +from abc import abstractmethod + +from ...virt_image import _VirtImage +from virttest.vt_resmgr import * + + +class _QemuVirtImage(_VirtImage): + """ + A virt image has one storage resource(volume), take qemu virt image + as an example, the cartesian params beginning with 'image_', e.g. + 'image_size' describe this object + """ + + @classmethod + @abstractmethod + def _define_config_legacy(cls, image_name, image_params): + raise NotImplemented + + @classmethod + def define_config(cls, image_name, image_params): + """ + Define the raw image configuration by its cartesian params. + Currently use the existing image params, in future, we'll design + a new set of params to describe an lower-level image. + """ + #TODO: Design new image params + #if image_params.get_boolean("image_new_params_enabled"): + # return cls._define_config_new(image_name, image_params) + return cls._define_config_legacy(image_name, image_params) + + def _create_volume(self): + volume_config = self.spec["volume"] + volume_id = create_resource(volume_config) + volume_config["meta"]["uuid"] = volume_id + + def create(self): + self._create_volume() + + def _destroy_volume(self): + volume_id = self.spec["volume"]["meta"]["uuid"] + destroy_resource(volume_id) + + def destroy(self): + self._destroy_volume() + + def info(self, force_share=False, output="human"): + pass + + def update(self, config): + pass diff --git a/virttest/vt_imgr/images/qemu/qemu_virt_image/raw_qemu_virt_image.py b/virttest/vt_imgr/images/qemu/qemu_virt_image/raw_qemu_virt_image.py new file mode 100644 index 0000000000..d306ba7d8f --- /dev/null +++ b/virttest/vt_imgr/images/qemu/qemu_virt_image/raw_qemu_virt_image.py @@ -0,0 +1,31 @@ +from copy import deepcopy + +from .qemu_virt_image import _QemuVirtImage +from virttest.vt_resmgr import * + + +class _RawQemuVirtImage(_QemuVirtImage): + _IMAGE_FORMAT = "raw" + + _VIRT_IMAGE_CONFIG_TEMP = deepcopy(_QemuVirtImage._VIRT_IMAGE_CONFIG_TEMP) + _VIRT_IMAGE_CONFIG_TEMP["spec"].update({"format": _IMAGE_FORMAT, + "preallocation": None, + "extent_size_hint": None}) + + @classmethod + def _define_config_legacy(cls, image_name, image_params): + def _set_value_by_param(d): + for k, v in d.items(): + if isinstance(v, str): + d[k] = image_params.get(v) + elif isinstance(v, dict) and v: + _set_value_by_param(v) + + config = deepcopy(cls._VIRT_IMAGE_CONFIG_TEMP) + config["spec"]["name"] = image_name + + spec = config["spec"] + spec.update({"preallocation": "preallocated", + "extent_size_hint": "image_extent_size_hint", + "format": "image_format"}) + _set_value_by_param(spec) diff --git a/virttest/vt_imgr/images/virt_image.py b/virttest/vt_imgr/images/virt_image.py new file mode 100644 index 0000000000..7c98fc9399 --- /dev/null +++ b/virttest/vt_imgr/images/virt_image.py @@ -0,0 +1,67 @@ +from abc import ABC, abstractmethod + + +class _VirtImage(ABC): + """ + The lower-level image, which has a storage resource(volume), is + defined by the cartesian params beginning with 'image_'. One or + more lower-level images can represent a upper-level image. + """ + + _IMAGE_FORMAT = None + + _VIRT_IMAGE_CONFIG_TEMP = { + "meta": {}, + "spec": {"name": None, + "format": None, + "volume": None} + } + + def __init__(self, config): + self._config = config + + @classmethod + def format(cls): + return cls._IMAGE_FORMAT + + @classmethod + @abstractmethod + def define_config(cls, image_name, image_params): + raise NotImplemented + + @property + def name(self): + return self.spec["name"] + + @property + def config(self): + return self._config + + @property + def spec(self): + return self.config["spec"] + + @property + def meta(self): + return self.config["meta"] + + @property + @abstractmethod + def keep(self): + raise NotImplemented + + @abstractmethod + def create(self): + raise NotImplemented + + @abstractmethod + def destroy(self): + raise NotImplemented + + @abstractmethod + def info(self): + raise NotImplemented + + @abstractmethod + def update(self, config): + raise NotImplemented diff --git a/virttest/vt_imgr/vt_imgr.py b/virttest/vt_imgr/vt_imgr.py new file mode 100644 index 0000000000..8db1e631aa --- /dev/null +++ b/virttest/vt_imgr/vt_imgr.py @@ -0,0 +1,206 @@ +""" +The upper-level image manager. + +from virttest.vt_imgr import vt_imgr + +# Define the image configuration +image_config = vt_imgr.define_image_config(image_name, params) + +# Create the upper-level image object +image_id = vt_imgr.create_image(image_config) + +# qemu-img create +vt_imgr.update_image(image_id, {"create":{}}) + +# Query the summary config of the "image1" +image_config = vt_imgr.query_image(image_id, query="config", verbose=False) +returned: + {"meta": {"uuid": "uuid-sn" + "name": "sn", + "type": "qemu", + "topology": {"chain": ["base", "sn"]}}, + "spec": {"images": {"base": {"meta": {}, + "spec": {"format": "raw", + "volume": "volume-uuid1"}}, + "sn": {"meta": {}, + "spec": {"format": "qcow2", + "volume": "volume-uuid2"} + } + } + } + } + +""" + +import logging + +from .images import get_image_class + +LOG = logging.getLogger("avocado." + __name__) + + +# TODO: +# Add drivers for diff handlers +# Add access permission for images +# serialize +class _VTImageManager(object): + + def __init__(self): + self._images = dict() + + def define_image_config(self, image_name, params): + """ + Define the upper-level image(e.g. in the context of a VM, it's + mapping to a VM's disk) configuration by its cartesian params. + E.g. A upper-level qemu image has an lower-level image chain + base ---> sn + | | + resource resource + :param image_name: The image tag defined in cartesian params, + e.g. for a qemu image, the tag should be the + top image("sn" in the example above) if the + "image_chain" is defined, usually it is + defined in the "images" param, e.g. "image1" + :type image_name: string + :param params: The params for all the lower-level images + Note it's *NOT* an image-specific params like + params.object_params("sn") + *BUT* the params for both "sn" and "base" + Examples: + 1. images_vm1 = "image1 sn" + image_chain_sn = "base sn" + image_name = "sn" + params = the_case_params.object_params('vm1') + 2. images = "image1 stg" + image_name = "image1" + params = the_case_params + :type params: A Params object + :return: The image configuration + :rtype: dict + """ + image_params = params.object_params(image_name) + image_type = image_params.get('image_type', 'qemu') + image_class = get_image_class(image_type) + return image_class.define_config(image_name, params) + + def create_image(self, image_config): + """ + Create an upper-level image(e.g. in the context of a VM, it's + mapping to a VM's disk) object by its configuration without + any storage allocation. All its lower-level images and their + mapping storage resource objects will be created. + :param image_config: The image configuration. + Call define_image_config to get it. + :type image_config: dict + :return: The image object id + :rtype: string + """ + image = image_class(image_config) + self._images[image.image_id] = image + image.create() + return image.image_id + + def destroy_image(self, image_id): + """ + Destroy a specified image. All its storage allocation should + be released. Note if 'remove_image=no', then don't release the + storage allocation. + :param image_id: The image id + :type image_id: string + """ + image = self._images.get(image_id) + image.destory() + if image.virt_images: + LOG.info("Keep the image") + else: + del(self._images[image_id]) + + def update_image(self, image_id, config): + """ + Update a specified upper-level image + Config format: + {cmd: arguments} + Supported commands for a qemu image: + create: Create the specified lower-level images(qemu-img create) + destroy: Destroy the specified lower-level images + resize: Resize the specified qcow2 lower-level images(qemu-img resize) + map: Map the qcow2 virt image, e.g. qemu-img map + convert: Convert the specified virt image to another, e.g. qemu-img convert + commit: Commit the specified virt image, e.g. qemu-img commit + snapshot: Create a snapshot, e.g. qemu-img snapshot + rebase: Rebase the virt image, e.g. qemu-img rebase + add: Add a lower-level image + delete: Delete a lower-level image + Note: Not all images support the above operations + The arguments is a dict object which contains all related settings + for a specific command + :param image_id: The image id + :type image_id: string + """ + image = self._images.get(image_id) + image.update(config) + + def backup_image(self, image_id): + image = self._images.get(image_id) + image.backup() + + def query_image(self, image_id, request, verbose=False): + """ + Query the configuration of a specified upper-level image, the + general format of the image configuration: + {"meta": {"uuid": "zzz" + "name": "xxx", + "type": "yyy" + "topology": {}}, + "spec": {"images":{}} + } + E.g. A qemu image having an image chain: + {"meta": {"uuid": "uuid-sn" + "name": "sn", + "type": "qemu", + "topology": {"chain": ["base", "sn"]}}, + "spec": {"images": {"base": {"meta": {}, + "spec": {"format": "raw", + "volume": {"meta": {"uuid": "id1"}, + "spec": {"size": 5678}} + } + }, + "sn": {"meta": {}, + "spec": {"format": "qcow2", + "volume": {"meta": {"uuid": "id2"}, + "spec": {"size": 5678}} + } + } + } + } + } + :param request: The query content, format: + config[.meta[.]] + config[.spec[.images.[.meta[.]]]] + config[.spec[.images.[.spec[.]]]] + Note the prefix "config.spec.images" can omitted when + querying a specific 's configuration: + [.meta[.]] + [.spec[.]] + Examples: + config + config.spec.images + config.spec.images.sn.spec.volume + sn.spec + sn.spec.volume.spec.size + :type request: string + :param verbose: False: Return a summary of the configuration + E.g. request = "sn.spec" + response = {"format": "qcow2", + "volume": "id1"} + True: Return the detailed configuration + :type verbose: boolean + :return: The upper-level image's configuration, or a snippet of + + :rtype: dict + """ + image = self._images.get(image_id) + return image.query(request, verbose) + + +vt_imgr = _VTImageManager() diff --git a/virttest/vt_resmgr/__init__.py b/virttest/vt_resmgr/__init__.py new file mode 100644 index 0000000000..0a0e47b0b0 --- /dev/null +++ b/virttest/vt_resmgr/__init__.py @@ -0,0 +1 @@ +from .api import * diff --git a/virttest/vt_resmgr/api.py b/virttest/vt_resmgr/api.py new file mode 100644 index 0000000000..cbef406fc8 --- /dev/null +++ b/virttest/vt_resmgr/api.py @@ -0,0 +1,249 @@ +""" +Th lower-level resource management APIs, open to the test cases. +A test case can call these APIs to handle the resource directly, e.g. +allocate a volume from a nfs pool, or call the upper-level APIs to +handle a volume indirectly, e.g. create a qcow2 qemu image, the image +manager call these APIs to allocate a volume. + +# Create a volume from a nfs pool +image_params = params.object_params("stg") +config = define_resource_config("volume", image_params) +res_id = create_resource(config) + +# Bind the nfs resource to worker nodes(resource allocated) +config = {'bind': {'nodes': ['node1', 'node2'], 'pool': 'nfspool1'}} +update_resource(res_id, config) + +# Unbind the nfs resource to node1 +config = {'unbind': {'nodes': ['node1']}} +update_resource(res_id, config) + +# Unbind the nfs resource(resource released) +config = {'unbind': {}} +update_resource(res_id, config) + +# Destroy the nfs resource +destroy_resource(res_id) +""" +from .vt_resmgr import vt_resmgr + + +class PoolNotFound(Exception): + def __init__(self, pool_id): + self._id = pool_id + + def __str__(self): + return 'Cannot find the pool(id="%s)"' % self._id + + +class UnknownPoolType(Exception): + def __init__(self, pool_type): + self._type = pool_type + + def __str__(self): + return 'Unknown pool type "%s"' % self._type + + +class ResourceNotFound(Exception): + pass + + +class ResourceBusy(Exception): + pass + + +class ResourceNotAvailable(Exception): + pass + + +class UnknownResourceType(Exception): + pass + + +def register_resouce_pool(pool_params): + """ + Register a resource pool, the pool should be ready for + use before registration + + :param pool_params: The pool's cartesian params, e.g. + :type pool_params: dict or Param + :return: The resource pool id + :rtype: string + """ + pool_id = vt_resmgr.register_pool(pool_params) + if pool_id is None: + raise UnknownPoolType(pool_params["pool_type"]) + return pool_id + + +def unregister_resouce_pool(pool_id): + """ + Unregister a resource pool + + :param pool_id: The id of the pool to unregister + :type pool_id: string + """ + vt_resmgr.unregister_pool(pool_id) + + +def attach_resource_pool(pool_id): + """ + Attach the registered pool to worker nodes, then the pool can be + accessed by the worker nodes + + :param pool_id: The id of the pool to attach + :type pool_id: string + """ + vt_resmgr.attach_pool(pool_id) + + +def detach_resource_pool(pool_id): + """ + Detach the pool from the worker nodes, after that, the pool cannot + be accessed + + :param pool_id: The id of the pool to detach + :type pool_id: string + """ + vt_resmgr.detach_pool(pool_id) + + +def define_resource_config(resource_type, resource_params): + """ + Define a resource's configuration by its cartesian params + + :param resource_type: The resource type, it's usually implied, e.g. + the image's storage resource is a "volume", + supported: "volume" + :type resource_type: string + :param resource_params: The resource's specific params, usually + defined by an upper-level object, e.g. + "image1" has a storage resource, so + resource_params = image1's params + i.e. use image1's params to define its + storage resource's configuration + :type resource_params: Param + :return: The resource's configuration, + format: {"meta":{...}, "spec":{...}} + The specific attributes depend on the specific resource + :rtype: dict + """ + pool_id = vt_resmgr.select_pool(resource_type, resource_params) + if pool_id is None: + raise ResourceNotAvailable() + pool = vt_resmgr.get_pool_by_id(pool_id) + return pool.define_resource_config(resource_type, resource_params) + + +def create_resource(config): + """ + Create a logical resource without any specific resource allocation, + the following is required to create a new resource: + 'meta': + It depends on the specific resource + 'spec': + 'type': The resource type, e.g. 'volume' + 'pool': The id of the pool where the resource will be allocated + The other attributes of a specific resource, e.g. the 'size' of + a file-based volume + Example: + {'meta':{},'spec':{'size':123,'pool':'nfs_pool1','name':'stg'}} + + :param config: The config includes the resource's meta and spec data, + this is generated by define_resource function + :type config: dict + :return: The resource id + :rtype: string + """ + pool_id = config["spec"]["pool"] + pool = vt_resmgr.get_pool_by_id(pool_id) + if pool is None: + raise PoolNotFound(pool_id) + return pool.create_resource(config) + + +def destroy_resource(resource_id): + """ + Destroy the logical resource, the specific resource allocation + will be released + + :param resource_id: The resource id + :type resource_id: string + """ + pool = vt_resmgr.get_pool_by_resource(resource_id) + pool.destroy_resource(resource_id) + + +def query_resource(resource_id, request): + """ + Query the configuration of a specified resource, the general format + of the resource configuration: + {"meta": {"uuid": "xxx" + "type": "yyy", + "bindings": []} + "spec": {"pool": "xxx", ...}} + E.g. A storage volume resource + {'meta': { + 'uuid': 'res_id1', + 'type': 'volume', + 'bindings': [{'node': 'node1', + 'backing': 'ref1'}, + ] + }, + 'spec': { + 'pool': 'nfs_pool1', + 'size': 65536, + 'path': '/mnt/sn.qcow2', + } + } + :param resource_id: The resource id + :type resource_id: string + :param request: The query content, format: + config[.meta[.]] + config[.spec[.]] + Examples: + config + config.meta + config.spec.pool + :type request: string + :return: The resource's configuration, it can be either the whole + one or a snippet + :rtype: dict + """ + pool = vt_resmgr.get_pool_by_resource(resource_id) + return pool.info_resource(resource_id) + + +def update_resource(resource_id, config): + """ + Update a resource, the config format: + {'command': arguments} + Supported commands: + 'bind': Bind a specified resource to one or more worker nodes in order + to access the specific resource allocation, note the allocation + is done within the bind command + 'unbind': Unbind a specified resource from one or more worker nodes, + the specific resource allocation will be released only when + all bindings are gone + 'resize': Resize a resource, it's only available for the storage file + based volume resource + The arguments is a dict object which contains all related settings for a + specific action + + Examples: + Bind a resource to one or more nodes + {'bind': {'nodes': ['node1']}} + {'bind': {'nodes': ['node1', 'node2']}} + Unbind a resource from one or more nodes + {'unbind': {'nodes': []}} + {'unbind': {'nodes': ['node1', 'node2']}} + Resize a specified storage volume resource + {'resize': {'size': 123456}} + + :param resource_id: The resource id + :type resource_id: string + :param config: The specified action and its arguments + :type config: dict + """ + pool = vt_resmgr.get_pool_by_resource(resource_id) + pool.update_resource(resource_id, config) diff --git a/virttest/vt_resmgr/resources/__init__.py b/virttest/vt_resmgr/resources/__init__.py new file mode 100644 index 0000000000..f2c0ae3f59 --- /dev/null +++ b/virttest/vt_resmgr/resources/__init__.py @@ -0,0 +1,17 @@ +#from .cvm import _SnpPool +#from .cvm import _TdxPool +from .storage import _CephPool +from .storage import _DirPool + +_pool_classes = dict() +#_pool_classes[_SnpPool.pool_type] = _SnpPool +#_pool_classes[_TdxPool.pool_type] = _TdxPool +_pool_classes[_CephPool.pool_type] = _CephPool +_pool_classes[_DirPool.pool_type] = _DirPool + + +def get_resource_pool_class(pool_type): + return _pool_classes.get(pool_type) + + +__all__ = ["get_resource_pool_class"] diff --git a/virttest/vt_resmgr/resources/cvm/__init__.py b/virttest/vt_resmgr/resources/cvm/__init__.py new file mode 100644 index 0000000000..389da0b231 --- /dev/null +++ b/virttest/vt_resmgr/resources/cvm/__init__.py @@ -0,0 +1 @@ +from .api import _cvm_resmgr diff --git a/virttest/vt_resmgr/resources/cvm/api.py b/virttest/vt_resmgr/resources/cvm/api.py new file mode 100644 index 0000000000..df858f695b --- /dev/null +++ b/virttest/vt_resmgr/resources/cvm/api.py @@ -0,0 +1,73 @@ +import logging + +from ...resmgr import Resource, ResMgr + + +LOG = logging.getLogger("avocado." + __name__) + + +class CVMResMgrError(Exception): + pass + + +class SEVResource(Resource): + TYPE = "sev" + + def _to_attributes(self, resource_params): + pass + + @property + def requests(self): + return {"type": self.TYPE} + + +class SNPResource(Resource): + TYPE = "snp" + + def _to_attributes(self, resource_params): + pass + + @property + def requests(self): + return {"type": self.TYPE} + + +class TDXResource(Resource): + TYPE = "tdx" + + def _to_attributes(self, resource_params): + pass + + @property + def requests(self): + return {"type": self.TYPE} + + +class CVMResMgr(ResMgr): + + def _initialize(self, config): + pass + + def check_resource_managed(self, spec): + pass + + def _get_resource_type(self, spec): + return spec["type"] + + def is_cvm_supported(node_uuid): + """ + Check if the platform supports CVM + """ + node = get_node(node_uuid) + return node.proxy.is_cvm_supported() + + def enabled(self, resource_type, node_uuid): + """ + Check if the platform supports a specific CVM type + e.g. a AMD SEV/SNP machine cannot allocate a TDX resource + """ + node = get_node(node_uuid) + return node.proxy.enabled(resource_type) + + +_cvm_resmgr = CVMResMgr() diff --git a/virttest/vt_resmgr/resources/cvm/conductor.py.bak b/virttest/vt_resmgr/resources/cvm/conductor.py.bak new file mode 100644 index 0000000000..6c5a3ddd4c --- /dev/null +++ b/virttest/vt_resmgr/resources/cvm/conductor.py.bak @@ -0,0 +1,58 @@ +class Conductor(object): + CHANNEL_TYPE = None + + def __init__(self, node_id): + self._channel = None + self._node_id = node_id + + def _worker(self, node_id): + return get_node(node_id) + + def create(self): + pass + + def destroy(self): + pass + + @property + def channel(self): + return self._channel + + +class RPCConductor(Conductor): + CHANNEL_TYPE = 'rpc' + + def __init__(self, node_id): + super().__init__(node_id) + + def create(self): + node = self._worker(self._node_id) + self._channel = node.proxy.virt + + def destroy(self): + self._channel = None + + +class SSHConductor(Conductor): + CHANNEL_TYPE = 'ssh' + + def __init__(self, node_id): + super().__init__(node_id) + + def create(self): + node = self._worker(self._node_id) + self._channel = node.connect(node.connection_auth) + + def destroy(self): + self._channel.close() + self._channel = None + + +class Channel(object): + @staticmethod + def channel(node_id, channel_type): + for cls in Conductor.__subclasses__: + if cls.CHANNEL_TYPE = channel_type: + return cls(node_id) + break + return None diff --git a/virttest/vt_resmgr/resources/pool.py b/virttest/vt_resmgr/resources/pool.py new file mode 100644 index 0000000000..d2a2e6eccc --- /dev/null +++ b/virttest/vt_resmgr/resources/pool.py @@ -0,0 +1,95 @@ +import uuid +from abc import ABC, abstractmethod + + +class _ResourcePool(ABC): + """ + A resource pool is used to manage resources. A resource must be + allocated from a specific pool, and a pool can hold many resources + """ + + _POOL_TYPE = None + + def __init__(self, pool_params): + self._uuid = uuid.uuid4() + self._resources = dict() # {resource id: resource object} + self._accesses = dict() # {node id: pool access object} + + @property + def pool_id(self): + return self._uuid + + @property + @abstractmethod + def pool_config(self): + raise NotImplementedError + + @abstractmethod + def meet_resource_request(self, resource_type, resource_params): + """ + Check if the pool can support a resource's allocation + """ + raise NotImplementedError + + def define_resource_config(self, resource_type, resource_params): + """ + Define the resource configuration, format: + {"meta": {...}, "spec": {...}} + It depends on the specific resource. + """ + res_cls = self.get_resource_class(resource_type) + config = res_cls.define_config(resource_params) + config["spec"]["pool"] = self.pool_id + return config + + @classmethod + @abstractmethod + def get_resource_class(cls, resource_type): + raise NotImplementedError + + def create_resource(self, resource_config): + """ + Create a resource object, no real resource allocated + """ + meta = resource_config["meta"] + res_cls = self.get_resource_class(meta["type"]) + res = res_cls(resource_config) + self._resources[res.resource_id] = res + return res.resource_id + + def destroy_resource(self, resource_id): + """ + Destroy the resource object, all its backings should be released + """ + res = self._resources[resource_id] + res.destroy() + del(self._resources[resource_id]) + + def update_resource(self, resource_id, new_config): + res = self._resources[resource_id] + cmd, args = list(new_config.items())[0] + handler = res.get_handler(cmd) + handler(args) + + def info_resource(self, resource_id): + """ + Get the reference of a specified resource + """ + res = self._resources.get(resource_id) + return res.resource_info + + @property + def attaching_nodes(self): + return self._accesses.keys() + + """ + @property + def pool_capability(self): + node_id = self.attaching_nodes.keys()[0] + node = get_node(node_id) + return node.proxy.get_pool_capability() + """ + + @classmethod + def pool_type(cls): + return cls._POOL_TYPE diff --git a/virttest/vt_resmgr/resources/resource.py b/virttest/vt_resmgr/resources/resource.py new file mode 100644 index 0000000000..a61587a165 --- /dev/null +++ b/virttest/vt_resmgr/resources/resource.py @@ -0,0 +1,125 @@ +import uuid +from abc import ABC, abstractmethod + + +class _ResourceBinding(object): + """ + A binding binds a resource to an allocated resource backing + at a worker node. A resource can have many bindings, but one + binding can only bind one backing at one worker node. + """ + + def __init__(self, node_id): + self._node_id = node_id + self._backing_id = None + + def create_backing(self, resource_config, need_allocate=False): + """ + Create a resource backing object via RPC + """ + node = get_node(self._node_id) + self._backing_id = node.proxy.create_backing(resource_config, need_allocate) + + def destroy_backing(self, need_release=False): + """ + Destroy the resource backing object via RPC + """ + node = get_node(self._node_id) + node.proxy.destroy_backing(self._backing_id, need_release) + + def update_backing(self, spec): + node = get_node(self._node_id) + node.proxy.update_backing(self._backing_id, spec) + + @property + def reference(self): + return {"node": self.node_id, "id": self.backing_id} + + @property + def node_id(self): + """ + Get the node id of the resource backing + """ + return self._node_id + + @property + def backing_id(self): + """ + Get the resource backing id + """ + return self._backing_id + + +class _Resource(ABC): + """ + A resource defines what users request, it's independent of a VM, + users can request a kind of resources for any purpose, it can bind + several allocated resource backings at different worker nodes. + """ + + _RESOURCE_TYPE = None + _RESOURCE_CONFIG_TEMP = {"meta": {"uuid": None, + "type": None, + "bindings": None}, + "spec": {"pool": None}} + + def __init__(self, resource_config): + self._config = resource_config + self.mata["uuid"] = uuid.uuid4() + self._bindings = dict() # {node id: backing id} + self._handlers = { + "bind": self.bind, + "unbind": self.unbind, + } + + @classmethod + def resource_type(cls): + raise cls._RESOURCE_TYPE + + @property + def resource_config(self): + return self._config + + @property + def resource_spec(self): + return self.resource_config["spec"] + + @property + def resource_meta(self): + return self.resource_config["meta"] + + @property + def resource_id(self): + return self.resource_meta["uuid"] + + @property + def resource_pool(self): + return self.resource_spec["pool"] + + @classmethod + @abstractmethod + def define_config(cls, resource_params): + raise NotImplemented + + @property + @abstractmethod + def backing_config(self): + """ + Define the required information of the resource, used + for allocating the resource on the worker nodes + """ + config = dict() + config["uuid"] = self.resource_id + config["pool"] = self.resource_pool + return config + + def get_handler(self, cmd): + return self._handlers.get(cmd) + + @abstractmethod + def bind(self, arguments): + raise NotImplemented + + @abstractmethod + def unbind(self, arguments): + raise NotImplemented diff --git a/virttest/vt_resmgr/resources/storage/__init__.py b/virttest/vt_resmgr/resources/storage/__init__.py new file mode 100644 index 0000000000..116d616d9b --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/__init__.py @@ -0,0 +1,14 @@ +from .dir import _DirPool +from .ceph import _CephPool +#from .nfs import _NfsPool +#from .nbd import _NbdPool +#from .iscsi_direct import _IscsiDirectPool + + +__all__ = ( + _CephPool, + _DirPool, +# _NfsPool, +# _NbdPool, +# _IscsiDirectPool, +) diff --git a/virttest/vt_resmgr/resources/storage/ceph/__init__.py b/virttest/vt_resmgr/resources/storage/ceph/__init__.py new file mode 100644 index 0000000000..8ec3b25a7a --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/ceph/__init__.py @@ -0,0 +1 @@ +from .ceph_pool import _CephPool diff --git a/virttest/vt_resmgr/resources/storage/dir/__init__.py b/virttest/vt_resmgr/resources/storage/dir/__init__.py new file mode 100644 index 0000000000..c09faaf942 --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/dir/__init__.py @@ -0,0 +1 @@ +from .dir_pool import _DirPool diff --git a/virttest/vt_resmgr/resources/storage/dir/dir_pool.py b/virttest/vt_resmgr/resources/storage/dir/dir_pool.py new file mode 100644 index 0000000000..9ca704095c --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/dir/dir_pool.py @@ -0,0 +1,41 @@ +import logging + +from ...pool import _ResourcePool +from .dir_resources import get_dir_resource_class + + +LOG = logging.getLogger("avocado." + __name__) + + +# cartesian params for a Directory pool +#[pool1] +#pool_type = "filesystem" +#pool_name = pool1 +#pool_access_nodes = "node1 node2" +#pool_root_dir = "/root/avocado/images" + +class _DirPool(_ResourcePool): + _POOL_TYPE = "filesystem" + + def __init__(self, pool_params): + super().__init__(pool_params) + self._root_dir = pool_params["pool_root_dir"] + + @classmethod + def get_resource_class(cls, resource_type): + return get_dir_resource_class(resource_type) + + def meet_resource_request(self, resource_type, resource_params): + # Check if the pool can supply a resource with a specified type + cls = self.get_resource_class(resource_type) + if cls is None: + return False + + # Check if this is the pool with the specified type + storage_type = resource_params.get("storage_type") + if storage_type: + if storage_type != self.pool_type: + return False + + # TODO: Check if the pool has capacity to allocate the resource + return True diff --git a/virttest/vt_resmgr/resources/storage/dir/dir_resources.py b/virttest/vt_resmgr/resources/storage/dir/dir_resources.py new file mode 100644 index 0000000000..3ffc907f7a --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/dir/dir_resources.py @@ -0,0 +1,70 @@ +import logging +import os +from copy import deepcopy + +from virttest import utils_numeric +from ..volume import _FileVolume + + +LOG = logging.getLogger("avocado." + __name__) + + +class _DirFileVolume(_FileVolume): + """ + The directory file-based volume + """ + + _RESOURCE_CONFIG_TEMP = deepcopy(_FileVolume._RESOURCE_CONFIG_TEMP) + + @classmethod + def _define_config_legacy(cls, resource_params): + config = deepcopy(_RESOURCE_CONFIG_TEMP) + spec = config["spec"] + spec["allocation"] = 0 + spec["size"] = utils_numeric.normalize_data_size( + resource_params.get("image_size", "20G"), order_magnitude="B" + ) + image_name = resource_params.get("image_name", "image") + if os.path.isabs(image_name): + spec["path"] = image_name + else: + image_format = params.get("image_format", "qcow2") + spec["filename"] = "%s.%s" % (image_name, image_format) + return config + + @classmethod + def define_config(cls, resource_params): + return _define_config_legacy(resource_params) + + def bind(self, arguments): + """ + Bind the resource to a backing on a worker node. A local dir + resource has one and only one binding, resource is allocated + when creating the binding + """ + nodes = arguments["nodes"] + if len(nodes) != 1: + LOG.warning("A dir resource should have one binding only") + + node = get_node(nodes[0]) + backing_id = node.proxy.create_backing(self.backing_config, True) + self._bindings[nodes[0]] = backing_id + + def unbind(self, arguments): + """ + Unbind the resource from a backing on a worker node, resource + is released when destroying the binding + """ + node_id, backing_id = self._bindings.popitem(0) + node = get_node(node_id) + node.proxy.destroy_backing(backing_id, True) + + def resize(self, arguments): + """ + Resize the local dir volume resource + """ + pass + + +def get_resource_class(resource_type): + return _DirFileVolume diff --git a/virttest/vt_resmgr/resources/storage/iscsi_direct/__init__.py b/virttest/vt_resmgr/resources/storage/iscsi_direct/__init__.py new file mode 100644 index 0000000000..4631b968fa --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/iscsi_direct/__init__.py @@ -0,0 +1 @@ +from .iscsi_direct_pool import _IscsiDirectPool diff --git a/virttest/vt_resmgr/resources/storage/iscsi_direct/iscsi_direct_pool.py b/virttest/vt_resmgr/resources/storage/iscsi_direct/iscsi_direct_pool.py new file mode 100644 index 0000000000..33738d1af9 --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/iscsi_direct/iscsi_direct_pool.py @@ -0,0 +1,20 @@ +import logging + +from ...resource import _Resource +from ...pool import _ResourcePool + + +LOG = logging.getLogger("avocado." + __name__) + + +class _IscsiDirectResource(_Resource): + """ + The iscsi-direct pool resource + """ + + def _initialize(self, config): + self._lun = config["lun"] + + +class _IscsiDirectPool(_ResourcePool): + POOL_TYPE = "iscsi-direct" diff --git a/virttest/vt_resmgr/resources/storage/nbd/__init__.py b/virttest/vt_resmgr/resources/storage/nbd/__init__.py new file mode 100644 index 0000000000..8a29e248f3 --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/nbd/__init__.py @@ -0,0 +1 @@ +from .nbd_pool import _NbdPool diff --git a/virttest/vt_resmgr/resources/storage/nfs/__init__.py b/virttest/vt_resmgr/resources/storage/nfs/__init__.py new file mode 100644 index 0000000000..a0e90ec573 --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/nfs/__init__.py @@ -0,0 +1 @@ +from .nfs_pool import _NfsPool diff --git a/virttest/vt_resmgr/resources/storage/nfs/nfs_pool.py b/virttest/vt_resmgr/resources/storage/nfs/nfs_pool.py new file mode 100644 index 0000000000..8b7ea10f87 --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/nfs/nfs_pool.py @@ -0,0 +1,55 @@ +import logging + +from ...pool import _ResourcePool +from .nfs_resources import get_resource_class + + +LOG = logging.getLogger("avocado." + __name__) + + +# Configure two nfs pools +#[nfs_pool1] +#pool_type = "nfs" +#pool_access_nodes = "node1 node2" +#nfs_server_ip = +#nfs_mount_src = +#nfs_mount_dir = +#nfs_mount_options = +#[nfs_pool2] +#pool_type = "nfs" +#pool_access_nodes = "node1 node2" +#nfs_server_ip = +#nfs_mount_src = +#nfs_mount_dir = +#nfs_mount_options = +class _NfsPool(_ResourcePool): + _POOL_TYPE = "nfs" + + def __init__(self, pool_params): + super().__init__(pool_params) + self._nfs_server = pool_params["nfs_server_ip"] + self._export_dir = pool_params["nfs_mount_src"] + + @classmethod + def get_resource_class(cls, resource_type): + return get_resource_class(resource_type) + + def meet_resource_request(self, resource_type, resource_params): + # Check if the pool is the specified one + pool_name = resource_params.get("image_pool_name") + if pool_name == self.pool_name: + return True + + # Check if the pool can supply a resource with a specified type + cls = self.get_resource_class(resource_type) + if cls is None: + return False + + # Check if the is the pool with the specified type + storage_type = resource_params.get("storage_type") + if storage_type: + if storage_type != self.pool_type: + return False + + # TODO: Check if the pool has capacity to allocate the resource + return True diff --git a/virttest/vt_resmgr/resources/storage/nfs/nfs_resources.py b/virttest/vt_resmgr/resources/storage/nfs/nfs_resources.py new file mode 100644 index 0000000000..e056ef0517 --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/nfs/nfs_resources.py @@ -0,0 +1,47 @@ +import logging + +from ..volume import _FileVolume +from .nfs_resource_handlers import get_nfs_resource_handler + + +LOG = logging.getLogger("avocado." + __name__) + + +class _NfsFileVolume(_FileVolume): + """ + The nfs file-based volume + """ + _RESOURCE_CONFIG_TEMP = deepcopy(_FileVolume._RESOURCE_CONFIG_TEMP) + + @classmethod + def define_config(cls, volume_name, volume_params): + pass + + def _create_binding(self, node_id, need_allocate=False): + binding = _ResourceBinding(node_id) + binding.create_backing(self.backing_config, need_allocate) + self._bindings[node_id] = binding + + def _destroy_binding(self, node_id): + need_release = True if len(self._bindings) == 1 else False + binding = self._bindings[node_id] + binding.destroy_backing(need_release) + del self._bindings[node_id] + + def bind(self, arguments): + nodes = arguments["nodes"] + for node_id in nodes: + self._create_binding(self, node_id) + + def unbind(self, arguments): + nodes = arguments.get("nodes") + nodes = nodes or list(self._bindings.keys()) + for node_id in nodes: + self._destroy_binding(self, node_id) + + def resize(self, arguments): + pass + + +def get_resource_class(resource_type): + return _NfsFileVolume diff --git a/virttest/vt_resmgr/resources/storage/storage_pool.py b/virttest/vt_resmgr/resources/storage/storage_pool.py new file mode 100644 index 0000000000..b6349b8c12 --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/storage_pool.py @@ -0,0 +1,46 @@ +import logging + +from ..pool import _ResourcePool + + +LOG = logging.getLogger("avocado." + __name__) + + +class _StoragePool(_ResourcePool): + + @classmethod + def define_resource_config(cls, resource_name, resource_type, resource_params): + cls = get_resource_class(resource_type) + config = cls.define_config(resource_name, resource_params) + config["spec"]["pool"] = self.pool_id + return config + + def meet_resource_request(self, resource_type, resource_params): + # Check if the pool is the specified one + pool_name = resource_params.get("image_pool_name") + if pool_name == self.pool_name: + return True + + # Check if the pool can supply a resource with a specified type + cls = get_resource_class(resource_type) + if cls is None: + return False + + # Check if the is the pool with the specified type + storage_type = resource_params.get("storage_type") + if storage_type: + if storage_type != self.pool_type: + return False + + # TODO: Check if the pool has capacity to allocate the resource + return True + + def create_resource(self, resource_config): + meta = resource_config["meta"] + cls = get_resource_class(meta["type"]) + res = cls(resource_config) + self._resources[res.resource_id] = res + return res.resource_id + + def update_resource(self, resource_id, config): + res = self._resources[resource_id] diff --git a/virttest/vt_resmgr/resources/storage/volume.py b/virttest/vt_resmgr/resources/storage/volume.py new file mode 100644 index 0000000000..29309a1a74 --- /dev/null +++ b/virttest/vt_resmgr/resources/storage/volume.py @@ -0,0 +1,92 @@ +from copy import deepcopy + +from ..resource import _Resource + + +class _Volume(_Resource): + """ + Storage volumes are abstractions of physical partitions, + LVM logical volumes, file-based disk images + """ + + _RESOURCE_TYPE = "volume" + _VOLUME_TYPE = None + _RESOURCE_CONFIG_TEMP = deepcopy(_Resource._RESOURCE_CONFIG_TEMP) + _RESOURCE_CONFIG_TEMP["meta"].update({"type": "volume", + "raw": None}) + _RESOURCE_CONFIG_TEMP["spec"].update({"size": None, + "allocation": None}) + + @classmethod + def volume_type(cls): + return cls._VOLUME_TYPE + + @classmethod + def define_config(cls, resource_params): + raise NotImplemented + + @property + def backing_config(self): + config = super().backing_config + config.update({ + "size": self.resource_spec["size"], + "type": self.resource_type, + "volume_type": self.volume_type, + }) + return config + + +class _FileVolume(_Volume): + """For file based volumes""" + + _VOLUME_TYPE = "file" + _RESOURCE_CONFIG_TEMP = deepcopy(_Volume._RESOURCE_CONFIG_TEMP) + _RESOURCE_CONFIG_TEMP["spec"].update({"path": None, + "filename": None}) + + def __init__(self, resource_config): + super().__init__(resource_config) + self._handlers.update({ + "resize": self.resize, + } + + @classmethod + def define_config(cls, resource_params): + raise NotImplemented + + @property + def backing_config(self): + config = super().backing_config + config.update({ + "path": self.resource_spec["path"], + }) + return config + + def resize(self, arguments): + raise NotImplemented + + +class _BlockVolume(_Volume): + """For disk, lvm, iscsi based volumes""" + + _VOLUME_TYPE = "block" + _RESOURCE_CONFIG_TEMP = deepcopy(_Volume._RESOURCE_CONFIG_TEMP) + _RESOURCE_CONFIG_TEMP["spec"].update({"path": None}) + + @classmethod + def define_config(cls, resource_params): + raise NotImplemented + + @property + def backing_config(self): + config = super().backing_config + config.update({ + "path": self.resource_spec["path"], + }) + return config + + +class _NetworkVolume(_Volume): + """For rbd, iscsi-direct based volumes""" + + _VOLUME_TYPE = "network" diff --git a/virttest/vt_resmgr/vt_resmgr.py b/virttest/vt_resmgr/vt_resmgr.py new file mode 100644 index 0000000000..ce90076ee2 --- /dev/null +++ b/virttest/vt_resmgr/vt_resmgr.py @@ -0,0 +1,117 @@ +from .resources import get_resource_pool_class + + +class _VTResourceManager(object): + + def __init__(self): + self._pools = dict() # {pool id: pool object} + + def setup(self, pool_config_list): + for config in pool_config_list: + pool_id = self.register_pool(config) + self.attach_pool(pool_id) + + def teardown(self): + for pool_id in self.pools: + self.detach_pool(pool_id) + self.unregister_pool(pool_id) + + def get_pool_by_name(self, pool_name): + pools = [p for p in self.pools.values() if p.pool_name == pool_name] + return pools[0] if pools else None + + def get_pool_by_id(self, pool_id): + return self.pools.get(pool_id) + + def get_pool_by_resource(self, resource_id): + pools = [p for p in self.pools.values() if resource_id in p.resources] + return pools[0] if pools else None + + def select_pool(self, resource_type, resource_params): + """ + Select the resource pool by its cartesian params + + :param resource_type: The resource's type, supported: + "volume" + :type resource_type: string + :param resource_params: The resource's specific params, e.g. + params.object_params('image1') + :type resource_params: dict or Param + :return: The resource pool id + :rtype: string + """ + for pool_id, pool in self.pools.items(): + if pool.meet_resource_request(resource_type, resource_params): + return pool_id + return None + + def register_pool(self, pool_params): + pool_type = pool_params["pool_type"] + pool_class = get_resource_pool_class(pool_type) + pool = pool_class(pool_params) + self._pools[pool.pool_id] = pool + return pool.pool_id + + def unregister_pool(self, pool_id): + """ + The pool should be detached from all worker nodes + """ + pool = self.pools[pool_id] + if pool.is_attached(): + raise + del self._pools[pool_id] + + def attach_pool_to(self, pool, node): + """ + Attach a pool to a specific node + """ + access_config = pool.attaching_nodes[node.node_id] + node.proxy.connect_pool(pool.pool_id, pool.pool_config) + + def attach_pool(self, pool_id): + pool = self.get_pool_by_id(pool_id) + for node_id in pool.attaching_nodes: + node = get_node(node_id) + self.attach_pool_to(pool, node) + + def detach_pool_from(self, pool, node): + """ + Detach a pool from a specific node + """ + node.proxy.disconnect_pool(pool.pool_id) + + def detach_pool(self, pool_id): + pool = self.get_pool_by_id(pool_id) + for node_id in pool.attaching_nodes: + node = get_node(node_id) + self.detach_pool_from(pool, node) + + def info_pool(self, pool_id): + """ + Get the pool's information, including 'meta' and 'spec': + meta: + e.g. version for tdx, 1.0 or 1.5 + spec: + common specific attributes + e.g. nfs_server for nfs pool + node-specific attributes + e.g. [node1:{path:/mnt1,permission:rw}, node2:{}] + """ + info = dict() + pool = self.get_pool_by_id(pool_id) + info.update(pool.pool_config) + for node_id in pool.attaching_nodes: + node = get_node(node_id) + access_info = node.proxy.get_pool_connection(pool_id) + info.update(access_info) + + def pool_capability(self, pool_id): + pool = self.get_pool_by_id(pool_id) + return pool.capability + + @property + def pools(self): + return self._pools + + +vt_resmgr = _VTResourceManager()