Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ build:
poetry build

test:
rm -rf ./Resources
cp -rp ./tests/Resources .
python3 -m pytest tests/test_sunfishcore_library.py -vvvv

clean:
Expand Down
108 changes: 104 additions & 4 deletions sunfish_plugins/events_handlers/redfish/redfish_event_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ def AggregationSourceDiscovered(cls, event_handler: EventHandlerInterface, event
connectionMethodId = event['OriginOfCondition']['@odata.id']
hostname = event['MessageArgs'][1] # Agent address

response = requests.get(f"{hostname}/{connectionMethodId}")
#response = requests.get(f"{hostname}/{connectionMethodId}")
response = requests.get(f"{hostname}{connectionMethodId}")
if response.status_code != 200:
raise Exception("Cannot find ConnectionMethod")
response = response.json()
Expand Down Expand Up @@ -83,7 +84,7 @@ def ResourceCreated(cls, event_handler: EventHandlerInterface, event: dict, cont
# sunfishAliasDB contains renaming data, the alias xref array, the boundaryLink
# data, and assorted flags that are used during upload renaming and final merge of
# boundary components based on boundary links.
#
#

logger.info("New resource created")

Expand Down Expand Up @@ -111,7 +112,7 @@ def ResourceCreated(cls, event_handler: EventHandlerInterface, event: dict, cont
if "@odata.id" not in response:
# should never hit this!
logger.warning(f"Resource {id} did not have @odata.id set when retrieved from Agent. Initializing its value with {id}")
response["odata.id"] = id
response["@odata.id"] = id

# New resource should not exist in Sunfish inventory
length = len(event_handler.core.conf["redfish_root"])
Expand All @@ -133,6 +134,76 @@ def ResourceCreated(cls, event_handler: EventHandlerInterface, event: dict, cont
logger.debug(f"\n{json.dumps(aggregation_source, indent=4)}")
return 200

@classmethod
def ResourceChanged(cls, event_handler: EventHandlerInterface, event: dict, context: str):
"""
Handles a ResourceChanged event from an agent.
This handler fetches the updated resource from the agent, translates its
URI to the corresponding Sunfish URI, and patches the existing object in
the database with the new properties.
"""
#pdb.set_trace()
try:
if "OriginOfCondition" not in event or not event["OriginOfCondition"].get("@odata.id"):
logger.error("ResourceChanged event is missing OriginOfCondition.")
return

if not context:
logger.error("No context (AggregationSource ID) in ResourceChanged event.")
return

aggregation_source_id = context
aggregation_source = event_handler.core.storage_backend.read(f"/redfish/v1/AggregationService/AggregationSources/{aggregation_source_id}")
host = aggregation_source["HostName"]
origin_of_condition = event["OriginOfCondition"]["@odata.id"]

# Fetch the updated resource from the agent
logger.info(f"Fetching updated resource {origin_of_condition} from agent {aggregation_source_id} at {host}")
resource_endpoint = host + origin_of_condition
response = requests.get(resource_endpoint)
if response.status_code != 200:
logger.error(f"Could not fetch resource {origin_of_condition} from agent {aggregation_source_id}. Status: {response.status_code}")
return
updated_resource = response.json()

# URI Aliasing to find the object in Sunfish
sunfish_uri = RedfishEventHandler.xlateToSunfishPath(event_handler.core, origin_of_condition, aggregation_source)

# Check if object exists before attempting to patch
try:
event_handler.core.storage_backend.read(sunfish_uri)
except NotFound:
logger.error(f"ResourceChanged event for a non-existent object. Agent URI: {origin_of_condition}, Sunfish URI: {sunfish_uri}")
return

# Get aliases for this agent to update links in the payload
uri_alias_file = os.path.join(os.getcwd(), event_handler.core.conf["backend_conf"]["fs_private"], 'URI_aliases.json')
agent_aliases = {}
if os.path.exists(uri_alias_file):
with open(uri_alias_file, 'r') as data_json:
uri_aliasDB = json.load(data_json)
owning_agent_id = aggregation_source["@odata.id"].split("/")[-1]
if owning_agent_id in uri_aliasDB.get('Agents_xref_URIs', {}) and 'aliases' in uri_aliasDB['Agents_xref_URIs'][owning_agent_id]:
agent_aliases = uri_aliasDB['Agents_xref_URIs'][owning_agent_id]['aliases']

# Update any internal @odata.id links in the fetched payload
updated_resource = RedfishEventHandler.update_aliased_links_in_object(event_handler.core, updated_resource, agent_aliases)

# Boundary Link Processing - check if this update establishes a new boundary link
if "Oem" in updated_resource and "Sunfish_RM" in updated_resource["Oem"] and updated_resource["Oem"]["Sunfish_RM"].get("BoundaryComponent") == "BoundaryPort":
RedfishEventHandler.track_boundary_port(event_handler.core, updated_resource, aggregation_source)

# Patch the existing object with the new data
logger.info(f"Patching resource at {sunfish_uri}")
event_handler.core.storage_backend.patch(sunfish_uri, updated_resource)

# After patching, check if any cross-agent links need to be updated
RedfishEventHandler.updateAllAgentsRedirectedLinks(event_handler.core)

except Exception:
logger.error("Exception in ResourceChanged handler", exc_info=True)


@classmethod
def TriggerEvent(cls, event_handler: EventHandlerInterface, event: dict, context: str):
###
Expand Down Expand Up @@ -199,6 +270,7 @@ class RedfishEventHandler(EventHandlerInterface):
dispatch_table = {
"AggregationSourceDiscovered": RedfishEventHandlersTable.AggregationSourceDiscovered,
"ResourceCreated": RedfishEventHandlersTable.ResourceCreated,
"ResourceChanged": RedfishEventHandlersTable.ResourceChanged,
"TriggerEvent": RedfishEventHandlersTable.TriggerEvent
}

Expand Down Expand Up @@ -524,7 +596,7 @@ def createInspectedObject(self,redfish_obj, aggregation_source):
redfish_obj['Id'] = sunfish_aliased_URI.split("/")[-1]
logger.debug(f"xlated agent_redfish_URI is {sunfish_aliased_URI}")
if 'Collection' in redfish_obj['@odata.type']:
logger.debug("This is a collection, ignore it until we need it")
logger.info("This is a collection, ignore it until we need it")
pass
else:
# use Sunfish (aliased) paths for conflict testing if it exists
Expand Down Expand Up @@ -631,6 +703,34 @@ def xlateToSunfishPath(self,agent_path, aggregation_source):
agent_path = agentFinal_obj_path
return agent_path

def update_aliased_links_in_object(self, obj, agent_aliases):
"""
Recursively traverses a dictionary/list object and updates any '@odata.id'
links based on the provided agent_aliases mapping.
"""
def findNestedURIs(obj, aliases):
if isinstance(obj, list):
for item in obj:
findNestedURIs(item, aliases)
elif isinstance(obj, dict):
for key, value in obj.items():
if key == '@odata.id' and value in aliases:
logger.info(f"Translating internal link {value} to {aliases[value]}")
obj[key] = aliases[value]
# Do not recurse into Sunfish_RM as its paths are not meant to be translated.
elif key != "Sunfish_RM" and isinstance(value, (dict, list)):
findNestedURIs(value, aliases)

if not isinstance(obj, dict) or "@odata.type" not in obj:
return obj

obj_type = obj["@odata.type"].split('.')[0].replace("#", "")
# Do not perform aliasing on the Members array of a Collection, as the Members array
# should contain both original and aliased URIs.
if "Collection" not in obj_type:
findNestedURIs(obj, agent_aliases)
return obj

def updateAllAliasedLinks(self,aggregation_source):
try:
uri_alias_file = os.path.join(os.getcwd(), self.conf["backend_conf"]["fs_private"], 'URI_aliases.json')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,13 @@ def findNestedURIs(self, URI_to_match, URI_to_sub, obj, path_to_nested_URI):
try:
#pdb.set_trace()
owning_agent_id = agent_obj["Oem"]["Sunfish_RM"]["ManagingAgent"]["@odata.id"].split("/")[-1]
# must also have agent_aliases in the database
agent_aliases = uri_aliasDB["Agents_xref_URIs"][owning_agent_id]["aliases"]
except (KeyError, TypeError):
# can't rename links without all the above keys in the uri_aliasDB
return False

try:
object_URI = agent_obj["@odata.id"]
aliasedNestedPaths=[]
obj_modified = False
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"@odata.id": "/redfish/v1/AggregationService/AggregationSources/feb7bb58-83f2-4945-a798-7c0811a29955",
"@odata.type": "#AggregationSource.v1_2_.AggregationSource",
"HostName": "http://127.0.0.1:8080",
"Id": "feb7bb58-83f2-4945-a798-7c0811a29955",
"Links": {
"ConnectionMethod": {
"@odata.id": "/redfish/v1/AggregationService/ConnectionMethods/Pytest1"
},
"ResourcesAccessed": [
"/redfish/v1/Fabrics/Pytest1"
]
}
}
11 changes: 11 additions & 0 deletions tests/Resources/AggregationService/AggregationSources/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"@odata.id": "/redfish/v1/AggregationService/AggregationSources",
"@odata.type": "#AggregationService/AggregationSourcesCollection.AggregationService/AggregationSourcesCollection",
"Members": [
{
"@odata.id": "/redfish/v1/AggregationService/AggregationSources/feb7bb58-83f2-4945-a798-7c0811a29955"
}
],
"Members@odata.count": 1,
"Name": "AggregationService/AggregationSources Collection"
}

This file was deleted.

6 changes: 5 additions & 1 deletion tests/Resources/AggregationService/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
"State": "Enabled",
"Health": "OK"
},
"AgregationSources": {
"@odata.id": "/redfish/v1/AggregationService/AggregationSources"
},

"ServiceEnabled": true,

"@odata.id": "/redfish/v1/AggregationService",
"@Redfish.Copyright": "Copyright 2023 OpenFabrics Alliance. All rights reserved."
}
}
30 changes: 30 additions & 0 deletions tests/Resources/SunfishPrivate/URI_aliases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"Sunfish_xref_URIs" : {
"aliases": {
"/example/Sunfish/URI": [
"/example/Agent1/URI",
"/example/Agent2/URI"
],
"/another/Sunfish/URI": [
"/different/Agent1/URI"
]
},
"missing": []
},

