diff --git a/releasenotes/notes/add-system-manager-linkage-86be69c9df4cb359.yaml b/releasenotes/notes/add-system-manager-linkage-86be69c9df4cb359.yaml new file mode 100644 index 00000000..69e0c003 --- /dev/null +++ b/releasenotes/notes/add-system-manager-linkage-86be69c9df4cb359.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Establishes ComputerSystem->Managers and Manager->ComputerSystems + references at sushy data abstraction level what make it possible to + look up Manager(s) responsible for a ComputerSystem and vice versa. diff --git a/sushy/resources/manager/manager.py b/sushy/resources/manager/manager.py index b4f2fd5d..9c17c706 100644 --- a/sushy/resources/manager/manager.py +++ b/sushy/resources/manager/manager.py @@ -195,6 +195,25 @@ class Manager(base.ResourceBase): self._conn, utils.get_sub_resource_path_by(self, 'VirtualMedia'), redfish_version=self.redfish_version) + @property + @utils.cache_it + def systems(self): + """A list of systems managed by this manager. + + Returns a list of `System` objects representing systems being + managed by this manager. + + :raises: MissingAttributeError if '@odata.id' field is missing. + :returns: A list of `System` instances + """ + paths = utils.get_sub_resource_path_by( + self, ["Links", "ManagerForServers"], is_collection=True) + + from sushy.resources.system import system + return [system.System(self._conn, path, + redfish_version=self.redfish_version) + for path in paths] + class ManagerCollection(base.ResourceCollectionBase): diff --git a/sushy/resources/system/system.py b/sushy/resources/system/system.py index 7ceda532..18851e7e 100644 --- a/sushy/resources/system/system.py +++ b/sushy/resources/system/system.py @@ -21,6 +21,7 @@ import logging from sushy import exceptions from sushy.resources import base from sushy.resources import common +from sushy.resources.manager import manager from sushy.resources import mappings as res_maps from sushy.resources.system import bios from sushy.resources.system import constants as sys_cons @@ -336,6 +337,24 @@ class System(base.ResourceBase): self._conn, utils.get_sub_resource_path_by(self, "Storage"), redfish_version=self.redfish_version) + @property + @utils.cache_it + def managers(self): + """A list of managers for this system. + + Returns a list of `Manager` objects representing the managers + that manage this system. + + :raises: MissingAttributeError if '@odata.id' field is missing. + :returns: A list of `Manager` instances + """ + paths = utils.get_sub_resource_path_by( + self, ["Links", "ManagedBy"], is_collection=True) + + return [manager.Manager(self._conn, path, + redfish_version=self.redfish_version) + for path in paths] + class SystemCollection(base.ResourceCollectionBase): diff --git a/sushy/tests/unit/resources/manager/test_manager.py b/sushy/tests/unit/resources/manager/test_manager.py index 108d749f..565cc7ca 100644 --- a/sushy/tests/unit/resources/manager/test_manager.py +++ b/sushy/tests/unit/resources/manager/test_manager.py @@ -18,6 +18,7 @@ import sushy from sushy import exceptions from sushy.resources.manager import manager from sushy.resources.manager import virtual_media +from sushy.resources.system import system from sushy.tests.unit import base @@ -266,6 +267,18 @@ class ManagerTestCase(base.TestCase): virtual_media.VirtualMediaCollection) self.assertFalse(vrt_media._is_stale) + def test_systems(self): + # | GIVEN | + with open('sushy/tests/unit/json_samples/' + 'system.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + # | WHEN & THEN | + actual_systems = self.manager.systems + self.assertIsInstance(actual_systems[0], system.System) + self.assertEqual( + '/redfish/v1/Systems/437XR1138R2', actual_systems[0].path) + class ManagerCollectionTestCase(base.TestCase): diff --git a/sushy/tests/unit/resources/system/test_system.py b/sushy/tests/unit/resources/system/test_system.py index eb96a7d2..f6deb747 100644 --- a/sushy/tests/unit/resources/system/test_system.py +++ b/sushy/tests/unit/resources/system/test_system.py @@ -20,6 +20,7 @@ import mock import sushy from sushy import exceptions from sushy.resources import constants as res_cons +from sushy.resources.manager import manager from sushy.resources.system import bios from sushy.resources.system import mappings as sys_map from sushy.resources.system import processor @@ -478,6 +479,18 @@ class SystemTestCase(base.TestCase): # | WHEN & THEN | self.assertIsInstance(self.sys_inst.storage, storage.StorageCollection) + def test_managers(self): + # | GIVEN | + with open('sushy/tests/unit/json_samples/' + 'manager.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + # | WHEN & THEN | + actual_managers = self.sys_inst.managers + self.assertIsInstance(actual_managers[0], manager.Manager) + self.assertEqual( + '/redfish/v1/Managers/BMC', actual_managers[0].path) + class SystemCollectionTestCase(base.TestCase): diff --git a/sushy/tests/unit/test_utils.py b/sushy/tests/unit/test_utils.py index 8be70ca0..899eee61 100644 --- a/sushy/tests/unit/test_utils.py +++ b/sushy/tests/unit/test_utils.py @@ -69,6 +69,14 @@ class UtilsTestCase(base.TestCase): subresource_path) self.assertEqual(expected_result, value) + def test_get_sub_resource_path_by_collection(self): + subresource_path = ["Links", "ManagedBy"] + expected_result = ['/redfish/v1/Managers/BMC'] + value = utils.get_sub_resource_path_by(self.sys_inst, + subresource_path, + is_collection=True) + self.assertEqual(expected_result, value) + def test_get_sub_resource_path_by_fails(self): subresource_path = ['Links', 'Chassis'] expected_result = 'attribute Links/Chassis/@odata.id is missing' diff --git a/sushy/utils.py b/sushy/utils.py index d9735afa..0ba6b9bd 100644 --- a/sushy/utils.py +++ b/sushy/utils.py @@ -66,13 +66,17 @@ def int_or_none(x): return int(x) -def get_sub_resource_path_by(resource, subresource_name): +def get_sub_resource_path_by(resource, subresource_name, is_collection=False): """Helper function to find the subresource path :param resource: ResourceBase instance on which the name gets queried upon. :param subresource_name: name of the resource field to fetch the '@odata.id' from. + :param is_collection: if `True`, expect a list of resources to + fetch the '@odata.id' from. + :returns: Resource path (if `is_collection` is `False`) or + a list of resource paths (if `is_collection` is `True`). """ if not subresource_name: raise ValueError('"subresource_name" cannot be empty') @@ -88,12 +92,24 @@ def get_sub_resource_path_by(resource, subresource_name): raise exceptions.MissingAttributeError( attribute='/'.join(subresource_name), resource=resource.path) - if '@odata.id' not in body: - raise exceptions.MissingAttributeError( - attribute='/'.join(subresource_name) + '/@odata.id', - resource=resource.path) + elements = [] - return body['@odata.id'] + try: + if is_collection: + for element in body: + elements.append(element['@odata.id']) + return elements + + else: + return body['@odata.id'] + + except (TypeError, KeyError): + attribute = '/'.join(subresource_name) + if is_collection: + attribute += '[%s]' % len(elements) + attribute += '/@odata.id' + raise exceptions.MissingAttributeError( + attribute=attribute, resource=resource.path) def max_safe(iterable, default=0):