diff --git a/rsd_lib/resources/v2_3/storage_service/storage_pool.py b/rsd_lib/resources/v2_3/storage_service/storage_pool.py new file mode 100644 index 0000000..a764b18 --- /dev/null +++ b/rsd_lib/resources/v2_3/storage_service/storage_pool.py @@ -0,0 +1,147 @@ +# Copyright 2018 Intel, Inc. +# All Rights Reserved. +# +# 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 logging + +from sushy import exceptions +from sushy.resources import base +from sushy import utils + +from rsd_lib.resources.v2_3.storage_service import volume +from rsd_lib import utils as rsd_lib_utils + + +LOG = logging.getLogger(__name__) + + +class StatusField(base.CompositeField): + state = base.Field('State') + health = base.Field('Health') + health_rollup = base.Field('HealthRollup') + + +class CapacityField(base.CompositeField): + allocated_bytes = base.Field(['Data', 'AllocatedBytes'], adapter=int) + consumed_bytes = base.Field(['Data', 'ConsumedBytes'], adapter=int) + guaranteed_bytes = base.Field(['Data', 'GuaranteedBytes'], adapter=int) + provisioned_bytes = base.Field(['Data', 'ProvisionedBytes'], adapter=int) + + +class CapacitySourcesField(base.ListField): + providing_drives = base.Field('ProvidingDrives', default=(), + adapter=utils.get_members_identities) + provided_capacity = CapacityField('ProvidedCapacity') + + +class StoragePool(base.ResourceBase): + + identity = base.Field('Id', required=True) + """The storage pool identity string""" + + description = base.Field('Description') + """The storage pool description string""" + + name = base.Field('Name') + """The storage pool name string""" + + status = StatusField('Status') + """The storage pool status""" + + capacity = CapacityField('Capacity') + """The storage pool capacity info""" + + capacity_sources = CapacitySourcesField('CapacitySources') + """The storage pool capacity source info""" + + _allocated_volumes = None # ref to allocated volumes collection + + _allocated_pools = None # ref to allocated pools collection + + def __init__(self, connector, identity, redfish_version=None): + """A class representing a LogicalDrive + + :param connector: A Connector instance + :param identity: The identity of the LogicalDrive resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of the given version. + """ + super(StoragePool, self).__init__(connector, identity, redfish_version) + + def _get_allocated_volumes_path(self): + """Helper function to find the AllocatedVolumes path""" + volume_col = self.json.get('AllocatedVolumes') + if not volume_col: + raise exceptions.MissingAttributeError( + attribute='AllocatedVolumes', resource=self._path) + return rsd_lib_utils.get_resource_identity(volume_col) + + @property + def allocated_volumes(self): + """Property to provide reference to `AllocatedVolumes` instance + + It is calculated once when it is queried for the first time. On + refresh, this property is reset. + """ + if self._allocated_volumes is None: + self._allocated_volumes = volume.VolumeCollection( + self._conn, self._get_allocated_volumes_path(), + redfish_version=self.redfish_version) + + return self._allocated_volumes + + def _get_allocated_pools_path(self): + """Helper function to find the AllocatedPools path""" + storage_pool_col = self.json.get('AllocatedPools') + if not storage_pool_col: + raise exceptions.MissingAttributeError( + attribute='AllocatedPools', resource=self._path) + return rsd_lib_utils.get_resource_identity(storage_pool_col) + + @property + def allocated_pools(self): + """Property to provide reference to `AllocatedPools` instance + + It is calculated once when it is queried for the first time. On + refresh, this property is reset. + """ + if self._allocated_pools is None: + self._allocated_pools = StoragePoolCollection( + self._conn, self._get_allocated_pools_path(), + redfish_version=self.redfish_version) + + return self._allocated_pools + + def refresh(self): + super(StoragePool, self).refresh() + self._allocated_volumes = None + self._allocated_pools = None + + +class StoragePoolCollection(base.ResourceCollectionBase): + + @property + def _resource_type(self): + return StoragePool + + def __init__(self, connector, path, redfish_version=None): + """A class representing a StoragePoolCollection + + :param connector: A Connector instance + :param path: The canonical path to the StoragePool collection resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of the given version. + """ + super(StoragePoolCollection, self).__init__(connector, path, + redfish_version) diff --git a/rsd_lib/resources/v2_3/storage_service/volume.py b/rsd_lib/resources/v2_3/storage_service/volume.py index f92ecfe..0500696 100644 --- a/rsd_lib/resources/v2_3/storage_service/volume.py +++ b/rsd_lib/resources/v2_3/storage_service/volume.py @@ -200,10 +200,10 @@ class VolumeCollection(base.ResourceCollectionBase): return Volume def __init__(self, connector, path, redfish_version=None): - """A class representing a ProcessorCollection + """A class representing a VolumeCollection :param connector: A Connector instance - :param path: The canonical path to the Processor collection resource + :param path: The canonical path to the Volume collection resource :param redfish_version: The version of RedFish. Used to construct the object according to schema of the given version. """ diff --git a/rsd_lib/tests/unit/json_samples/v2_3/storage_pool.json b/rsd_lib/tests/unit/json_samples/v2_3/storage_pool.json new file mode 100644 index 0000000..0e23f17 --- /dev/null +++ b/rsd_lib/tests/unit/json_samples/v2_3/storage_pool.json @@ -0,0 +1,37 @@ +{ + "@odata.context": "/redfish/v1/$metadata#StoragePool.StoragePool", + "@odata.id": "/redfish/v1/StorageServices/NVMeoE1/StoragePools/2", + "@odata.type": "#StoragePool.v1_0_0.StoragePool", + "Description": "Base storage pool", + "Id": "2", + "Name": "BasePool", + "AllocatedVolumes": { + "@odata.id": "/redfish/v1/StorageServices/NVMeoE1/StoragePools/2/AllocatedVolumes" + }, + "AllocatedPools": { + "@odata.id": "/redfish/v1/StorageServices/NVMeoE1/StoragePools/2/AllocatedPools" + }, + "Capacity": { + "Data": { + "AllocatedBytes": 512174850048, + "ConsumedBytes": 3071983104 + } + }, + "CapacitySources": [{ + "ProvidingDrives": [{ + "@odata.id": "/redfish/v1/Chassis/1/Drives/2" + }], + "ProvidedCapacity": { + "Data": { + "AllocatedBytes": 512174850048, + "ConsumedBytes": 3071983104 + } + } + }], + "Oem": {}, + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + } +} \ No newline at end of file diff --git a/rsd_lib/tests/unit/json_samples/v2_3/storage_pool_collection.json b/rsd_lib/tests/unit/json_samples/v2_3/storage_pool_collection.json new file mode 100644 index 0000000..10e6a0f --- /dev/null +++ b/rsd_lib/tests/unit/json_samples/v2_3/storage_pool_collection.json @@ -0,0 +1,16 @@ +{ + "@odata.context": "/redfish/v1/$metadata#StoragePools", + "@odata.id": "/redfish/v1/StorageServices/NVMeoE1/StoragePools", + "@odata.type": "#StoragePoolCollection.StoragePoolCollection", + "Description": "Collection of Storage Pools", + "Members": [ + { + "@odata.id": "/redfish/v1/StorageServices/NVMeoE1/StoragePools/1" + }, + { + "@odata.id": "/redfish/v1/StorageServices/NVMeoE1/StoragePools/2" + } + ], + "Members@odata.count": 2, + "Name": "StoragePools Collection" +} \ No newline at end of file diff --git a/rsd_lib/tests/unit/resources/v2_3/storage_service/test_storage_pool.py b/rsd_lib/tests/unit/resources/v2_3/storage_service/test_storage_pool.py new file mode 100644 index 0000000..810fa0b --- /dev/null +++ b/rsd_lib/tests/unit/resources/v2_3/storage_service/test_storage_pool.py @@ -0,0 +1,245 @@ +# Copyright 2018 Intel, Inc. +# All Rights Reserved. +# +# 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 +import testtools + +from sushy import exceptions + +from rsd_lib.resources.v2_3.storage_service import storage_pool +from rsd_lib.resources.v2_3.storage_service import volume + + +class StoragePoolTestCase(testtools.TestCase): + + def setUp(self): + super(StoragePoolTestCase, self).setUp() + self.conn = mock.Mock() + with open('rsd_lib/tests/unit/json_samples/v2_3/storage_pool.json', + 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + + self.storage_pool_inst = storage_pool.StoragePool( + self.conn, '/redfish/v1/StorageServices/NVMeoE1/StoragePool/2', + redfish_version='1.0.2') + + def test__parse_attributes(self): + self.storage_pool_inst._parse_attributes() + self.assertEqual('1.0.2', self.storage_pool_inst.redfish_version) + self.assertEqual('Base storage pool', + self.storage_pool_inst.description) + self.assertEqual('2', self.storage_pool_inst.identity) + self.assertEqual('BasePool', self.storage_pool_inst.name) + self.assertEqual('Enabled', self.storage_pool_inst.status.state) + self.assertEqual('OK', self.storage_pool_inst.status.health) + self.assertEqual('OK', self.storage_pool_inst.status.health_rollup) + self.assertEqual(512174850048, + self.storage_pool_inst.capacity.allocated_bytes) + self.assertEqual(3071983104, + self.storage_pool_inst.capacity.consumed_bytes) + self.assertEqual(None, + self.storage_pool_inst.capacity.guaranteed_bytes) + self.assertEqual(None, + self.storage_pool_inst.capacity.provisioned_bytes) + self.assertEqual( + ('/redfish/v1/Chassis/1/Drives/2',), + self.storage_pool_inst.capacity_sources[0].providing_drives) + self.assertEqual( + 512174850048, + self.storage_pool_inst.capacity_sources[0]. + provided_capacity.allocated_bytes) + self.assertEqual( + 3071983104, + self.storage_pool_inst.capacity_sources[0]. + provided_capacity.consumed_bytes) + self.assertEqual( + None, + self.storage_pool_inst.capacity_sources[0]. + provided_capacity.guaranteed_bytes) + self.assertEqual( + None, + self.storage_pool_inst.capacity_sources[0]. + provided_capacity.provisioned_bytes) + + def test__get_allocated_volumes_path(self): + expected = '/redfish/v1/StorageServices/NVMeoE1/StoragePools/'\ + '2/AllocatedVolumes' + result = self.storage_pool_inst._get_allocated_volumes_path() + self.assertEqual(expected, result) + + def test__get_allocated_volumes_path_missing_processors_attr(self): + self.storage_pool_inst._json.pop('AllocatedVolumes') + self.assertRaisesRegex( + exceptions.MissingAttributeError, 'attribute AllocatedVolumes', + self.storage_pool_inst._get_allocated_volumes_path) + + def test_allocated_volumes(self): + # check for the underneath variable value + self.assertIsNone(self.storage_pool_inst._allocated_volumes) + # | GIVEN | + self.conn.get.return_value.json.reset_mock() + with open('rsd_lib/tests/unit/json_samples/v2_3/' + 'volume_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + # | WHEN | + actual_volumes = self.storage_pool_inst.allocated_volumes + # | THEN | + self.assertIsInstance(actual_volumes, + volume.VolumeCollection) + 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_volumes, + self.storage_pool_inst.allocated_volumes) + self.conn.get.return_value.json.assert_not_called() + + def test_allocated_volumes_on_refresh(self): + # | GIVEN | + with open('rsd_lib/tests/unit/json_samples/v2_3/' + 'volume_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + # | WHEN & THEN | + self.assertIsInstance(self.storage_pool_inst.allocated_volumes, + volume.VolumeCollection) + + # On refreshing the storage service instance... + with open('rsd_lib/tests/unit/json_samples/v2_3/' + 'storage_pool.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + self.storage_pool_inst.refresh() + + # | WHEN & THEN | + self.assertIsNone(self.storage_pool_inst._allocated_volumes) + + # | GIVEN | + with open('rsd_lib/tests/unit/json_samples/v2_3/' + 'volume_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + # | WHEN & THEN | + self.assertIsInstance(self.storage_pool_inst.allocated_volumes, + volume.VolumeCollection) + + def test__get_allocated_pools_path(self): + expected = '/redfish/v1/StorageServices/NVMeoE1/StoragePools/'\ + '2/AllocatedPools' + result = self.storage_pool_inst._get_allocated_pools_path() + self.assertEqual(expected, result) + + def test__get_allocated_pools_path_missing_processors_attr(self): + self.storage_pool_inst._json.pop('AllocatedPools') + self.assertRaisesRegex( + exceptions.MissingAttributeError, 'attribute AllocatedPools', + self.storage_pool_inst._get_allocated_pools_path) + + def test_allocated_pools(self): + # check for the underneath variable value + self.assertIsNone(self.storage_pool_inst._allocated_pools) + # | GIVEN | + self.conn.get.return_value.json.reset_mock() + with open('rsd_lib/tests/unit/json_samples/v2_3/' + 'storage_pool_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + # | WHEN | + actual_storage_pools = self.storage_pool_inst.allocated_pools + # | THEN | + self.assertIsInstance(actual_storage_pools, + storage_pool.StoragePoolCollection) + 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_pools, + self.storage_pool_inst.allocated_pools) + self.conn.get.return_value.json.assert_not_called() + + def test_allocated_pools_on_refresh(self): + # | GIVEN | + with open('rsd_lib/tests/unit/json_samples/v2_3/' + 'storage_pool_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + # | WHEN & THEN | + self.assertIsInstance(self.storage_pool_inst.allocated_pools, + storage_pool.StoragePoolCollection) + + # On refreshing the storage service instance... + with open('rsd_lib/tests/unit/json_samples/v2_3/' + 'storage_pool.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + self.storage_pool_inst.refresh() + + # | WHEN & THEN | + self.assertIsNone(self.storage_pool_inst._allocated_pools) + + # | GIVEN | + with open('rsd_lib/tests/unit/json_samples/v2_3/' + 'storage_pool_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + # | WHEN & THEN | + self.assertIsInstance(self.storage_pool_inst.allocated_pools, + storage_pool.StoragePoolCollection) + + +class StoragePoolCollectionTestCase(testtools.TestCase): + + def setUp(self): + super(StoragePoolCollectionTestCase, self).setUp() + self.conn = mock.Mock() + with open('rsd_lib/tests/unit/json_samples/v2_3/' + 'storage_pool_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + + self.storage_pool_col = storage_pool.StoragePoolCollection( + self.conn, '/redfish/v1/StorageServices/NVMeoE1/StoragePools', + redfish_version='1.0.2') + + def test__parse_attributes(self): + self.storage_pool_col._parse_attributes() + self.assertEqual('1.0.2', self.storage_pool_col.redfish_version) + self.assertEqual('StoragePools Collection', + self.storage_pool_col.name) + self.assertEqual( + ('/redfish/v1/StorageServices/NVMeoE1/StoragePools/1', + '/redfish/v1/StorageServices/NVMeoE1/StoragePools/2'), + self.storage_pool_col.members_identities) + + @mock.patch.object(storage_pool, 'StoragePool', autospec=True) + def test_get_member(self, mock_storage_pool): + self.storage_pool_col.get_member( + '/redfish/v1/StorageServices/NVMeoE1/StoragePools/1') + mock_storage_pool.assert_called_once_with( + self.storage_pool_col._conn, + '/redfish/v1/StorageServices/NVMeoE1/StoragePools/1', + redfish_version=self.storage_pool_col.redfish_version) + + @mock.patch.object(storage_pool, 'StoragePool', autospec=True) + def test_get_members(self, mock_storage_pool): + members = self.storage_pool_col.get_members() + calls = [ + mock.call(self.storage_pool_col._conn, + '/redfish/v1/StorageServices/NVMeoE1/StoragePools/1', + redfish_version=self.storage_pool_col.redfish_version), + mock.call(self.storage_pool_col._conn, + '/redfish/v1/StorageServices/NVMeoE1/StoragePools/2', + redfish_version=self.storage_pool_col.redfish_version) + ] + mock_storage_pool.assert_has_calls(calls) + self.assertIsInstance(members, list) + self.assertEqual(2, len(members))