diff --git a/requirements.txt b/requirements.txt index 1d18dd3..60ded62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ # process, which may cause wedges in the gate later. pbr>=2.0 # Apache-2.0 +sushy>=0.1.0 # Apache-2.0 diff --git a/rsd_lib/main.py b/rsd_lib/main.py new file mode 100644 index 0000000..716dfca --- /dev/null +++ b/rsd_lib/main.py @@ -0,0 +1,44 @@ +# Copyright 2017 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 sushy +from sushy.resources import base + +from rsd_lib.resources.node import node + + +class RSDLib(sushy.Sushy): + + _nodes_path = base.Field(['Nodes', '@odata.id'], required=True) + """NodeCollection path""" + + def get_node_collection(self): + """Get the NodeCollection object + + :raises: MissingAttributeError, if the collection attribute is + not found + :returns: a NodeCollection object + """ + return node.NodeCollection(self._conn, self._nodes_path, + redfish_version=self.redfish_version) + + def get_node(self, identity): + """Given the identity return a Node object + + :param identity: The identity of the Node resource + :returns: The Node object + """ + return node.Node(self._conn, identity, + redfish_version=self.redfish_version) diff --git a/rsd_lib/resources/__init__.py b/rsd_lib/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rsd_lib/resources/node/__init__.py b/rsd_lib/resources/node/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rsd_lib/resources/node/constants.py b/rsd_lib/resources/node/constants.py new file mode 100644 index 0000000..42242c8 --- /dev/null +++ b/rsd_lib/resources/node/constants.py @@ -0,0 +1,100 @@ +# Copyright 2017 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. + +# Values comes from the Redfish System json-schema 1.0.0: +# http://redfish.dmtf.org/schemas/v1/ComputerSystem.v1_0_0.json#/definitions/ComputerSystem # noqa + +# Reset action constants + +RESET_ON = 'on' +RESET_FORCE_OFF = 'force off' +RESET_GRACEFUL_SHUTDOWN = 'graceful shutdown' +RESET_GRACEFUL_RESTART = 'graceful restart' +RESET_FORCE_RESTART = 'force restart' +RESET_NMI = 'nmi' +RESET_FORCE_ON = 'force on' +RESET_PUSH_POWER_BUTTON = 'push power button' + +# Node PowerState constants + +NODE_POWER_STATE_ON = 'on' +"""The system is powered on""" + +NODE_POWER_STATE_OFF = 'off' +"""The system is powered off, although some components may continue to + have AUX power such as management controller""" + +NODE_POWER_STATE_POWERING_ON = 'powering on' +"""A temporary state between Off and On. This temporary state can + be very short""" + +NODE_POWER_STATE_POWERING_OFF = 'powering off' +"""A temporary state between On and Off. The power off action can take + time while the OS is in the shutdown process""" + +# Composed Node State constants + +COMPOSED_NODE_STATE_ALLOCATING = 'allocating' +"""Allocating resources for node is in progress. Next state can be + Allocated or Failed""" + +COMPOSED_NODE_STATE_ALLOCATED = 'allocated' +"""Node resources have been allocated, but assembly not started yet. + After ComposedNode.Assemble action state will progress to Assembling""" + +COMPOSED_NODE_STATE_ASSEMBLING = 'assembling' +"""Assembly process initiated, but not finished yet. When assembly + is done it will change into Assembled""" + +COMPOSED_NODE_STATE_ASSEMBLED = 'assembled' +"""Node successfully assembled""" + +COMPOSED_NODE_STATE_FAILED = 'failed' +"""Allocation or assembly process failed, or in runtime one of composing + components was removed or transitioned in error state""" + +# Boot source target constants + +BOOT_SOURCE_TARGET_NONE = 'none' +"""Boot from the normal boot device""" + +BOOT_SOURCE_TARGET_PXE = 'pxe' +"""Boot from the Pre-Boot EXecution (PXE) environment""" + +BOOT_SOURCE_TARGET_HDD = 'hdd' +"""Boot from a hard drive""" + +# Boot source mode constants + +BOOT_SOURCE_MODE_LEGACY = 'legacy' +BOOT_SOURCE_MODE_UEFI = 'uefi' + +# Boot source enabled constants + +BOOT_SOURCE_ENABLED_ONCE = 'once' +BOOT_SOURCE_ENABLED_CONTINUOUS = 'continuous' +BOOT_SOURCE_ENABLED_DISABLED = 'disabled' + +# Processor related constants +# Values comes from the Redfish Processor json-schema 1.0.0: +# http://redfish.dmtf.org/schemas/v1/Processor.v1_0_0.json + +# Processor Architecture constants + +PROCESSOR_ARCH_x86 = 'x86 or x86-64' +PROCESSOR_ARCH_IA_64 = 'Intel Itanium' +PROCESSOR_ARCH_ARM = 'ARM' +PROCESSOR_ARCH_MIPS = 'MIPS' +PROCESSOR_ARCH_OEM = 'OEM-defined' diff --git a/rsd_lib/resources/node/mappings.py b/rsd_lib/resources/node/mappings.py new file mode 100644 index 0000000..2f8e815 --- /dev/null +++ b/rsd_lib/resources/node/mappings.py @@ -0,0 +1,84 @@ +# Copyright 2017 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. + +from sushy import utils + +from rsd_lib.resources.node import constants as node_cons + +RESET_NODE_VALUE_MAP = { + 'On': node_cons.RESET_ON, + 'ForceOff': node_cons.RESET_FORCE_OFF, + 'GracefulShutdown': node_cons.RESET_GRACEFUL_SHUTDOWN, + 'GracefulRestart': node_cons.RESET_GRACEFUL_RESTART, + 'ForceRestart': node_cons.RESET_FORCE_RESTART, + 'Nmi': node_cons.RESET_NMI, + 'ForceOn': node_cons.RESET_FORCE_ON, + 'PushPowerButton': node_cons.RESET_PUSH_POWER_BUTTON, +} + +RESET_NODE_VALUE_MAP_REV = utils.revert_dictionary(RESET_NODE_VALUE_MAP) + +NODE_POWER_STATE_MAP = { + 'On': node_cons.NODE_POWER_STATE_ON, + 'Off': node_cons.NODE_POWER_STATE_OFF, + 'PoweringOn': node_cons.NODE_POWER_STATE_POWERING_ON, + 'PoweringOff': node_cons.NODE_POWER_STATE_POWERING_OFF, +} + +NODE_POWER_STATE_MAP_REV = utils.revert_dictionary(NODE_POWER_STATE_MAP) + +COMPOSED_NODE_STATE_MAP = { + 'Allocating': node_cons.COMPOSED_NODE_STATE_ALLOCATING, + 'Allocated': node_cons.COMPOSED_NODE_STATE_ALLOCATED, + 'Assembling': node_cons.COMPOSED_NODE_STATE_ASSEMBLING, + 'Assembled': node_cons.COMPOSED_NODE_STATE_ASSEMBLED, + 'Failed': node_cons.COMPOSED_NODE_STATE_FAILED, +} + +COMPOSED_NODE_STATE_MAP_REV = utils.revert_dictionary(COMPOSED_NODE_STATE_MAP) + +BOOT_SOURCE_TARGET_MAP = { + 'None': node_cons.BOOT_SOURCE_TARGET_NONE, + 'Pxe': node_cons.BOOT_SOURCE_TARGET_PXE, + 'Hdd': node_cons.BOOT_SOURCE_TARGET_HDD, +} + +BOOT_SOURCE_TARGET_MAP_REV = utils.revert_dictionary(BOOT_SOURCE_TARGET_MAP) + +BOOT_SOURCE_MODE_MAP = { + 'Legacy': node_cons.BOOT_SOURCE_MODE_LEGACY, + 'UEFI': node_cons.BOOT_SOURCE_MODE_UEFI, +} + +BOOT_SOURCE_MODE_MAP_REV = utils.revert_dictionary(BOOT_SOURCE_MODE_MAP) + +BOOT_SOURCE_ENABLED_MAP = { + 'Once': node_cons.BOOT_SOURCE_ENABLED_ONCE, + 'Continuous': node_cons.BOOT_SOURCE_ENABLED_CONTINUOUS, + 'Disabled': node_cons.BOOT_SOURCE_ENABLED_DISABLED, +} + +BOOT_SOURCE_ENABLED_MAP_REV = utils.revert_dictionary(BOOT_SOURCE_ENABLED_MAP) + +PROCESSOR_ARCH_VALUE_MAP = { + 'x86': node_cons.PROCESSOR_ARCH_x86, + 'IA-64': node_cons.PROCESSOR_ARCH_IA_64, + 'ARM': node_cons.PROCESSOR_ARCH_ARM, + 'MIPS': node_cons.PROCESSOR_ARCH_MIPS, + 'OEM': node_cons.PROCESSOR_ARCH_OEM, +} + +PROCESSOR_ARCH_VALUE_MAP_REV = ( + utils.revert_dictionary(PROCESSOR_ARCH_VALUE_MAP)) diff --git a/rsd_lib/resources/node/node.py b/rsd_lib/resources/node/node.py new file mode 100644 index 0000000..0db9d64 --- /dev/null +++ b/rsd_lib/resources/node/node.py @@ -0,0 +1,271 @@ +# Copyright 2017 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.resources import common +from sushy.resources.system import processor + +from rsd_lib.resources.node import constants as node_cons +from rsd_lib.resources.node import mappings as node_maps + + +LOG = logging.getLogger(__name__) + + +class AssembleActionField(base.CompositeField): + target_uri = base.Field('target', required=True) + + +class AttachEndpointActionField(base.CompositeField): + allowed_values = base.Field('Resource@Redfish.AllowableValues', + adapter=list) + + target_uri = base.Field('target', required=True) + + +class DetachEndpointActionField(base.CompositeField): + allowed_values = base.Field('Resource@Redfish.AllowableValues', + adapter=list) + + target_uri = base.Field('target', required=True) + + +class ActionsField(base.CompositeField): + reset = common.ResetActionField('#ComposedNode.Reset') + assemble = AssembleActionField('#ComposedNode.Assemble') + attach_endpoint = AttachEndpointActionField('#ComposedNode.AttachEndpoint') + detach_endpoint = DetachEndpointActionField('#ComposedNode.DetachEndpoint') + + +class BootField(base.CompositeField): + allowed_values = base.Field( + 'BootSourceOverrideTarget@Redfish.AllowableValues', + adapter=list) + + enabled = base.MappedField('BootSourceOverrideEnabled', + node_maps.BOOT_SOURCE_ENABLED_MAP) + + mode = base.MappedField('BootSourceOverrideMode', + node_maps.BOOT_SOURCE_MODE_MAP) + + target = base.MappedField('BootSourceOverrideTarget', + node_maps.BOOT_SOURCE_TARGET_MAP) + + +class MemorySummaryField(base.CompositeField): + health = base.Field(['Status', 'Health']) + """The overall health state of memory. + + This signifies health state of memory along with its dependent resources. + """ + + size_gib = base.Field('TotalSystemMemoryGiB', adapter=int) + """The size of memory of the node in GiB. + + This signifies the total installed, operating system-accessible memory + (RAM), measured in GiB. + """ + + +class Node(base.ResourceBase): + + boot = BootField('Boot', required=True) + """A dictionary containg the current boot device, frequency and mode""" + + composed_node_state = base.MappedField('ComposedNodeState', + node_maps.COMPOSED_NODE_STATE_MAP) + """Current state of assembly process for this node""" + + description = base.Field('Description') + """The node description""" + + identity = base.Field('Id', required=True) + """The node identity string""" + + name = base.Field('Name') + """The node name""" + + power_state = base.MappedField('PowerState', + node_maps.NODE_POWER_STATE_MAP) + """The node power state""" + + uuid = base.Field('UUID') + """The node UUID""" + + memory_summary = MemorySummaryField('Memory') + """The summary info of memory of the node in general detail""" + + _processors = None # ref to ProcessorCollection instance + + _actions = ActionsField('Actions', required=True) + + def __init__(self, connector, identity, redfish_version=None): + """A class representing a ComposedNode + + :param connector: A Connector instance + :param identity: The identity of the Node resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of the given version. + """ + super(Node, self).__init__(connector, identity, redfish_version) + + def _get_reset_action_element(self): + reset_action = self._actions.reset + if not reset_action: + raise exceptions.MissingActionError(action='#ComposedNode.Reset', + resource=self._path) + return reset_action + + def get_allowed_reset_node_values(self): + """Get the allowed values for resetting the node. + + :returns: A set with the allowed values. + """ + reset_action = self._get_reset_action_element() + + if not reset_action.allowed_values: + LOG.warning('Could not figure out the allowed values for the ' + 'reset node action for Node %s', self.identity) + return set(node_maps.RESET_NODE_VALUE_MAP_REV) + + return set([node_maps.RESET_NODE_VALUE_MAP[v] for v in + set(node_maps.RESET_NODE_VALUE_MAP). + intersection(reset_action.allowed_values)]) + + def reset_node(self, value): + """Reset the node. + + :param value: The target value. + :raises: InvalidParameterValueError, if the target value is not + allowed. + """ + valid_resets = self.get_allowed_reset_node_values() + if value not in valid_resets: + raise exceptions.InvalidParameterValueError( + parameter='value', value=value, valid_values=valid_resets) + + value = node_maps.RESET_NODE_VALUE_MAP_REV[value] + target_uri = self._get_reset_action_element().target_uri + + self._conn.post(target_uri, data={'ResetType': value}) + + def get_allowed_node_boot_source_values(self): + """Get the allowed values for changing the boot source. + + :returns: A set with the allowed values. + """ + if not self.boot.allowed_values: + LOG.warning('Could not figure out the allowed values for ' + 'configuring the boot source for Node %s', + self.identity) + return set(node_maps.BOOT_SOURCE_TARGET_MAP_REV) + + return set([node_maps.BOOT_SOURCE_TARGET_MAP[v] for v in + set(node_maps.BOOT_SOURCE_TARGET_MAP). + intersection(self.boot.allowed_values)]) + + def set_node_boot_source(self, target, + enabled=node_cons.BOOT_SOURCE_ENABLED_ONCE, + mode=None): + """Set the boot source. + + Set the boot source to use on next reboot of the Node. + + :param target: The target boot source. + :param enabled: The frequency, whether to set it for the next + reboot only (BOOT_SOURCE_ENABLED_ONCE) or persistent to all + future reboots (BOOT_SOURCE_ENABLED_CONTINUOUS) or disabled + (BOOT_SOURCE_ENABLED_DISABLED). + :param mode: The boot mode, UEFI (BOOT_SOURCE_MODE_UEFI) or + Legacy (BOOT_SOURCE_MODE_LEGACY). + :raises: InvalidParameterValueError, if any information passed is + invalid. + """ + valid_targets = self.get_allowed_node_boot_source_values() + if target not in valid_targets: + raise exceptions.InvalidParameterValueError( + parameter='target', value=target, valid_values=valid_targets) + + if enabled not in node_maps.BOOT_SOURCE_ENABLED_MAP_REV: + raise exceptions.InvalidParameterValueError( + parameter='enabled', value=enabled, + valid_values=list(node_maps.BOOT_SOURCE_TARGET_MAP_REV)) + + data = { + 'Boot': { + 'BootSourceOverrideTarget': + node_maps.BOOT_SOURCE_TARGET_MAP_REV[target], + 'BootSourceOverrideEnabled': + node_maps.BOOT_SOURCE_ENABLED_MAP_REV[enabled] + } + } + + if mode is not None: + if mode not in node_maps.BOOT_SOURCE_MODE_MAP_REV: + raise exceptions.InvalidParameterValueError( + parameter='mode', value=mode, + valid_values=list(node_maps.BOOT_SOURCE_MODE_MAP_REV)) + + data['Boot']['BootSourceOverrideMode'] = ( + node_maps.BOOT_SOURCE_MODE_MAP_REV[mode]) + + self._conn.patch(self.path, data=data) + + def _get_processor_collection_path(self): + """Helper function to find the ProcessorCollection path""" + processor_col = self.json.get('Processors') + if not processor_col: + raise exceptions.MissingAttributeError(attribute='Processors', + resource=self._path) + return processor_col.get('@odata.id') + + @property + def processors(self): + """Property to provide reference to `ProcessorCollection` instance + + It is calculated once when the first time it is queried. On refresh, + this property gets reset. + """ + if self._processors is None: + self._processors = processor.ProcessorCollection( + self._conn, self._get_processor_collection_path(), + redfish_version=self.redfish_version) + + return self._processors + + def refresh(self): + super(Node, self).refresh() + self._processors = None + + +class NodeCollection(base.ResourceCollectionBase): + + @property + def _resource_type(self): + return Node + + def __init__(self, connector, path, redfish_version=None): + """A class representing a ComposedNodeCollection + + :param connector: A Connector instance + :param path: The canonical path to the Node collection resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of the given version. + """ + super(NodeCollection, self).__init__(connector, path, + redfish_version) diff --git a/rsd_lib/tests/test_rsd_lib.py b/rsd_lib/tests/test_rsd_lib.py deleted file mode 100644 index c0d9a25..0000000 --- a/rsd_lib/tests/test_rsd_lib.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- - -# 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. - -""" -test_rsd_lib ----------------------------------- - -Tests for `rsd_lib` module. -""" - -from rsd_lib.tests import base - - -class TestRsd_lib(base.TestCase): - - def test_something(self): - pass diff --git a/rsd_lib/tests/unit/__init__.py b/rsd_lib/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rsd_lib/tests/unit/json_samples/node.json b/rsd_lib/tests/unit/json_samples/node.json new file mode 100644 index 0000000..fd06cbd --- /dev/null +++ b/rsd_lib/tests/unit/json_samples/node.json @@ -0,0 +1,115 @@ +{ + "@odata.context": "/redfish/v1/$metadata#Nodes/Members/$entity", + "@odata.id": "/redfish/v1/Nodes/Node1", + "@odata.type": "#ComposedNode.1.1.0.ComposedNode", + "Id": "Node1", + "Name": "Composed Node", + "Description": "Node #1", + "UUID": "fa39d108-7d70-400a-9db2-6940375c31c2", + "PowerState": "On", + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "OK" + }, + "Processors": { + "Count": 2, + "Model": "Multi-Core Intel(R) Xeon(R) processor 7xxx Series", + "Status": { + "State": "Enabled", + "Health": "OK" + } + }, + "Memory": { + "TotalSystemMemoryGiB": 32, + "Status": { + "State": "Enabled", + "Health": "OK" + } + }, + "ComposedNodeState": "Allocated", + "Boot": { + "BootSourceOverrideEnabled": "Disabled", + "BootSourceOverrideTarget": "None", + "BootSourceOverrideTarget@Redfish.AllowableValues": [ + "None", + "Pxe", + "Hdd", + "RemoteDrive" + ], + "BootSourceOverrideMode": "Legacy", + "BootSourceOverrideMode@Redfish.AllowableValues": ["Legacy", + "UEFI"] + }, + "Oem": {}, + "Links": { + "ComputerSystem": { + "@odata.id": "/redfish/v1/Systems/System1" + }, + "Processors": [ + { + "@odata.id": "/redfish/v1/Systems/System1/Processors/CPU1" + } + ], + "Memory": [ + { + "@odata.id": "/redfish/v1/Systems/System1/Memory/Dimm1" + } + ], + "EthernetInterfaces": [ + { + "@odata.id": + "/redfish/v1/Systems/System1/EthernetInterfaces/LAN1" + } + ], + "LocalDrives": [ + { + "@odata.id": "/redfish/v1/Chassis/Blade1/Drives/1" + } + ], + "RemoteDrives": [ + { + "@odata.id": "/redfish/v1/Services/RSS1/Targets/target1" + } + ], + "ManagedBy": [ + { + "@odata.id": "/redfish/v1/Managers/PODM" + } + ], + "Oem": {} + }, + "Actions": { + "#ComposedNode.Reset": { + "target": "/redfish/v1/Nodes/Node1/Actions/ComposedNode.Reset", + "ResetType@Redfish.AllowableValues": [ + "On", + "ForceOff", + "GracefulRestart", + "ForceRestart", + "Nmi", + "ForceOn", + "PushPowerButton", + "GracefulShutdown" + ] + }, + "#ComposedNode.Assemble": { + "target": "/redfish/v1/Nodes/Node1/Actions/ComposedNode.Assemble" + }, + "#ComposedNode.AttachEndpoint": { + "target": + "/redfish/v1/Nodes/Node1/Actions/ComposedNode.AttachEndpoint", + "Resource@Redfish.AllowableValues": [ + {"@odata.id":"/redfish/v1/Chassis/PCIeSwitchChassis/Drives/Disk.Bay.1"}, + {"@odata.id":"/redfish/v1/Chassis/PCIeSwitchChassis/Drives/Disk.Bay.2"} + ] + }, + "#ComposedNode.DetachEndpoint": { + "target": + "/redfish/v1/Nodes/Node1/Actions/ComposedNode.DetachEndpoint", + "Resource@Redfish.AllowableValues": [ + {"@odata.id":"/redfish/v1/Chassis/PCIeSwitchChassis/Drives/Disk.Bay.3"} + ] + } + } +} diff --git a/rsd_lib/tests/unit/json_samples/node_collection.json b/rsd_lib/tests/unit/json_samples/node_collection.json new file mode 100644 index 0000000..2aca387 --- /dev/null +++ b/rsd_lib/tests/unit/json_samples/node_collection.json @@ -0,0 +1,17 @@ +{ + "@odata.type": "#ComputerSystemCollection.ComposedNodeCollection", + "Name": "Composed Nodes Collection", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Nodes/Node1" + } + ], + "@odata.context": "/redfish/v1/$metadata#Nodes", + "@odata.id": "/redfish/v1/Nodes", + "Actions": { + "#ComposedNodeCollection.Allocate": { + "target": "/redfish/v1/Nodes/Actions/Allocate" + } + } +} diff --git a/rsd_lib/tests/unit/json_samples/processor.json b/rsd_lib/tests/unit/json_samples/processor.json new file mode 100644 index 0000000..19ce642 --- /dev/null +++ b/rsd_lib/tests/unit/json_samples/processor.json @@ -0,0 +1,28 @@ +{ + "@odata.type": "#Processor.v1_0_2.Processor", + "Id": "CPU1", + "Socket": "CPU 1", + "ProcessorType": "CPU", + "ProcessorArchitecture": "x86", + "InstructionSet": "x86-64", + "Manufacturer": "Intel(R) Corporation", + "Model": "Multi-Core Intel(R) Xeon(R) processor 7xxx Series", + "ProcessorID": { + "VendorID": "GenuineIntel", + "IdentificationRegisters": "0x34AC34DC8901274A", + "EffectiveFamily": "0x42", + "EffectiveModel": "0x61", + "Step": "0x1", + "MicrocodeInfo": "0x429943" + }, + "MaxSpeedMHz": 3700, + "TotalCores": 8, + "TotalThreads": 16, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "@odata.context": "/redfish/v1/$metadata#Systems/Members/437XR1138R2/Processors/Members/$entity", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU1", + "@Redfish.Copyright": "Copyright 2014-2016 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} diff --git a/rsd_lib/tests/unit/json_samples/processor2.json b/rsd_lib/tests/unit/json_samples/processor2.json new file mode 100644 index 0000000..a050851 --- /dev/null +++ b/rsd_lib/tests/unit/json_samples/processor2.json @@ -0,0 +1,12 @@ +{ + "@odata.type": "#Processor.v1_0_2.Processor", + "Id": "CPU2", + "Socket": "CPU 2", + "ProcessorType": "CPU", + "Status": { + "State": "Absent" + }, + "@odata.context": "/redfish/v1/$metadata#Systems/Members/437XR1138R2/Processors/Members/$entity", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU2", + "@Redfish.Copyright": "Copyright 2014-2016 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} diff --git a/rsd_lib/tests/unit/json_samples/processor_collection.json b/rsd_lib/tests/unit/json_samples/processor_collection.json new file mode 100644 index 0000000..8373180 --- /dev/null +++ b/rsd_lib/tests/unit/json_samples/processor_collection.json @@ -0,0 +1,16 @@ +{ + "@odata.type": "#ProcssorCollection.ProcessorCollection", + "Name": "Processors Collection", + "Members@odata.count": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU1" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU2" + } + ], + "@odata.context": "/redfish/v1/$metadata#Systems/Links/Members/437XR1138R2/Processors/#entity", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors", + "@Redfish.Copyright": "Copyright 2014-2016 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} diff --git a/rsd_lib/tests/unit/json_samples/root.json b/rsd_lib/tests/unit/json_samples/root.json new file mode 100644 index 0000000..060efb5 --- /dev/null +++ b/rsd_lib/tests/unit/json_samples/root.json @@ -0,0 +1,40 @@ +{ + "@odata.type": "#ServiceRoot.v1_0_2.ServiceRoot", + "Id": "RootService", + "Name": "Root Service", + "RedfishVersion": "1.0.2", + "UUID": "92384634-2938-2342-8820-489239905423", + "Systems": { + "@odata.id": "/redfish/v1/Systems" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis" + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, + "Tasks": { + "@odata.id": "/redfish/v1/TaskService" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService" + }, + "AccountService": { + "@odata.id": "/redfish/v1/AccountService" + }, + "EventService": { + "@odata.id": "/redfish/v1/EventService" + }, + "Nodes": { + "@odata.id": "/redfish/v1/Nodes" + }, + "Links": { + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + } + }, + "Oem": {}, + "@odata.context": "/redfish/v1/$metadata#ServiceRoot", + "@odata.id": "/redfish/v1/", + "@Redfish.Copyright": "Copyright 2014-2016 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} diff --git a/rsd_lib/tests/unit/resources/__init__.py b/rsd_lib/tests/unit/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rsd_lib/tests/unit/resources/node/__init__.py b/rsd_lib/tests/unit/resources/node/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rsd_lib/tests/unit/resources/node/test_node.py b/rsd_lib/tests/unit/resources/node/test_node.py new file mode 100644 index 0000000..01502c4 --- /dev/null +++ b/rsd_lib/tests/unit/resources/node/test_node.py @@ -0,0 +1,347 @@ +# Copyright 2017 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 +from sushy import exceptions +from sushy.resources.system import processor +import testtools + +from rsd_lib.resources.node import constants as node_cons +from rsd_lib.resources.node import node + + +class NodeTestCase(testtools.TestCase): + + def setUp(self): + super(NodeTestCase, self).setUp() + self.conn = mock.Mock() + with open('rsd_lib/tests/unit/json_samples/node.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + + self.node_inst = node.Node( + self.conn, '/redfish/v1/Nodes/Node1', + redfish_version='1.0.2') + + def test__parse_attributes(self): + self.node_inst._parse_attributes() + self.assertEqual('1.0.2', self.node_inst.redfish_version) + self.assertEqual('Node #1', self.node_inst.description) + self.assertEqual(node_cons.COMPOSED_NODE_STATE_ALLOCATED, + self.node_inst.composed_node_state) + self.assertEqual('Node1', self.node_inst.identity) + self.assertEqual('Composed Node', self.node_inst.name) + self.assertEqual('fa39d108-7d70-400a-9db2-6940375c31c2', + self.node_inst.uuid) + self.assertEqual(node_cons.NODE_POWER_STATE_ON, + self.node_inst.power_state) + self.assertEqual(32, self.node_inst.memory_summary.size_gib) + self.assertEqual("OK", self.node_inst.memory_summary.health) + self.assertIsNone(self.node_inst._processors) + + def test__parse_attributes_missing_actions(self): + self.node_inst.json.pop('Actions') + self.assertRaisesRegex( + exceptions.MissingAttributeError, 'attribute Actions', + self.node_inst._parse_attributes) + + def test__parse_attributes_missing_boot(self): + self.node_inst.json.pop('Boot') + self.assertRaisesRegex( + exceptions.MissingAttributeError, 'attribute Boot', + self.node_inst._parse_attributes) + + def test__parse_attributes_missing_reset_target(self): + self.node_inst.json['Actions']['#ComposedNode.Reset'].pop( + 'target') + self.assertRaisesRegex( + exceptions.MissingAttributeError, + 'attribute Actions/#ComposedNode.Reset/target', + self.node_inst._parse_attributes) + + def test_get__reset_action_element(self): + value = self.node_inst._get_reset_action_element() + self.assertEqual("/redfish/v1/Nodes/Node1/Actions/" + "ComposedNode.Reset", + value.target_uri) + self.assertEqual(["On", + "ForceOff", + "GracefulRestart", + "ForceRestart", + "Nmi", + "ForceOn", + "PushPowerButton", + "GracefulShutdown" + ], + value.allowed_values) + + def test_get__reset_action_element_missing_reset_action(self): + self.node_inst._actions.reset = None + self.assertRaisesRegex( + exceptions.MissingActionError, 'action #ComposedNode.Reset', + self.node_inst._get_reset_action_element) + + def test_get_allowed_reset_node_values(self): + values = self.node_inst.get_allowed_reset_node_values() + expected = set([node_cons.RESET_GRACEFUL_SHUTDOWN, + node_cons.RESET_GRACEFUL_RESTART, + node_cons.RESET_FORCE_RESTART, + node_cons.RESET_FORCE_OFF, + node_cons.RESET_FORCE_ON, + node_cons.RESET_ON, + node_cons.RESET_NMI, + node_cons.RESET_PUSH_POWER_BUTTON]) + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + @mock.patch.object(node.LOG, 'warning', autospec=True) + def test_get_allowed_reset_system_values_no_values_specified( + self, mock_log): + self.node_inst._actions.reset.allowed_values = {} + values = self.node_inst.get_allowed_reset_node_values() + # Assert it returns all values if it can't get the specific ones + expected = set([node_cons.RESET_GRACEFUL_SHUTDOWN, + node_cons.RESET_GRACEFUL_RESTART, + node_cons.RESET_FORCE_RESTART, + node_cons.RESET_FORCE_OFF, + node_cons.RESET_FORCE_ON, + node_cons.RESET_ON, + node_cons.RESET_NMI, + node_cons.RESET_PUSH_POWER_BUTTON]) + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + self.assertEqual(1, mock_log.call_count) + + def test_reset_node(self): + self.node_inst.reset_node(node_cons.RESET_FORCE_OFF) + self.node_inst._conn.post.assert_called_once_with( + '/redfish/v1/Nodes/Node1/Actions/ComposedNode.Reset', + data={'ResetType': 'ForceOff'}) + + def test_reset_node_invalid_value(self): + self.assertRaises(exceptions.InvalidParameterValueError, + self.node_inst.reset_node, 'invalid-value') + + def test_get_allowed_node_boot_source_values(self): + values = self.node_inst.get_allowed_node_boot_source_values() + expected = set([node_cons.BOOT_SOURCE_TARGET_NONE, + node_cons.BOOT_SOURCE_TARGET_PXE, + node_cons.BOOT_SOURCE_TARGET_HDD]) + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + @mock.patch.object(node.LOG, 'warning', autospec=True) + def test_get_allowed_node_boot_source_values_no_values_specified( + self, mock_log): + self.node_inst.boot.allowed_values = None + values = self.node_inst.get_allowed_node_boot_source_values() + # Assert it returns all values if it can't get the specific ones + expected = set([node_cons.BOOT_SOURCE_TARGET_NONE, + node_cons.BOOT_SOURCE_TARGET_PXE, + node_cons.BOOT_SOURCE_TARGET_HDD]) + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + self.assertEqual(1, mock_log.call_count) + + def test_set_node_boot_source(self): + self.node_inst.set_node_boot_source( + node_cons.BOOT_SOURCE_TARGET_PXE, + enabled=node_cons.BOOT_SOURCE_ENABLED_CONTINUOUS, + mode=node_cons.BOOT_SOURCE_MODE_UEFI) + self.node_inst._conn.patch.assert_called_once_with( + '/redfish/v1/Nodes/Node1', + data={'Boot': {'BootSourceOverrideEnabled': 'Continuous', + 'BootSourceOverrideTarget': 'Pxe', + 'BootSourceOverrideMode': 'UEFI'}}) + + def test_set_node_boot_source_no_mode_specified(self): + self.node_inst.set_node_boot_source( + node_cons.BOOT_SOURCE_TARGET_HDD, + enabled=node_cons.BOOT_SOURCE_ENABLED_ONCE) + self.node_inst._conn.patch.assert_called_once_with( + '/redfish/v1/Nodes/Node1', + data={'Boot': {'BootSourceOverrideEnabled': 'Once', + 'BootSourceOverrideTarget': 'Hdd'}}) + + def test_set_node_boot_source_invalid_target(self): + self.assertRaises(exceptions.InvalidParameterValueError, + self.node_inst.set_node_boot_source, + 'invalid-target') + + def test_set_node_boot_source_invalid_enabled(self): + self.assertRaises(exceptions.InvalidParameterValueError, + self.node_inst.set_node_boot_source, + node_cons.BOOT_SOURCE_TARGET_HDD, + enabled='invalid-enabled') + + def test__get_processor_collection_path_missing_processors_attr(self): + self.node_inst._json.pop('Processors') + self.assertRaisesRegex( + exceptions.MissingAttributeError, 'attribute Processors', + self.node_inst._get_processor_collection_path) + + def test_memory_summary_missing_attr(self): + # | GIVEN | + self.node_inst._json['Memory']['Status'].pop('Health') + # | WHEN | + self.node_inst._parse_attributes() + # | THEN | + self.assertEqual(32, self.node_inst.memory_summary.size_gib) + self.assertEqual(None, self.node_inst.memory_summary.health) + + # | GIVEN | + self.node_inst._json['Memory'].pop('Status') + # | WHEN | + self.node_inst._parse_attributes() + # | THEN | + self.assertEqual(32, self.node_inst.memory_summary.size_gib) + self.assertEqual(None, self.node_inst.memory_summary.health) + + # | GIVEN | + self.node_inst._json['Memory'].pop('TotalSystemMemoryGiB') + # | WHEN | + self.node_inst._parse_attributes() + # | THEN | + self.assertEqual(None, self.node_inst.memory_summary.size_gib) + self.assertEqual(None, self.node_inst.memory_summary.health) + + # | GIVEN | + self.node_inst._json.pop('Memory') + # | WHEN | + self.node_inst._parse_attributes() + # | THEN | + self.assertEqual(None, self.node_inst.memory_summary) + + def test_processors(self): + # check for the underneath variable value + self.assertIsNone(self.node_inst._processors) + # | GIVEN | + self.conn.get.return_value.json.reset_mock() + with open('rsd_lib/tests/unit/json_samples/processor_collection.json', + 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + # | WHEN | + actual_processors = self.node_inst.processors + # | THEN | + self.assertIsInstance(actual_processors, + processor.ProcessorCollection) + 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_processors, + self.node_inst.processors) + self.conn.get.return_value.json.assert_not_called() + + def test_processors_on_refresh(self): + # | GIVEN | + with open('rsd_lib/tests/unit/json_samples/processor_collection.json', + 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + # | WHEN & THEN | + self.assertIsInstance(self.node_inst.processors, + processor.ProcessorCollection) + + # On refreshing the system instance... + with open('rsd_lib/tests/unit/json_samples/node.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + self.node_inst.refresh() + + # | WHEN & THEN | + self.assertIsNone(self.node_inst._processors) + + # | GIVEN | + with open('rsd_lib/tests/unit/json_samples/processor_collection.json', + 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + # | WHEN & THEN | + self.assertIsInstance(self.node_inst.processors, + processor.ProcessorCollection) + + def _setUp_processor_summary(self): + self.conn.get.return_value.json.reset_mock() + with open('rsd_lib/tests/unit/json_samples/processor_collection.json', + 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + + # fetch processors for the first time + self.node_inst.processors + + successive_return_values = [] + with open('rsd_lib/tests/unit/json_samples/processor.json', 'r') as f: + successive_return_values.append(json.loads(f.read())) + with open('rsd_lib/tests/unit/json_samples/processor2.json', 'r') as f: + successive_return_values.append(json.loads(f.read())) + + self.conn.get.return_value.json.side_effect = successive_return_values + + def test_processor_summary(self): + # | GIVEN | + self._setUp_processor_summary() + # | WHEN | + actual_processor_summary = self.node_inst.processors.summary + # | THEN | + self.assertEqual((16, node_cons.PROCESSOR_ARCH_x86), + actual_processor_summary) + self.assertEqual(16, actual_processor_summary.count) + self.assertEqual(node_cons.PROCESSOR_ARCH_x86, + actual_processor_summary.architecture) + + # reset mock + self.conn.get.return_value.json.reset_mock() + + # | WHEN & THEN | + # tests for same object on invoking subsequently + self.assertIs(actual_processor_summary, + self.node_inst.processors.summary) + self.conn.get.return_value.json.assert_not_called() + + +class NodeCollectionTestCase(testtools.TestCase): + + def setUp(self): + super(NodeCollectionTestCase, self).setUp() + self.conn = mock.Mock() + with open('rsd_lib/tests/unit/json_samples/node_collection.json', + 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + self.node_col = node.NodeCollection( + self.conn, '/redfish/v1/Nodes', redfish_version='1.0.2') + + def test__parse_attributes(self): + self.node_col._parse_attributes() + self.assertEqual('1.0.2', self.node_col.redfish_version) + self.assertEqual('Composed Nodes Collection', self.node_col.name) + self.assertEqual(('/redfish/v1/Nodes/Node1',), + self.node_col.members_identities) + + @mock.patch.object(node, 'Node', autospec=True) + def test_get_member(self, mock_node): + self.node_col.get_member('/redfish/v1/Nodes/Node1') + mock_node.assert_called_once_with( + self.node_col._conn, '/redfish/v1/Nodes/Node1', + redfish_version=self.node_col.redfish_version) + + @mock.patch.object(node, 'Node', autospec=True) + def test_get_members(self, mock_node): + members = self.node_col.get_members() + mock_node.assert_called_once_with( + self.node_col._conn, '/redfish/v1/Nodes/Node1', + redfish_version=self.node_col.redfish_version) + self.assertIsInstance(members, list) + self.assertEqual(1, len(members)) diff --git a/rsd_lib/tests/unit/test_main.py b/rsd_lib/tests/unit/test_main.py new file mode 100644 index 0000000..c34a34c --- /dev/null +++ b/rsd_lib/tests/unit/test_main.py @@ -0,0 +1,50 @@ +# Copyright 2017 Red Hat, 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 +from sushy import connector +import testtools + +from rsd_lib import main +from rsd_lib.resources.node import node + + +class RSDLibTestCase(testtools.TestCase): + + @mock.patch.object(connector, 'Connector', autospec=True) + def setUp(self, mock_connector): + super(RSDLibTestCase, self).setUp() + self.conn = mock.Mock() + mock_connector.return_value = self.conn + with open('rsd_lib/tests/unit/json_samples/root.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + self.rsd = main.RSDLib('http://foo.bar:8442', username='foo', + password='bar', verify=True) + + @mock.patch.object(node, 'NodeCollection', autospec=True) + def test_get_node_collection(self, mock_node_collection): + self.rsd.get_node_collection() + mock_node_collection.assert_called_once_with( + self.rsd._conn, '/redfish/v1/Nodes', + redfish_version=self.rsd.redfish_version) + + @mock.patch.object(node, 'Node', autospec=True) + def test_get_node(self, mock_node): + self.rsd.get_node('fake-node-id') + mock_node.assert_called_once_with( + self.rsd._conn, 'fake-node-id', + redfish_version=self.rsd.redfish_version)