"Agents_xref_URIs" : {
"exampleAgentContex": {
"aliases": {
"/example/Agent/URI": "/exampleRenamed/Sunfish/URI",
"/another/Agent/URI": "/anotherRenamed/Sunfish/URI"
},
"foreignBoundaryComponents": {
"/example/Agent/boundary/URI": "/exampleRenamed/Sunfish/boundary/URI",
"/another/Agent/boundary/URI": "/anotherRenamed/Sunfish/boundary/URI"

}

}

}
}
2 changes: 1 addition & 1 deletion tests/conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"backend_conf" : {
"fs_root": "Resources",
"fs_private": "SunfishPrivate",
"fs_private": "Resources/SunfishPrivate",
"subscribers_root": "EventService/Subscriptions"
},
"events_handler": {
Expand Down
2 changes: 1 addition & 1 deletion tests/conf_broken_module.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"backend_conf" : {
"fs_root": "Resources",
"fs_private": "SunfishPrivate",
"fs_private": "Resources/SunfishPrivate",
"subscribers_root": "EventService/Subscriptions"
},
"event_handler": {
Expand Down
44 changes: 44 additions & 0 deletions tests/test_sunfishcore_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import os
import logging
import pytest
import shutil
import pdb
from pathlib import Path
from pytest_httpserver import HTTPServer
from sunfish.lib.core import Core
from sunfish.lib.exceptions import *
Expand Down Expand Up @@ -58,6 +61,15 @@ def test_delete(self):
self.core.delete_object(system_url)
assert test_utils.check_delete(system_url) == True

# reset the test directory
def test_reset_directories(self):
base_path = Path(__file__).parent.parent
src = base_path / "tests/Resources"
dst = base_path / "Resources"
# Copy the test tree over the existing Resources tree
shutil.rmtree(dst, ignore_errors=True)
shutil.copytree(src, dst, dirs_exist_ok=True)

def test_delete_exception(self):
system_url = os.path.join(self.conf["redfish_root"], 'Systems', '-1')
# raise exception if element doesnt exist
Expand Down Expand Up @@ -188,6 +200,38 @@ def test_agent_delete_forwarding(self, httpserver: HTTPServer):

assert resp == f"Object {connection_path} deleted"

# test agent register and agent upload event handlers
def test_agent_register(self, httpserver: HTTPServer):
connection_path = os.path.join(self.conf['redfish_root'], "AggregationService/ConnectionMethods/Pytest2")
httpserver.expect_ordered_request(connection_path, method="GET").respond_with_json(tests_template.connection_method_pytest2)
connection_path = os.path.join(self.conf['redfish_root'], "EventService/Subscriptions/SunfishServer")
httpserver.expect_ordered_request(connection_path, method="PATCH").respond_with_data("OK")
resp = self.core.handle_event(tests_template.reg_event)
assert len(httpserver.log) == 2

assert len(resp) == 0

def test_agent_upload(self, httpserver: HTTPServer):
#pdb.set_trace()
# arm the httpserver with agent's response to GET on OriginOfCondition
connection_path = os.path.join(self.conf['redfish_root'], "Fabrics/Pytest1")
httpserver.expect_ordered_request(connection_path, method="GET").respond_with_json(tests_template.fabrics_pytest1)
# the above is actually retrieved again at start of recursive fetch (upload)
connection_path = os.path.join(self.conf['redfish_root'], "Fabrics/Pytest1")
httpserver.expect_ordered_request(connection_path, method="GET").respond_with_json(tests_template.fabrics_pytest1)
# arm the httpserver with agent's response to GET on subordinate Switches collection
connection_path = os.path.join(self.conf['redfish_root'], "Fabrics/Pytest1/Switches")
httpserver.expect_ordered_request(connection_path, method="GET").respond_with_json(tests_template.fabrics_switch_collection)
# arm the httpserver with agent's response to GET on Switch object
connection_path = os.path.join(self.conf['redfish_root'], "Fabrics/Pytest1/Switches/Pytest1")
httpserver.expect_ordered_request(connection_path, method="GET").respond_with_json(tests_template.fabrics_switch_pytest1)
resp = self.core.handle_event(tests_template.upload_event)
assert len(httpserver.log) == 4
# TODO
# should verify the two objects got uploaded and written to the Sunfish DB

assert len(resp) == 0

# deletes all the subscriptions
@pytest.mark.order("last")
def test_clean_up(self):
Expand Down
Loading
Loading