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
This commit is contained in:
Lucas Alvares Gomes 2017-02-27 17:03:28 +00:00
parent 1fcb6c52a2
commit c21149454a
20 changed files with 1196 additions and 0 deletions

View File

@ -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

View File

@ -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")

View File

@ -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)

35
ironic/conf/redfish.py Normal file
View File

@ -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')

View File

@ -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 <property name>:<property description> 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)

View File

@ -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 <property name>:<property description> 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)

View File

@ -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})

32
ironic/drivers/redfish.py Normal file
View File

@ -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]

View File

@ -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),

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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',
)

View File

@ -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'])

View File

@ -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

View File

@ -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

View File

@ -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