Add Huawei iBMC driver support

This patch proposes to adding iBMC driver for deploying the
Huawei 2288H V5, CH121 V5 series servers.
The driver aims to add management and power interfaces using
Huawei iBMC RESTful APIs for those series servers.

Change-Id: Ic5e920e4e58811c6a6dfe927732595950aea64e7
Story: 2004635
Task: 28566
changes/88/639288/16
Qianbiao NG 4 years ago
parent ec2f7f992e
commit f1f4f892fe
  1. 1
      doc/source/admin/drivers.rst
  2. 119
      doc/source/admin/drivers/ibmc.rst
  3. 1309
      doc/source/images/ironic_standalone_with_ibmc_driver.svg
  4. 3
      driver-requirements.txt
  5. 8
      ironic/common/exception.py
  6. 2
      ironic/conf/__init__.py
  7. 35
      ironic/conf/ibmc.py
  8. 40
      ironic/drivers/ibmc.py
  9. 0
      ironic/drivers/modules/ibmc/__init__.py
  10. 237
      ironic/drivers/modules/ibmc/management.py
  11. 70
      ironic/drivers/modules/ibmc/mappings.py
  12. 145
      ironic/drivers/modules/ibmc/power.py
  13. 177
      ironic/drivers/modules/ibmc/utils.py
  14. 87
      ironic/drivers/modules/ibmc/vendor.py
  15. 9
      ironic/tests/unit/db/utils.py
  16. 0
      ironic/tests/unit/drivers/modules/ibmc/__init__.py
  17. 42
      ironic/tests/unit/drivers/modules/ibmc/base.py
  18. 276
      ironic/tests/unit/drivers/modules/ibmc/test_management.py
  19. 284
      ironic/tests/unit/drivers/modules/ibmc/test_power.py
  20. 172
      ironic/tests/unit/drivers/modules/ibmc/test_utils.py
  21. 60
      ironic/tests/unit/drivers/modules/ibmc/test_vendor.py
  22. 47
      ironic/tests/unit/drivers/test_ibmc.py
  23. 7
      ironic/tests/unit/drivers/third_party_driver_mock_specs.py
  24. 43
      ironic/tests/unit/drivers/third_party_driver_mocks.py
  25. 10
      releasenotes/notes/ibmc-driver-45fcf9f50ebf0193.yaml
  26. 4
      setup.cfg

@ -18,6 +18,7 @@ Hardware Types
:maxdepth: 1
drivers/cimc
drivers/ibmc
drivers/idrac
drivers/ilo
drivers/ipmitool

@ -0,0 +1,119 @@
===============
iBMC driver
===============
Overview
========
The ``ibmc`` driver is targeted for Huawei rack server 2288H V5, CH121 V5.
The iBMC hardware type enables the user to take advantage of features of
`Huawei iBMC`_ to control Huawei server.
Prerequisites
=============
The `HUAWEI iBMC Client library`_ should be installed on the ironic conductor
node(s).
For example, it can be installed with ``pip``::
sudo pip install python-ibmcclient
Enabling the iBMC driver
============================
#. Add ``ibmc`` to the list of ``enabled_hardware_types``,
``enabled_power_interfaces``, ``enabled_vendor_interfaces``
and ``enabled_management_interfaces`` in ``/etc/ironic/ironic.conf``. For example::
[DEFAULT]
...
enabled_hardware_types = ibmc,ipmi
enabled_power_interfaces = ibmc,ipmitool
enabled_management_interfaces = ibmc,ipmitool
enabled_vendor_interfaces = ibmc
#. Restart the ironic conductor service::
sudo service ironic-conductor restart
# Or, for RDO:
sudo systemctl restart openstack-ironic-conductor
Registering a node with the iBMC driver
===========================================
Nodes configured to use the driver should have the ``driver`` property
set to ``ibmc``.
The following properties are specified in the node's ``driver_info``
field:
- ``ibmc_address``:
The URL address to the ibmc controller. It must
include the authority portion of the URL, and can
optionally include the scheme. If the scheme is
missing, https is assumed.
For example: https://ibmc.example.com. This is required.
- ``ibmc_username``:
User account with admin/server-profile access
privilege. This is required.
- ``ibmc_password``:
User account password. This is required.
- ``ibmc_verify_ca``:
If ibmc_address has the **https** scheme, the
driver will use a secure (TLS_) connection when
talking to the ibmc controller. By default
(if this is set to True), the driver will try to
verify the host certificates. This can be set to
the path of a certificate file or directory with
trusted certificates that the driver will use for
verification. To disable verifying TLS_, set this
to False. This is optional.
The ``openstack baremetal node create`` command can be used to enroll
a node with the ``ibmc`` driver. For example:
.. code-block:: bash
openstack baremetal node create --driver ibmc
--driver-info ibmc_address=https://example.com \
--driver-info ibmc_username=admin \
--driver-info ibmc_password=password
For more information about enrolling nodes see :ref:`enrollment`
in the install guide.
Features of the ``ibmc`` hardware type
=========================================
Query boot up sequence
^^^^^^^^^^^^^^^^^^^^^^
The ``ibmc`` hardware type can query current boot up sequence from the
bare metal node
.. code-block:: bash
openstack baremetal node passthru call --http-method GET \
<node id or node name> boot_up_seq
PXE Boot and iSCSI Deploy Process with Ironic Standalone Environment
====================================================================
.. figure:: ../../images/ironic_standalone_with_ibmc_driver.svg
:width: 960px
:align: left
:alt: Ironic standalone with iBMC driver node
.. _Huawei iBMC: https://e.huawei.com/en/products/cloud-computing-dc/servers/accessories/ibmc
.. _TLS: https://en.wikipedia.org/wiki/Transport_Layer_Security
.. _HUAWEI iBMC Client library: https://pypi.org/project/python-ibmcclient/

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 130 KiB

