diff --git a/releasenotes/notes/add-storage-and-simple-storage-attributes-to-system-16e81f9b15b1897d.yaml b/releasenotes/notes/add-storage-and-simple-storage-attributes-to-system-16e81f9b15b1897d.yaml new file mode 100644 index 00000000..4af67c99 --- /dev/null +++ b/releasenotes/notes/add-storage-and-simple-storage-attributes-to-system-16e81f9b15b1897d.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Exposes the ``simple_storage`` and ``storage`` properties from system + resource in sushy. + + * ``simple_storage`` property indicates a collection of storage + controllers and their directly-attached devices associated with the + system. + * ``storage`` property refers to a collection of storage subsystem + associated with system. Resources such as drives and volumes can be + accessed from that subsystem. diff --git a/sushy/resources/system/system.py b/sushy/resources/system/system.py index be109bc4..14064cdf 100644 --- a/sushy/resources/system/system.py +++ b/sushy/resources/system/system.py @@ -23,6 +23,8 @@ from sushy.resources.system import constants as sys_cons from sushy.resources.system import ethernet_interface from sushy.resources.system import mappings as sys_maps from sushy.resources.system import processor +from sushy.resources.system import simple_storage as sys_simple_storage +from sushy.resources.system.storage import storage as sys_storage from sushy import utils @@ -119,14 +121,23 @@ class System(base.ResourceBase): memory_summary = MemorySummaryField('MemorySummary') """The summary info of memory of the system in general detail""" - _processors = None # ref to ProcessorCollection instance - _actions = ActionsField('Actions', required=True) + # reference to ProcessorCollection instance + _processors = None + + # reference to EthernetInterfaceCollection instance _ethernet_interfaces = None + # reference to BIOS instance _bios = None + # reference to SimpleStorageCollection instance + _simple_storage = None + + # reference to StorageCollection instance + _storage = None + def __init__(self, connector, identity, redfish_version=None): """A class representing a ComputerSystem @@ -303,6 +314,56 @@ class System(base.ResourceBase): self._bios.refresh(force=False) return self._bios + @property + def simple_storage(self): + """A collection of simple storage associated with system. + + This returns a reference to `SimpleStorageCollection` instance. + SimpleStorage represents the properties of a storage controller and its + directly-attached devices. + + It is set once when the first time it is queried. On refresh, + this property is marked as stale (greedy-refresh not done). + Here the actual refresh of the sub-resource happens, if stale. + + :raises: MissingAttributeError if 'SimpleStorage/@odata.id' field + is missing. + :returns: `SimpleStorageCollection` instance + """ + if self._simple_storage is None: + self._simple_storage = sys_simple_storage.SimpleStorageCollection( + self._conn, + utils.get_sub_resource_path_by(self, "SimpleStorage"), + redfish_version=self.redfish_version) + + self._simple_storage.refresh(force=False) + return self._simple_storage + + @property + def storage(self): + """A collection of storage subsystems associated with system. + + This returns a reference to `StorageCollection` instance. + A storage subsystem represents a set of storage controllers (physical + or virtual) and the resources such as drives and volumes that can be + accessed from that subsystem. + + It is set once when the first time it is queried. On refresh, + this property is marked as stale (greedy-refresh not done). + Here the actual refresh of the sub-resource happens, if stale. + + :raises: MissingAttributeError if 'Storage/@odata.id' field + is missing. + :returns: `StorageCollection` instance + """ + if self._storage is None: + self._storage = sys_storage.StorageCollection( + self._conn, utils.get_sub_resource_path_by(self, "Storage"), + redfish_version=self.redfish_version) + + self._storage.refresh(force=False) + return self._storage + def _do_refresh(self, force=False): """Do custom resource specific refresh activities @@ -316,6 +377,10 @@ class System(base.ResourceBase): self._ethernet_interfaces.invalidate(force) if self._bios is not None: self._bios.invalidate(force) + if self._simple_storage is not None: + self._simple_storage.invalidate(force) + if self._storage is not None: + self._storage.invalidate(force) class SystemCollection(base.ResourceCollectionBase): diff --git a/sushy/tests/unit/json_samples/system.json b/sushy/tests/unit/json_samples/system.json index 5dd7da6d..c331d024 100644 --- a/sushy/tests/unit/json_samples/system.json +++ b/sushy/tests/unit/json_samples/system.json @@ -93,6 +93,9 @@ "SimpleStorage": { "@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage" }, + "Storage": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage" + }, "LogServices": { "@odata.id": "/redfish/v1/Systems/437XR1138R2/LogServices" }, diff --git a/sushy/tests/unit/resources/system/test_system.py b/sushy/tests/unit/resources/system/test_system.py index 5bcd93f6..4642d9ad 100644 --- a/sushy/tests/unit/resources/system/test_system.py +++ b/sushy/tests/unit/resources/system/test_system.py @@ -24,6 +24,8 @@ from sushy.resources.system import bios from sushy.resources.system import ethernet_interface from sushy.resources.system import mappings as sys_map from sushy.resources.system import processor +from sushy.resources.system import simple_storage +from sushy.resources.system.storage import storage from sushy.resources.system import system from sushy.tests.unit import base @@ -392,6 +394,120 @@ class SystemTestCase(base.TestCase): self.assertEqual('BIOS Configuration Current Settings', self.sys_inst.bios.name) + def test_simple_storage_for_missing_attr(self): + self.sys_inst.json.pop('SimpleStorage') + with self.assertRaisesRegex( + exceptions.MissingAttributeError, 'attribute SimpleStorage'): + self.sys_inst.simple_storage + + def test_simple_storage(self): + # check for the underneath variable value + self.assertIsNone(self.sys_inst._simple_storage) + # | GIVEN | + self.conn.get.return_value.json.reset_mock() + with open('sushy/tests/unit/json_samples/' + 'simple_storage_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + # | WHEN | + actual_simple_storage = self.sys_inst.simple_storage + # | THEN | + self.assertIsInstance(actual_simple_storage, + simple_storage.SimpleStorageCollection) + self.conn.get.return_value.json.assert_called_once_with() + + # reset mock + self.conn.get.return_value.json.reset_mock() + # | WHEN & THEN | + # tests for same object on invoking subsequently + self.assertIs(actual_simple_storage, + self.sys_inst.simple_storage) + self.conn.get.return_value.json.assert_not_called() + + def test_simple_storage_on_refresh(self): + # | GIVEN | + with open('sushy/tests/unit/json_samples/' + 'simple_storage_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + # | WHEN & THEN | + self.assertIsInstance(self.sys_inst.simple_storage, + simple_storage.SimpleStorageCollection) + + # On refreshing the system instance... + with open('sushy/tests/unit/json_samples/system.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + self.sys_inst.invalidate() + self.sys_inst.refresh(force=False) + + # | WHEN & THEN | + self.assertIsNotNone(self.sys_inst._simple_storage) + self.assertTrue(self.sys_inst._simple_storage._is_stale) + + # | GIVEN | + with open('sushy/tests/unit/json_samples/' + 'simple_storage_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + # | WHEN & THEN | + self.assertIsInstance(self.sys_inst.simple_storage, + simple_storage.SimpleStorageCollection) + self.assertFalse(self.sys_inst._simple_storage._is_stale) + + def test_storage_for_missing_attr(self): + self.sys_inst.json.pop('Storage') + with self.assertRaisesRegex( + exceptions.MissingAttributeError, 'attribute Storage'): + self.sys_inst.storage + + def test_storage(self): + # check for the underneath variable value + self.assertIsNone(self.sys_inst._storage) + # | GIVEN | + self.conn.get.return_value.json.reset_mock() + with open('sushy/tests/unit/json_samples/' + 'storage_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + # | WHEN | + actual_storage = self.sys_inst.storage + # | THEN | + self.assertIsInstance(actual_storage, storage.StorageCollection) + self.conn.get.return_value.json.assert_called_once_with() + + # reset mock + self.conn.get.return_value.json.reset_mock() + # | WHEN & THEN | + # tests for same object on invoking subsequently + self.assertIs(actual_storage, self.sys_inst.storage) + self.conn.get.return_value.json.assert_not_called() + + def test_storage_on_refresh(self): + # | GIVEN | + with open('sushy/tests/unit/json_samples/' + 'storage_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + # | WHEN & THEN | + self.assertIsInstance(self.sys_inst.storage, + storage.StorageCollection) + + # On refreshing the system instance... + with open('sushy/tests/unit/json_samples/system.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + self.sys_inst.invalidate() + self.sys_inst.refresh(force=False) + + # | WHEN & THEN | + self.assertIsNotNone(self.sys_inst._storage) + self.assertTrue(self.sys_inst._storage._is_stale) + + # | GIVEN | + with open('sushy/tests/unit/json_samples/' + 'storage_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + # | WHEN & THEN | + self.assertIsInstance(self.sys_inst.storage, + storage.StorageCollection) + self.assertFalse(self.sys_inst._storage._is_stale) + class SystemCollectionTestCase(base.TestCase): diff --git a/sushy/utils.py b/sushy/utils.py index 0f687d07..5da076cf 100644 --- a/sushy/utils.py +++ b/sushy/utils.py @@ -102,7 +102,7 @@ def max_safe(iterable, default=0): """ try: - return max([x for x in iterable if x is not None]) + return max(x for x in iterable if x is not None) except ValueError: # TypeError is not caught here as that should be thrown. return default