From bdce45aa43a52d6fc6dc78204592ac3e9b5b43e9 Mon Sep 17 00:00:00 2001 From: Debayan Ray Date: Fri, 3 Mar 2017 12:48:22 -0500 Subject: [PATCH] Add manager resource This patch adds the Manager/ManagerCollection resource. Manager resource will provide information about remote access controls like serial console, command shell, graphical console etc. It will also serve as the gateway to virtual media, ethernet interfaces (NICs) information. 'ResetActionField' moved to a new file, "resources/common.py", to be reused both by system and manager resource. Necessary changes are also made to the usage documentation page pertaining to Manager resource. Partial-Bug: #1673886 Change-Id: I1d1b9b3f13002e654e4dc62d0558f5a2afbd8662 --- doc/source/index.rst | 2 +- doc/source/usage.rst | 75 +++++- sushy/__init__.py | 1 + sushy/main.py | 56 +++-- sushy/resources/common.py | 20 ++ sushy/resources/manager/__init__.py | 0 sushy/resources/manager/constants.py | 78 ++++++ sushy/resources/manager/manager.py | 197 +++++++++++++++ sushy/resources/manager/mappings.py | 61 +++++ sushy/resources/system/system.py | 10 +- sushy/tests/unit/json_samples/manager.json | 86 +++++++ .../unit/json_samples/manager_collection.json | 14 ++ .../tests/unit/resources/manager/__init__.py | 0 .../unit/resources/manager/test_manager.py | 235 ++++++++++++++++++ sushy/tests/unit/test_main.py | 35 +-- 15 files changed, 824 insertions(+), 46 deletions(-) create mode 100644 sushy/resources/common.py create mode 100644 sushy/resources/manager/__init__.py create mode 100644 sushy/resources/manager/constants.py create mode 100644 sushy/resources/manager/manager.py create mode 100644 sushy/resources/manager/mappings.py create mode 100644 sushy/tests/unit/json_samples/manager.json create mode 100644 sushy/tests/unit/json_samples/manager_collection.json create mode 100644 sushy/tests/unit/resources/manager/__init__.py create mode 100644 sushy/tests/unit/resources/manager/test_manager.py diff --git a/doc/source/index.rst b/doc/source/index.rst index b928df57..111f9317 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -9,7 +9,7 @@ Welcome to Sushy's documentation! Contents: .. toctree:: - :maxdepth: 1 + :maxdepth: 2 About Sushy installation diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 2640543f..a0973a09 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -6,6 +6,10 @@ Usage To use sushy in a project: +---------------------------------------- +Creating and using a sushy system object +---------------------------------------- + .. code-block:: python import logging @@ -17,7 +21,8 @@ To use sushy in a project: LOG.setLevel(logging.DEBUG) LOG.addHandler(logging.StreamHandler()) - s = sushy.Sushy('http://localhost:8000', username='foo', password='bar') + s = sushy.Sushy('http://localhost:8000/redfish/v1', + username='foo', password='bar') # Get the Redfish version print(s.redfish_version) @@ -79,6 +84,74 @@ To use sushy in a project: print(sys_inst.processors.summary) +----------------------------------------- +Creating and using a sushy manager object +----------------------------------------- + +.. code-block:: python + + import logging + + import sushy + + # Enable logging at DEBUG level + LOG = logging.getLogger('sushy') + LOG.setLevel(logging.DEBUG) + LOG.addHandler(logging.StreamHandler()) + + s = sushy.Sushy('http://localhost:8000/redfish/v1', + username='foo', password='bar') + + # Instantiate a manager object + mgr_inst = s.get_manager('BMC') + + # Get the manager name & description + print(mgr_inst.name) + print(mgr_inst.description) + + + # Using manager collections + + + # Instantiate a ManagerCollection object + mgr_col = s.get_manager_collection() + + # Print the ID of the managers available in the collection + print(mgr_col.members_identities) + + # Get a list of manager objects available in the collection + mgr_insts = mgr_col.get_members() + + # Instantiate a manager object, same as getting it directly + # from the s.get_manager() + mgr_inst = mgr_col.get_member(mgr_col.members_identities[0]) + + # Refresh the manager collection object + mgr_col.refresh() + + + # Using manager actions + + + # Get supported graphical console types + print(mgr_inst.get_supported_graphical_console_types()) + + # Get supported serial console types + print(mgr_inst.get_supported_serial_console_types()) + + # Get supported command shell types + print(mgr_inst.get_supported_command_shell_types()) + + # Get a list of allowed manager reset values + print(mgr_inst.get_allowed_reset_manager_values()) + + # Reset the manager + mgr_inst.reset_manager(sushy.RESET_MANAGER_FORCE_RESTART) + + # Refresh the manager object + mgr_inst.refresh() + + If you do not have any real baremetal machine that supports the Redfish protocol you can look at the :ref:`contributing` page to learn how to run a Redfish emulator. diff --git a/sushy/__init__.py b/sushy/__init__.py index d635aae1..afc6c8ad 100644 --- a/sushy/__init__.py +++ b/sushy/__init__.py @@ -18,6 +18,7 @@ import pbr.version from sushy.main import Sushy from sushy.resources.system.constants import * # noqa +from sushy.resources.manager.constants import * # noqa __all__ = ('Sushy',) __version__ = pbr.version.VersionInfo( diff --git a/sushy/main.py b/sushy/main.py index e3a56716..a5e25208 100644 --- a/sushy/main.py +++ b/sushy/main.py @@ -14,21 +14,27 @@ # under the License. from sushy import connector -from sushy import exceptions from sushy.resources import base +from sushy.resources.manager import manager from sushy.resources.system import system class Sushy(base.ResourceBase): - identity = None - """The Redfish system identity""" + identity = base.Field('Id', required=True) + """The Redfish root service identity""" - name = None - """The Redfish system name""" + name = base.Field('Name') + """The Redfish root service name""" - uuid = None - """The Redfish system UUID""" + uuid = base.Field('UUID') + """The Redfish root service UUID""" + + _systems_path = base.Field(['Systems', '@odata.id'], required=True) + """SystemCollection path""" + + _managers_path = base.Field(['Managers', '@odata.id'], required=True) + """ManagerCollection path""" def __init__(self, base_url, username=None, password=None, root_prefix='/redfish/v1/', verify=True): @@ -55,18 +61,8 @@ class Sushy(base.ResourceBase): path=self._root_prefix) def _parse_attributes(self): - self.identity = self.json.get('Id') - self.name = self.json.get('Name') + super(Sushy, self)._parse_attributes() self.redfish_version = self.json.get('RedfishVersion') - self.uuid = self.json.get('UUID') - - def _get_system_collection_path(self): - """Helper function to find the SystemCollection path""" - systems_col = self.json.get('Systems') - if not systems_col: - raise exceptions.MissingAttributeError(attribute='Systems', - resource=self._path) - return systems_col.get('@odata.id') def get_system_collection(self): """Get the SystemCollection object @@ -75,9 +71,8 @@ class Sushy(base.ResourceBase): not found :returns: a SystemCollection object """ - return system.SystemCollection( - self._conn, self._get_system_collection_path(), - redfish_version=self.redfish_version) + return system.SystemCollection(self._conn, self._systems_path, + redfish_version=self.redfish_version) def get_system(self, identity): """Given the identity return a System object @@ -87,3 +82,22 @@ class Sushy(base.ResourceBase): """ return system.System(self._conn, identity, redfish_version=self.redfish_version) + + def get_manager_collection(self): + """Get the ManagerCollection object + + :raises: MissingAttributeError, if the collection attribute is + not found + :returns: a ManagerCollection object + """ + return manager.ManagerCollection(self._conn, self._managers_path, + redfish_version=self.redfish_version) + + def get_manager(self, identity): + """Given the identity return a Manager object + + :param identity: The identity of the Manager resource + :returns: The Manager object + """ + return manager.Manager(self._conn, identity, + redfish_version=self.redfish_version) diff --git a/sushy/resources/common.py b/sushy/resources/common.py new file mode 100644 index 00000000..24322cf6 --- /dev/null +++ b/sushy/resources/common.py @@ -0,0 +1,20 @@ +# 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 import base + + +class ResetActionField(base.CompositeField): + allowed_values = base.Field('ResetType@Redfish.AllowableValues', + adapter=list) + + target_uri = base.Field('target', required=True) diff --git a/sushy/resources/manager/__init__.py b/sushy/resources/manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sushy/resources/manager/constants.py b/sushy/resources/manager/constants.py new file mode 100644 index 00000000..a1b3a9f6 --- /dev/null +++ b/sushy/resources/manager/constants.py @@ -0,0 +1,78 @@ +# 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/Manager.v1_0_0.json#/definitions/Manager # noqa + +# Manager Reset action constants + +RESET_MANAGER_GRACEFUL_RESTART = 'graceful restart' +RESET_MANAGER_FORCE_RESTART = 'force restart' + +# Manager Type constants + +MANAGER_TYPE_MANAGEMENT_CONTROLLER = 'management controller' +"""A controller used primarily to monitor or manage the operation of + a device or system""" + +MANAGER_TYPE_ENCLOSURE_MANAGER = 'enclosure manager' +"""A controller which provides management functions for a chassis + or group of devices or systems""" + +MANAGER_TYPE_BMC = 'bmc' +"""A controller which provides management functions for a single + computer system""" + +MANAGER_TYPE_RACK_MANAGER = 'rack manager' +"""A controller which provides management functions for a whole or part + of a rack""" + +MANAGER_TYPE_AUXILIARY_CONTROLLER = 'auxiliary controller' +"""A controller which provides management functions for a particular + subsystem or group of devices""" + +# Graphical Console constants + +GRAPHICAL_CONSOLE_KVMIP = 'graphical console kvmip' +"""Graphical Console connection using a KVM-IP (redirection of Keyboard, + Video, Mouse over IP) protocol""" + +GRAPHICAL_CONSOLE_OEM = 'graphical console oem' +"""Graphical Console connection using an OEM-specific protocol""" + +# Serial Console constants + +SERIAL_CONSOLE_SSH = 'serial console ssh' +"""Serial Console connection using the SSH protocol""" + +SERIAL_CONSOLE_TELNET = 'serial console telnet' +"""Serial Console connection using the Telnet protocol""" + +SERIAL_CONSOLE_IPMI = 'serial console ipmi' +"""Serial Console connection using the IPMI Serial-over-LAN (SOL) protocol""" + +SERIAL_CONSOLE_OEM = 'serial console oem' +"""Serial Console connection using an OEM-specific protocol""" + +# Command Shell constants + +COMMAND_SHELL_SSH = 'command shell ssh' +"""Command Shell connection using the SSH protocol""" + +COMMAND_SHELL_TELNET = 'command shell telnet' +"""Command Shell connection using the Telnet protocol""" + +COMMAND_SHELL_IPMI = 'command shell ipmi' +"""Command Shell connection using the IPMI Serial-over-LAN (SOL) protocol""" + +COMMAND_SHELL_OEM = 'command shell oem' +"""Command Shell connection using an OEM-specific protocol""" diff --git a/sushy/resources/manager/manager.py b/sushy/resources/manager/manager.py new file mode 100644 index 00000000..d027947e --- /dev/null +++ b/sushy/resources/manager/manager.py @@ -0,0 +1,197 @@ +# 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.manager import mappings as mgr_maps + +LOG = logging.getLogger(__name__) + + +class ActionsField(base.CompositeField): + reset = common.ResetActionField('#Manager.Reset') + + +class RemoteAccessField(base.CompositeField): + service_enabled = base.Field('ServiceEnabled') + + max_concurrent_sessions = base.Field('MaxConcurrentSessions') + + connect_types_supported = base.Field('ConnectTypesSupported', + adapter=list) + + +class Manager(base.ResourceBase): + + firmware_version = base.Field('FirmwareVersion') + """The manager firmware version""" + + graphical_console = RemoteAccessField('GraphicalConsole') + """A dictionary containing the remote access support service via + graphical console (e.g. KVMIP) and max concurrent sessions + """ + + serial_console = RemoteAccessField('SerialConsole') + """A dictionary containing the remote access support service via + serial console (e.g. Telnet, SSH, IPMI) and max concurrent sessions + """ + + command_shell = RemoteAccessField('CommandShell') + """A dictionary containing the remote access support service via + command shell (e.g. Telnet, SSH) and max concurrent sessions + """ + + description = base.Field('Description') + """The manager description""" + + identity = base.Field('Id', required=True) + """The manager identity string""" + + name = base.Field('Name') + """The manager name""" + + model = base.Field('Model') + """The manager model""" + + manager_type = base.MappedField('ManagerType', + mgr_maps.MANAGER_TYPE_VALUE_MAP) + """The manager type""" + + uuid = base.Field('UUID') + """The manager UUID""" + + _actions = ActionsField('Actions', required=True) + + def __init__(self, connector, identity, redfish_version=None): + """A class representing a Manager + + :param connector: A Connector instance + :param identity: The identity of the Manager resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of the given version. + """ + super(Manager, self).__init__(connector, identity, redfish_version) + + def get_supported_graphical_console_types(self): + """Get the supported values for Graphical Console connection types. + + :returns: A set of supported values. + """ + if (not self.graphical_console + or not self.graphical_console.connect_types_supported): + LOG.warning('Could not figure out the supported values for ' + 'remote access via graphical console for Manager %s', + self.identity) + return set(mgr_maps.GRAPHICAL_CONSOLE_VALUE_MAP_REV) + + return set([mgr_maps.GRAPHICAL_CONSOLE_VALUE_MAP[v] for v in + set(mgr_maps.GRAPHICAL_CONSOLE_VALUE_MAP). + intersection(self.graphical_console. + connect_types_supported)]) + + def get_supported_serial_console_types(self): + """Get the supported values for Serial Console connection types. + + :returns: A set of supported values. + """ + if (not self.serial_console + or not self.serial_console.connect_types_supported): + LOG.warning('Could not figure out the supported values for ' + 'remote access via serial console for Manager %s', + self.identity) + return set(mgr_maps.SERIAL_CONSOLE_VALUE_MAP_REV) + + return set([mgr_maps.SERIAL_CONSOLE_VALUE_MAP[v] for v in + set(mgr_maps.SERIAL_CONSOLE_VALUE_MAP). + intersection(self.serial_console.connect_types_supported)]) + + def get_supported_command_shell_types(self): + """Get the supported values for Command Shell connection types. + + :returns: A set of supported values. + """ + if (not self.command_shell + or not self.command_shell.connect_types_supported): + LOG.warning('Could not figure out the supported values for ' + 'remote access via command shell for Manager %s', + self.identity) + return set(mgr_maps.COMMAND_SHELL_VALUE_MAP_REV) + + return set([mgr_maps.COMMAND_SHELL_VALUE_MAP[v] for v in + set(mgr_maps.COMMAND_SHELL_VALUE_MAP). + intersection(self.command_shell.connect_types_supported)]) + + def _get_reset_action_element(self): + reset_action = self._actions.reset + + if not reset_action: + raise exceptions.MissingActionError(action='#Manager.Reset', + resource=self._path) + return reset_action + + def get_allowed_reset_manager_values(self): + """Get the allowed values for resetting the manager. + + :returns: A set of allowed values. + :raises: MissingAttributeError, if Actions/#Manager.Reset attribute + not present. + """ + 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 manager action for Manager %s', self.identity) + return set(mgr_maps.RESET_MANAGER_VALUE_MAP_REV) + + return set([mgr_maps.RESET_MANAGER_VALUE_MAP[v] for v in + set(mgr_maps.RESET_MANAGER_VALUE_MAP). + intersection(reset_action.allowed_values)]) + + def reset_manager(self, value): + """Reset the manager. + + :param value: The target value. + :raises: InvalidParameterValueError, if the target value is not + allowed. + """ + valid_resets = self.get_allowed_reset_manager_values() + if value not in valid_resets: + raise exceptions.InvalidParameterValueError( + parameter='value', value=value, valid_values=valid_resets) + + value = mgr_maps.RESET_MANAGER_VALUE_MAP_REV[value] + target_uri = self._get_reset_action_element().target_uri + + LOG.debug('Resetting the Manager %s ...', self.identity) + self._conn.post(target_uri, data={'ResetType': value}) + LOG.info('The Manager %s is being reset', self.identity) + + +class ManagerCollection(base.ResourceCollectionBase): + + @property + def _resource_type(self): + return Manager + + def __init__(self, connector, path, redfish_version=None): + """A class representing a ManagerCollection + + :param connector: A Connector instance + :param path: The canonical path to the Manager collection resource + :param redfish_version: The version of RedFish. Used to construct + the object according to schema of the given version. + """ + super(ManagerCollection, self).__init__(connector, path, + redfish_version) diff --git a/sushy/resources/manager/mappings.py b/sushy/resources/manager/mappings.py new file mode 100644 index 00000000..451dc6e1 --- /dev/null +++ b/sushy/resources/manager/mappings.py @@ -0,0 +1,61 @@ +# 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.manager import constants as mgr_cons +from sushy import utils + + +RESET_MANAGER_VALUE_MAP = { + 'GracefulRestart': mgr_cons.RESET_MANAGER_GRACEFUL_RESTART, + 'ForceRestart': mgr_cons.RESET_MANAGER_FORCE_RESTART, +} + +RESET_MANAGER_VALUE_MAP_REV = utils.revert_dictionary(RESET_MANAGER_VALUE_MAP) + +MANAGER_TYPE_VALUE_MAP = { + 'ManagementController': mgr_cons.MANAGER_TYPE_MANAGEMENT_CONTROLLER, + 'EnclosureManager': mgr_cons.MANAGER_TYPE_ENCLOSURE_MANAGER, + 'BMC': mgr_cons.MANAGER_TYPE_BMC, + 'RackManager': mgr_cons.MANAGER_TYPE_RACK_MANAGER, + 'AuxiliaryController': mgr_cons.MANAGER_TYPE_AUXILIARY_CONTROLLER +} + +MANAGER_TYPE_VALUE_MAP_REV = ( + utils.revert_dictionary(MANAGER_TYPE_VALUE_MAP)) + +GRAPHICAL_CONSOLE_VALUE_MAP = { + 'KVMIP': mgr_cons.GRAPHICAL_CONSOLE_KVMIP, + 'Oem': mgr_cons.GRAPHICAL_CONSOLE_OEM, +} + +GRAPHICAL_CONSOLE_VALUE_MAP_REV = ( + utils.revert_dictionary(GRAPHICAL_CONSOLE_VALUE_MAP)) + +SERIAL_CONSOLE_VALUE_MAP = { + 'SSH': mgr_cons.SERIAL_CONSOLE_SSH, + 'Telnet': mgr_cons.SERIAL_CONSOLE_TELNET, + 'IPMI': mgr_cons.SERIAL_CONSOLE_IPMI, + 'Oem': mgr_cons.SERIAL_CONSOLE_OEM, +} + +SERIAL_CONSOLE_VALUE_MAP_REV = ( + utils.revert_dictionary(SERIAL_CONSOLE_VALUE_MAP)) + +COMMAND_SHELL_VALUE_MAP = { + 'SSH': mgr_cons.COMMAND_SHELL_SSH, + 'Telnet': mgr_cons.COMMAND_SHELL_TELNET, + 'IPMI': mgr_cons.COMMAND_SHELL_IPMI, + 'Oem': mgr_cons.COMMAND_SHELL_OEM, +} + +COMMAND_SHELL_VALUE_MAP_REV = ( + utils.revert_dictionary(COMMAND_SHELL_VALUE_MAP)) diff --git a/sushy/resources/system/system.py b/sushy/resources/system/system.py index 038586ba..f9eee1a3 100644 --- a/sushy/resources/system/system.py +++ b/sushy/resources/system/system.py @@ -17,6 +17,7 @@ import logging from sushy import exceptions from sushy.resources import base +from sushy.resources import common from sushy.resources.system import constants as sys_cons from sushy.resources.system import mappings as sys_maps from sushy.resources.system import processor @@ -25,15 +26,8 @@ from sushy.resources.system import processor LOG = logging.getLogger(__name__) -class ResetActionField(base.CompositeField): - allowed_values = base.Field('ResetType@Redfish.AllowableValues', - adapter=list) - - target_uri = base.Field('target', required=True) - - class ActionsField(base.CompositeField): - reset = ResetActionField('#ComputerSystem.Reset') + reset = common.ResetActionField('#ComputerSystem.Reset') class BootField(base.CompositeField): diff --git a/sushy/tests/unit/json_samples/manager.json b/sushy/tests/unit/json_samples/manager.json new file mode 100644 index 00000000..e79e3865 --- /dev/null +++ b/sushy/tests/unit/json_samples/manager.json @@ -0,0 +1,86 @@ +{ + "@odata.type": "#Manager.v1_1_0.Manager", + "Id": "BMC", + "Name": "Manager", + "ManagerType": "BMC", + "Description": "Contoso BMC", + "ServiceEntryPointUUID": "92384634-2938-2342-8820-489239905423", + "UUID": "58893887-8974-2487-2389-841168418919", + "Model": "Joo Janta 200", + "DateTime": "2015-03-13T04:14:33+06:00", + "DateTimeLocalOffset": "+06:00", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "GraphicalConsole": { + "ServiceEnabled": true, + "MaxConcurrentSessions": 2, + "ConnectTypesSupported": [ + "KVMIP" + ] + }, + "SerialConsole": { + "ServiceEnabled": true, + "MaxConcurrentSessions": 1, + "ConnectTypesSupported": [ + "Telnet", + "SSH", + "IPMI" + ] + }, + "CommandShell": { + "ServiceEnabled": true, + "MaxConcurrentSessions": 4, + "ConnectTypesSupported": [ + "Telnet", + "SSH" + ] + }, + "FirmwareVersion": "1.00", + "NetworkProtocol": { + "@odata.id": "/redfish/v1/Managers/BMC/NetworkProtocol" + }, + "EthernetInterfaces": { + "@odata.id": "/redfish/v1/Managers/BMC/NICs" + }, + "SerialInterfaces": { + "@odata.id": "/redfish/v1/Managers/BMC/SerialInterfaces" + }, + "LogServices": { + "@odata.id": "/redfish/v1/Managers/BMC/LogServices" + }, + "VirtualMedia": { + "@odata.id": "/redfish/v1/Managers/BMC/VirtualMedia" + }, + "Links": { + "ManagerForServers": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + } + ], + "ManagerForChassis": [ + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ], + "ManagerInChassis": { + "@odata.id": "/redfish/v1/Chassis/1U" + }, + "Oem": {} + }, + "Actions": { + "#Manager.Reset": { + "target": "/redfish/v1/Managers/BMC/Actions/Manager.Reset", + "ResetType@Redfish.AllowableValues": [ + "ForceRestart", + "GracefulRestart" + ] + }, + "Oem": {} + }, + "Oem": {}, + "@odata.context": "/redfish/v1/$metadata#Manager.Manager", + "@odata.id": "/redfish/v1/Managers/BMC", + "@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/sushy/tests/unit/json_samples/manager_collection.json b/sushy/tests/unit/json_samples/manager_collection.json new file mode 100644 index 00000000..f0818846 --- /dev/null +++ b/sushy/tests/unit/json_samples/manager_collection.json @@ -0,0 +1,14 @@ +{ + "@odata.type": "#ManagerCollection.ManagerCollection", + "Name": "Manager Collection", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/BMC" + } + ], + "Oem": {}, + "@odata.context": "/redfish/v1/$metadata#Managers", + "@odata.id": "/redfish/v1/Managers", + "@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/sushy/tests/unit/resources/manager/__init__.py b/sushy/tests/unit/resources/manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sushy/tests/unit/resources/manager/test_manager.py b/sushy/tests/unit/resources/manager/test_manager.py new file mode 100644 index 00000000..1e78be72 --- /dev/null +++ b/sushy/tests/unit/resources/manager/test_manager.py @@ -0,0 +1,235 @@ +# 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 sushy +from sushy import exceptions +from sushy.resources.manager import manager +from sushy.tests.unit import base + + +class ManagerTestCase(base.TestCase): + + def setUp(self): + super(ManagerTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/manager.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + + self.manager = manager.Manager(self.conn, '/redfish/v1/Managers/BMC', + redfish_version='1.0.2') + + def test__parse_attributes(self): + # | WHEN | + self.manager._parse_attributes() + # | THEN | + self.assertEqual('1.0.2', self.manager.redfish_version) + self.assertEqual('1.00', self.manager.firmware_version) + self.assertEqual(True, self.manager.graphical_console.service_enabled) + self.assertEqual( + 2, self.manager.graphical_console.max_concurrent_sessions) + self.assertEqual(True, self.manager.serial_console.service_enabled) + self.assertEqual( + 1, self.manager.serial_console.max_concurrent_sessions) + self.assertEqual(True, self.manager.command_shell.service_enabled) + self.assertEqual( + 4, self.manager.command_shell.max_concurrent_sessions) + self.assertEqual('Contoso BMC', self.manager.description) + self.assertEqual('BMC', self.manager.identity) + self.assertEqual('Manager', self.manager.name) + self.assertEqual('Joo Janta 200', self.manager.model) + self.assertEqual(sushy.MANAGER_TYPE_BMC, self.manager.manager_type) + self.assertEqual('58893887-8974-2487-2389-841168418919', + self.manager.uuid) + + def test_get_supported_graphical_console_types(self): + # | GIVEN | + expected = set([sushy.GRAPHICAL_CONSOLE_KVMIP]) + # | WHEN | + values = self.manager.get_supported_graphical_console_types() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_supported_graphical_console_types_for_no_connect_types(self): + # | GIVEN | + graphical_console = self.manager.graphical_console + expected = set([sushy.GRAPHICAL_CONSOLE_KVMIP, + sushy.GRAPHICAL_CONSOLE_OEM]) + + for val in [None, []]: + graphical_console.connect_types_supported = val + # | WHEN | + values = self.manager.get_supported_graphical_console_types() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_supported_graphical_console_types_missing_graphcon_attr(self): + # | GIVEN | + self.manager.graphical_console = None + expected = set([sushy.GRAPHICAL_CONSOLE_KVMIP, + sushy.GRAPHICAL_CONSOLE_OEM]) + # | WHEN | + values = self.manager.get_supported_graphical_console_types() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_supported_serial_console_types(self): + # | GIVEN | + expected = set([sushy.SERIAL_CONSOLE_SSH, + sushy.SERIAL_CONSOLE_TELNET, + sushy.SERIAL_CONSOLE_IPMI]) + # | WHEN | + values = self.manager.get_supported_serial_console_types() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_supported_serial_console_types_for_no_connect_types(self): + # | GIVEN | + serial_console = self.manager.serial_console + expected = set([sushy.SERIAL_CONSOLE_SSH, + sushy.SERIAL_CONSOLE_TELNET, + sushy.SERIAL_CONSOLE_IPMI, + sushy.SERIAL_CONSOLE_OEM]) + + for val in [None, []]: + serial_console.connect_types_supported = val + # | WHEN | + values = self.manager.get_supported_serial_console_types() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_supported_serial_console_types_missing_serialcon_attr(self): + # | GIVEN | + self.manager.serial_console = None + expected = set([sushy.SERIAL_CONSOLE_SSH, + sushy.SERIAL_CONSOLE_TELNET, + sushy.SERIAL_CONSOLE_IPMI, + sushy.SERIAL_CONSOLE_OEM]) + # | WHEN | + values = self.manager.get_supported_serial_console_types() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_supported_command_shell_types(self): + # | GIVEN | + expected = set([sushy.COMMAND_SHELL_SSH, + sushy.COMMAND_SHELL_TELNET]) + # | WHEN | + values = self.manager.get_supported_command_shell_types() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_supported_command_shell_types_for_no_connect_types(self): + # | GIVEN | + command_shell = self.manager.command_shell + expected = set([sushy.COMMAND_SHELL_SSH, + sushy.COMMAND_SHELL_TELNET, + sushy.COMMAND_SHELL_IPMI, + sushy.COMMAND_SHELL_OEM]) + + for val in [None, []]: + command_shell.connect_types_supported = val + # | WHEN | + values = self.manager.get_supported_command_shell_types() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_supported_command_shell_types_missing_cmdshell_attr(self): + # | GIVEN | + self.manager.command_shell = None + expected = set([sushy.COMMAND_SHELL_SSH, + sushy.COMMAND_SHELL_TELNET, + sushy.COMMAND_SHELL_IPMI, + sushy.COMMAND_SHELL_OEM]) + # | WHEN | + values = self.manager.get_supported_command_shell_types() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_allowed_reset_manager_values(self): + # | GIVEN | + expected = set([sushy.RESET_MANAGER_GRACEFUL_RESTART, + sushy.RESET_MANAGER_FORCE_RESTART]) + # | WHEN | + values = self.manager.get_allowed_reset_manager_values() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_allowed_reset_manager_values_for_no_values_set(self): + # | GIVEN | + self.manager._actions.reset.allowed_values = [] + expected = set([sushy.RESET_MANAGER_GRACEFUL_RESTART, + sushy.RESET_MANAGER_FORCE_RESTART]) + # | WHEN | + values = self.manager.get_allowed_reset_manager_values() + # | THEN | + self.assertEqual(expected, values) + self.assertIsInstance(values, set) + + def test_get_allowed_reset_manager_values_missing_action_reset_attr(self): + # | GIVEN | + self.manager._actions.reset = None + # | WHEN & THEN | + self.assertRaisesRegex( + exceptions.MissingActionError, 'action #Manager.Reset', + self.manager.get_allowed_reset_manager_values) + + def test_reset_manager(self): + self.manager.reset_manager(sushy.RESET_MANAGER_GRACEFUL_RESTART) + self.manager._conn.post.assert_called_once_with( + '/redfish/v1/Managers/BMC/Actions/Manager.Reset', + data={'ResetType': 'GracefulRestart'}) + + def test_reset_manager_with_invalid_value(self): + self.assertRaises(exceptions.InvalidParameterValueError, + self.manager.reset_manager, 'invalid-value') + + +class ManagerCollectionTestCase(base.TestCase): + + def setUp(self): + super(ManagerCollectionTestCase, self).setUp() + self.conn = mock.Mock() + with open('sushy/tests/unit/json_samples/' + 'manager_collection.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) + self.managers = manager.ManagerCollection( + self.conn, '/redfish/v1/Managers', redfish_version='1.0.2') + + @mock.patch.object(manager, 'Manager', autospec=True) + def test_get_member(self, Manager_mock): + self.managers.get_member('/redfish/v1/Managers/BMC') + Manager_mock.assert_called_once_with( + self.managers._conn, '/redfish/v1/Managers/BMC', + redfish_version=self.managers.redfish_version) + + @mock.patch.object(manager, 'Manager', autospec=True) + def test_get_members(self, Manager_mock): + members = self.managers.get_members() + Manager_mock.assert_called_once_with( + self.managers._conn, '/redfish/v1/Managers/BMC', + redfish_version=self.managers.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 87504572..6f300bec 100644 --- a/sushy/tests/unit/test_main.py +++ b/sushy/tests/unit/test_main.py @@ -18,8 +18,8 @@ import json import mock from sushy import connector -from sushy import exceptions from sushy import main +from sushy.resources.manager import manager from sushy.resources.system import system from sushy.tests.unit import base @@ -30,12 +30,12 @@ class MainTestCase(base.TestCase): def setUp(self, mock_connector): super(MainTestCase, self).setUp() self.conn = mock.Mock() - mock_connector.return_code = self.conn + mock_connector.return_value = self.conn + with open('sushy/tests/unit/json_samples/root.json', 'r') as f: + self.conn.get.return_value.json.return_value = json.loads(f.read()) self.root = main.Sushy( 'http://foo.bar:1234', username='foo', password='bar', verify=True) - with open('sushy/tests/unit/json_samples/root.json', 'r') as f: - self.root._json = json.loads(f.read()) mock_connector.assert_called_once_with( 'http://foo.bar:1234', 'foo', 'bar', True) @@ -46,17 +46,8 @@ class MainTestCase(base.TestCase): self.assertEqual('1.0.2', self.root.redfish_version) self.assertEqual('92384634-2938-2342-8820-489239905423', self.root.uuid) - - def test__get_system_collection_path(self): - self.assertEqual( - '/redfish/v1/Systems', self.root._get_system_collection_path()) - - def test__get_system_collection_path_missing_systems_attr(self): - self.root._json.pop('Systems') - self.assertRaisesRegex( - exceptions.MissingAttributeError, - 'The attribute Systems is missing', - self.root._get_system_collection_path) + self.assertEqual('/redfish/v1/Systems', self.root._systems_path) + self.assertEqual('/redfish/v1/Managers', self.root._managers_path) @mock.patch.object(system, 'SystemCollection', autospec=True) def test_get_system_collection(self, mock_system_collection): @@ -71,3 +62,17 @@ class MainTestCase(base.TestCase): mock_system.assert_called_once_with( self.root._conn, 'fake-system-id', redfish_version=self.root.redfish_version) + + @mock.patch.object(manager, 'ManagerCollection', autospec=True) + def test_get_manager_collection(self, ManagerCollection_mock): + self.root.get_manager_collection() + ManagerCollection_mock.assert_called_once_with( + self.root._conn, '/redfish/v1/Managers', + redfish_version=self.root.redfish_version) + + @mock.patch.object(manager, 'Manager', autospec=True) + def test_get_manager(self, Manager_mock): + self.root.get_manager('fake-manager-id') + Manager_mock.assert_called_once_with( + self.root._conn, 'fake-manager-id', + redfish_version=self.root.redfish_version)