@ -20,3 +20,6 @@ sushy>=1.6.0
# Ansible-deploy interface
ansible>=2.4
# HUAWEI iBMC hardware type uses the python-ibmcclient library
python-ibmcclient>=0.1.0

@ -823,3 +823,11 @@ class DeployTemplateNotFound(NotFound):
class InvalidDeployTemplate(Invalid):
_msg_fmt = _("Deploy template invalid: %(err)s.")
class IBMCError(DriverOperationError):
_msg_fmt = _("IBMC exception occurred on node %(node)s. Error: %(error)s")
class IBMCConnectionError(IBMCError):
_msg_fmt = _("IBMC connection failed for node %(node)s: %(error)s")

@ -30,6 +30,7 @@ from ironic.conf import dhcp
from ironic.conf import drac
from ironic.conf import glance
from ironic.conf import healthcheck
from ironic.conf import ibmc
from ironic.conf import ilo
from ironic.conf import inspector
from ironic.conf import ipmi
@ -63,6 +64,7 @@ drac.register_opts(CONF)
dhcp.register_opts(CONF)
glance.register_opts(CONF)
healthcheck.register_opts(CONF)
ibmc.register_opts(CONF)
ilo.register_opts(CONF)
inspector.register_opts(CONF)
ipmi.register_opts(CONF)

@ -0,0 +1,35 @@
#
# 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.
# Version 1.0.0
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 iBMC')),
cfg.IntOpt('connection_retry_interval',
min=1,
default=4,
help=_('Number of seconds to wait between attempts to '
'connect to iBMC'))
]
def register_opts(conf):
conf.register_opts(opts, group='ibmc')

@ -0,0 +1,40 @@
#
# 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.
"""
iBMC Driver for managing HUAWEI Huawei 2288H V5, CH121 V5 series servers.
"""
from ironic.drivers import generic
from ironic.drivers.modules.ibmc import management as ibmc_mgmt
from ironic.drivers.modules.ibmc import power as ibmc_power
from ironic.drivers.modules.ibmc import vendor as ibmc_vendor
from ironic.drivers.modules import noop
class IBMCHardware(generic.GenericHardware):
"""Huawei iBMC hardware type."""
@property
def supported_management_interfaces(self):
"""List of supported management interfaces."""
return [ibmc_mgmt.IBMCManagement]
@property
def supported_power_interfaces(self):
"""List of supported power interfaces."""
return [ibmc_power.IBMCPower]
@property
def supported_vendor_interfaces(self):
"""List of supported vendor interfaces."""
return [ibmc_vendor.IBMCVendor, noop.NoVendor]

