From 0bf7cbf14db35a6546bcda38621309b9531af3d0 Mon Sep 17 00:00:00 2001 From: Ilya Etingof Date: Mon, 3 Dec 2018 15:41:25 +0100 Subject: [PATCH] Add System<->Manager linkage Redfish data model rests on three interlinked entities - ComputerSystem(s), Manager(s) and Chassis. As of this moment, sushy does not support traversing between these entities at the Sushy abstraction level, despite the availability of such linkage in the JSON documents sushy feeds on. This change establishes System->Managers and Managers->System links. Change-Id: I54b0fdeebdea1e13c2b6912ee4c97776ebccaf03 Story: 2004512 Task: 28240 --- ...stem-manager-linkage-86be69c9df4cb359.yaml | 6 ++++ sushy/resources/manager/manager.py | 19 +++++++++++++ sushy/resources/system/system.py | 19 +++++++++++++ .../unit/resources/manager/test_manager.py | 13 +++++++++ .../unit/resources/system/test_system.py | 13 +++++++++ sushy/tests/unit/test_utils.py | 8 ++++++ sushy/utils.py | 28 +++++++++++++++---- 7 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/add-system-manager-linkage-86be69c9df4cb359.yaml 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 96b81333..ef7b51ce 100644 --- a/sushy/resources/manager/manager.py +++ b/sushy/resources/manager/manager.py @@ -188,6 +188,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 37819235..a0b5e4d7 100644 --- a/sushy/resources/system/system.py +++ b/sushy/resources/system/system.py @@ -18,6 +18,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 @@ -333,6 +334,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 5c98ef69..f860578d 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 @@ -265,6 +266,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):