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 # The CIMC drivers use the Cisco IMC SDK version 0.7.2 or greater
ImcSdk>=0.7.2 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): class StorageError(IronicException):
_msg_fmt = _("Storage operation failure.") _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 neutron
from ironic.conf import oneview from ironic.conf import oneview
from ironic.conf import pxe from ironic.conf import pxe
from ironic.conf import redfish
from ironic.conf import service_catalog from ironic.conf import service_catalog
from ironic.conf import snmp from ironic.conf import snmp
from ironic.conf import ssh from ironic.conf import ssh
@ -70,6 +71,7 @@ metrics_statsd.register_opts(CONF)
neutron.register_opts(CONF) neutron.register_opts(CONF)
oneview.register_opts(CONF) oneview.register_opts(CONF)
pxe.register_opts(CONF) pxe.register_opts(CONF)
redfish.register_opts(CONF)
service_catalog.register_opts(CONF) service_catalog.register_opts(CONF)
snmp.register_opts(CONF) snmp.register_opts(CONF)
ssh.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): def get_test_portgroup(**kw):
return { return {
'id': kw.get('id', 654), '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_RESETTING',
'ONEVIEW_ERROR', '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: if 'ironic.drivers.modules.cimc' in sys.modules:
six.moves.reload_module( six.moves.reload_module(
sys.modules['ironic.drivers.modules.cimc']) 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 oslo.utils>=3.20.0 # Apache-2.0
pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD 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 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 six>=1.9.0 # MIT
jsonpatch>=1.1 # BSD jsonpatch>=1.1 # BSD
WSME>=0.8 # MIT WSME>=0.8 # MIT

View File

@ -105,6 +105,7 @@ ironic.hardware.interfaces.management =
fake = ironic.drivers.modules.fake:FakeManagement fake = ironic.drivers.modules.fake:FakeManagement
ipmitool = ironic.drivers.modules.ipmitool:IPMIManagement ipmitool = ironic.drivers.modules.ipmitool:IPMIManagement
irmc = ironic.drivers.modules.irmc.management:IRMCManagement irmc = ironic.drivers.modules.irmc.management:IRMCManagement
redfish = ironic.drivers.modules.redfish.management:RedfishManagement
ironic.hardware.interfaces.network = ironic.hardware.interfaces.network =
flat = ironic.drivers.modules.network.flat:FlatNetwork flat = ironic.drivers.modules.network.flat:FlatNetwork
@ -115,6 +116,7 @@ ironic.hardware.interfaces.power =
fake = ironic.drivers.modules.fake:FakePower fake = ironic.drivers.modules.fake:FakePower
ipmitool = ironic.drivers.modules.ipmitool:IPMIPower ipmitool = ironic.drivers.modules.ipmitool:IPMIPower
irmc = ironic.drivers.modules.irmc.power:IRMCPower irmc = ironic.drivers.modules.irmc.power:IRMCPower
redfish = ironic.drivers.modules.redfish.power:RedfishPower
ironic.hardware.interfaces.raid = ironic.hardware.interfaces.raid =
agent = ironic.drivers.modules.agent:AgentRAID agent = ironic.drivers.modules.agent:AgentRAID
@ -137,6 +139,7 @@ ironic.hardware.types =
manual-management = ironic.drivers.generic:ManualManagementHardware manual-management = ironic.drivers.generic:ManualManagementHardware
ipmi = ironic.drivers.ipmi:IPMIHardware ipmi = ironic.drivers.ipmi:IPMIHardware
irmc = ironic.drivers.irmc:IRMCHardware irmc = ironic.drivers.irmc:IRMCHardware
redfish = ironic.drivers.redfish:RedfishHardware
ironic.database.migration_backend = ironic.database.migration_backend =
sqlalchemy = ironic.db.sqlalchemy.migration sqlalchemy = ironic.db.sqlalchemy.migration