From c21149454a02514bd3889a793eadff712e4e6c33 Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Mon, 27 Feb 2017 17:03:28 +0000 Subject: [PATCH] Add redfish driver This patch is adding a redfish driver based on the sushy library. This is just a basic driver that currently supports: * Power: Hard power on/off/reboot, soft power off/reboot * Management: Setting the boot device (PXE, disk, cd-rom and bios) and its frequency (persistent or not) * Management: NMI Injection * SSL authentication Unittest coverage for the redfish modules is now in 100%, let's try to keep it this way (-: Documentation and DevStack updates will be done on subsequent patches. Partial-Bug: #1526477 Change-Id: I14470edff65cd14bb73263ec7310559a8eaa6c84 --- driver-requirements.txt | 3 + ironic/common/exception.py | 8 + ironic/conf/__init__.py | 2 + ironic/conf/redfish.py | 35 ++++ ironic/drivers/modules/redfish/__init__.py | 0 ironic/drivers/modules/redfish/management.py | 173 ++++++++++++++++ ironic/drivers/modules/redfish/power.py | 144 +++++++++++++ ironic/drivers/modules/redfish/utils.py | 172 ++++++++++++++++ ironic/drivers/redfish.py | 32 +++ ironic/tests/unit/db/utils.py | 9 + .../unit/drivers/modules/redfish/__init__.py | 0 .../modules/redfish/test_management.py | 189 ++++++++++++++++++ .../drivers/modules/redfish/test_power.py | 173 ++++++++++++++++ .../drivers/modules/redfish/test_utils.py | 149 ++++++++++++++ ironic/tests/unit/drivers/test_redfish.py | 44 ++++ .../drivers/third_party_driver_mock_specs.py | 19 ++ .../unit/drivers/third_party_driver_mocks.py | 28 +++ ...added-redfish-driver-00ff5e3f7e9d6ee8.yaml | 12 ++ requirements.txt | 1 + setup.cfg | 3 + 20 files changed, 1196 insertions(+) create mode 100644 ironic/conf/redfish.py create mode 100644 ironic/drivers/modules/redfish/__init__.py create mode 100644 ironic/drivers/modules/redfish/management.py create mode 100644 ironic/drivers/modules/redfish/power.py create mode 100644 ironic/drivers/modules/redfish/utils.py create mode 100644 ironic/drivers/redfish.py create mode 100644 ironic/tests/unit/drivers/modules/redfish/__init__.py create mode 100644 ironic/tests/unit/drivers/modules/redfish/test_management.py create mode 100644 ironic/tests/unit/drivers/modules/redfish/test_power.py create mode 100644 ironic/tests/unit/drivers/modules/redfish/test_utils.py create mode 100644 ironic/tests/unit/drivers/test_redfish.py create mode 100644 releasenotes/notes/added-redfish-driver-00ff5e3f7e9d6ee8.yaml diff --git a/driver-requirements.txt b/driver-requirements.txt index 19f3afd79b..6855f2b3d8 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -14,3 +14,6 @@ python-dracclient>=0.1.0 # The CIMC drivers use the Cisco IMC SDK version 0.7.2 or greater ImcSdk>=0.7.2 + +# The Redfish hardware type uses the Sushy library +sushy diff --git a/ironic/common/exception.py b/ironic/common/exception.py index 7c72b1ef36..a46fb7e927 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -742,3 +742,11 @@ class NotificationPayloadError(IronicException): class StorageError(IronicException): _msg_fmt = _("Storage operation failure.") + + +class RedfishError(IronicException): + _msg_fmt = _("Redfish exception occurred. Error: %(error)s") + + +class RedfishConnectionError(RedfishError): + _msg_fmt = _("Redfish connection failed for node %(node)s: %(error)s") diff --git a/ironic/conf/__init__.py b/ironic/conf/__init__.py index 7f7db8532f..d490ecb7e3 100644 --- a/ironic/conf/__init__.py +++ b/ironic/conf/__init__.py @@ -39,6 +39,7 @@ from ironic.conf import metrics_statsd from ironic.conf import neutron from ironic.conf import oneview from ironic.conf import pxe +from ironic.conf import redfish from ironic.conf import service_catalog from ironic.conf import snmp from ironic.conf import ssh @@ -70,6 +71,7 @@ metrics_statsd.register_opts(CONF) neutron.register_opts(CONF) oneview.register_opts(CONF) pxe.register_opts(CONF) +redfish.register_opts(CONF) service_catalog.register_opts(CONF) snmp.register_opts(CONF) ssh.register_opts(CONF) diff --git a/ironic/conf/redfish.py b/ironic/conf/redfish.py new file mode 100644 index 0000000000..7b5026bec7 --- /dev/null +++ b/ironic/conf/redfish.py @@ -0,0 +1,35 @@ +# 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. + +from oslo_config import cfg + +from ironic.common.i18n import _ + +opts = [ + cfg.IntOpt('connection_attempts', + min=1, + default=5, + help=_('Maximum number of attempts to try to connect ' + 'to Redfish')), + cfg.IntOpt('connection_retry_interval', + min=1, + default=4, + help=_('Number of seconds to wait between attempts to ' + 'connect to Redfish')) +] + + +def register_opts(conf): + conf.register_opts(opts, group='redfish') diff --git a/ironic/drivers/modules/redfish/__init__.py b/ironic/drivers/modules/redfish/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ironic/drivers/modules/redfish/management.py b/ironic/drivers/modules/redfish/management.py new file mode 100644 index 0000000000..7562b99106 --- /dev/null +++ b/ironic/drivers/modules/redfish/management.py @@ -0,0 +1,173 @@ +# 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. + +from oslo_log import log +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.conductor import task_manager +from ironic.drivers import base +from ironic.drivers.modules.redfish import utils as redfish_utils + +LOG = log.getLogger(__name__) + +sushy = importutils.try_import('sushy') + +if sushy: + BOOT_DEVICE_MAP = { + sushy.BOOT_SOURCE_TARGET_PXE: boot_devices.PXE, + sushy.BOOT_SOURCE_TARGET_HDD: boot_devices.DISK, + sushy.BOOT_SOURCE_TARGET_CD: boot_devices.CDROM, + sushy.BOOT_SOURCE_TARGET_BIOS_SETUP: boot_devices.BIOS + } + + BOOT_DEVICE_MAP_REV = {v: k for k, v in BOOT_DEVICE_MAP.items()} + + BOOT_DEVICE_PERSISTENT_MAP = { + sushy.BOOT_SOURCE_ENABLED_CONTINUOUS: True, + sushy.BOOT_SOURCE_ENABLED_ONCE: False + } + + BOOT_DEVICE_PERSISTENT_MAP_REV = {v: k for k, v in + BOOT_DEVICE_PERSISTENT_MAP.items()} + + +class RedfishManagement(base.ManagementInterface): + + def __init__(self): + """Initialize the Redfish management interface. + + :raises: DriverLoadError if the driver can't be loaded due to + missing dependencies + """ + super(RedfishManagement, self).__init__() + if not sushy: + raise exception.DriverLoadError( + driver='redfish', + reason=_('Unable to import the sushy library')) + + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + return redfish_utils.COMMON_PROPERTIES.copy() + + def validate(self, task): + """Validates the driver information needed by the redfish driver. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue on malformed parameter(s) + :raises: MissingParameterValue on missing parameter(s) + """ + redfish_utils.parse_driver_info(task.node) + + def get_supported_boot_devices(self, task): + """Get a list of the supported boot devices. + + :param task: a task from TaskManager. + :returns: A list with the supported boot devices defined + in :mod:`ironic.common.boot_devices`. + """ + return list(BOOT_DEVICE_MAP_REV) + + @task_manager.require_exclusive_lock + def set_boot_device(self, task, device, persistent=False): + """Set the boot device for a node. + + Set the boot device to use on next reboot of the node. + + :param task: a task from TaskManager. + :param device: the boot device, one of + :mod:`ironic.common.boot_devices`. + :param persistent: Boolean value. True if the boot device will + persist to all future boots, False if not. + Default: False. + :raises: InvalidParameterValue on malformed parameter(s) + :raises: MissingParameterValue on missing parameter(s) + :raises: RedfishConnectionError when it fails to connect to Redfish + :raises: RedfishError on an error from the Sushy library + """ + system = redfish_utils.get_system(task.node) + # TODO(lucasagomes): set_system_boot_source() also supports mode + # for UEFI and BIOS we should get it from instance_info and pass + # it along this call + try: + system.set_system_boot_source( + BOOT_DEVICE_MAP_REV[device], + enabled=BOOT_DEVICE_PERSISTENT_MAP_REV[persistent]) + except sushy.exceptions.SushyError as e: + error_msg = (_('Redfish set boot device failed for node ' + '%(node)s. Error: %(error)s') % + {'node': task.node.uuid, 'error': e}) + LOG.error(error_msg) + raise exception.RedfishError(error=error_msg) + + def get_boot_device(self, task): + """Get the current boot device for a node. + + :param task: a task from TaskManager. + :raises: InvalidParameterValue on malformed parameter(s) + :raises: MissingParameterValue on missing parameter(s) + :raises: RedfishConnectionError when it fails to connect to Redfish + :raises: RedfishError on an error from the Sushy library + :returns: a dictionary containing: + + :boot_device: + the boot device, one of :mod:`ironic.common.boot_devices` or + None if it is unknown. + :persistent: + Boolean value or None, True if the boot device persists, + False otherwise. None if it's unknown. + + + """ + system = redfish_utils.get_system(task.node) + return {'boot_device': BOOT_DEVICE_MAP.get(system.boot.get('target')), + 'persistent': BOOT_DEVICE_PERSISTENT_MAP.get( + system.boot.get('enabled'))} + + def get_sensors_data(self, task): + """Get sensors data. + + Not implemented for this driver. + + :raises: NotImplementedError + """ + raise NotImplementedError() + + @task_manager.require_exclusive_lock + def inject_nmi(self, task): + """Inject NMI, Non Maskable Interrupt. + + Inject NMI (Non Maskable Interrupt) for a node immediately. + + :param task: A TaskManager instance containing the node to act on. + :raises: InvalidParameterValue on malformed parameter(s) + :raises: MissingParameterValue on missing parameter(s) + :raises: RedfishConnectionError when it fails to connect to Redfish + :raises: RedfishError on an error from the Sushy library + """ + system = redfish_utils.get_system(task.node) + try: + system.reset_system(sushy.RESET_NMI) + except sushy.exceptions.SushyError as e: + error_msg = (_('Redfish inject NMI failed for node %(node)s. ' + 'Error: %(error)s') % {'node': task.node.uuid, + 'error': e}) + LOG.error(error_msg) + raise exception.RedfishError(error=error_msg) diff --git a/ironic/drivers/modules/redfish/power.py b/ironic/drivers/modules/redfish/power.py new file mode 100644 index 0000000000..b01eb3aced --- /dev/null +++ b/ironic/drivers/modules/redfish/power.py @@ -0,0 +1,144 @@ +# 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. + +from oslo_log import log +from oslo_utils import importutils + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers import base +from ironic.drivers.modules.redfish import utils as redfish_utils + +LOG = log.getLogger(__name__) + +sushy = importutils.try_import('sushy') + +if sushy: + GET_POWER_STATE_MAP = { + sushy.SYSTEM_POWER_STATE_ON: states.POWER_ON, + sushy.SYSTEM_POWER_STATE_POWERING_ON: states.POWER_ON, + sushy.SYSTEM_POWER_STATE_OFF: states.POWER_OFF, + sushy.SYSTEM_POWER_STATE_POWERING_OFF: states.POWER_OFF + } + + SET_POWER_STATE_MAP = { + states.POWER_ON: sushy.RESET_ON, + states.POWER_OFF: sushy.RESET_FORCE_OFF, + states.REBOOT: sushy.RESET_FORCE_RESTART, + states.SOFT_REBOOT: sushy.RESET_GRACEFUL_RESTART, + states.SOFT_POWER_OFF: sushy.RESET_GRACEFUL_SHUTDOWN + } + + +class RedfishPower(base.PowerInterface): + + def __init__(self): + """Initialize the Redfish power interface. + + :raises: DriverLoadError if the driver can't be loaded due to + missing dependencies + """ + super(RedfishPower, self).__init__() + if not sushy: + raise exception.DriverLoadError( + driver='redfish', + reason=_('Unable to import the sushy library')) + + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + return redfish_utils.COMMON_PROPERTIES.copy() + + def validate(self, task): + """Validates the driver information needed by the redfish driver. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue on malformed parameter(s) + :raises: MissingParameterValue on missing parameter(s) + """ + redfish_utils.parse_driver_info(task.node) + + def get_power_state(self, task): + """Get the current power state of the task's node. + + :param task: a TaskManager instance containing the node to act on. + :returns: a power state. One of :mod:`ironic.common.states`. + :raises: InvalidParameterValue on malformed parameter(s) + :raises: MissingParameterValue on missing parameter(s) + :raises: RedfishConnectionError when it fails to connect to Redfish + :raises: RedfishError on an error from the Sushy library + """ + system = redfish_utils.get_system(task.node) + return GET_POWER_STATE_MAP.get(system.power_state) + + @task_manager.require_exclusive_lock + def set_power_state(self, task, power_state, timeout=None): + """Set the power state of the task's node. + + :param task: a TaskManager instance containing the node to act on. + :param power_state: Any power state from :mod:`ironic.common.states`. + :param timeout: Not used by this driver. + :raises: MissingParameterValue if a required parameter is missing. + :raises: RedfishConnectionError when it fails to connect to Redfish + :raises: RedfishError on an error from the Sushy library + """ + system = redfish_utils.get_system(task.node) + try: + system.reset_system(SET_POWER_STATE_MAP.get(power_state)) + except sushy.exceptions.SushyError as e: + error_msg = (_('Redfish set power state failed for node ' + '%(node)s. Error: %(error)s') % + {'node': task.node.uuid, 'error': e}) + LOG.error(error_msg) + raise exception.RedfishError(error=error_msg) + + @task_manager.require_exclusive_lock + def reboot(self, task, timeout=None): + """Perform a hard reboot of the task's node. + + :param task: a TaskManager instance containing the node to act on. + :param timeout: Not used by this driver. + :raises: MissingParameterValue if a required parameter is missing. + :raises: RedfishConnectionError when it fails to connect to Redfish + :raises: RedfishError on an error from the Sushy library + """ + system = redfish_utils.get_system(task.node) + current_power_state = GET_POWER_STATE_MAP.get(system.power_state) + + try: + if current_power_state == states.POWER_ON: + system.reset_system(SET_POWER_STATE_MAP.get(states.REBOOT)) + else: + system.reset_system(SET_POWER_STATE_MAP.get(states.POWER_ON)) + except sushy.exceptions.SushyError as e: + error_msg = (_('Redfish reboot failed for node %(node)s. ' + 'Error: %(error)s') % {'node': task.node.uuid, + 'error': e}) + LOG.error(error_msg) + raise exception.RedfishError(error=error_msg) + + def get_supported_power_states(self, task): + """Get a list of the supported power states. + + :param task: A TaskManager instance containing the node to act on. + Not used by this driver at the moment. + :returns: A list with the supported power states defined + in :mod:`ironic.common.states`. + """ + return list(SET_POWER_STATE_MAP) diff --git a/ironic/drivers/modules/redfish/utils.py b/ironic/drivers/modules/redfish/utils.py new file mode 100644 index 0000000000..c70fbfeb87 --- /dev/null +++ b/ironic/drivers/modules/redfish/utils.py @@ -0,0 +1,172 @@ +# 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 os + +from oslo_log import log +from oslo_utils import excutils +from oslo_utils import importutils +import retrying +import rfc3986 +import six + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.conf import CONF + +sushy = importutils.try_import('sushy') + +LOG = log.getLogger(__name__) + +REQUIRED_PROPERTIES = { + 'redfish_address': _('The URL address to the Redfish controller. It ' + 'should include scheme and authority portion of ' + 'the URL. For example: https://mgmt.vendor.com. ' + 'Required'), + 'redfish_system_id': _('The canonical path to the ComputerSystem ' + 'resource that the driver will interact with. ' + 'It should include the root service, version and ' + 'the unique resource path to a ComputerSystem ' + 'within the same authority as the redfish_address ' + 'property. For example: /redfish/v1/Systems/1. ' + 'Required') +} + +OPTIONAL_PROPERTIES = { + 'redfish_username': _('User account with admin/server-profile access ' + 'privilege. Although this property is not ' + 'mandatory it\'s highly recommended to set a ' + 'username. Optional'), + 'redfish_password': _('User account password. Although this property is ' + 'not mandatory, it\'s highly recommended to set a ' + 'password. Optional'), + 'redfish_verify_ca': _('Either a Boolean value, a path to a CA_BUNDLE ' + 'file or directory with certificates of trusted ' + 'CAs. If set to True the driver will verify the ' + 'host certificates; if False the driver will ' + 'ignore verifying the SSL certificate. If it\'s ' + 'a path the driver will use the specified ' + 'certificate or one of the certificates in the ' + 'directory. Defaults to True. Optional') +} + +COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() +COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) + + +def parse_driver_info(node): + """Parse the information required for Ironic to connect to Redfish. + + :param node: an Ironic node object + :returns: dictionary of parameters + :raises: InvalidParameterValue on malformed parameter(s) + :raises: MissingParameterValue on missing parameter(s) + """ + driver_info = node.driver_info or {} + missing_info = [key for key in REQUIRED_PROPERTIES + if not driver_info.get(key)] + if missing_info: + raise exception.MissingParameterValue(_( + 'Missing the following Redfish properties in node ' + '%(node)s driver_info: %(info)s') % {'node': node.uuid, + 'info': missing_info}) + + # Validate the Redfish address + address = driver_info['redfish_address'] + if not rfc3986.is_valid_uri(address, require_scheme=True, + require_authority=True): + raise exception.InvalidParameterValue( + _('Invalid Redfish address %(address)s set in ' + 'driver_info/redfish_address on node %(node)s') % + {'address': address, 'node': node.uuid}) + + system_id = driver_info['redfish_system_id'] + + # Check if verify_ca is a Boolean or a file/directory in the file-system + verify_ca = driver_info.get('redfish_verify_ca', True) + if isinstance(verify_ca, six.string_types): + if not os.path.exists(verify_ca): + raise exception.InvalidParameterValue( + _('Invalid value "%(value)s" set in ' + 'driver_info/redfish_verify_ca on node %(node)s. ' + 'The value should be either a Boolean, a path to a ' + 'CA_BUNDLE file or directory with certificates of ' + 'trusted CAs') % {'value': verify_ca, 'node': node.uuid}) + elif isinstance(verify_ca, bool): + # If it's a boolean it's grand, we don't need to do anything + pass + else: + raise exception.InvalidParameterValue( + _('Invalid value type set in driver_info/redfish_verify_ca ' + 'on node %(node)s. The value should be a Boolean or the path ' + 'to a file/directory, not "%(value)s"') % {'value': verify_ca, + 'node': node.uuid}) + + return {'address': address, + 'system_id': system_id, + 'username': driver_info.get('redfish_username'), + 'password': driver_info.get('redfish_password'), + 'verify_ca': verify_ca, + 'node_uuid': node.uuid} + + +def get_system(node): + """Get a Redfish System that represents a node. + + :param node: an Ironic node object + :raises: RedfishConnectionError when it fails to connect to Redfish + :raises: RedfishError if the System is not registered in Redfish + """ + driver_info = parse_driver_info(node) + address = driver_info['address'] + system_id = driver_info['system_id'] + + @retrying.retry( + retry_on_exception=( + lambda e: isinstance(e, exception.RedfishConnectionError)), + stop_max_attempt_number=CONF.redfish.connection_attempts, + wait_fixed=CONF.redfish.connection_retry_interval * 1000) + def _get_system(): + try: + # TODO(lucasagomes): We should look into a mechanism to + # cache the connections (and maybe even system's instances) + # to avoid unnecessary requests to the Redfish controller + conn = sushy.Sushy(address, username=driver_info['username'], + password=driver_info['password'], + verify=driver_info['verify_ca']) + return conn.get_system(system_id) + except sushy.exceptions.ResourceNotFoundError as e: + LOG.error('The Redfish System "%(system)s" was not found for ' + 'node %(node)s. Error %(error)s', + {'system': system_id, 'node': node.uuid, 'error': e}) + raise exception.RedfishError(error=e) + # TODO(lucasagomes): We should look at other types of + # ConnectionError such as AuthenticationError or SSLError and stop + # retrying on them + except sushy.exceptions.ConnectionError as e: + LOG.warning('For node %(node)s, got a connection error from ' + 'Redfish at address "%(address)s" when fetching ' + 'System "%(system)s". Error: %(error)s', + {'system': system_id, 'address': address, + 'node': node.uuid, 'error': e}) + raise exception.RedfishConnectionError(node=node.uuid, error=e) + + try: + return _get_system() + except exception.RedfishConnectionError as e: + with excutils.save_and_reraise_exception(): + LOG.error('Failed to connect to Redfish at %(address)s for ' + 'node %(node)s. Error: %(error)s', + {'address': address, 'node': node.uuid, 'error': e}) diff --git a/ironic/drivers/redfish.py b/ironic/drivers/redfish.py new file mode 100644 index 0000000000..ecf7c789a1 --- /dev/null +++ b/ironic/drivers/redfish.py @@ -0,0 +1,32 @@ +# 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. + +from ironic.drivers import generic +from ironic.drivers.modules.redfish import management as redfish_mgmt +from ironic.drivers.modules.redfish import power as redfish_power + + +class RedfishHardware(generic.GenericHardware): + """Redfish hardware type.""" + + @property + def supported_management_interfaces(self): + """List of supported management interfaces.""" + return [redfish_mgmt.RedfishManagement] + + @property + def supported_power_interfaces(self): + """List of supported power interfaces.""" + return [redfish_power.RedfishPower] diff --git a/ironic/tests/unit/db/utils.py b/ironic/tests/unit/db/utils.py index 3c85969e99..581e3a5304 100644 --- a/ironic/tests/unit/db/utils.py +++ b/ironic/tests/unit/db/utils.py @@ -417,6 +417,15 @@ def get_test_oneview_driver_info(): } +def get_test_redfish_info(): + return { + "redfish_address": "http://example.com", + "redfish_system_id": "/redfish/v1/Systems/FAKESYSTEM", + "redfish_username": "username", + "redfish_password": "password" + } + + def get_test_portgroup(**kw): return { 'id': kw.get('id', 654), diff --git a/ironic/tests/unit/drivers/modules/redfish/__init__.py b/ironic/tests/unit/drivers/modules/redfish/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ironic/tests/unit/drivers/modules/redfish/test_management.py b/ironic/tests/unit/drivers/modules/redfish/test_management.py new file mode 100644 index 0000000000..8b4ce1a9ee --- /dev/null +++ b/ironic/tests/unit/drivers/modules/redfish/test_management.py @@ -0,0 +1,189 @@ +# 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 mock +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.redfish import management as redfish_mgmt +from ironic.drivers.modules.redfish import utils as redfish_utils +from ironic.tests.unit.conductor import mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.objects import utils as obj_utils + +sushy = importutils.try_import('sushy') + +INFO_DICT = db_utils.get_test_redfish_info() + + +class MockedSushyError(Exception): + pass + + +class RedfishManagementTestCase(db_base.DbTestCase): + + def setUp(self): + super(RedfishManagementTestCase, self).setUp() + self.config(enabled_hardware_types=['redfish'], + enabled_power_interfaces=['redfish'], + enabled_management_interfaces=['redfish']) + mgr_utils.mock_the_extension_manager( + driver='redfish', namespace='ironic.hardware.types') + self.node = obj_utils.create_test_node( + self.context, driver='redfish', driver_info=INFO_DICT) + + @mock.patch.object(redfish_mgmt, 'sushy', None) + def test_loading_error(self): + self.assertRaisesRegex( + exception.DriverLoadError, + 'Unable to import the sushy library', + redfish_mgmt.RedfishManagement) + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + properties = task.driver.get_properties() + for prop in redfish_utils.COMMON_PROPERTIES: + self.assertIn(prop, properties) + + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + def test_validate(self, mock_parse_driver_info): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.management.validate(task) + mock_parse_driver_info.assert_called_once_with(task.node) + + def test_get_supported_boot_devices(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + supported_boot_devices = ( + task.driver.management.get_supported_boot_devices(task)) + self.assertEqual(list(redfish_mgmt.BOOT_DEVICE_MAP_REV), + supported_boot_devices) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_set_boot_device(self, mock_get_system): + fake_system = mock.Mock() + mock_get_system.return_value = fake_system + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + expected_values = [ + (boot_devices.PXE, sushy.BOOT_SOURCE_TARGET_PXE), + (boot_devices.DISK, sushy.BOOT_SOURCE_TARGET_HDD), + (boot_devices.CDROM, sushy.BOOT_SOURCE_TARGET_CD), + (boot_devices.BIOS, sushy.BOOT_SOURCE_TARGET_BIOS_SETUP) + ] + + for target, expected in expected_values: + task.driver.management.set_boot_device(task, target) + + # Asserts + fake_system.set_system_boot_source.assert_called_once_with( + expected, enabled=sushy.BOOT_SOURCE_ENABLED_ONCE) + mock_get_system.assert_called_once_with(task.node) + + # Reset mocks + fake_system.set_system_boot_source.reset_mock() + mock_get_system.reset_mock() + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_set_boot_device_persistency(self, mock_get_system): + fake_system = mock.Mock() + mock_get_system.return_value = fake_system + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + expected_values = [ + (True, sushy.BOOT_SOURCE_ENABLED_CONTINUOUS), + (False, sushy.BOOT_SOURCE_ENABLED_ONCE) + ] + + for target, expected in expected_values: + task.driver.management.set_boot_device( + task, boot_devices.PXE, persistent=target) + + fake_system.set_system_boot_source.assert_called_once_with( + sushy.BOOT_SOURCE_TARGET_PXE, enabled=expected) + mock_get_system.assert_called_once_with(task.node) + + # Reset mocks + fake_system.set_system_boot_source.reset_mock() + mock_get_system.reset_mock() + + @mock.patch('ironic.drivers.modules.redfish.management.sushy') + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_set_boot_device_fail(self, mock_get_system, mock_sushy): + fake_system = mock.Mock() + mock_sushy.exceptions.SushyError = MockedSushyError + fake_system.set_system_boot_source.side_effect = MockedSushyError + mock_get_system.return_value = fake_system + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaisesRegex( + exception.RedfishError, 'Redfish set boot device', + task.driver.management.set_boot_device, task, boot_devices.PXE) + fake_system.set_system_boot_source.assert_called_once_with( + sushy.BOOT_SOURCE_TARGET_PXE, + enabled=sushy.BOOT_SOURCE_ENABLED_ONCE) + mock_get_system.assert_called_once_with(task.node) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_get_boot_device(self, mock_get_system): + boot_attribute = { + 'target': sushy.BOOT_SOURCE_TARGET_PXE, + 'enabled': sushy.BOOT_SOURCE_ENABLED_CONTINUOUS + } + fake_system = mock.Mock(boot=boot_attribute) + mock_get_system.return_value = fake_system + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + response = task.driver.management.get_boot_device(task) + expected = {'boot_device': boot_devices.PXE, + 'persistent': True} + self.assertEqual(expected, response) + + def test_get_sensors_data(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(NotImplementedError, + task.driver.management.get_sensors_data, task) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_inject_nmi(self, mock_get_system): + fake_system = mock.Mock() + mock_get_system.return_value = fake_system + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.inject_nmi(task) + fake_system.reset_system.assert_called_once_with(sushy.RESET_NMI) + mock_get_system.assert_called_once_with(task.node) + + @mock.patch('ironic.drivers.modules.redfish.management.sushy') + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_inject_nmi_fail(self, mock_get_system, mock_sushy): + fake_system = mock.Mock() + mock_sushy.exceptions.SushyError = MockedSushyError + fake_system.reset_system.side_effect = MockedSushyError + mock_get_system.return_value = fake_system + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaisesRegex( + exception.RedfishError, 'Redfish inject NMI', + task.driver.management.inject_nmi, task) + fake_system.reset_system.assert_called_once_with( + mock_sushy.RESET_NMI) + mock_get_system.assert_called_once_with(task.node) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_power.py b/ironic/tests/unit/drivers/modules/redfish/test_power.py new file mode 100644 index 0000000000..d8e9a5ec1b --- /dev/null +++ b/ironic/tests/unit/drivers/modules/redfish/test_power.py @@ -0,0 +1,173 @@ +# 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 mock +from oslo_utils import importutils + +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules.redfish import power as redfish_power +from ironic.drivers.modules.redfish import utils as redfish_utils +from ironic.tests.unit.conductor import mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.objects import utils as obj_utils + +sushy = importutils.try_import('sushy') + +INFO_DICT = db_utils.get_test_redfish_info() + + +class MockedSushyError(Exception): + pass + + +class RedfishPowerTestCase(db_base.DbTestCase): + + def setUp(self): + super(RedfishPowerTestCase, self).setUp() + self.config(enabled_hardware_types=['redfish'], + enabled_power_interfaces=['redfish'], + enabled_management_interfaces=['redfish']) + mgr_utils.mock_the_extension_manager( + driver='redfish', namespace='ironic.hardware.types') + self.node = obj_utils.create_test_node( + self.context, driver='redfish', driver_info=INFO_DICT) + + @mock.patch.object(redfish_power, 'sushy', None) + def test_loading_error(self): + self.assertRaisesRegex( + exception.DriverLoadError, + 'Unable to import the sushy library', + redfish_power.RedfishPower) + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + properties = task.driver.get_properties() + for prop in redfish_utils.COMMON_PROPERTIES: + self.assertIn(prop, properties) + + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + def test_validate(self, mock_parse_driver_info): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.power.validate(task) + mock_parse_driver_info.assert_called_once_with(task.node) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_get_power_state(self, mock_get_system): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + expected_values = [ + (sushy.SYSTEM_POWER_STATE_ON, states.POWER_ON), + (sushy.SYSTEM_POWER_STATE_POWERING_ON, states.POWER_ON), + (sushy.SYSTEM_POWER_STATE_OFF, states.POWER_OFF), + (sushy.SYSTEM_POWER_STATE_POWERING_OFF, states.POWER_OFF) + ] + for current, expected in expected_values: + mock_get_system.return_value = mock.Mock(power_state=current) + self.assertEqual(expected, + task.driver.power.get_power_state(task)) + mock_get_system.assert_called_once_with(task.node) + mock_get_system.reset_mock() + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_set_power_state(self, mock_get_system): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + expected_values = [ + (states.POWER_ON, sushy.RESET_ON), + (states.POWER_OFF, sushy.RESET_FORCE_OFF), + (states.REBOOT, sushy.RESET_FORCE_RESTART), + (states.SOFT_REBOOT, sushy.RESET_GRACEFUL_RESTART), + (states.SOFT_POWER_OFF, sushy.RESET_GRACEFUL_SHUTDOWN) + ] + + fake_system = mock_get_system.return_value + for target, expected in expected_values: + task.driver.power.set_power_state(task, target) + + # Asserts + fake_system.reset_system.assert_called_once_with(expected) + mock_get_system.assert_called_once_with(task.node) + + # Reset mocks + fake_system.reset_system.reset_mock() + mock_get_system.reset_mock() + + @mock.patch('ironic.drivers.modules.redfish.power.sushy') + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_set_power_state_fail(self, mock_get_system, mock_sushy): + fake_system = mock_get_system.return_value + mock_sushy.exceptions.SushyError = MockedSushyError + fake_system.reset_system.side_effect = MockedSushyError() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaisesRegex( + exception.RedfishError, 'Redfish set power state', + task.driver.power.set_power_state, task, states.POWER_ON) + fake_system.reset_system.assert_called_once_with( + sushy.RESET_ON) + mock_get_system.assert_called_once_with(task.node) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_reboot(self, mock_get_system): + fake_system = mock_get_system.return_value + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + expected_values = [ + (sushy.SYSTEM_POWER_STATE_ON, sushy.RESET_FORCE_RESTART), + (sushy.SYSTEM_POWER_STATE_OFF, sushy.RESET_ON) + ] + + for current, expected in expected_values: + fake_system.power_state = current + task.driver.power.reboot(task) + + # Asserts + fake_system.reset_system.assert_called_once_with(expected) + mock_get_system.assert_called_once_with(task.node) + + # Reset mocks + fake_system.reset_system.reset_mock() + mock_get_system.reset_mock() + + @mock.patch('ironic.drivers.modules.redfish.power.sushy') + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_reboot_fail(self, mock_get_system, mock_sushy): + fake_system = mock_get_system.return_value + mock_sushy.exceptions.SushyError = MockedSushyError + fake_system.reset_system.side_effect = MockedSushyError() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + fake_system.power_state = sushy.SYSTEM_POWER_STATE_ON + self.assertRaisesRegex( + exception.RedfishError, 'Redfish reboot failed', + task.driver.power.reboot, task) + fake_system.reset_system.assert_called_once_with( + sushy.RESET_FORCE_RESTART) + mock_get_system.assert_called_once_with(task.node) + + def test_get_supported_power_states(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + supported_power_states = ( + task.driver.power.get_supported_power_states(task)) + self.assertEqual(list(redfish_power.SET_POWER_STATE_MAP), + supported_power_states) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_utils.py b/ironic/tests/unit/drivers/modules/redfish/test_utils.py new file mode 100644 index 0000000000..9d74afbc60 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/redfish/test_utils.py @@ -0,0 +1,149 @@ +# 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 os + +import mock +from oslo_utils import importutils + +from ironic.common import exception +from ironic.drivers.modules.redfish import utils as redfish_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.objects import utils as obj_utils + +sushy = importutils.try_import('sushy') + +INFO_DICT = db_utils.get_test_redfish_info() + + +class MockedConnectionError(Exception): + pass + + +class MockedResourceNotFoundError(Exception): + pass + + +class RedfishUtilsTestCase(db_base.DbTestCase): + + def setUp(self): + super(RedfishUtilsTestCase, self).setUp() + # Default configurations + self.config(enabled_hardware_types=['redfish'], + enabled_power_interfaces=['redfish'], + enabled_management_interfaces=['redfish']) + # Redfish specific configurations + self.config(connection_attempts=1, group='redfish') + self.config(connection_retry_interval=0, group='redfish') + self.node = obj_utils.create_test_node( + self.context, driver='redfish', driver_info=INFO_DICT) + self.parsed_driver_info = { + 'address': 'http://example.com', + 'system_id': '/redfish/v1/Systems/FAKESYSTEM', + 'username': 'username', + 'password': 'password', + 'verify_ca': True, + 'node_uuid': self.node.uuid + } + + def test_parse_driver_info(self): + response = redfish_utils.parse_driver_info(self.node) + self.assertEqual(self.parsed_driver_info, response) + + def test_parse_driver_info_missing_info(self): + for prop in redfish_utils.REQUIRED_PROPERTIES: + self.node.driver_info = INFO_DICT.copy() + self.node.driver_info.pop(prop) + self.assertRaises(exception.MissingParameterValue, + redfish_utils.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_address(self): + self.node.driver_info['redfish_address'] = 'this-is-a-bad-address' + self.assertRaisesRegex(exception.InvalidParameterValue, + 'Invalid Redfish address', + redfish_utils.parse_driver_info, self.node) + + @mock.patch.object(os.path, 'exists', autospec=True) + def test_parse_driver_info_path_verify_ca(self, mock_path_exists): + mock_path_exists.return_value = True + fake_path = '/path/to/a/valid/CA' + self.node.driver_info['redfish_verify_ca'] = fake_path + self.parsed_driver_info['verify_ca'] = fake_path + + response = redfish_utils.parse_driver_info(self.node) + self.assertEqual(self.parsed_driver_info, response) + mock_path_exists.assert_called_once_with(fake_path) + + @mock.patch.object(os.path, 'exists', autospec=True) + def test_parse_driver_info_invalid_path_verify_ca(self, mock_path_exists): + mock_path_exists.return_value = False + fake_path = '/this/path/doesnt/exist' + self.node.driver_info['redfish_verify_ca'] = fake_path + self.assertRaisesRegex(exception.InvalidParameterValue, + 'path to a CA_BUNDLE', + redfish_utils.parse_driver_info, self.node) + mock_path_exists.assert_called_once_with(fake_path) + + def test_parse_driver_info_invalid_value_verify_ca(self): + # Integers are not supported + self.node.driver_info['redfish_verify_ca'] = 123456 + self.assertRaisesRegex(exception.InvalidParameterValue, + 'Invalid value type', + redfish_utils.parse_driver_info, self.node) + + @mock.patch('ironic.drivers.modules.redfish.utils.sushy') + def test_get_system(self, mock_sushy): + fake_conn = mock_sushy.Sushy.return_value + fake_system = fake_conn.get_system.return_value + + response = redfish_utils.get_system(self.node) + self.assertEqual(fake_system, response) + fake_conn.get_system.assert_called_once_with( + '/redfish/v1/Systems/FAKESYSTEM') + + @mock.patch('ironic.drivers.modules.redfish.utils.sushy') + def test_get_system_resource_not_found(self, mock_sushy): + fake_conn = mock_sushy.Sushy.return_value + mock_sushy.exceptions.ResourceNotFoundError = ( + MockedResourceNotFoundError) + fake_conn.get_system.side_effect = MockedResourceNotFoundError() + + self.assertRaises(exception.RedfishError, + redfish_utils.get_system, self.node) + fake_conn.get_system.assert_called_once_with( + '/redfish/v1/Systems/FAKESYSTEM') + + @mock.patch('ironic.drivers.modules.redfish.utils.sushy') + def test_get_system_resource_connection_error_retry(self, mock_sushy): + # Redfish specific configurations + self.config(connection_attempts=3, group='redfish') + self.config(connection_retry_interval=0, group='redfish') + + fake_conn = mock_sushy.Sushy.return_value + mock_sushy.exceptions.ResourceNotFoundError = ( + MockedResourceNotFoundError) + mock_sushy.exceptions.ConnectionError = MockedConnectionError + fake_conn.get_system.side_effect = MockedConnectionError() + + self.assertRaises(exception.RedfishConnectionError, + redfish_utils.get_system, self.node) + + expected_get_system_calls = [ + mock.call(self.parsed_driver_info['system_id']), + mock.call(self.parsed_driver_info['system_id']), + mock.call(self.parsed_driver_info['system_id']), + ] + fake_conn.get_system.assert_has_calls(expected_get_system_calls) diff --git a/ironic/tests/unit/drivers/test_redfish.py b/ironic/tests/unit/drivers/test_redfish.py new file mode 100644 index 0000000000..bddd63ce47 --- /dev/null +++ b/ironic/tests/unit/drivers/test_redfish.py @@ -0,0 +1,44 @@ +# 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. + +from ironic.conductor import task_manager +from ironic.drivers.modules import iscsi_deploy +from ironic.drivers.modules import noop +from ironic.drivers.modules import pxe +from ironic.drivers.modules.redfish import management as redfish_mgmt +from ironic.drivers.modules.redfish import power as redfish_power +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as obj_utils + + +class RedfishHardwareTestCase(db_base.DbTestCase): + + def setUp(self): + super(RedfishHardwareTestCase, self).setUp() + self.config(enabled_hardware_types=['redfish'], + enabled_power_interfaces=['redfish'], + enabled_management_interfaces=['redfish']) + + def test_default_interfaces(self): + node = obj_utils.create_test_node(self.context, driver='redfish') + with task_manager.acquire(self.context, node.id) as task: + self.assertIsInstance(task.driver.management, + redfish_mgmt.RedfishManagement) + self.assertIsInstance(task.driver.power, + redfish_power.RedfishPower) + self.assertIsInstance(task.driver.boot, pxe.PXEBoot) + self.assertIsInstance(task.driver.deploy, iscsi_deploy.ISCSIDeploy) + self.assertIsInstance(task.driver.console, noop.NoConsole) + self.assertIsInstance(task.driver.raid, noop.NoRAID) diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py index ec4023f1df..8a51c89faf 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -121,3 +121,22 @@ ONEVIEWCLIENT_STATES_SPEC = ( 'ONEVIEW_RESETTING', 'ONEVIEW_ERROR', ) + +SUSHY_CONSTANTS_SPEC = ( + 'BOOT_SOURCE_TARGET_PXE', + 'BOOT_SOURCE_TARGET_HDD', + 'BOOT_SOURCE_TARGET_CD', + 'BOOT_SOURCE_TARGET_BIOS_SETUP', + 'SYSTEM_POWER_STATE_ON', + 'SYSTEM_POWER_STATE_POWERING_ON', + 'SYSTEM_POWER_STATE_OFF', + 'SYSTEM_POWER_STATE_POWERING_OFF', + 'RESET_ON', + 'RESET_FORCE_OFF', + 'RESET_GRACEFUL_SHUTDOWN', + 'RESET_GRACEFUL_RESTART', + 'RESET_FORCE_RESTART', + 'RESET_NMI', + 'BOOT_SOURCE_ENABLED_CONTINUOUS', + 'BOOT_SOURCE_ENABLED_ONCE', +) diff --git a/ironic/tests/unit/drivers/third_party_driver_mocks.py b/ironic/tests/unit/drivers/third_party_driver_mocks.py index 7d3934cdef..0cb3ebcbbf 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mocks.py +++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py @@ -223,3 +223,31 @@ if not imcsdk: if 'ironic.drivers.modules.cimc' in sys.modules: six.moves.reload_module( sys.modules['ironic.drivers.modules.cimc']) + + +sushy = importutils.try_import('sushy') +if not sushy: + sushy = mock.MagicMock( + spec_set=mock_specs.SUSHY_CONSTANTS_SPEC, + BOOT_SOURCE_TARGET_PXE='Pxe', + BOOT_SOURCE_TARGET_HDD='Hdd', + BOOT_SOURCE_TARGET_CD='Cd', + BOOT_SOURCE_TARGET_BIOS_SETUP='BiosSetup', + SYSTEM_POWER_STATE_ON='on', + SYSTEM_POWER_STATE_POWERING_ON='powering on', + SYSTEM_POWER_STATE_OFF='off', + SYSTEM_POWER_STATE_POWERING_OFF='powering off', + 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', + BOOT_SOURCE_ENABLED_CONTINUOUS='continuous', + BOOT_SOURCE_ENABLED_ONCE='once' + ) + + sys.modules['sushy'] = sushy + if 'ironic.drivers.modules.redfish' in sys.modules: + six.moves.reload_module( + sys.modules['ironic.drivers.modules.redfish']) diff --git a/releasenotes/notes/added-redfish-driver-00ff5e3f7e9d6ee8.yaml b/releasenotes/notes/added-redfish-driver-00ff5e3f7e9d6ee8.yaml new file mode 100644 index 0000000000..d034f31b12 --- /dev/null +++ b/releasenotes/notes/added-redfish-driver-00ff5e3f7e9d6ee8.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Adds support for Redfish with: + + * ``redfish`` hardware type + * ``redfish`` power interface that provides hard power + [on, off, reboot] and soft power [off, reboot] + * ``redfish`` management interface to set the boot device (PXE, disk, + cd-rom and bios) and its frequency (persistent or not); and NMI + injection SSL authentication + diff --git a/requirements.txt b/requirements.txt index 70092a844a..aba64fe627 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,7 @@ oslo.service>=1.10.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 +rfc3986>=0.3.1 # Apache-2.0 six>=1.9.0 # MIT jsonpatch>=1.1 # BSD WSME>=0.8 # MIT diff --git a/setup.cfg b/setup.cfg index c2663ecf05..b801a563ef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -105,6 +105,7 @@ ironic.hardware.interfaces.management = fake = ironic.drivers.modules.fake:FakeManagement ipmitool = ironic.drivers.modules.ipmitool:IPMIManagement irmc = ironic.drivers.modules.irmc.management:IRMCManagement + redfish = ironic.drivers.modules.redfish.management:RedfishManagement ironic.hardware.interfaces.network = flat = ironic.drivers.modules.network.flat:FlatNetwork @@ -115,6 +116,7 @@ ironic.hardware.interfaces.power = fake = ironic.drivers.modules.fake:FakePower ipmitool = ironic.drivers.modules.ipmitool:IPMIPower irmc = ironic.drivers.modules.irmc.power:IRMCPower + redfish = ironic.drivers.modules.redfish.power:RedfishPower ironic.hardware.interfaces.raid = agent = ironic.drivers.modules.agent:AgentRAID @@ -137,6 +139,7 @@ ironic.hardware.types = manual-management = ironic.drivers.generic:ManualManagementHardware ipmi = ironic.drivers.ipmi:IPMIHardware irmc = ironic.drivers.irmc:IRMCHardware + redfish = ironic.drivers.redfish:RedfishHardware ironic.database.migration_backend = sqlalchemy = ironic.db.sqlalchemy.migration