@ -0,0 +1,237 @@
# Copyright 2019 HUAWEI, Inc. All Rights Reserved.
# 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.
"""
iBMC Management Interface
"""
from oslo_log import log
from oslo_utils import importutils
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.ibmc import mappings
from ironic.drivers.modules.ibmc import utils
constants = importutils.try_import('ibmc_client.constants')
ibmc_client = importutils.try_import('ibmc_client')
LOG = log.getLogger(__name__)
class IBMCManagement(base.ManagementInterface):
def __init__(self):
"""Initialize the iBMC management interface
:raises: DriverLoadError if the driver can't be loaded due to
missing dependencies
"""
super(IBMCManagement, self).__init__()
if not ibmc_client:
raise exception.DriverLoadError(
driver='ibmc',
reason=_('Unable to import the python-ibmcclient library'))
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
return utils.COMMON_PROPERTIES.copy()
def validate(self, task):
"""Validates the driver information needed by the iBMC driver.
:param task: A TaskManager instance containing the node to act on.
:raises: InvalidParameterValue on malformed parameter(s)
:raises: MissingParameterValue on missing parameter(s)
"""
utils.parse_driver_info(task.node)
@utils.handle_ibmc_exception('get iBMC supported boot devices')
def get_supported_boot_devices(self, task):
"""Get a list of the supported boot devices.
:param task: a task from TaskManager.
:raises: InvalidParameterValue on malformed parameter(s)
:raises: MissingParameterValue on missing parameter(s)
:raises: IBMCConnectionError when it fails to connect to iBMC
:raises: IBMCError when iBMC responses an error information
:returns: A list with the supported boot devices defined
in :mod:`ironic.common.boot_devices`.
"""
ibmc = utils.parse_driver_info(task.node)
with ibmc_client.connect(**ibmc) as conn:
system = conn.system.get()
boot_source_override = system.boot_source_override
return list(map(mappings.GET_BOOT_DEVICE_MAP.get,
boot_source_override.supported_boot_devices))
@task_manager.require_exclusive_lock
@utils.handle_ibmc_exception('set iBMC boot device')
def set_boot_device(self, task, device, persistent=False):
"""Set the boot device for a node.
:param task: A task from TaskManager.
:param device: The boot device, one of
:mod:`ironic.common.boot_device`.
: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: IBMCConnectionError when it fails to connect to iBMC
:raises: IBMCError when iBMC responses an error information
"""
ibmc = utils.parse_driver_info(task.node)
with ibmc_client.connect(**ibmc) as conn:
boot_device = mappings.SET_BOOT_DEVICE_MAP[device]
enabled = mappings.SET_BOOT_DEVICE_PERSISTENT_MAP[persistent]
conn.system.set_boot_source(boot_device, enabled=enabled)
@utils.handle_ibmc_exception('get iBMC boot device')
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: IBMCConnectionError when it fails to connect to iBMC
:raises: IBMCError when iBMC responses an error information
: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 disabled.
"""
ibmc = utils.parse_driver_info(task.node)
with ibmc_client.connect(**ibmc) as conn:
system = conn.system.get()
boot_source_override = system.boot_source_override
boot_device = boot_source_override.target
enabled = boot_source_override.enabled
return {
'boot_device': mappings.GET_BOOT_DEVICE_MAP.get(boot_device),
'persistent':
mappings.GET_BOOT_DEVICE_PERSISTENT_MAP.get(enabled)
}
def get_supported_boot_modes(self, task):
"""Get a list of the supported boot modes.
:param task: A task from TaskManager.
:returns: A list with the supported boot modes defined
in :mod:`ironic.common.boot_modes`. If boot
mode support can't be determined, empty list
is returned.
"""
return list(mappings.SET_BOOT_MODE_MAP)
@task_manager.require_exclusive_lock
@utils.handle_ibmc_exception('set iBMC boot mode')
def set_boot_mode(self, task, mode):
"""Set the boot mode for a node.
Set the boot mode to use on next reboot of the node.
:param task: A task from TaskManager.
:param mode: The boot mode, one of
:mod:`ironic.common.boot_modes`.
:raises: InvalidParameterValue on malformed parameter(s)
:raises: MissingParameterValue on missing parameter(s)
:raises: IBMCConnectionError when it fails to connect to iBMC
:raises: IBMCError when iBMC responses an error information
"""
ibmc = utils.parse_driver_info(task.node)
with ibmc_client.connect(**ibmc) as conn:
system = conn.system.get()
boot_source_override = system.boot_source_override
boot_device = boot_source_override.target
boot_override = boot_source_override.enabled
# Copied from redfish driver
# TODO(Qianbiao.NG) what if boot device is "NONE"?
if not boot_device:
error_msg = (_('Cannot change boot mode on node %(node)s '
'because its boot device is not set.') %
{'node': task.node.uuid})
LOG.error(error_msg)
raise exception.IBMCError(error_msg)
# TODO(Qianbiao.NG) what if boot override is "disabled"?
if not boot_override:
i18n = _('Cannot change boot mode on node %(node)s '
'because its boot source override is not set.')
error_msg = i18n % {'node': task.node.uuid}
LOG.error(error_msg)
raise exception.IBMCError(error_msg)
boot_mode = mappings.SET_BOOT_MODE_MAP[mode]
conn.system.set_boot_source(boot_device,
enabled=boot_override,
mode=boot_mode)
@utils.handle_ibmc_exception('get iBMC boot mode')
def get_boot_mode(self, task):
"""Get the current boot mode for a node.
Provides the current boot mode of the node.
:param task: A task from TaskManager.
:raises: InvalidParameterValue on malformed parameter(s)
:raises: MissingParameterValue on missing parameter(s)
:raises: IBMCConnectionError when it fails to connect to iBMC
:raises: IBMCError when iBMC responses an error information
:returns: The boot mode, one of :mod:`ironic.common.boot_mode` or
None if it is unknown.
"""
ibmc = utils.parse_driver_info(task.node)
with ibmc_client.connect(**ibmc) as conn:
system = conn.system.get()
boot_source_override = system.boot_source_override
boot_mode = boot_source_override.mode
return mappings.GET_BOOT_MODE_MAP.get(boot_mode)
def get_sensors_data(self, task):
"""Get sensors data.
Not implemented for this driver.
:raises: NotImplementedError
"""
raise NotImplementedError()
@task_manager.require_exclusive_lock
@utils.handle_ibmc_exception('inject iBMC NMI')
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: IBMCConnectionError when it fails to connect to iBMC
:raises: IBMCError when iBMC responses an error information
"""
ibmc = utils.parse_driver_info(task.node)
with ibmc_client.connect(**ibmc) as conn:
conn.system.reset(constants.RESET_NMI)

