From 396da34b7591dad8a5f8382839fcfe775b02ae2b Mon Sep 17 00:00:00 2001 From: dnuka Date: Fri, 5 Oct 2018 20:06:35 +0530 Subject: [PATCH] Add support for the `CompositionService` resource Adds the `CompositionService` resource of Redfish standard schema. The `CompositionService` is the top level resource for all things related to Composability. If a Redfish service supports Composability, the Service Root resource will contain the `CompositionService` property. Implemented according to the latest Redfish schema versions[1]. [1]https://redfish.dmtf.org/schemas/ Story: #2003853 Task: #26650 Change-Id: I135d9d58e6693647a53cdd405b3c841edad4772a --- ..._composition_service-84750d8d1d96474a.yaml | 8 ++ sushy/main.py | 20 ++++ .../resources/compositionservice/__init__.py | 0 .../compositionservice/compositionservice.py | 96 +++++++++++++++ .../resources/compositionservice/constants.py | 31 +++++ .../resources/compositionservice/mappings.py | 40 +++++++ .../compositionservice/resourceblock.py | 112 ++++++++++++++++++ .../compositionservice/resourcezone.py | 92 ++++++++++++++ .../unit/json_samples/compositionservice.json | 21 ++++ .../unit/json_samples/resourceblock.json | 48 ++++++++ .../resourceblock_collection.json | 11 ++ .../tests/unit/json_samples/resourcezone.json | 50 ++++++++ .../json_samples/resourcezone_collection.json | 13 ++ sushy/tests/unit/json_samples/root.json | 3 + .../resources/compositionservice/__init__.py | 0 .../test_compositionservice.py | 49 ++++++++ .../compositionservice/test_resourceblock.py | 107 +++++++++++++++++ .../compositionservice/test_resourcezone.py | 93 +++++++++++++++ sushy/tests/unit/test_main.py | 17 +++ 19 files changed, 811 insertions(+) create mode 100644 releasenotes/notes/add_composition_service-84750d8d1d96474a.yaml create mode 100644 sushy/resources/compositionservice/__init__.py create mode 100644 sushy/resources/compositionservice/compositionservice.py create mode 100644 sushy/resources/compositionservice/constants.py create mode 100644 sushy/resources/compositionservice/mappings.py create mode 100644 sushy/resources/compositionservice/resourceblock.py create mode 100644 sushy/resources/compositionservice/resourcezone.py create mode 100644 sushy/tests/unit/json_samples/compositionservice.json create mode 100644 sushy/tests/unit/json_samples/resourceblock.json create mode 100644 sushy/tests/unit/json_samples/resourceblock_collection.json create mode 100644 sushy/tests/unit/json_samples/resourcezone.json create mode 100644 sushy/tests/unit/json_samples/resourcezone_collection.json create mode 100644 sushy/tests/unit/resources/compositionservice/__init__.py create mode 100644 sushy/tests/unit/resources/compositionservice/test_compositionservice.py create mode 100644 sushy/tests/unit/resources/compositionservice/test_resourceblock.py create mode 100644 sushy/tests/unit/resources/compositionservice/test_resourcezone.py diff --git a/releasenotes/notes/add_composition_service-84750d8d1d96474a.yaml b/releasenotes/notes/add_composition_service-84750d8d1d96474a.yaml new file mode 100644 index 00000000..c6585cb2 --- /dev/null +++ b/releasenotes/notes/add_composition_service-84750d8d1d96474a.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds support for the CompositionService resource to the library. + + The `CompositionService` is the top level resource for all things + related to Composability. If a Redfish service supports Composability, + the Service Root resource will contain the `CompositionService` property. diff --git a/sushy/main.py b/sushy/main.py index 12e26726..b1c59633 100644 --- a/sushy/main.py +++ b/sushy/main.py @@ -19,6 +19,7 @@ from sushy import connector as sushy_connector from sushy import exceptions from sushy.resources import base from sushy.resources.chassis import chassis +from sushy.resources.compositionservice import compositionservice from sushy.resources.manager import manager from sushy.resources.registry import message_registry_file from sushy.resources.sessionservice import session @@ -65,6 +66,10 @@ class Sushy(base.ResourceBase): 'ProtocolFeaturesSupported') """The information about protocol features supported by the service""" + _composition_service_path = base.Field( + ['CompositionService', '@odata.id']) + """CompositionService path""" + _systems_path = base.Field(['Systems', '@odata.id']) """SystemCollection path""" @@ -257,3 +262,18 @@ class Sushy(base.ResourceBase): self._conn, self._registries_path, redfish_version=self.redfish_version) + + def get_composition_service(self): + """Get the CompositionService object + + :raises: MissingAttributeError, if the composition service + attribute is not found + :returns: The CompositionService object + """ + if not self._composition_service_path: + raise exceptions.MissingAttributeError( + attribute='CompositionService/@odata.id', + resource=self._path) + return compositionservice.CompositionService( + self._conn, self._composition_service_path, + redfish_version=self.redfish_version) diff --git a/sushy/resources/compositionservice/__init__.py b/sushy/resources/compositionservice/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sushy/resources/compositionservice/compositionservice.py b/sushy/resources/compositionservice/compositionservice.py new file mode 100644 index 00000000..a70ea32f --- /dev/null +++ b/sushy/resources/compositionservice/compositionservice.py @@ -0,0 +1,96 @@ +# 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. +# https://redfish.dmtf.org/schemas/CompositionService.v1_1_0.json + +import logging + +from sushy import exceptions +from sushy.resources import base +from sushy.resources import common +from sushy.resources.compositionservice import resourceblock +from sushy.resources.compositionservice import resourcezone +from sushy import utils + +LOG = logging.getLogger(__name__) + + +class CompositionService(base.ResourceBase): + + allow_overprovisioning = base.Field('AllowOverprovisioning') + """This indicates whether this service is allowed to overprovision""" + + allow_zone_affinity = base.Field('AllowZoneAffinity') + """This indicates whether a client is allowed to request that given + composition request""" + + description = base.Field('Description') + """The composition service description""" + + identity = base.Field('Id', required=True) + """The composition service identity string""" + + name = base.Field('Name', required=True) + """The composition service name""" + + status = common.StatusField('Status') + """The status of composition service""" + + service_enabled = base.Field('ServiceEnabled') + """The status of composition service is enabled""" + + def __init__(self, connector, identity, redfish_version=None): + """A class representing a CompositionService + + :param connector: A connector instance + :param identity: The identity of the CompositionService resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of given version + """ + super(CompositionService, self).__init__( + connector, + identity, + redfish_version) + + def _get_resource_blocks_collection_path(self): + """Helper function to find the ResourceBlockCollections path""" + res_block_col = self.json.get('ResourceBlocks') + if not res_block_col: + raise exceptions.MissingAttributeError( + attribute='ResourceBlocks', resource=self._path) + return res_block_col.get('@odata.id') + + def _get_resource_zones_collection_path(self): + """Helper function to find the ResourceZoneCollections path""" + res_zone_col = self.json.get('ResourceZones') + if not res_zone_col: + raise exceptions.MissingAttributeError( + attribute='ResourceZones', resource=self._path) + return res_zone_col.get('@odata.id') + + @property + @utils.cache_it + def resource_blocks(self): + """Property to reference `ResourceBlockCollection` instance""" + return resourceblock.ResourceBlockCollection( + self.conn, self._get_resource_blocks_collection_path, + redfish_version=self.redfish_version) + + @property + @utils.cache_it + def resource_zones(self): + """Property to reference `ResourceZoneCollection` instance""" + return resourcezone.ResourceZoneCollection( + self.conn, self._get_resource_zones_collection_path, + redfish_version=self.redfish_version) diff --git a/sushy/resources/compositionservice/constants.py b/sushy/resources/compositionservice/constants.py new file mode 100644 index 00000000..ad128592 --- /dev/null +++ b/sushy/resources/compositionservice/constants.py @@ -0,0 +1,31 @@ +# 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. + +# Values come from the Redfish ResourceBlock json-schema. +# https://redfish.dmtf.org/schemas/ResourceBlock.v1_3_0.json + +# Composition state related constants +COMPOSITION_STATE_COMPOSING = 'Composing' +COMPOSITION_STATE_COMPOSED_AND_AVAILABLE = 'ComposedAndAvailable' +COMPOSITION_STATE_COMPOSED = 'Composed' +COMPOSITION_STATE_UNUSED = 'Unused' +COMPOSITION_STATE_FAILED = 'Failed' +COMPOSITION_STATE_UNAVAILABLE = 'Unavailable' + +# Resource Block type related constants +RESOURCE_BLOCK_TYPE_COMPUTE = 'Compute' +RESOURCE_BLOCK_TYPE_PROCESSOR = 'Processor' +RESOURCE_BLOCK_TYPE_MEMORY = 'Memory' +RESOURCE_BLOCK_TYPE_NETWORK = 'Network' +RESOURCE_BLOCK_TYPE_STORAGE = 'Storage' +RESOURCE_BLOCK_TYPE_COMPUTERSYSTEM = 'ComputerSystem' +RESOURCE_BLOCK_TYPE_EXPANSION = 'Expansion' diff --git a/sushy/resources/compositionservice/mappings.py b/sushy/resources/compositionservice/mappings.py new file mode 100644 index 00000000..5e09f91b --- /dev/null +++ b/sushy/resources/compositionservice/mappings.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. + +from sushy.resources.compositionservice import constants as comp_cons +from sushy import utils + + +COMPOSITION_STATE_VALUE_MAP = { + 'Composing': comp_cons.COMPOSITION_STATE_COMPOSING, + 'ComposedAndAvailable': comp_cons.COMPOSITION_STATE_COMPOSED_AND_AVAILABLE, + 'Composed': comp_cons.COMPOSITION_STATE_COMPOSED, + 'Unused': comp_cons.COMPOSITION_STATE_UNUSED, + 'Failed': comp_cons.COMPOSITION_STATE_FAILED, + 'Unavailable': comp_cons.COMPOSITION_STATE_UNAVAILABLE +} + +COMPOSITION_STATE_VALUE_MAP_REV = ( + utils.revert_dictionary(COMPOSITION_STATE_VALUE_MAP)) + +RESOURCE_BLOCK_TYPE_VALUE_MAP = { + 'Compute': comp_cons.RESOURCE_BLOCK_TYPE_COMPUTE, + 'Processor': comp_cons.RESOURCE_BLOCK_TYPE_PROCESSOR, + 'Memory': comp_cons.RESOURCE_BLOCK_TYPE_MEMORY, + 'Network': comp_cons.RESOURCE_BLOCK_TYPE_NETWORK, + 'Storage': comp_cons.RESOURCE_BLOCK_TYPE_STORAGE, + 'ComputerSystem': comp_cons.RESOURCE_BLOCK_TYPE_COMPUTERSYSTEM, + 'Expansion': comp_cons.RESOURCE_BLOCK_TYPE_EXPANSION +} + +RESOURCE_BLOCK_TYPE_VALUE_MAP_REV = ( + utils.revert_dictionary(RESOURCE_BLOCK_TYPE_VALUE_MAP)) diff --git a/sushy/resources/compositionservice/resourceblock.py b/sushy/resources/compositionservice/resourceblock.py new file mode 100644 index 00000000..8fabfbf6 --- /dev/null +++ b/sushy/resources/compositionservice/resourceblock.py @@ -0,0 +1,112 @@ +# 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. +# https://redfish.dmtf.org/schemas/ResourceBlock.v1_1_0.json + +import logging + +from sushy.resources import base +from sushy.resources import common +from sushy.resources.compositionservice import mappings as res_maps + +LOG = logging.getLogger(__name__) + + +class CompositionStatusField(base.CompositeField): + + composition_state = base.MappedField( + 'CompositionState', + res_maps.COMPOSITION_STATE_VALUE_MAP, + required=True) + """Inform the client, state of the resource block""" + + max_compositions = base.Field('MaxCompositions') + """The maximum number of compositions""" + + number_of_compositions = base.Field('NumberOfCompositions') + """The number of compositions""" + + reserved_state = base.Field('Reserved') + """Inform the resource block has been identified by a client""" + + sharing_capable = base.Field('SharingCapable') + """Indicates if this Resource Block is capable of participating in + multiple compositions simultaneously""" + + sharing_enabled = base.Field('SharingEnabled') + """Indicates if this Resource Block is allowed to participate in + multiple compositions simultaneously""" + + +class ResourceBlock(base.ResourceBase): + + composition_status = CompositionStatusField( + 'CompositionStatus', + required=True) + """The composition state of resource block""" + + description = base.Field('Description') + """The resource block description""" + + identity = base.Field('Id', required=True) + """The resource block identity string""" + + name = base.Field('Name', required=True) + """The resource block name""" + + resource_block_type = base.MappedField( + 'ResourceBlockType', + res_maps.RESOURCE_BLOCK_TYPE_VALUE_MAP, + required=True) + """The type of resource block""" + + status = common.StatusField('Status') + """The status of resource block""" + + def __init__(self, connector, identity, redfish_version=None): + """A class representing a ResourceBlock + + :param connector: A Connector instance + :param identity: The identity of the ResourceBlock resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of given version. + """ + super(ResourceBlock, self).__init__( + connector, + identity, + redfish_version) + + +class ResourceBlockCollection(base.ResourceCollectionBase): + + name = base.Field('Name') + """The resource block collection name""" + + description = base.Field('Description') + """The resource block collection description""" + + @property + def _resource_type(self): + return ResourceBlock + + def __init__(self, connector, identity, redfish_version=None): + """A class representing a ResourceBlockCollection + + :param connector: A Connector instance + :param identity: A identity of the ResourceBlock resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of given version. + """ + super(ResourceBlockCollection, self).__init__( + connector, identity, redfish_version) diff --git a/sushy/resources/compositionservice/resourcezone.py b/sushy/resources/compositionservice/resourcezone.py new file mode 100644 index 00000000..1a6b5bf3 --- /dev/null +++ b/sushy/resources/compositionservice/resourcezone.py @@ -0,0 +1,92 @@ +# 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. +# https://redfish.dmtf.org/schemas/Zone.v1_2_0.json + +import logging + +from sushy.resources import base +from sushy.resources import common + +LOG = logging.getLogger(__name__) + + +class LinksField(base.CompositeField): + + endpoints = base.Field('Endpoints') + """The references to the endpoints that are contained in this zone""" + + involved_switches = base.Field('InvolvedSwitches') + """The references to the switches in this zone""" + + resource_blocks = base.Field('ResourceBlocks') + """The references to the Resource Blocks that are used in this zone""" + + +class ResourceZone(base.ResourceBase): + + # Note(dnuka): This patch doesn't contain 100% of the ResourceZone + + description = base.Field('Description') + """The resources zone description""" + + identity = base.Field('Id', required=True) + """The resource zone identity string""" + + links = LinksField('Links') + """The references to other resources that are related to this + resource""" + + name = base.Field('Name', required=True) + """The resource zone name""" + + status = common.StatusField('Status') + """The resource zone status""" + + def __init__(self, connector, identity, redfish_version=None): + """A class representing a ResourceZone + + :param connector: A Connector instance + :param identity: The identity of the ResourceZone resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of given version. + """ + super(ResourceZone, self).__init__( + connector, + identity, + redfish_version) + + +class ResourceZoneCollection(base.ResourceCollectionBase): + + name = base.Field('Name') + """The resource zone collection name""" + + description = base.Field('Description') + """The resource zone collection description""" + + @property + def _resource_type(self): + return ResourceZone + + def __init__(self, connector, identity, redfish_version=None): + """A class representing a ResourceZoneCollection + + :param connector: A Connector instance + :param identity: The identity of the ResourceZone resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of given version. + """ + super(ResourceZoneCollection, self).__init__( + connector, identity, redfish_version) diff --git a/sushy/tests/unit/json_samples/compositionservice.json b/sushy/tests/unit/json_samples/compositionservice.json new file mode 100644 index 00000000..07bd0e23 --- /dev/null +++ b/sushy/tests/unit/json_samples/compositionservice.json @@ -0,0 +1,21 @@ +{ + "@odata.context": "/redfish/v1/$metadata#CompositionService.CompositionService", + "@odata.type": "#CompositionService.v1_1_0.CompositionService", + "@odata.id": "/redfish/v1/CompositionService", + "AllowOverprovisioning": false, + "AllowZoneAffinity": true, + "Description": "CompositionService1", + "Id": "CompositionService", + "Name": "Composition Service", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ServiceEnabled": true, + "ResourceBlocks": { + "@odata.id": "/redfish/v1/CompositionService/ResourceBlocks" + }, + "ResourceZones": { + "@odata.id": "/redfish/v1/CompositionService/ResourceZones" + } +} diff --git a/sushy/tests/unit/json_samples/resourceblock.json b/sushy/tests/unit/json_samples/resourceblock.json new file mode 100644 index 00000000..871b88f3 --- /dev/null +++ b/sushy/tests/unit/json_samples/resourceblock.json @@ -0,0 +1,48 @@ +{ + "@odata.context": "/redfish/v1/$metadata#ResourceBlock.ResourceBlock", + "@odata.type": "#ResourceBlock.v1_3_0.ResourceBlock", + "@odata.id": "/redfish/v1/CompositionService/ResourceBlocks/DriveBlock3", + "Id": "DriveBlock3", + "Name": "Drive Block 3", + "Description": "ResourceBlock1", + "ResourceBlockType": "Storage", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "CompositionStatus": { + "Reserved": false, + "CompositionState": "Composed", + "MaxCompositions": 1, + "NumberOfCompositions": 0, + "SharingCapable": true, + "SharingEnabled": false + }, + "Processors": [], + "Memory": [], + "Storage": [ + { + "@odata.id": "/redfish/v1/CompositionService/ResourceBlocks/DriveBlock3/Storage/Block3NVMe" + } + ], + "Links": { + "ComputerSystems": [ + { + "@odata.id": "/redfish/v1/Systems/ComposedSystem" + } + ], + "Chassis": [ + { + "@odata.id": "/redfish/v1/Chassis/ComposableModule3" + } + ], + "Zones": [ + { + "@odata.id": "/redfish/v1/CompositionService/ResourceZones/1" + }, + { + "@odata.id": "/redfish/v1/CompositionService/ResourceZones/2" + } + ] + } +} diff --git a/sushy/tests/unit/json_samples/resourceblock_collection.json b/sushy/tests/unit/json_samples/resourceblock_collection.json new file mode 100644 index 00000000..de337377 --- /dev/null +++ b/sushy/tests/unit/json_samples/resourceblock_collection.json @@ -0,0 +1,11 @@ +{ + "@odata.type": "#ResourceBlockCollection.ResourceBlockCollection", + "@odata.id": "/redfish/v1/CompositionService/ResourceBlocks", + "Name": "Resource Block Collection", + "Members@odata.count": 1, + "Members": [ + { "@odata.id": "/redfish/v1/CompositionService/ResourceBlocks/ComputeBlock1" } + ] +} + + diff --git a/sushy/tests/unit/json_samples/resourcezone.json b/sushy/tests/unit/json_samples/resourcezone.json new file mode 100644 index 00000000..31c4ef34 --- /dev/null +++ b/sushy/tests/unit/json_samples/resourcezone.json @@ -0,0 +1,50 @@ +{ + "@odata.context": "/redfish/v1/$metadata#Zone.Zone", + "@odata.type": "#Zone.v1_2_1.Zone", + "@odata.id": "/redfish/v1/CompositionService/ResourceZones/1", + "Id": "1", + "Name": "Resource Zone 1", + "Description": "ResourceZone1", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Links": { + "ResourceBlocks": [ + { + "@odata.id": "/redfish/v1/CompositionService/ResourceBlocks/ComputeBlock1" + }, + { + "@odata.id": "/redfish/v1/CompositionService/ResourceBlocks/DriveBlock3" + }, + { + "@odata.id": "/redfish/v1/CompositionService/ResourceBlocks/DriveBlock4" + }, + { + "@odata.id": "/redfish/v1/CompositionService/ResourceBlocks/DriveBlock5" + }, + { + "@odata.id": "/redfish/v1/CompositionService/ResourceBlocks/DriveBlock6" + }, + { + "@odata.id": "/redfish/v1/CompositionService/ResourceBlocks/DriveBlock7" + } + ] + }, + "@Redfish.CollectionCapabilities": { + "@odata.type": "#CollectionCapabilities.v1_0_0.CollectionCapabilities", + "Capabilities": [ + { + "CapabilitiesObject": { + "@odata.id": "/redfish/v1/Systems/Capabilities" + }, + "UseCase": "ComputerSystemComposition", + "Links": { + "TargetCollection": { + "@odata.id": "/redfish/v1/Systems" + } + } + } + ] + } +} diff --git a/sushy/tests/unit/json_samples/resourcezone_collection.json b/sushy/tests/unit/json_samples/resourcezone_collection.json new file mode 100644 index 00000000..423405a3 --- /dev/null +++ b/sushy/tests/unit/json_samples/resourcezone_collection.json @@ -0,0 +1,13 @@ +{ + "@odata.type": "#ZoneCollection.ZoneCollection", + "@odata.id": "/redfish/v1/CompositionService/ResourceZones", + "Name": "Resource Zone Collection", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/CompositionService/ResourceZones/1" + } + ] +} + + diff --git a/sushy/tests/unit/json_samples/root.json b/sushy/tests/unit/json_samples/root.json index ae2de369..6e770eff 100644 --- a/sushy/tests/unit/json_samples/root.json +++ b/sushy/tests/unit/json_samples/root.json @@ -33,6 +33,9 @@ "AccountService": { "@odata.id": "/redfish/v1/AccountService" }, + "CompositionService": { + "@odata.id": "/redfish/v1/CompositionService" + }, "EventService": { "@odata.id": "/redfish/v1/EventService" }, diff --git a/sushy/tests/unit/resources/compositionservice/__init__.py b/sushy/tests/unit/resources/compositionservice/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sushy/tests/unit/resources/compositionservice/test_compositionservice.py b/sushy/tests/unit/resources/compositionservice/test_compositionservice.py new file mode 100644 index 00000000..110c3b1a --- /dev/null +++ b/sushy/tests/unit/resources/compositionservice/test_compositionservice.py @@ -0,0 +1,49 @@ +# 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.compositionservice import compositionservice +from sushy.resources import constants as res_cons +from sushy.tests.unit import base + + +class CompositionServiceTestCase(base.TestCase): + + def setUp(self): + super(CompositionServiceTestCase, self).setUp() + self.conn = mock.Mock() + with open( + 'sushy/tests/unit/json_samples/compositionservice.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + self.comp_ser = compositionservice.CompositionService( + self.conn, + '/redfish/v1/CompositionService', + redfish_version='1.0.2') + + def test__parse_attributes(self): + self.comp_ser._parse_attributes() + self.assertFalse(self.comp_ser.allow_overprovisioning) + self.assertTrue(self.comp_ser.allow_zone_affinity) + self.assertTrue(self.comp_ser.description, 'CompositionService1') + self.assertEqual( + 'CompositionService', + self.comp_ser.identity) + self.assertEqual( + 'Composition Service', + self.comp_ser.name) + self.assertEqual(res_cons.STATE_ENABLED, self.comp_ser.status.state) + self.assertEqual(res_cons.HEALTH_OK, self.comp_ser.status.health) + self.assertTrue(self.comp_ser.service_enabled) diff --git a/sushy/tests/unit/resources/compositionservice/test_resourceblock.py b/sushy/tests/unit/resources/compositionservice/test_resourceblock.py new file mode 100644 index 00000000..af315bfe --- /dev/null +++ b/sushy/tests/unit/resources/compositionservice/test_resourceblock.py @@ -0,0 +1,107 @@ +# 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 import exceptions +from sushy.resources.compositionservice import constants as res_block_cons +from sushy.resources.compositionservice import resourceblock +from sushy.resources import constants as res_cons + +from sushy.tests.unit import base + + +class ResourceBlockTestCase(base.TestCase): + + def setUp(self): + super(ResourceBlockTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/resourceblock.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + self.res_block = resourceblock.ResourceBlock( + self.conn, + '/redfish/v1/CompositionService/ResourceBlocks/DriveBlock3', + redfish_version='1.0.2') + + def test__parse_attributes(self): + self.res_block._parse_attributes() + self.assertEqual( + res_block_cons.COMPOSITION_STATE_COMPOSED, + self.res_block.composition_status.composition_state) + self.assertEqual(1, self.res_block.composition_status.max_compositions) + self.assertEqual( + 0, self.res_block.composition_status.number_of_compositions) + self.assertFalse(self.res_block.composition_status.reserved_state) + self.assertTrue(self.res_block.composition_status.sharing_capable) + self.assertFalse(self.res_block.composition_status.sharing_enabled) + self.assertEqual('ResourceBlock1', self.res_block.description) + self.assertEqual('DriveBlock3', self.res_block.identity) + self.assertEqual('Drive Block 3', self.res_block.name) + self.assertEqual( + res_block_cons.RESOURCE_BLOCK_TYPE_STORAGE, + self.res_block.resource_block_type) + self.assertEqual( + res_cons.STATE_ENABLED, + self.res_block.status.state) + self.assertEqual(res_cons.HEALTH_OK, self.res_block.status.health) + exp_path = '/redfish/v1/CompositionService/ResourceBlocks/DriveBlock3' + self.assertEqual(exp_path, self.res_block.path) + + def test__parse_attributes_missing_identity(self): + self.res_block.json.pop('Id') + self.assertRaisesRegex( + exceptions.MissingAttributeError, 'attribute Id', + self.res_block._parse_attributes) + + +class ResourceBlockCollectionTestCase(base.TestCase): + + def setUp(self): + super(ResourceBlockCollectionTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/' + 'resourceblock_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + self.res_block_col = resourceblock.ResourceBlockCollection( + self.conn, '/redfish/v1/CompositionService/ResourceBlocks', + redfish_version='1.0.2') + + def test__parse_attributes(self): + path = '/redfish/v1/CompositionService/ResourceBlocks/ComputeBlock1' + self.res_block_col._parse_attributes() + self.assertEqual('1.0.2', self.res_block_col.redfish_version) + self.assertEqual( + 'Resource Block Collection', + self.res_block_col.name) + self.assertEqual((path,), self.res_block_col.members_identities) + + @mock.patch.object(resourceblock, 'ResourceBlock', autospec=True) + def test_get_member(self, mock_resourceblock): + path = '/redfish/v1/CompositionService/ResourceBlocks/ComputeBlock1' + self.res_block_col.get_member(path) + mock_resourceblock.assert_called_once_with( + self.res_block_col._conn, path, + redfish_version=self.res_block_col.redfish_version) + + @mock.patch.object(resourceblock, 'ResourceBlock', autospec=True) + def test_get_members(self, mock_resourceblock): + path = '/redfish/v1/CompositionService/ResourceBlocks/ComputeBlock1' + members = self.res_block_col.get_members() + mock_resourceblock.assert_called_once_with( + self.res_block_col._conn, path, + redfish_version=self.res_block_col.redfish_version) + self.assertIsInstance(members, list) + self.assertEqual(1, len(members)) diff --git a/sushy/tests/unit/resources/compositionservice/test_resourcezone.py b/sushy/tests/unit/resources/compositionservice/test_resourcezone.py new file mode 100644 index 00000000..200daad3 --- /dev/null +++ b/sushy/tests/unit/resources/compositionservice/test_resourcezone.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 import exceptions +from sushy.resources.compositionservice import resourcezone +from sushy.resources import constants as res_cons +from sushy.tests.unit import base + + +class ResourceZoneTestCase(base.TestCase): + + def setUp(self): + super(ResourceZoneTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/resourcezone.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + self.res_zone = resourcezone.ResourceZone( + self.conn, + '/redfish/v1/CompositionService/ResourceZones/1', + redfish_version='1.0.2') + + def test__parse_attributes(self): + self.res_zone._parse_attributes() + self.assertEqual('ResourceZone1', self.res_zone.description) + self.assertEqual('1', self.res_zone.identity) + self.assertEqual('Resource Zone 1', self.res_zone.name) + self.assertEqual( + res_cons.STATE_ENABLED, + self.res_zone.status.state) + self.assertEqual( + res_cons.HEALTH_OK, + self.res_zone.status.health) + exp_path = '/redfish/v1/CompositionService/ResourceZones/1' + self.assertEqual(exp_path, self.res_zone.path) + + def test__parse_attributes_missing_identity(self): + self.res_zone.json.pop('Id') + self.assertRaisesRegex( + exceptions.MissingAttributeError, 'attribute Id', + self.res_zone._parse_attributes) + + +class ResourceZoneCollectionTestCase(base.TestCase): + + def setUp(self): + super(ResourceZoneCollectionTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/' + 'resourcezone_collection.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + + self.res_zone_col = resourcezone.ResourceZoneCollection( + self.conn, '/redfish/v1/CompositionService/ResourceZones', + redfish_version='1.0.2') + + def test__parse_attributes(self): + path = '/redfish/v1/CompositionService/ResourceZones/1' + self.res_zone_col._parse_attributes() + self.assertEqual('1.0.2', self.res_zone_col.redfish_version) + self.assertEqual('Resource Zone Collection', self.res_zone_col.name) + self.assertEqual((path,), self.res_zone_col.members_identities) + + @mock.patch.object(resourcezone, 'ResourceZone', autospec=True) + def test_get_member(self, mock_resourcezone): + path = '/redfish/v1/CompositionService/ResourceZones/1' + self.res_zone_col.get_member(path) + mock_resourcezone.assert_called_once_with( + self.res_zone_col._conn, path, + redfish_version=self.res_zone_col.redfish_version) + + @mock.patch.object(resourcezone, 'ResourceZone', autospec=True) + def test_get_members(self, mock_resourcezone): + path = '/redfish/v1/CompositionService/ResourceZones/1' + members = self.res_zone_col.get_members() + mock_resourcezone.assert_called_once_with( + self.res_zone_col._conn, path, + redfish_version=self.res_zone_col.redfish_version) + self.assertIsInstance(members, list) + self.assertEqual(1, len(members)) diff --git a/sushy/tests/unit/test_main.py b/sushy/tests/unit/test_main.py index 972c84fd..a343f3c6 100644 --- a/sushy/tests/unit/test_main.py +++ b/sushy/tests/unit/test_main.py @@ -22,6 +22,7 @@ from sushy import connector from sushy import exceptions from sushy import main from sushy.resources.chassis import chassis +from sushy.resources.compositionservice import compositionservice from sushy.resources.manager import manager from sushy.resources.registry import message_registry_file from sushy.resources.sessionservice import session @@ -69,6 +70,8 @@ class MainTestCase(base.TestCase): self.assertEqual('/redfish/v1/Chassis', self.root._chassis_path) self.assertEqual('/redfish/v1/SessionService', self.root._session_service_path) + self.assertEqual('/redfish/v1/CompositionService', + self.root._composition_service_path) @mock.patch.object(connector, 'Connector', autospec=True) def test__init_throws_exception(self, mock_Connector): @@ -161,6 +164,14 @@ class MainTestCase(base.TestCase): self.root._conn, '/redfish/v1/Registries', redfish_version=self.root.redfish_version) + @mock.patch.object( + compositionservice, 'CompositionService', autospec=True) + def test_get_composition_service(self, mock_comp_ser): + self.root.get_composition_service() + mock_comp_ser.assert_called_once_with( + self.root._conn, '/redfish/v1/CompositionService', + redfish_version=self.root.redfish_version) + class BareMinimumMainTestCase(base.TestCase): @@ -198,5 +209,11 @@ class BareMinimumMainTestCase(base.TestCase): exceptions.MissingAttributeError, 'UpdateService/@odata.id', self.root.get_update_service) + def test_get_composition_service_when_compositionservice_attr_absent( + self): + self.assertRaisesRegex( + exceptions.MissingAttributeError, + 'CompositionService/@odata.id', self.root.get_composition_service) + def test__get_registry_collection_when_registries_attr_absent(self): self.assertIsNone(self.root._get_registry_collection())