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:
parent
1fcb6c52a2
commit
c21149454a
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
35
ironic/conf/redfish.py
Normal 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')
|
0
ironic/drivers/modules/redfish/__init__.py
Normal file
0
ironic/drivers/modules/redfish/__init__.py
Normal file
173
ironic/drivers/modules/redfish/management.py
Normal file
173
ironic/drivers/modules/redfish/management.py
Normal 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)
|
144
ironic/drivers/modules/redfish/power.py
Normal file
144
ironic/drivers/modules/redfish/power.py
Normal 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)
|
172
ironic/drivers/modules/redfish/utils.py
Normal file
172
ironic/drivers/modules/redfish/utils.py
Normal 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
32
ironic/drivers/redfish.py
Normal 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]
|
@ -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),
|
||||
|
189
ironic/tests/unit/drivers/modules/redfish/test_management.py
Normal file
189
ironic/tests/unit/drivers/modules/redfish/test_management.py
Normal 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)
|
173
ironic/tests/unit/drivers/modules/redfish/test_power.py
Normal file
173
ironic/tests/unit/drivers/modules/redfish/test_power.py
Normal 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)
|
149
ironic/tests/unit/drivers/modules/redfish/test_utils.py
Normal file
149
ironic/tests/unit/drivers/modules/redfish/test_utils.py
Normal 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)
|
44
ironic/tests/unit/drivers/test_redfish.py
Normal file
44
ironic/tests/unit/drivers/test_redfish.py
Normal 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)
|
@ -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',
|
||||
)
|
||||
|
@ -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'])
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user