@ -0,0 +1,70 @@
#
# 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.
"""
iBMC and Ironic constants mapping
"""
from oslo_utils import importutils
from ironic.common import boot_devices
from ironic.common import boot_modes
from ironic.common import states
from ironic.drivers.modules.ibmc import utils
constants = importutils.try_import('ibmc_client.constants')
if constants:
# Set power state mapping
SET_POWER_STATE_MAP = {
states.POWER_ON: constants.RESET_ON,
states.POWER_OFF: constants.RESET_FORCE_OFF,
states.REBOOT: constants.RESET_FORCE_RESTART,
states.SOFT_REBOOT: constants.RESET_FORCE_POWER_CYCLE,
states.SOFT_POWER_OFF: constants.RESET_GRACEFUL_SHUTDOWN,
}
# Get power state mapping
GET_POWER_STATE_MAP = {
constants.SYSTEM_POWER_STATE_ON: states.POWER_ON,
constants.SYSTEM_POWER_STATE_OFF: states.POWER_OFF,
}
# Boot device mapping
GET_BOOT_DEVICE_MAP = {
constants.BOOT_SOURCE_TARGET_NONE: 'none',
constants.BOOT_SOURCE_TARGET_PXE: boot_devices.PXE,
constants.BOOT_SOURCE_TARGET_FLOPPY: 'floppy',
constants.BOOT_SOURCE_TARGET_CD: boot_devices.CDROM,
constants.BOOT_SOURCE_TARGET_HDD: boot_devices.DISK,
constants.BOOT_SOURCE_TARGET_BIOS_SETUP: boot_devices.BIOS,
}
SET_BOOT_DEVICE_MAP = utils.revert_dictionary(GET_BOOT_DEVICE_MAP)
# Boot mode mapping
GET_BOOT_MODE_MAP = {
constants.BOOT_SOURCE_MODE_BIOS: boot_modes.LEGACY_BIOS,
constants.BOOT_SOURCE_MODE_UEFI: boot_modes.UEFI,
}
SET_BOOT_MODE_MAP = utils.revert_dictionary(GET_BOOT_MODE_MAP)
# Boot device persistent mapping
GET_BOOT_DEVICE_PERSISTENT_MAP = {
constants.BOOT_SOURCE_ENABLED_ONCE: False,
constants.BOOT_SOURCE_ENABLED_CONTINUOUS: True,
constants.BOOT_SOURCE_ENABLED_DISABLED: None,
}
SET_BOOT_DEVICE_PERSISTENT_MAP = utils.revert_dictionary(
GET_BOOT_DEVICE_PERSISTENT_MAP)

