Skip to content

Commit

Permalink
Plugin mechanisms for redfish objects management
Browse files Browse the repository at this point in the history
Signed-off-by: Christian Pinto <[email protected]>
  • Loading branch information
christian-pinto committed Jun 25, 2024
1 parent cfb2228 commit 5f605b9
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 103 deletions.
102 changes: 38 additions & 64 deletions sunfish/lib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@
from typing import Optional

from sunfish.lib.exceptions import CollectionNotSupported, ResourceNotFound, AgentForwardingFailure, PropertyNotFound
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
Expand All @@ -34,17 +32,21 @@ def __init__(self, conf):
# 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", with subfolders named "storage" and/or "event_handlers" and/or "objects_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
# │ └──my_handler_package <--- User defined
# │ ├── __init__.py
# │ └── my_handler.py
# ├── object_handlers
# │ └──my_objects_handler_package <--- User defined
# │ ├── __init__.py
# │ └── my_objects_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:
Expand All @@ -53,16 +55,20 @@ def __init__(self, conf):
# "module_name": "storage.my_storage_package.my_storage_backend",
# "class_name": "StorageBackend"
# },
# "event_backend" : {
# "module_name": "event-handlers.my_handler_package.my_handler",
# "event_handler" : {
# "module_name": "event_handlers.my_handler_package.my_handler",
# "class_name": "StorageBackend"
# },
# "objects_handler" : {
# "module_name": "objects_handlers.my_objects_handler_package.my_objects_handler",
# "class_name": "StorageBackend"
# },
#
# In both cases "class_name" represents the name of the class that is initialized and implements the respective
# In all 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:
if "storage_backend" not in conf:
storage_plugin = {
"module_name": "storage.file_system_backend.backend_FS",
"class_name": "BackendFS"
Expand All @@ -73,7 +79,7 @@ def __init__(self, conf):
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:
if "event_handler" not in conf:
event_plugin = {
"module_name": "event_handlers.redfish.redfish_event_handler",
"class_name": "RedfishEventHandler"
Expand All @@ -83,6 +89,17 @@ def __init__(self, conf):
event_cl = plugin_modules.load_plugin(event_plugin)
self.event_handler = event_cl(self)

# Default objects_handler plugin loaded if nothing is specified in the configuration
if "objects_handler" not in conf:
objects_plugin = {
"module_name": "objects_handlers.sunfish_server.redfish_object_handler",
"class_name": "RedfishObjectHandler"
}
else:
objects_plugin = conf["objects_handler"]
objects_handler_cl = plugin_modules.load_plugin(objects_plugin)
self.objects_handler = objects_handler_cl(self)

if conf['handlers']['subscription_handler'] == 'redfish':
self.subscription_handler = RedfishSubscriptionHandler(self)

Expand Down Expand Up @@ -123,7 +140,7 @@ def create_object(self, path: string, payload: dict):
# before to add the ID and to call the methods there should be the json validation

# generate unique uuid if is not present
if not '@odata.id' in payload and not 'Id' in payload:
if '@odata.id' not in payload and 'Id' not in payload:
id = str(uuid.uuid4())
to_add = {
'Id': id,
Expand All @@ -142,11 +159,11 @@ def create_object(self, path: string, payload: dict):
# 1. check the path target of the operation exists
# self.storage_backend.read(path)
# 2. is needed first forward the request to the agent managing the object
agent_response = self._forward_to_agent(SunfishRequestType.CREATE, path, payload=payload)
agent_response = self.objects_handler.forward_to_manager(SunfishRequestType.CREATE, path, payload=payload)
if agent_response:
payload_to_write = agent_response
# 3. Execute any custom handler for this object type
RedfishObjectHandler.dispatch(self, object_type, path, SunfishRequestType.CREATE, payload=payload)
self.objects_handler.dispatch(object_type, path, SunfishRequestType.CREATE, payload=payload)
except ResourceNotFound:
logger.error("The collection where the resource is to be created does not exist.")
except AgentForwardingFailure as e:
Expand Down Expand Up @@ -175,9 +192,9 @@ def replace_object(self, path: str, payload: dict):
# 1. check the path target of the operation exists
self.storage_backend.read(path)
# 2. is needed first forward the request to the agent managing the object
self._forward_to_agent(SunfishRequestType.REPLACE, path, payload=payload)
self.objects_handler.forward_to_manager(SunfishRequestType.REPLACE, path, payload=payload)
# 3. Execute any custom handler for this object type
RedfishObjectHandler.dispatch(self, object_type, path, SunfishRequestType.REPLACE, payload=payload)
self.objects_handler.dispatch(object_type, path, SunfishRequestType.REPLACE, payload=payload)
except ResourceNotFound:
logger.error(logger.error(f"The resource to be replaced ({path}) does not exist."))
except AttributeError:
Expand Down Expand Up @@ -205,9 +222,9 @@ def patch_object(self, path: str, payload: dict):
# 1. check the path target of the operation exists
self.storage_backend.read(path)
# 2. is needed first forward the request to the agent managing the object
self._forward_to_agent(SunfishRequestType.PATCH, path, payload=payload)
self.objects_handler.forward_to_manager(SunfishRequestType.PATCH, path, payload=payload)
# 3. Execute any custom handler for this object type
RedfishObjectHandler.dispatch(self, object_type, path, SunfishRequestType.PATCH, payload=payload)
self.objects_handler.dispatch(object_type, path, SunfishRequestType.PATCH, payload=payload)
except ResourceNotFound:
logger.error(f"The resource to be patched ({path}) does not exist.")
except AttributeError:
Expand Down Expand Up @@ -236,9 +253,9 @@ def delete_object(self, path: string):
# 1. check the path target of the operation exists
self.storage_backend.read(path)
# 2. is needed first forward the request to the agent managing the object
self._forward_to_agent(SunfishRequestType.DELETE, path)
self.objects_handler.forward_to_manager(SunfishRequestType.DELETE, path)
# 3. Execute any custom handler for this object type
RedfishObjectHandler.dispatch(self, object_type, path, SunfishRequestType.DELETE)
self.objects_handler.dispatch(object_type, path, SunfishRequestType.DELETE)
except ResourceNotFound:
logger.error(f"The resource to be deleted ({path}) does not exist.")
except AttributeError:
Expand Down Expand Up @@ -277,46 +294,3 @@ def _get_type(self, payload: dict, path: str = None):
raise PropertyNotFound("@odata.type")

return object_type

def _forward_to_agent(self, request_type: SunfishRequestType, path: string, payload: dict = None) -> Optional[dict]:
agent_response = None
path_to_check = path
if request_type == SunfishRequestType.CREATE:
# When creating an object, the request must be done on the Collection. Since collections are generally not
# marked with the managing agent we check whether the parent of the collection, that must be a single entity
# is managed by an agent.
# Example create a Fabric connections on a fabric named CXL would be issued against
# /redfish/v1/Fabrics/CXL/Connections
# The connections collection does not have an agent but the parent CXL fabric does and that's what we are
# going to use.
# The only place where this might not be working is if the collection we post to is a top level one like:
# /redfish/v1/Systems
# in this case there would be no parent to inherit the agent from. Here this creation request should be
# rejected because in Sunfish only agents can create elements in the top level directories and this is done
# via events.
path_elems = path.split("/")[1:-1]
path_to_check = "".join(f"/{e}" for e in path_elems)
# get the parent path
logger.debug(f"Checking managing agent for path: {path_to_check}")
agent = Agent.is_agent_managed(self, path_to_check)
if agent:
logger.debug(f"{path} is managed by an agent, forwarding the request")
try:
agent_response = agent.forward_request(request_type, path, payload=payload)
except AgentForwardingFailure as e:
raise e

if request_type == SunfishRequestType.CREATE:
# mark the resource with the managing agent
oem = {
"@odata.type": "#SunfishExtensions.v1_0_0.ResourceExtensions",
"ManagingAgent": {
"@odata.id": agent.get_id()
}
}
if "Oem" not in agent_response:
agent_response["Oem"] = {}
agent_response["Oem"]["Sunfish_RM"] = oem
else:
logger.debug(f"{path} is not managed by an agent")
return agent_response
39 changes: 0 additions & 39 deletions sunfish/lib/object_handler.py

This file was deleted.

18 changes: 18 additions & 0 deletions sunfish/lib/object_handler_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 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 string
from abc import abstractmethod
from typing import Optional


class ObjectHandlerInterface:
@abstractmethod
def dispatch(self, object_type: str, path: str,
operation: 'sunfish.models.types.SunfishRequestType', payload: dict = None):
pass

@abstractmethod
def forward_to_manager(self, request_type: 'sunfish.models.types.SunfishRequestType', path: string, payload: dict = None) -> Optional[dict]:
pass

Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright IBM Corp. 2023
# 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 logging
import string
from typing import Optional

import sunfish.lib.core
from sunfish.lib.agents_management import Agent
from sunfish.lib.exceptions import AgentForwardingFailure
from sunfish.lib.object_handler_interface import ObjectHandlerInterface
from sunfish.models.types import *

logger = logging.getLogger("RedfishObjectHandler")


class RedfishObjectHandlersTable:
@classmethod
def ComputerSystem(cls, core: 'sunfish.lib.core.Core', path: str, operation: SunfishRequestType, payload: dict):
return "ObjectHandler ComputerSystem"

@classmethod
def EventDestination(cls, core: 'sunfish.lib.core.Core', path: str, operation: SunfishRequestType, payload: dict):
if operation == SunfishRequestType.CREATE:
core.subscription_handler.new_subscription(payload)
elif operation == SunfishRequestType.REPLACE or operation == SunfishRequestType.PATCH:
core.subscription_handler.delete_subscription(payload)
core.subscription_handler.new_subscription(payload)
elif operation == SunfishRequestType.DELETE:
core.subscription_handler.delete_subscription(path)


class RedfishObjectHandler(ObjectHandlerInterface):
dispatch_table = {
"ComputerSystem": RedfishObjectHandlersTable.ComputerSystem,
"EventDestination": RedfishObjectHandlersTable.EventDestination
}

def __init__(self, core: 'sunfish.lib.core.Core'):
self.core = core

def dispatch(self, object_type: str, path: str,
operation: SunfishRequestType, payload: dict = None):
if object_type in self.dispatch_table:
return self.dispatch_table[object_type](self.core, path, operation, payload)
logger.debug(f"Object type '{object_type}' does not have a custom handler")

def forward_to_manager(self, request_type: 'sunfish.models.types.SunfishRequestType', path: string, payload: dict = None) -> Optional[dict]:
agent_response = None
path_to_check = path
if request_type == SunfishRequestType.CREATE:
# When creating an object, the request must be done on the Collection. Since collections are generally not
# marked with the managing agent we check whether the parent of the collection, that must be a single entity
# is managed by an agent.
# Example create a Fabric connections on a fabric named CXL would be issued against
# /redfish/v1/Fabrics/CXL/Connections
# The connections collection does not have an agent but the parent CXL fabric does and that's what we are
# going to use.
# The only place where this might not be working is if the collection we post to is a top level one like:
# /redfish/v1/Systems
# in this case there would be no parent to inherit the agent from. Here this creation request should be
# rejected because in Sunfish only agents can create elements in the top level directories and this is done
# via events.
path_elems = path.split("/")[1:-1]
path_to_check = "".join(f"/{e}" for e in path_elems)
# get the parent path
logger.debug(f"Checking managing agent for path: {path_to_check}")
agent = Agent.is_agent_managed(self.core, path_to_check)
if agent:
logger.debug(f"{path} is managed by an agent, forwarding the request")
try:
agent_response = agent.forward_request(request_type, path, payload=payload)
except AgentForwardingFailure as e:
raise e

if request_type == SunfishRequestType.CREATE:
# mark the resource with the managing agent
oem = {
"@odata.type": "#SunfishExtensions.v1_0_0.ResourceExtensions",
"ManagingAgent": {
"@odata.id": agent.get_id()
}
}
if "Oem" not in agent_response:
agent_response["Oem"] = {}
agent_response["Oem"]["Sunfish_RM"] = oem
else:
logger.debug(f"{path} is not managed by an agent")
return agent_response
4 changes: 4 additions & 0 deletions tests/conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@
"handlers": {
"subscription_handler": "redfish",
"event_handler": "redfish"
},
"objects_handler": {
"module_name": "objects_handlers.sunfish_server.redfish_object_handler",
"class_name": "RedfishObjectHandler"
}
}

0 comments on commit 5f605b9

Please sign in to comment.