diff --git a/sushy/resources/system/storage/__init__.py b/sushy/resources/system/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sushy/resources/system/storage/volume.py b/sushy/resources/system/storage/volume.py new file mode 100644 index 00000000..6e6c558f --- /dev/null +++ b/sushy/resources/system/storage/volume.py @@ -0,0 +1,57 @@ +# 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/Volume.v1_0_3.json + +import logging + +from sushy.resources import base +from sushy import utils + +LOG = logging.getLogger(__name__) + + +class Volume(base.ResourceBase): + """This class adds the Storage Volume resource""" + + identity = base.Field('Id', required=True) + """The Volume 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 Volume.""" + + +class VolumeCollection(base.ResourceCollectionBase): + """This class represents the Storage Volume collection""" + + _max_size_bytes = None + + @property + def _resource_type(self): + return Volume + + @property + def max_size_bytes(self): + """Max size available in bytes among all Volumes of this collection.""" + if self._max_size_bytes is None: + self._max_size_bytes = ( + utils.max_safe([vol.capacity_bytes + for vol in self.get_members()])) + return self._max_size_bytes + + def _do_refresh(self, force=False): + # invalidate the attribute + self._max_size_bytes = None diff --git a/sushy/tests/unit/json_samples/volume.json b/sushy/tests/unit/json_samples/volume.json new file mode 100644 index 00000000..f19b528b --- /dev/null +++ b/sushy/tests/unit/json_samples/volume.json @@ -0,0 +1,44 @@ +{ + "@odata.type": "#Volume.v1_0_3.Volume", + "Id": "1", + "Name": "Virtual Disk 1", + "Status": { + "@odata.type": "#Resource.Status", + "State": "Enabled", + "Health": "OK" + }, + "Encrypted": false, + "VolumeType": "Mirrored", + "CapacityBytes": 899527000000, + "Identifiers": [ + { + "@odata.type": "#Resource.v1_1_0.Identifier", + "DurableNameFormat": "UUID", + "DurableName": "38f1818b-111e-463a-aa19-fa54f792e468" + } + ], + "Links": { + "@odata.type": "#Volume.v1_0_0.Links", + "Drives": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/3F5A8C54207B7233" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/35D38F11ACEF7BD3" + } + ] + }, + "Actions": { + "@odata.type": "#Volume.v1_0_0.Actions", + "#Volume.Initialize": { + "target": "/redfish/v1/Systems/3/Storage/RAIDIntegrated/Volumes/1/Actions/Volume.Initialize", + "InitializeType@Redfish.AllowableValues": [ + "Fast", + "Slow" + ] + } + }, + "@odata.context": "/redfish/v1/$metadata#Volume.Volume", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/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/json_samples/volume2.json b/sushy/tests/unit/json_samples/volume2.json new file mode 100644 index 00000000..a1804f14 --- /dev/null +++ b/sushy/tests/unit/json_samples/volume2.json @@ -0,0 +1,41 @@ +{ + "@odata.type": "#Volume.v1_0_3.Volume", + "Id": "2", + "Name": "Virtual Disk 2", + "Status": { + "@odata.type": "#Resource.Status", + "State": "Enabled", + "Health": "OK" + }, + "Encrypted": false, + "VolumeType": "NonRedundant", + "CapacityBytes": 107374182400, + "Identifiers": [ + { + "@odata.type": "#Resource.v1_1_0.Identifier", + "DurableNameFormat": "UUID", + "DurableName": "0324c96c-8031-4f5e-886c-50cd90aca854" + } + ], + "Links": { + "@odata.type": "#Volume.v1_0_0.Links", + "Drives": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/3D58ECBC375FD9F2" + } + ] + }, + "Actions": { + "@odata.type": "#Volume.v1_0_0.Actions", + "#Volume.Initialize": { + "target": "/redfish/v1/Systems/3/Storage/RAIDIntegrated/Volumes/1/Actions/Volume.Initialize", + "InitializeType@Redfish.AllowableValues": [ + "Fast", + "Slow" + ] + } + }, + "@odata.context": "/redfish/v1/$metadata#Volume.Volume", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/2", + "@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/volume3.json b/sushy/tests/unit/json_samples/volume3.json new file mode 100644 index 00000000..d5b6b865 --- /dev/null +++ b/sushy/tests/unit/json_samples/volume3.json @@ -0,0 +1,41 @@ +{ + "@odata.type": "#Volume.v1_0_3.Volume", + "Id": "3", + "Name": "Virtual Disk 3", + "Status": { + "@odata.type": "#Resource.Status", + "State": "Enabled", + "Health": "OK" + }, + "Encrypted": false, + "VolumeType": "NonRedundant", + "CapacityBytes": 1073741824000, + "Identifiers": [ + { + "@odata.type": "#Resource.v1_1_0.Identifier", + "DurableNameFormat": "UUID", + "DurableName": "eb179a30-6f87-4fdb-8f92-639eb7aaabcb" + } + ], + "Links": { + "@odata.type": "#Volume.v1_0_0.Links", + "Drives": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Drives/3D58ECBC375FD9F2" + } + ] + }, + "Actions": { + "@odata.type": "#Volume.v1_0_0.Actions", + "#Volume.Initialize": { + "target": "/redfish/v1/Systems/3/Storage/RAIDIntegrated/Volumes/1/Actions/Volume.Initialize", + "InitializeType@Redfish.AllowableValues": [ + "Fast", + "Slow" + ] + } + }, + "@odata.context": "/redfish/v1/$metadata#Volume.Volume", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/3", + "@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/volume_collection.json b/sushy/tests/unit/json_samples/volume_collection.json new file mode 100644 index 00000000..0643e682 --- /dev/null +++ b/sushy/tests/unit/json_samples/volume_collection.json @@ -0,0 +1,21 @@ +{ + "@odata.type": "#VolumeCollection.VolumeCollection", + "Name": "Storage Volume Collection", + "Description": "Storage Volume Collection", + "Members@odata.count": 3, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/1" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/2" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/3" + } + ], + "Oem": {}, + "@odata.context": "/redfish/v1/$metadata#VolumeCollection.VolumeCollection", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes", + "@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/__init__.py b/sushy/tests/unit/resources/system/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sushy/tests/unit/resources/system/storage/test_volume.py b/sushy/tests/unit/resources/system/storage/test_volume.py new file mode 100644 index 00000000..a595f741 --- /dev/null +++ b/sushy/tests/unit/resources/system/storage/test_volume.py @@ -0,0 +1,123 @@ +# 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 volume +from sushy.tests.unit import base + + +class VolumeTestCase(base.TestCase): + + def setUp(self): + super(VolumeTestCase, self).setUp() + self.conn = mock.Mock() + volume_file = 'sushy/tests/unit/json_samples/volume.json' + with open(volume_file, 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + + self.stor_volume = volume.Volume( + self.conn, '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/1', + redfish_version='1.0.2') + + def test__parse_attributes(self): + self.stor_volume._parse_attributes() + self.assertEqual('1.0.2', self.stor_volume.redfish_version) + self.assertEqual('1', self.stor_volume.identity) + self.assertEqual('Virtual Disk 1', self.stor_volume.name) + self.assertEqual(899527000000, self.stor_volume.capacity_bytes) + + +class VolumeCollectionTestCase(base.TestCase): + + def setUp(self): + super(VolumeCollectionTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/' + 'volume_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + self.stor_vol_col = volume.VolumeCollection( + self.conn, '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes', + redfish_version='1.0.2') + + def test__parse_attributes(self): + self.stor_vol_col._parse_attributes() + self.assertEqual(( + '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/1', + '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/2', + '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/3'), + self.stor_vol_col.members_identities) + + @mock.patch.object(volume, 'Volume', autospec=True) + def test_get_member(self, Volume_mock): + self.stor_vol_col.get_member( + '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/1') + Volume_mock.assert_called_once_with( + self.stor_vol_col._conn, + '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/1', + redfish_version=self.stor_vol_col.redfish_version) + + @mock.patch.object(volume, 'Volume', autospec=True) + def test_get_members(self, Volume_mock): + members = self.stor_vol_col.get_members() + calls = [ + mock.call(self.stor_vol_col._conn, + '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/1', + redfish_version=self.stor_vol_col.redfish_version), + mock.call(self.stor_vol_col._conn, + '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/2', + redfish_version=self.stor_vol_col.redfish_version), + mock.call(self.stor_vol_col._conn, + '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/3', + redfish_version=self.stor_vol_col.redfish_version), + ] + Volume_mock.assert_has_calls(calls) + self.assertIsInstance(members, list) + self.assertEqual(3, len(members)) + + def test_max_size_bytes(self): + self.assertIsNone(self.stor_vol_col._max_size_bytes) + self.conn.get.return_value.json.reset_mock() + + successive_return_values = [] + with open('sushy/tests/unit/json_samples/volume.json', 'r') as f: + successive_return_values.append(json.loads(f.read())) + with open('sushy/tests/unit/json_samples/volume2.json', 'r') as f: + successive_return_values.append(json.loads(f.read())) + with open('sushy/tests/unit/json_samples/volume3.json', 'r') as f: + successive_return_values.append(json.loads(f.read())) + self.conn.get.return_value.json.side_effect = successive_return_values + + self.assertEqual(1073741824000, self.stor_vol_col.max_size_bytes) + + # for any subsequent fetching it gets it from the cached value + self.conn.get.return_value.json.reset_mock() + self.assertEqual(1073741824000, self.stor_vol_col.max_size_bytes) + self.conn.get.return_value.json.assert_not_called() + + def test_max_size_bytes_after_refresh(self): + self.stor_vol_col.refresh() + self.assertIsNone(self.stor_vol_col._max_size_bytes) + self.conn.get.return_value.json.reset_mock() + + successive_return_values = [] + with open('sushy/tests/unit/json_samples/volume.json', 'r') as f: + successive_return_values.append(json.loads(f.read())) + with open('sushy/tests/unit/json_samples/volume2.json', 'r') as f: + successive_return_values.append(json.loads(f.read())) + with open('sushy/tests/unit/json_samples/volume3.json', 'r') as f: + successive_return_values.append(json.loads(f.read())) + self.conn.get.return_value.json.side_effect = successive_return_values + + self.assertEqual(1073741824000, self.stor_vol_col.max_size_bytes) diff --git a/sushy/tests/unit/test_utils.py b/sushy/tests/unit/test_utils.py index de75de49..115bfed6 100644 --- a/sushy/tests/unit/test_utils.py +++ b/sushy/tests/unit/test_utils.py @@ -90,3 +90,9 @@ class UtilsTestCase(base.TestCase): '"subresource_name" cannot be empty', utils.get_sub_resource_path_by, self.sys_inst, '') + + def test_max_safe(self): + self.assertEqual(10, utils.max_safe([1, 3, 2, 8, 5, 10, 6])) + self.assertEqual(821, utils.max_safe([15, 300, 270, None, 821, None])) + self.assertEqual(0, utils.max_safe([])) + self.assertIsNone(utils.max_safe([], default=None)) diff --git a/sushy/utils.py b/sushy/utils.py index 15519cc1..0f687d07 100644 --- a/sushy/utils.py +++ b/sushy/utils.py @@ -89,3 +89,20 @@ def get_sub_resource_path_by(resource, subresource_name): resource=resource.path) return body['@odata.id'] + + +def max_safe(iterable, default=0): + """Helper wrapper over builtin max() function. + + This function is just a wrapper over builtin max() w/o ``key`` argument. + The ``default`` argument specifies an object to return if the provided + ``iterable`` is empty. Also it filters out the None type values. + :param iterable: an iterable + :param default: 0 by default + """ + + try: + 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