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
|
# 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
|
||||||
|
@ -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")
|
||||||
|
@ -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
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):
|
def get_test_portgroup(**kw):
|
||||||
return {
|
return {
|
||||||
'id': kw.get('id', 654),
|
'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_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',
|
||||||
|
)
|
||||||
|
@ -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'])
|
||||||
|
@ -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
|
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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user