diff --git a/.gitignore b/.gitignore index bef4369..01dae40 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,8 @@ tests/.pytest_cache # Remove directory __pycache__ __pycache__ venv + +.idea +build +sunfish.egg-info + diff --git a/pyproject.toml b/pyproject.toml index ed558b1..9b09bf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,9 +5,14 @@ description = "" authors = ["Erika Rosaverde "] readme = "README.md" packages = [ - { include = "sunfish" } + { include = "sunfish" }, + { include = "sunfish_plugins", from = "." } ] +[tool.setuptools.packages.find] +where = ["."] +include = ["sunfish_plugins.*"] + [tool.poetry.dependencies] python = ">=3.9" flask = "^2.3.3" diff --git a/sunfish/lib/core.py b/sunfish/lib/core.py index 42de8db..fc509a7 100644 --- a/sunfish/lib/core.py +++ b/sunfish/lib/core.py @@ -8,15 +8,14 @@ import logging from typing import Optional -from sunfish.storage.backend_FS import BackendFS from sunfish.lib.exceptions import CollectionNotSupported, ResourceNotFound, AgentForwardingFailure, PropertyNotFound -from sunfish.events.redfish_event_handler import RedfishEventHandler, RedfishEventHandlersTable +from sunfish_plugins.event_handlers.redfish.redfish_event_handler import RedfishEventHandler from sunfish.events.redfish_subscription_handler import RedfishSubscriptionHandler from sunfish.lib.object_handler import RedfishObjectHandler from sunfish.models.types import * from sunfish.lib.agents_management import Agent - +import sunfish.models.plugins as plugin_modules logger = logging.getLogger(__name__) @@ -31,17 +30,62 @@ def __init__(self, conf): self.conf = conf - if conf['storage_backend'] == 'FS': - self.storage_backend = BackendFS(self.conf) - # elif conf['storage_backend'] == '': - # ... + # The Sunfish core library uses a plugin mechanism that allows dynamic loading of certain classes. This helps + # users with updating the behavior of the sunfish library without having to modify its core classes. + # At the moment we support plugins for the storage backend and for the redfish event handlers. + # Plugins are implemented as namespaced packages and must be placed in a folder at the top of the project named + # "sunfish_plugins", with subfolders named "storage" and/or "event_handlers". The python packages defined inside + # each subfolder are totally user defined. + # ── sunfish_plugins + # ├── storage + # │ └──my_storage_package <--- User defined + # │ ├── __init__.py + # │ └── my_storage_backend.py + # └── event_handlers + # └──my_handler_package <--- User defined + # ├── __init__.py + # └── my_handler.py + + # When initializing the Sunfish libraries can load their storage or event handler plugin by specifying them in + # the configuration as in the below example: + # + # "storage_backend" : { + # "module_name": "storage.my_storage_package.my_storage_backend", + # "class_name": "StorageBackend" + # }, + # "event_backend" : { + # "module_name": "event-handlers.my_handler_package.my_handler", + # "class_name": "StorageBackend" + # }, + # + # In both cases "class_name" represents the name of the class that is initialized and implements the respective + # interface. + + # Default storage plugin loaded if nothing is specified in the configuration + if not "storage_backend" in conf: + storage_plugin = { + "module_name": "storage.file_system_backend.backend_FS", + "class_name": "BackendFS" + } + else: + storage_plugin = conf["storage_backend"] + storage_cl = plugin_modules.load_plugin(storage_plugin) + self.storage_backend = storage_cl(self.conf) + + # Default event_handler plugin loaded if nothing is specified in the configuration + if not "event_handler" in conf: + event_plugin = { + "module_name": "event_handlers.redfish.redfish_event_handler", + "class_name": "RedfishEventHandler" + } + else: + event_plugin = conf["event_handler"] + event_cl = plugin_modules.load_plugin(event_plugin) + self.event_handler = event_cl(self) if conf['handlers']['subscription_handler'] == 'redfish': self.subscription_handler = RedfishSubscriptionHandler(self) - if conf['handlers']['event_handler'] == 'redfish': - self.event_handler = RedfishEventHandler(self) - def get_object(self, path: string): """Calls the correspondent read function from the backend implementation and checks that the path is valid. diff --git a/sunfish/models/plugins.py b/sunfish/models/plugins.py new file mode 100644 index 0000000..80f3ccd --- /dev/null +++ b/sunfish/models/plugins.py @@ -0,0 +1,19 @@ +# Copyright IBM Corp. 2024 +# This software is available to you under a BSD 3-Clause License. +# The full license terms are available here: https://github.com/OpenFabrics/sunfish_library_reference/blob/main/LICENSE + +import importlib +import logging + +logger = logging.getLogger(__name__) + +plugins_namespace_name = "sunfish_plugins" + + +def load_plugin(plugin: dict): + module_name = f"{plugins_namespace_name}.{plugin['module_name']}" + logger.info(f"Loading plugin {module_name}...") + plugin_module = importlib.import_module(module_name) + logger.info("Plugin loaded") + return getattr(plugin_module, plugin["class_name"]) + diff --git a/sunfish_plugins/event_handlers/redfish/__init__.py b/sunfish_plugins/event_handlers/redfish/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sunfish/events/redfish_event_handler.py b/sunfish_plugins/event_handlers/redfish/redfish_event_handler.py similarity index 99% rename from sunfish/events/redfish_event_handler.py rename to sunfish_plugins/event_handlers/redfish/redfish_event_handler.py index bcf84d2..13b57d0 100644 --- a/sunfish/events/redfish_event_handler.py +++ b/sunfish_plugins/event_handlers/redfish/redfish_event_handler.py @@ -4,7 +4,6 @@ import json import logging import os -from uuid import uuid4 import warnings import requests diff --git a/sunfish_plugins/storage/file_system_backend/__init__.py b/sunfish_plugins/storage/file_system_backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sunfish/storage/backend_FS.py b/sunfish_plugins/storage/file_system_backend/backend_FS.py similarity index 99% rename from sunfish/storage/backend_FS.py rename to sunfish_plugins/storage/file_system_backend/backend_FS.py index e3bdbaa..c9aed0b 100644 --- a/sunfish/storage/backend_FS.py +++ b/sunfish_plugins/storage/file_system_backend/backend_FS.py @@ -8,7 +8,7 @@ import shutil from sunfish.storage.backend_interface import BackendInterface -from sunfish.storage import utils +from sunfish_plugins.storage.file_system_backend import utils from sunfish.lib.exceptions import * logger = logging.getLogger(__name__) diff --git a/sunfish/storage/utils.py b/sunfish_plugins/storage/file_system_backend/utils.py similarity index 100% rename from sunfish/storage/utils.py rename to sunfish_plugins/storage/file_system_backend/utils.py diff --git a/tests/conf.json b/tests/conf.json index 6689fd2..2a0347f 100644 --- a/tests/conf.json +++ b/tests/conf.json @@ -1,10 +1,17 @@ { - "storage_backend": "FS", "redfish_root": "/redfish/v1/", + "storage_backend": { + "module_name": "storage.file_system_backend.backend_FS", + "class_name": "BackendFS" + }, "backend_conf" : { "fs_root": "Resources", "subscribers_root": "EventService/Subscriptions" }, + "event_handler": { + "module_name": "event_handlers.redfish.redfish_event_handler", + "class_name": "RedfishEventHandler" + }, "handlers": { "subscription_handler": "redfish", "event_handler": "redfish" diff --git a/tests/test_sunfishcore_library.py b/tests/test_sunfishcore_library.py index 44fd9ab..7220823 100644 --- a/tests/test_sunfishcore_library.py +++ b/tests/test_sunfishcore_library.py @@ -6,15 +6,12 @@ # from http.server import BaseHTTPRequestHandler import json import os -import shutil import logging import pytest -import requests from pytest_httpserver import HTTPServer from sunfish.lib.core import Core from sunfish.lib.exceptions import * from tests import test_utils, tests_template - class TestSunfishcoreLibrary(): @classmethod def setup_class(cls): @@ -27,6 +24,17 @@ def setup_class(cls): cls.core = Core(cls.conf) + @pytest.mark.order("first") + def test_init_core(self): + path = os.path.join(os.getcwd(), 'tests', 'conf.json') + try: + json_data = open(path) + conf = json.load(json_data) + except FileNotFoundError as e: + raise ResourceNotFound('conf.json') + + core = Core(conf) + # TEST REST # Delete @pytest.mark.order("last")