diff --git a/sushy/resources/base.py b/sushy/resources/base.py index 43d053f8..d3704439 100644 --- a/sushy/resources/base.py +++ b/sushy/resources/base.py @@ -224,7 +224,7 @@ class ResourceBase(object): time from here (constructor). :param connector: A Connector instance :param path: sub-URI path to the resource. - :param redfish_version: The version of RedFish. Used to construct + :param redfish_version: The version of Redfish. Used to construct the object according to schema of the given version. """ self._conn = connector @@ -328,11 +328,11 @@ class ResourceCollectionBase(ResourceBase): def __init__(self, connector, path, redfish_version=None): """A class representing the base of any Redfish resource collection - It gets inherited ``ResourceBase`` and invokes the base class + It gets inherited from ``ResourceBase`` and invokes the base class constructor. :param connector: A Connector instance :param path: sub-URI path to the resource collection. - :param redfish_version: The version of RedFish. Used to construct + :param redfish_version: The version of Redfish. Used to construct the object according to schema of the given version. """ super(ResourceCollectionBase, self).__init__(connector, path, diff --git a/sushy/resources/system/storage/drive.py b/sushy/resources/system/storage/drive.py new file mode 100644 index 00000000..a6d0f926 --- /dev/null +++ b/sushy/resources/system/storage/drive.py @@ -0,0 +1,34 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# This is referred from Redfish standard schema. +# http://redfish.dmtf.org/schemas/v1/Drive.v1_3_0.json + +import logging + +from sushy.resources import base +from sushy import utils + +LOG = logging.getLogger(__name__) + + +class Drive(base.ResourceBase): + """This class represents a disk drive or other physical storage medium.""" + + identity = base.Field('Id', required=True) + """The Drive identity string""" + + name = base.Field('Name') + """The name of the resource""" + + capacity_bytes = base.Field('CapacityBytes', adapter=utils.int_or_none) + """The size in bytes of this Drive""" diff --git a/sushy/resources/system/storage/storage.py b/sushy/resources/system/storage/storage.py new file mode 100644 index 00000000..5fe16d6b --- /dev/null +++ b/sushy/resources/system/storage/storage.py @@ -0,0 +1,71 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# This is referred from Redfish standard schema. +# http://redfish.dmtf.org/schemas/v1/Storage.v1_4_0.json + +import logging + +from sushy.resources import base +from sushy.resources.system.storage import drive +from sushy import utils + +LOG = logging.getLogger(__name__) + + +class Storage(base.ResourceBase): + """This class represents resources that represent a storage subsystem. + + 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. + """ + + identity = base.Field('Id', required=True) + """The Storage identity string""" + + name = base.Field('Name') + """The name of the resource""" + + drives_identities = base.Field('Drives', + adapter=utils.get_members_identities) + """A tuple with the drive identities""" + + _drives_max_size_bytes = None + + def get_drive(self, drive_identity): + """Given the drive identity return a ``Drive`` object + + :param identity: The identity of the ``Drive`` + :returns: The ``Drive`` object + :raises: ResourceNotFoundError + """ + return drive.Drive(self._conn, drive_identity, + redfish_version=self.redfish_version) + + @property + def drives_max_size_bytes(self): + """Max size available in bytes among all Drives of this collection.""" + if self._drives_max_size_bytes is None: + self._drives_max_size_bytes = ( + utils.max_safe(self.get_drive(drive_id).capacity_bytes + for drive_id in self.drives_identities)) + return self._drives_max_size_bytes + + def _do_refresh(self, force=False): + """Do resource specific refresh activities + + On refresh, all sub-resources are marked as stale, i.e. + greedy-refresh not done for them unless forced by ``force`` + argument. + """ + self._drives_max_size_bytes = None diff --git a/sushy/tests/unit/json_samples/drive.json b/sushy/tests/unit/json_samples/drive.json new file mode 100644 index 00000000..a3f1184b --- /dev/null +++ b/sushy/tests/unit/json_samples/drive.json @@ -0,0 +1,46 @@ +{ + "@odata.type": "#Drive.v1_2_0.Drive", + "Id": "32ADF365C6C1B7BD", + "Name": "Drive Sample", + "IndicatorLED": "Lit", + "Model": "C123", + "Revision": "100A", + "Status": { + "@odata.type": "#Resource.Status", + "State": "Enabled", + "Health": "OK" + }, + "CapacityBytes": 899527000000, + "FailurePredicted": false, + "Protocol": "SAS", + "MediaType": "HDD", + "Manufacturer": "Contoso", + "SerialNumber": "1234570", + "PartNumber": "C123-1111", + "Identifiers": [ + { + "@odata.type": "#Resource.v1_1_0.Identifier", + "DurableNameFormat": "NAA", + "DurableName": "32ADF365C6C1B7BD" + } + ], + "HotspareType": "Global", + "EncryptionAbility": "SelfEncryptingDrive", + "EncryptionStatus": "Unlocked", + "RotationSpeedRPM": 15000, + "BlockSizeBytes": 512, + "CapableSpeedGbs": 12, + "NegotiatedSpeedGbs": 12, + "Links": { + "@odata.type": "#Drive.v1_2_0.Links" + }, + "Actions": { + "@odata.type": "#Drive.v1_0_0.Actions", + "#Drive.SecureErase": { + "target": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/32ADF365C6C1B7BD/Actions/Drive.SecureErase" + } + }, + "@odata.context": "/redfish/v1/$metadata#Drive.Drive", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/32ADF365C6C1B7BD", + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/sushy/tests/unit/json_samples/drive2.json b/sushy/tests/unit/json_samples/drive2.json new file mode 100644 index 00000000..ab05b6f1 --- /dev/null +++ b/sushy/tests/unit/json_samples/drive2.json @@ -0,0 +1,51 @@ +{ + "@odata.type": "#Drive.v1_2_0.Drive", + "Id": "35D38F11ACEF7BD3", + "Name": "Drive Sample", + "IndicatorLED": "Lit", + "Model": "C123", + "Revision": "100A", + "Status": { + "@odata.type": "#Resource.Status", + "State": "Enabled", + "Health": "OK" + }, + "CapacityBytes": 899527000000, + "FailurePredicted": false, + "Protocol": "SAS", + "MediaType": "HDD", + "Manufacturer": "Contoso", + "SerialNumber": "1234567", + "PartNumber": "C123-1111", + "Identifiers": [ + { + "@odata.type": "#Resource.v1_1_0.Identifier", + "DurableNameFormat": "NAA", + "DurableName": "35D38F11ACEF7BD3" + } + ], + "HotspareType": "None", + "EncryptionAbility": "SelfEncryptingDrive", + "EncryptionStatus": "Unlocked", + "RotationSpeedRPM": 15000, + "BlockSizeBytes": 512, + "CapableSpeedGbs": 12, + "NegotiatedSpeedGbs": 12, + "Links": { + "@odata.type": "#Drive.v1_2_0.Links", + "Volumes": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/1" + } + ] + }, + "Actions": { + "@odata.type": "#Drive.v1_0_0.Actions", + "#Drive.SecureErase": { + "target": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/35D38F11ACEF7BD3/Actions/Drive.SecureErase" + } + }, + "@odata.context": "/redfish/v1/$metadata#Drive.Drive", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/35D38F11ACEF7BD3", + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/sushy/tests/unit/json_samples/drive3.json b/sushy/tests/unit/json_samples/drive3.json new file mode 100644 index 00000000..9a4faf99 --- /dev/null +++ b/sushy/tests/unit/json_samples/drive3.json @@ -0,0 +1,54 @@ +{ + "@odata.type": "#Drive.v1_2_0.Drive", + "Id": "3D58ECBC375FD9F2", + "Name": "Drive Sample", + "IndicatorLED": "Lit", + "Model": "C123", + "Revision": "100A", + "Status": { + "@odata.type": "#Resource.Status", + "State": "Enabled", + "Health": "OK" + }, + "CapacityBytes": 899527000000, + "FailurePredicted": false, + "Protocol": "SAS", + "MediaType": "HDD", + "Manufacturer": "Contoso", + "SerialNumber": "1234568", + "PartNumber": "C123-1111", + "Identifiers": [ + { + "@odata.type": "#Resource.v1_1_0.Identifier", + "DurableNameFormat": "NAA", + "DurableName": "32ADF365C6C1B7BD" + } + ], + "HotspareType": "None", + "EncryptionAbility": "SelfEncryptingDrive", + "EncryptionStatus": "Unlocked", + "RotationSpeedRPM": 15000, + "BlockSizeBytes": 512, + "CapableSpeedGbs": 12, + "NegotiatedSpeedGbs": 12, + "Links": { + "@odata.type": "#Drive.v1_2_0.Links", + "Volumes": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/2" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/3" + } + ] + }, + "Actions": { + "@odata.type": "#Drive.v1_0_0.Actions", + "#Drive.SecureErase": { + "target": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/3D58ECBC375FD9F2/Actions/Drive.SecureErase" + } + }, + "@odata.context": "/redfish/v1/$metadata#Drive.Drive", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/3D58ECBC375FD9F2", + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/sushy/tests/unit/json_samples/storage.json b/sushy/tests/unit/json_samples/storage.json new file mode 100644 index 00000000..e272cce2 --- /dev/null +++ b/sushy/tests/unit/json_samples/storage.json @@ -0,0 +1,74 @@ +{ + "@odata.type": "#Storage.v1_3_0.Storage", + "Id": "1", + "Name": "Local Storage Controller", + "Description": "Integrated RAID Controller", + "Status": { + "@odata.type": "#Resource.Status", + "State": "Enabled", + "Health": "OK", + "HealthRollup": "OK" + }, + "StorageControllers": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1#/StorageControllers/0", + "@odata.type": "#Storage.v1_3_0.StorageController", + "MemberId": "0", + "Name": "Contoso Integrated RAID", + "Status": { + "@odata.type": "#Resource.Status", + "State": "Enabled", + "Health": "OK" + }, + "Identifiers": [ + { + "@odata.type": "#Resource.v1_1_0.Identifier", + "DurableNameFormat": "NAA", + "DurableName": "345C59DBD970859C" + } + ], + "Manufacturer": "Contoso", + "Model": "12Gbs Integrated RAID", + "SerialNumber": "2M220100SL", + "PartNumber": "CT18754", + "SpeedGbps": 12, + "FirmwareVersion": "1.0.0.7", + "SupportedControllerProtocols": [ + "PCIe" + ], + "SupportedDeviceProtocols": [ + "SAS", + "SATA" + ] + } + ], + "Drives": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/35D38F11ACEF7BD3" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/3F5A8C54207B7233" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/32ADF365C6C1B7BD" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/3D58ECBC375FD9F2" + } + ], + "Volumes": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes" + }, + "Links": { + "@odata.type": "#Storage.v1_0_0.Storage" + }, + "Actions": { + "@odata.type": "#Storage.v1_0_0.Actions", + "#Storage.SetEncryptionKey": { + "target": "/redfish/v1/Systems/437XR1138R2/Storage/1/Actions/Storage.SetEncryptionKey" + } + }, + "@odata.context": "/redfish/v1/$metadata#Storage.Storage", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1", + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/sushy/tests/unit/resources/system/storage/test_drive.py b/sushy/tests/unit/resources/system/storage/test_drive.py new file mode 100644 index 00000000..30c89c45 --- /dev/null +++ b/sushy/tests/unit/resources/system/storage/test_drive.py @@ -0,0 +1,40 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +import mock + +from sushy.resources.system.storage import drive +from sushy.tests.unit import base + + +class DriveTestCase(base.TestCase): + + def setUp(self): + super(DriveTestCase, self).setUp() + self.conn = mock.Mock() + drive_file = 'sushy/tests/unit/json_samples/drive.json' + with open(drive_file, 'r') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + self.stor_drive = drive.Drive( + self.conn, + '/redfish/v1/Systems/437XR1138/Storage/1/Drives/32ADF365C6C1B7BD', + redfish_version='1.0.2') + + def test__parse_attributes(self): + self.stor_drive._parse_attributes() + self.assertEqual('1.0.2', self.stor_drive.redfish_version) + self.assertEqual('32ADF365C6C1B7BD', self.stor_drive.identity) + self.assertEqual('Drive Sample', self.stor_drive.name) + self.assertEqual(899527000000, self.stor_drive.capacity_bytes) diff --git a/sushy/tests/unit/resources/system/storage/test_storage.py b/sushy/tests/unit/resources/system/storage/test_storage.py new file mode 100644 index 00000000..60133209 --- /dev/null +++ b/sushy/tests/unit/resources/system/storage/test_storage.py @@ -0,0 +1,93 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +import mock + +from sushy.resources.system.storage import drive +from sushy.resources.system.storage import storage +from sushy.tests.unit import base + + +STORAGE_DRIVE_FILE_NAMES = [ + 'sushy/tests/unit/json_samples/drive.json', + 'sushy/tests/unit/json_samples/drive2.json', + 'sushy/tests/unit/json_samples/drive3.json' +] + + +class StorageTestCase(base.TestCase): + + def setUp(self): + super(StorageTestCase, self).setUp() + self.conn = mock.Mock() + storage_file = 'sushy/tests/unit/json_samples/storage.json' + with open(storage_file, 'r') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + self.storage = storage.Storage( + self.conn, '/redfish/v1/Systems/437XR1138R2/Storage/1', + redfish_version='1.0.2') + + def test__parse_attributes(self): + self.storage._parse_attributes() + self.assertEqual('1.0.2', self.storage.redfish_version) + self.assertEqual('1', self.storage.identity) + self.assertEqual('Local Storage Controller', self.storage.name) + self.assertEqual( + ('/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/35D38F11ACEF7BD3', # noqa + '/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/3F5A8C54207B7233', # noqa + '/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/32ADF365C6C1B7BD', # noqa + '/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/3D58ECBC375FD9F2', # noqa + ), self.storage.drives_identities) + + def test_get_drive(self): + # | WHEN | + actual_drive = self.storage.get_drive( + '/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/' + '35D38F11ACEF7BD3') + # | THEN | + self.assertIsInstance(actual_drive, drive.Drive) + self.assertTrue(self.conn.get.return_value.json.called) + + def test_drives_max_size_bytes(self): + self.assertIsNone(self.storage._drives_max_size_bytes) + self.conn.get.return_value.json.reset_mock() + + successive_return_values = [] + # repeating the 3rd one to provide mock data for 4th iteration. + for fname in STORAGE_DRIVE_FILE_NAMES + [STORAGE_DRIVE_FILE_NAMES[-1]]: + with open(fname, 'r') as f: + successive_return_values.append(json.load(f)) + self.conn.get.return_value.json.side_effect = successive_return_values + + self.assertEqual(899527000000, self.storage.drives_max_size_bytes) + + # for any subsequent fetching it gets it from the cached value + self.conn.get.return_value.json.reset_mock() + self.assertEqual(899527000000, self.storage.drives_max_size_bytes) + self.conn.get.return_value.json.assert_not_called() + + def test_drives_max_size_bytes_after_refresh(self): + self.storage.refresh() + self.assertIsNone(self.storage._drives_max_size_bytes) + self.conn.get.return_value.json.reset_mock() + + successive_return_values = [] + # repeating the 3rd one to provide mock data for 4th iteration. + for fname in STORAGE_DRIVE_FILE_NAMES + [STORAGE_DRIVE_FILE_NAMES[-1]]: + with open(fname, 'r') as f: + successive_return_values.append(json.load(f)) + self.conn.get.return_value.json.side_effect = successive_return_values + + self.assertEqual(899527000000, self.storage.drives_max_size_bytes)