@ -0,0 +1,145 @@
#
# 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.
"""
iBMC Power Interface
"""
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.conductor import utils as cond_utils
from ironic.drivers import base
from ironic.drivers.modules.ibmc import mappings
from ironic.drivers.modules.ibmc import utils
constants = importutils.try_import('ibmc_client.constants')
ibmc_client = importutils.try_import('ibmc_client')
LOG = log.getLogger(__name__)
EXPECT_POWER_STATE_MAP = {
states.REBOOT: states.POWER_ON,
states.SOFT_REBOOT: states.POWER_ON,
states.SOFT_POWER_OFF: states.POWER_OFF,
}
class IBMCPower(base.PowerInterface):
def __init__(self):
"""Initialize the iBMC power interface.
:raises: DriverLoadError if the driver can't be loaded due to
missing dependencies
"""
super(IBMCPower, self).__init__()
if not ibmc_client:
raise exception.DriverLoadError(
driver='ibmc',
reason=_('Unable to import the python-ibmcclient library'))
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
return utils.COMMON_PROPERTIES.copy()
def validate(self, task):
"""Validates the driver information needed by the iBMC driver.
:param task: A TaskManager instance containing the node to act on.
:raises: InvalidParameterValue on malformed parameter(s)
:raises: MissingParameterValue on missing parameter(s)
"""
utils.parse_driver_info(task.node)
@utils.handle_ibmc_exception('get iBMC power state')
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: IBMCConnectionError when it fails to connect to iBMC
:raises: IBMCError when iBMC responses an error information
"""
ibmc = utils.parse_driver_info(task.node)
with ibmc_client.connect(**ibmc) as conn:
system = conn.system.get()
return mappings.GET_POWER_STATE_MAP.get(system.power_state)
@task_manager.require_exclusive_lock
@utils.handle_ibmc_exception('set iBMC power state')
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: Time to wait for the node to reach the requested state.
:raises: InvalidParameterValue on malformed parameter(s)
:raises: MissingParameterValue if a required parameter is missing.
:raises: IBMCConnectionError when it fails to connect to iBMC
:raises: IBMCError when iBMC responses an error information
"""
ibmc = utils.parse_driver_info(task.node)
with ibmc_client.connect(**ibmc) as conn:
reset_type = mappings.SET_POWER_STATE_MAP.get(power_state)
conn.system.reset(reset_type)
target_state = EXPECT_POWER_STATE_MAP.get(power_state, power_state)
cond_utils.node_wait_for_power_state(task, target_state,
timeout=timeout)
@task_manager.require_exclusive_lock
@utils.handle_ibmc_exception('reboot iBMC')
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: Time to wait for the node to become powered on.
:raises: InvalidParameterValue on malformed parameter(s)
:raises: MissingParameterValue if a required parameter is missing.
:raises: IBMCConnectionError when it fails to connect to iBMC
:raises: IBMCError when iBMC responses an error information
"""
ibmc = utils.parse_driver_info(task.node)
with ibmc_client.connect(**ibmc) as conn:
system = conn.system.get()
current_power_state = (
mappings.GET_POWER_STATE_MAP.get(system.power_state)
)
if current_power_state == states.POWER_ON:
conn.system.reset(
mappings.SET_POWER_STATE_MAP.get(states.REBOOT))
else:
conn.system.reset(
mappings.SET_POWER_STATE_MAP.get(states.POWER_ON))
cond_utils.node_wait_for_power_state(task, states.POWER_ON,
timeout=timeout)
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(mappings.SET_POWER_STATE_MAP)

@ -0,0 +1,177 @@
#
# 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.
"""
iBMC Driver common utils
"""
import os
from oslo_log import log
from oslo_utils import importutils
from oslo_utils import netutils
from oslo_utils import strutils
import retrying
import six
from ironic.common import exception
from ironic.common.i18n import _
from ironic.conductor import task_manager
from ironic.conf import CONF
ibmc_client = importutils.try_import('ibmcclient')
ibmc_error = importutils.try_import('ibmc_client.exceptions')
LOG = log.getLogger(__name__)
REQUIRED_PROPERTIES = {
'ibmc_address': _('The URL address to the iBMC controller. It must '
'include the authority portion of the URL. '
'If the scheme is missing, https is assumed. '
'For example: https://mgmt.vendor.com. Required.'),
'ibmc_username': _('User account with admin/server-profile access '
'privilege. Required.'),
'ibmc_password': _('User account password. Required.'),
}
OPTIONAL_PROPERTIES = {
'ibmc_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 iBMC.
: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 iBMC properties in node '
'%(node)s driver_info: %(info)s') % {'node': node.uuid,
'info': missing_info})
# Validate the iBMC address
address = driver_info['ibmc_address']
parsed = netutils.urlsplit(address)
if parsed.scheme == '':
address = 'https://%s' % address
parsed = netutils.urlsplit(address)
if parsed.netloc == '':
raise exception.InvalidParameterValue(
_('Invalid iBMC address %(address)s set in '
'driver_info/ibmc_address on node %(node)s') %
{'address': address, 'node': node.uuid})
# Check if verify_ca is a Boolean or a file/directory in the file-system
verify_ca = driver_info.get('ibmc_verify_ca', True)
if isinstance(verify_ca, six.string_types):
if os.path.isdir(verify_ca) or os.path.isfile(verify_ca):
pass
else:
try:
verify_ca = strutils.bool_from_string(verify_ca, strict=True)
except ValueError:
raise exception.InvalidParameterValue(
_('Invalid value type set in driver_info/'
'ibmc_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})
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/ibmc_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,
'username': driver_info.get('ibmc_username'),
'password': driver_info.get('ibmc_password'),
'verify_ca': verify_ca}
def revert_dictionary(d):
return {v: k for k, v in d.items()}
def handle_ibmc_exception(action):
"""Decorator to handle iBMC client exception.
Decorated functions must take a :class:`TaskManager` as the first
parameter.
"""
def decorator(f):
def should_retry(e):
connect_error = isinstance(e, exception.IBMCConnectionError)
if connect_error:
LOG.info(_('Failed to connect to iBMC, will retry now. '
'Max retry times is %(retry_times)d.'),
{'retry_times': CONF.ibmc.connection_attempts})
return connect_error
@retrying.retry(
retry_on_exception=should_retry,
stop_max_attempt_number=CONF.ibmc.connection_attempts,
wait_fixed=CONF.ibmc.connection_retry_interval * 1000)
@six.wraps(f)
def wrapper(*args, **kwargs):
# NOTE(dtantsur): this code could be written simpler, but then unit
# testing decorated functions is pretty hard, as we usually pass a
# Mock object instead of TaskManager there.
if len(args) > 1:
is_task_mgr = isinstance(args[1], task_manager.TaskManager)
task = args[1] if is_task_mgr else args[0]
else:
task = args[0]
node = task.node
try:
return f(*args, **kwargs)
except ibmc_error.ConnectionError as e:
error = (_('Failed to connect to iBMC for node %(node)s, '
'Error: %(error)s')
% {'node': node.uuid, 'error': e})
LOG.error(error)
raise exception.IBMCConnectionError(node=node.uuid,
error=error)
except ibmc_error.IBMCClientError as e:
error = (_('Failed to %(action)s for node %(node)s, '
'Error %(error)s')
% {'node': node.uuid, 'action': action, 'error': e})
LOG.error(error)
raise exception.IBMCError(node=node.uuid, error=error)
return wrapper
return decorator

@ -0,0 +1,87 @@
#
# 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.
"""
iBMC Vendor Interface
"""
from oslo_log import log
from oslo_utils import importutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.drivers import base
from ironic.drivers.modules.ibmc import utils
ibmc_client = importutils.try_import('ibmc_client')
LOG = log.getLogger(__name__)
class IBMCVendor(base.VendorInterface):
def __init__(self):
"""Initialize the iBMC vendor interface.
:raises: DriverLoadError if the driver can't be loaded due to
missing dependencies
"""
super(IBMCVendor, self).__init__()
if not ibmc_client:
raise exception.DriverLoadError(
driver='ibmc',
reason=_('Unable to import the python-ibmcclient library'))
def validate(self, task, method=None, **kwargs):
"""Validate vendor-specific actions.
If invalid, raises an exception; otherwise returns None.
:param task: A task from TaskManager.
:param method: Method to be validated
:param kwargs: Info for action.
:raises: UnsupportedDriverExtension if 'method' can not be mapped to
the supported interfaces.
:raises: InvalidParameterValue if kwargs does not contain 'method'.
:raises: MissingParameterValue
"""
utils.parse_driver_info(task.node)
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
return utils.COMMON_PROPERTIES.copy()
@base.passthru(['GET'], async_call=False,
description=_('Returns a dictionary, '
'containing node boot up sequence, '
'in ascending order'))
@utils.handle_ibmc_exception('get iBMC boot up sequence')
def boot_up_seq(self, task, **kwargs):
"""List boot type order of the node.
:param task: A TaskManager instance containing the node to act on.
:param kwargs: Not used.
:raises: InvalidParameterValue if kwargs does not contain 'method'.
:raises: MissingParameterValue
:raises: IBMCConnectionError when it fails to connect to iBMC
:raises: IBMCError when iBMC responses an error information
:returns: A dictionary, containing node boot up sequence,
in ascending order.
"""
driver_info = utils.parse_driver_info(task.node)
with ibmc_client.connect(**driver_info) as conn:
system = conn.system.get()
boot_sequence = system.boot_sequence
return {'boot_up_sequence': boot_sequence}

@ -671,3 +671,12 @@ def create_test_deploy_template(**kw):
if 'id' not in kw_step:
del template_step['id']
return dbapi.create_deploy_template(template)
def get_test_ibmc_info():
return {
"ibmc_address": "https://example.com",
"ibmc_username": "username",
"ibmc_password": "password",
"verify_ca": False,
}

@ -0,0 +1,42 @@
#
# 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.
"""Test base class for iBMC Driver."""
import mock
from ironic.drivers.modules.ibmc import 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
class IBMCTestCase(db_base.DbTestCase):
def setUp(self):
super(IBMCTestCase, self).setUp()
self.driver_info = db_utils.get_test_ibmc_info()
self.config(enabled_hardware_types=['ibmc'],
enabled_power_interfaces=['ibmc'],
enabled_management_interfaces=['ibmc'],
enabled_vendor_interfaces=['ibmc'])
self.node = obj_utils.create_test_node(
self.context, driver='ibmc', driver_info=self.driver_info)
self.ibmc = utils.parse_driver_info(self.node)
@staticmethod
def mock_ibmc_conn(ibmc_client_connect):
conn = mock.Mock(system=mock.PropertyMock())
conn.__enter__ = mock.Mock(return_value=conn)
conn.__exit__ = mock.Mock(return_value=None)
ibmc_client_connect.return_value = conn
return conn

@ -0,0 +1,276 @@
#
# 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.
"""Test class for iBMC Management interface."""
import itertools
import mock
from oslo_utils import importutils
from ironic.common import boot_devices
from ironic.common import boot_modes
from ironic.common import exception
from ironic.conductor import task_manager
from ironic.drivers.modules.ibmc import mappings
from ironic.drivers.modules.ibmc import utils
from ironic.tests.unit.drivers.modules.ibmc import base
constants = importutils.try_import('ibmc_client.constants')
ibmc_client = importutils.try_import('ibmc_client')
ibmc_error = importutils.try_import('ibmc_client.exceptions')
class IBMCManagementTestCase(base.IBMCTestCase):
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 utils.COMMON_PROPERTIES:
self.assertIn(prop, properties)
@mock.patch.object(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)
@mock.patch.object(ibmc_client, 'connect', autospec=True)
def test_get_supported_boot_devices(self, connect_ibmc):
conn = self.mock_ibmc_conn(connect_ibmc)
# mock return value
_supported_boot_devices = list(mappings.GET_BOOT_DEVICE_MAP)
conn.system.get.return_value = mock.Mock(
boot_source_override=mock.Mock(
supported_boot_devices=_supported_boot_devices
)
)
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
supported_boot_devices = (
task.driver.management.get_supported_boot_devices(task))
connect_ibmc.assert_called_once_with(**self.ibmc)
expect = sorted(list(mappings.GET_BOOT_DEVICE_MAP.values()))
self.assertEqual(expect, sorted(supported_boot_devices))
@mock.patch.object(ibmc_client, 'connect', autospec=True)
def test_set_boot_device(self, connect_ibmc):
conn = self.mock_ibmc_conn(connect_ibmc)
# mock return value
conn.system.set_boot_source.return_value = None
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
device_mapping = [
(boot_devices.PXE, constants.BOOT_SOURCE_TARGET_PXE),
(boot_devices.DISK, constants.BOOT_SOURCE_TARGET_HDD),
(boot_devices.CDROM, constants.BOOT_SOURCE_TARGET_CD),
(boot_devices.BIOS,
constants.BOOT_SOURCE_TARGET_BIOS_SETUP),
('floppy', constants.BOOT_SOURCE_TARGET_FLOPPY),
]
persistent_mapping = [
(True, constants.BOOT_SOURCE_ENABLED_CONTINUOUS),
(False, constants.BOOT_SOURCE_ENABLED_ONCE)
]
data_source = list(itertools.product(device_mapping,
persistent_mapping))
for (device, persistent) in data_source:
task.driver.management.set_boot_device(
task, device[0], persistent=persistent[0])
connect_ibmc.assert_called_once_with(**self.ibmc)
conn.system.set_boot_source.assert_called_once_with(
device[1],
enabled=persistent[1])
# Reset mocks
connect_ibmc.reset_mock()
conn.system.set_boot_source.reset_mock()
@mock.patch.object(ibmc_client, 'connect', autospec=True)
def test_set_boot_device_fail(self, connect_ibmc):
conn = self.mock_ibmc_conn(connect_ibmc)
# mock return value
conn.system.set_boot_source.side_effect = (
ibmc_error.IBMCClientError
)
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.assertRaisesRegex(
exception.IBMCError, 'set iBMC boot device',
task.driver.management.set_boot_device, task,
boot_devices.PXE)
connect_ibmc.assert_called_once_with(**self.ibmc)
conn.system.set_boot_source.assert_called_once_with(
constants.BOOT_SOURCE_TARGET_PXE,
enabled=constants.BOOT_SOURCE_ENABLED_ONCE)
@mock.patch.object(ibmc_client, 'connect', autospec=True)
def test_get_boot_device(self, connect_ibmc):
conn = self.mock_ibmc_conn(connect_ibmc)
# mock return value
conn.system.get.return_value = mock.Mock(
boot_source_override=mock.Mock(
target=constants.BOOT_SOURCE_TARGET_PXE,
enabled=constants.BOOT_SOURCE_ENABLED_CONTINUOUS
)
)
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
result_boot_device = task.driver.management.get_boot_device(task)
conn.system.get.assert_called_once()
connect_ibmc.assert_called_once_with(**self.ibmc)
expected = {'boot_device': boot_devices.PXE,
'persistent': True}
self.assertEqual(expected, result_boot_device)
def test_get_supported_boot_modes(self):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
supported_boot_modes = (
task.driver.management.get_supported_boot_modes(task))
self.assertEqual(list(mappings.SET_BOOT_MODE_MAP),
supported_boot_modes)
@mock.patch.object(ibmc_client, 'connect', autospec=True)
def test_set_boot_mode(self, connect_ibmc):
conn = self.mock_ibmc_conn(connect_ibmc)
# mock system boot source override return value
conn.system.get.return_value = mock.Mock(
boot_source_override=mock.Mock(
target=constants.BOOT_SOURCE_TARGET_PXE,
enabled=constants.BOOT_SOURCE_ENABLED_CONTINUOUS
)
)
conn.system.set_boot_source.return_value = None
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
expected_values = [
(boot_modes.LEGACY_BIOS, constants.BOOT_SOURCE_MODE_BIOS),
(boot_modes.UEFI, constants.BOOT_SOURCE_MODE_UEFI)
]
for ironic_boot_mode, ibmc_boot_mode in expected_values:
task.driver.management.set_boot_mode(task,
mode=ironic_boot_mode)
conn.system.get.assert_called_once()
connect_ibmc.assert_called_once_with(**self.ibmc)
conn.system.set_boot_source.assert_called_once_with(
constants.BOOT_SOURCE_TARGET_PXE,
enabled=constants.BOOT_SOURCE_ENABLED_CONTINUOUS,
mode=ibmc_boot_mode)
# Reset
connect_ibmc.reset_mock()
conn.system.set_boot_source.reset_mock()
conn.system.get.reset_mock()
@mock.patch.object(ibmc_client, 'connect', autospec=True)
def test_set_boot_mode_fail(self, connect_ibmc):
conn = self.mock_ibmc_conn(connect_ibmc)
# mock system boot source override return value
conn.system.get.return_value = mock.Mock(
boot_source_override=mock.Mock(
target=constants.BOOT_SOURCE_TARGET_PXE,
enabled=constants.BOOT_SOURCE_ENABLED_CONTINUOUS
)
)
conn.system.set_boot_source.side_effect = (
ibmc_error.IBMCClientError
)
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
expected_values = [
(boot_modes.LEGACY_BIOS, constants.BOOT_SOURCE_MODE_BIOS),
(boot_modes.UEFI, constants.BOOT_SOURCE_MODE_UEFI)
]
for ironic_boot_mode, ibmc_boot_mode in expected_values:
self.assertRaisesRegex(
exception.IBMCError, 'set iBMC boot mode',
task.driver.management.set_boot_mode, task,
ironic_boot_mode)
conn.system.set_boot_source.assert_called_once_with(
constants.BOOT_SOURCE_TARGET_PXE,
enabled=constants.BOOT_SOURCE_ENABLED_CONTINUOUS,
mode=ibmc_boot_mode)
conn.system.get.assert_called_once()
connect_ibmc.assert_called_once_with(**self.ibmc)
# Reset
connect_ibmc.reset_mock()
conn.system.set_boot_source.reset_mock()
conn.system.get.reset_mock()
@mock.patch.object(ibmc_client, 'connect', autospec=True)
def test_get_boot_mode(self, connect_ibmc):
conn = self.mock_ibmc_conn(connect_ibmc)
# mock system boot source override return value
conn.system.get.return_value = mock.Mock(
boot_source_override=mock.Mock(
target=constants.BOOT_SOURCE_TARGET_PXE,
enabled=constants.BOOT_SOURCE_ENABLED_CONTINUOUS,
mode=constants.BOOT_SOURCE_MODE_BIOS,
)
)
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
response = task.driver.management.get_boot_mode(task)
conn.system.get.assert_called_once()
connect_ibmc.assert_called_once_with(**self.ibmc)
expected = boot_modes.LEGACY_BIOS
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(ibmc_client, 'connect', autospec=True)
def test_inject_nmi(self, connect_ibmc):
conn = self.mock_ibmc_conn(connect_ibmc)
# mock system boot source override return value
conn.system.reset.return_value = None
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.management.inject_nmi(task)
connect_ibmc.assert_called_once_with(**self.ibmc)
conn.system.reset.assert_called_once_with(constants.RESET_NMI)
@mock.patch.object(ibmc_client, 'connect', autospec=True)
def test_inject_nmi_fail(self, connect_ibmc):
conn = self.mock_ibmc_conn(connect_ibmc)
# mock system boot source override return value
conn.system.reset.side_effect = (
ibmc_error.IBMCClientError
)
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.assertRaisesRegex(
exception.IBMCError, 'inject iBMC NMI',
task.driver.management.inject_nmi, task)
connect_ibmc.assert_called_once_with(**self.ibmc)
conn.system.reset.assert_called_once_with(constants.RESET_NMI)

@ -0,0 +1,284 @@
#
# 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.
"""Test class for iBMC Power interface."""
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.ibmc import mappings
from ironic.drivers.modules.ibmc import utils
from ironic.tests.unit.drivers.modules.ibmc import base
constants = importutils.try_import('ibmc_client.constants')
ibmc_client = importutils.try_import('ibmc_client')
ibmc_error = importutils.try_import('ibmc_client.exceptions')
@mock.patch('eventlet.greenthread.sleep', lambda _t: None)
class IBMCPowerTestCase(base.IBMCTestCase):
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 utils.COMMON_PROPERTIES:
self.assertIn(prop, properties)
@mock.patch.object(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(ibmc_client, 'connect', autospec=True)
def test_get_power_state(self, connect_ibmc):
conn = self.mock_ibmc_conn(connect_ibmc)
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
expected_values = mappings.GET_POWER_STATE_MAP
for current, expected in expected_values.items():
# Mock
conn.system.get.return_value = mock.Mock(
power_state=current