Merge "Add Huawei iBMC driver support"
This commit is contained in:
commit
f944f60041
|
@ -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
|
||||
|
|
|
@ -825,3 +825,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
|
||||
)
|
||||
|
||||
# Asserts
|
||||
self.assertEqual(expected,
|
||||
task.driver.power.get_power_state(task))
|
||||
|
||||
conn.system.get.assert_called_once()
|
||||
connect_ibmc.assert_called_once_with(**self.ibmc)
|
||||
|
||||
# Reset Mock
|
||||
conn.system.get.reset_mock()
|
||||
connect_ibmc.reset_mock()
|
||||
|
||||
@mock.patch.object(ibmc_client, 'connect', autospec=True)
|
||||
def test_set_power_state(self, connect_ibmc):
|
||||
conn = self.mock_ibmc_conn(connect_ibmc)
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
state_mapping = mappings.SET_POWER_STATE_MAP
|
||||
for (expect_state, reset_type) in state_mapping.items():
|
||||
if expect_state in (states.POWER_OFF, states.SOFT_POWER_OFF):
|
||||
final = constants.SYSTEM_POWER_STATE_OFF
|
||||
transient = constants.SYSTEM_POWER_STATE_ON
|
||||
else:
|
||||
final = constants.SYSTEM_POWER_STATE_ON
|
||||
transient = constants.SYSTEM_POWER_STATE_OFF
|
||||
|
||||
# Mocks
|
||||
mock_system_get_results = (
|
||||
[mock.Mock(power_state=transient)] * 3 +
|
||||
[mock.Mock(power_state=final)])
|
||||
conn.system.get.side_effect = mock_system_get_results
|
||||
|
||||
task.driver.power.set_power_state(task, expect_state)
|
||||
|
||||
# Asserts
|
||||
connect_ibmc.assert_called_with(**self.ibmc)
|
||||
conn.system.reset.assert_called_once_with(reset_type)
|
||||
self.assertEqual(4, conn.system.get.call_count)
|
||||
|
||||
# Reset Mocks
|
||||
# TODO(Qianbiao.NG) why reset_mock does not reset call_count
|
||||
connect_ibmc.reset_mock()
|
||||
conn.system.get.reset_mock()
|
||||
conn.system.reset.reset_mock()
|
||||
|
||||
@mock.patch.object(ibmc_client, 'connect', autospec=True)
|
||||
def test_set_power_state_not_reached(self, connect_ibmc):
|
||||
conn = self.mock_ibmc_conn(connect_ibmc)
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
self.config(power_state_change_timeout=2, group='conductor')
|
||||
|
||||
state_mapping = mappings.SET_POWER_STATE_MAP
|
||||
for (expect_state, reset_type) in state_mapping.items():
|
||||
if expect_state in (states.POWER_OFF, states.SOFT_POWER_OFF):
|
||||
final = constants.SYSTEM_POWER_STATE_OFF
|
||||
transient = constants.SYSTEM_POWER_STATE_ON
|
||||
else:
|
||||
final = constants.SYSTEM_POWER_STATE_ON
|
||||
transient = constants.SYSTEM_POWER_STATE_OFF
|
||||
|
||||
# Mocks
|
||||
mock_system_get_results = (
|
||||
[mock.Mock(power_state=transient)] * 5 +
|
||||
[mock.Mock(power_state=final)])
|
||||
conn.system.get.side_effect = mock_system_get_results
|
||||
|
||||
self.assertRaises(exception.PowerStateFailure,
|
||||
task.driver.power.set_power_state,
|
||||
task, expect_state)
|
||||
|
||||
# Asserts
|
||||
connect_ibmc.assert_called_with(**self.ibmc)
|
||||
conn.system.reset.assert_called_once_with(reset_type)
|
||||
|
||||
# Reset Mocks
|
||||
connect_ibmc.reset_mock()
|
||||
conn.system.get.reset_mock()
|
||||
conn.system.reset.reset_mock()
|
||||
|
||||
@mock.patch.object(ibmc_client, 'connect', autospec=True)
|
||||
def test_set_power_state_fail(self, connect_ibmc):
|
||||
conn = self.mock_ibmc_conn(connect_ibmc)
|
||||
|
||||
# Mocks
|
||||
conn.system.reset.side_effect = (
|
||||
ibmc_error.IBMCClientError
|
||||
)
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
# Asserts
|
||||
self.assertRaisesRegex(
|
||||
exception.IBMCError, 'set iBMC power state',
|
||||
task.driver.power.set_power_state, task, states.POWER_ON)
|
||||
connect_ibmc.assert_called_with(**self.ibmc)
|
||||
conn.system.reset.assert_called_once_with(constants.RESET_ON)
|
||||
|
||||
@mock.patch.object(ibmc_client, 'connect', autospec=True)
|
||||
def test_set_power_state_timeout(self, connect_ibmc):
|
||||
conn = self.mock_ibmc_conn(connect_ibmc)
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
self.config(power_state_change_timeout=2, group='conductor')
|
||||
|
||||
# Mocks
|
||||
conn.system.get.side_effect = (
|
||||
[mock.Mock(power_state=constants.SYSTEM_POWER_STATE_OFF)] * 3
|
||||
)
|
||||
|
||||
# Asserts
|
||||
self.assertRaisesRegex(
|
||||
exception.PowerStateFailure,
|
||||
'Failed to set node power state to power on',
|
||||
task.driver.power.set_power_state, task, states.POWER_ON)
|
||||
|
||||
connect_ibmc.assert_called_with(**self.ibmc)
|
||||
conn.system.reset.assert_called_once_with(constants.RESET_ON)
|
||||
|
||||
@mock.patch.object(ibmc_client, 'connect', autospec=True)
|
||||
def test_reboot(self, connect_ibmc):
|
||||
conn = self.mock_ibmc_conn(connect_ibmc)
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
self.config(power_state_change_timeout=2, group='conductor')
|
||||
expected_values = [
|
||||
(constants.SYSTEM_POWER_STATE_OFF, constants.RESET_ON),
|
||||
(constants.SYSTEM_POWER_STATE_ON,
|
||||
constants.RESET_FORCE_RESTART)
|
||||
]
|
||||
|
||||
# for (expect_state, reset_type) in state_mapping.items():
|
||||
for current, reset_type in expected_values:
|
||||
mock_system_get_results = [
|
||||
# Initial state
|
||||
mock.Mock(power_state=current),
|
||||
# Transient state - powering off
|
||||
mock.Mock(power_state=constants.SYSTEM_POWER_STATE_OFF),
|
||||
# Final state - down powering off
|
||||
mock.Mock(power_state=constants.SYSTEM_POWER_STATE_ON)
|
||||
]
|
||||
conn.system.get.side_effect = mock_system_get_results
|
||||
|
||||
task.driver.power.reboot(task)
|
||||
|
||||
# Asserts
|
||||
connect_ibmc.assert_called_with(**self.ibmc)
|
||||
conn.system.reset.assert_called_once_with(reset_type)
|
||||
|
||||
# Reset Mocks
|
||||
connect_ibmc.reset_mock()
|
||||
conn.system.get.reset_mock()
|
||||
conn.system.reset.reset_mock()
|
||||
|
||||
@mock.patch.object(ibmc_client, 'connect', autospec=True)
|
||||
def test_reboot_not_reached(self, connect_ibmc):
|
||||
conn = self.mock_ibmc_conn(connect_ibmc)
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
self.config(power_state_change_timeout=2, group='conductor')
|
||||
|
||||
# Mocks
|
||||
conn.system.get.return_value = mock.Mock(
|
||||
power_state=constants.SYSTEM_POWER_STATE_OFF)
|
||||
self.assertRaisesRegex(
|
||||
exception.PowerStateFailure,
|
||||
'Failed to set node power state to power on',
|
||||
task.driver.power.reboot, task)
|
||||
|
||||
# Asserts
|
||||
connect_ibmc.assert_called_with(**self.ibmc)
|
||||
conn.system.reset.assert_called_once_with(constants.RESET_ON)
|
||||
|
||||
@mock.patch.object(ibmc_client, 'connect', autospec=True)
|
||||
def test_reboot_fail(self, connect_ibmc):
|
||||
conn = self.mock_ibmc_conn(connect_ibmc)
|
||||
|
||||
# Mocks
|
||||
conn.system.reset.side_effect = (
|
||||
ibmc_error.IBMCClientError
|
||||
)
|
||||
conn.system.get.return_value = mock.Mock(
|
||||
power_state=constants.SYSTEM_POWER_STATE_ON
|
||||
)
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
# Asserts
|
||||
self.assertRaisesRegex(
|
||||
exception.IBMCError, 'reboot iBMC',
|
||||
task.driver.power.reboot, task)
|
||||
connect_ibmc.assert_called_with(**self.ibmc)
|
||||
conn.system.get.assert_called_once()
|
||||
conn.system.reset.assert_called_once_with(
|
||||
constants.RESET_FORCE_RESTART)
|
||||
|
||||
@mock.patch.object(ibmc_client, 'connect', autospec=True)
|
||||
def test_reboot_timeout(self, connect_ibmc):
|
||||
conn = self.mock_ibmc_conn(connect_ibmc)
|
||||
|
||||
# Mocks
|
||||
conn.system.get.side_effect = [mock.Mock(
|
||||
power_state=constants.SYSTEM_POWER_STATE_OFF
|
||||
)] * 5
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=False) as task:
|
||||
self.config(power_state_change_timeout=2, group='conductor')
|
||||
|
||||
# Asserts
|
||||
self.assertRaisesRegex(
|
||||
exception.PowerStateFailure,
|
||||
'Failed to set node power state to power on',
|
||||
task.driver.power.reboot, task)
|
||||
|
||||
# Asserts
|
||||
connect_ibmc.assert_called_with(**self.ibmc)
|
||||
conn.system.reset.assert_called_once_with(
|
||||
constants.RESET_ON)
|
||||
|
||||
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(sorted(list(mappings.SET_POWER_STATE_MAP)),
|
||||
sorted(supported_power_states))
|
|
@ -0,0 +1,172 @@
|
|||
#
|
||||
# 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 Driver common utils."""
|
||||
|
||||
import copy
|
||||
import os
|
||||
|
||||
import mock
|
||||
from oslo_utils import importutils
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.conductor import task_manager
|
||||
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 IBMCUtilsTestCase(base.IBMCTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(IBMCUtilsTestCase, self).setUp()
|
||||
# Redfish specific configurations
|
||||
self.config(connection_attempts=2, group='ibmc')
|
||||
self.parsed_driver_info = {
|
||||
'address': 'https://example.com',
|
||||
'username': 'username',
|
||||
'password': 'password',
|
||||
'verify_ca': True,
|
||||
}
|
||||
|
||||
def test_parse_driver_info(self):
|
||||
response = utils.parse_driver_info(self.node)
|
||||
self.assertEqual(self.parsed_driver_info, response)
|
||||
|
||||
def test_parse_driver_info_default_scheme(self):
|
||||
self.node.driver_info['ibmc_address'] = 'example.com'
|
||||
response = utils.parse_driver_info(self.node)
|
||||
self.assertEqual(self.parsed_driver_info, response)
|
||||
|
||||
def test_parse_driver_info_default_scheme_with_port(self):
|
||||
self.node.driver_info['ibmc_address'] = 'example.com:42'
|
||||
self.parsed_driver_info['address'] = 'https://example.com:42'
|
||||
response = utils.parse_driver_info(self.node)
|
||||
self.assertEqual(self.parsed_driver_info, response)
|
||||
|
||||
def test_parse_driver_info_missing_info(self):
|
||||
for prop in utils.REQUIRED_PROPERTIES:
|
||||
self.node.driver_info = self.driver_info.copy()
|
||||
self.node.driver_info.pop(prop)
|
||||
self.assertRaises(exception.MissingParameterValue,
|
||||
utils.parse_driver_info, self.node)
|
||||
|
||||
def test_parse_driver_info_invalid_address(self):
|
||||
for value in ['/banana!', '#location', '?search=hello']:
|
||||
self.node.driver_info['ibmc_address'] = value
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
'Invalid iBMC address',
|
||||
utils.parse_driver_info, self.node)
|
||||
|
||||
@mock.patch.object(os.path, 'isdir', autospec=True)
|
||||
def test_parse_driver_info_path_verify_ca(self,
|
||||
mock_isdir):
|
||||
mock_isdir.return_value = True
|
||||
fake_path = '/path/to/a/valid/CA'
|
||||
self.node.driver_info['ibmc_verify_ca'] = fake_path
|
||||
self.parsed_driver_info['verify_ca'] = fake_path
|
||||
|
||||
response = utils.parse_driver_info(self.node)
|
||||
self.assertEqual(self.parsed_driver_info, response)
|
||||
mock_isdir.assert_called_once_with(fake_path)
|
||||
|
||||
@mock.patch.object(os.path, 'isfile', autospec=True)
|
||||
def test_parse_driver_info_valid_capath(self, mock_isfile):
|
||||
mock_isfile.return_value = True
|
||||
fake_path = '/path/to/a/valid/CA.pem'
|
||||
self.node.driver_info['ibmc_verify_ca'] = fake_path
|
||||
self.parsed_driver_info['verify_ca'] = fake_path
|
||||
|
||||
response = utils.parse_driver_info(self.node)
|
||||
self.assertEqual(self.parsed_driver_info, response)
|
||||
mock_isfile.assert_called_once_with(fake_path)
|
||||
|
||||
def test_parse_driver_info_invalid_value_verify_ca(self):
|
||||
# Integers are not supported
|
||||
self.node.driver_info['ibmc_verify_ca'] = 123456
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
'Invalid value type',
|
||||
utils.parse_driver_info, self.node)
|
||||
|
||||
def test_parse_driver_info_valid_string_value_verify_ca(self):
|
||||
for value in ('0', 'f', 'false', 'off', 'n', 'no'):
|
||||
self.node.driver_info['ibmc_verify_ca'] = value
|
||||
response = utils.parse_driver_info(self.node)
|
||||
parsed_driver_info = copy.deepcopy(self.parsed_driver_info)
|
||||
parsed_driver_info['verify_ca'] = False
|
||||
self.assertEqual(parsed_driver_info, response)
|
||||
|
||||
for value in ('1', 't', 'true', 'on', 'y', 'yes'):
|
||||
self.node.driver_info['ibmc_verify_ca'] = value
|
||||
response = utils.parse_driver_info(self.node)
|
||||
self.assertEqual(self.parsed_driver_info, response)
|
||||
|
||||
def test_parse_driver_info_invalid_string_value_verify_ca(self):
|
||||
for value in ('xyz', '*', '!123', '123'):
|
||||
self.node.driver_info['ibmc_verify_ca'] = value
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
'The value should be a Boolean',
|
||||
utils.parse_driver_info, self.node)
|
||||
|
||||
def test_revert_dictionary(self):
|
||||
data = {
|
||||
"key1": "value1",
|
||||
"key2": "value2"
|
||||
}
|
||||
|
||||
revert = utils.revert_dictionary(data)
|
||||
self.assertEqual({
|
||||
"value1": "key1",
|
||||
"value2": "key2"
|
||||
}, revert)
|
||||
|
||||
@mock.patch.object(ibmc_client, 'connect', autospec=True)
|
||||
def test_handle_ibmc_exception_retry(self, connect_ibmc):
|
||||
|
||||
@utils.handle_ibmc_exception('get IBMC system')
|
||||
def get_ibmc_system(_task):
|
||||
driver_info = utils.parse_driver_info(_task.node)
|
||||
with ibmc_client.connect(**driver_info) as _conn:
|
||||
return _conn.system.get()
|
||||
|
||||
conn = self.mock_ibmc_conn(connect_ibmc)
|
||||
# Mocks
|
||||
conn.system.get.side_effect = [
|
||||
ibmc_error.ConnectionError(url=self.ibmc['address'],
|
||||
error='Failed to connect to host'),
|
||||
mock.PropertyMock(
|
||||
boot_source_override=mock.PropertyMock(
|
||||
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:
|
||||
system = get_ibmc_system(task)
|
||||
|
||||
# Asserts
|
||||
self.assertEqual(constants.BOOT_SOURCE_TARGET_PXE,
|
||||
system.boot_source_override.target)
|
||||
self.assertEqual(constants.BOOT_SOURCE_ENABLED_CONTINUOUS,
|
||||
system.boot_source_override.enabled)
|
||||
|
||||
# 1 failed, 1 succeed
|
||||
connect_ibmc.assert_called_with(**self.ibmc)
|
||||
self.assertEqual(2, connect_ibmc.call_count)
|
||||
|
||||
# 1 failed, 1 succeed
|
||||
self.assertEqual(2, conn.system.get.call_count)
|
|
@ -0,0 +1,60 @@
|
|||
#
|
||||
# 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 vendor interface."""
|
||||
|
||||
import mock
|
||||
from oslo_utils import importutils
|
||||
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.drivers.modules.ibmc import utils
|
||||
from ironic.tests.unit.drivers.modules.ibmc import base
|
||||
|
||||
ibmc_client = importutils.try_import('ibmc_client')
|
||||
|
||||
|
||||
@mock.patch('eventlet.greenthread.sleep', lambda _t: None)
|
||||
class IBMCVendorTestCase(base.IBMCTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(IBMCVendorTestCase, self).setUp()
|
||||
|
||||
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_list_boot_type_order(self, connect_ibmc):
|
||||
# Mocks
|
||||
conn = self.mock_ibmc_conn(connect_ibmc)
|
||||
boot_up_seq = ['Pxe', 'Hdd', 'Others', 'Cd']
|
||||
conn.system.get.return_value = mock.Mock(
|
||||
boot_sequence=['Pxe', 'Hdd', 'Others', 'Cd']
|
||||
)
|
||||
|
||||
expected = {'boot_up_sequence': boot_up_seq}
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
seq = task.driver.vendor.boot_up_seq(task)
|
||||
conn.system.get.assert_called_once()
|
||||
connect_ibmc.assert_called_once_with(**self.ibmc)
|
||||
self.assertEqual(expected, seq)
|
|
@ -0,0 +1,47 @@
|
|||
#
|
||||
# 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 ironic.conductor import task_manager
|
||||
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 iscsi_deploy
|
||||
from ironic.drivers.modules import noop
|
||||
from ironic.drivers.modules import pxe
|
||||
from ironic.tests.unit.db import base as db_base
|
||||
from ironic.tests.unit.objects import utils as obj_utils
|
||||
|
||||
|
||||
class IBMCHardwareTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(IBMCHardwareTestCase, self).setUp()
|
||||
self.config(enabled_hardware_types=['ibmc'],
|
||||
enabled_power_interfaces=['ibmc'],
|
||||
enabled_management_interfaces=['ibmc'],
|
||||
enabled_vendor_interfaces=['ibmc'])
|
||||
|
||||
def test_default_interfaces(self):
|
||||
node = obj_utils.create_test_node(self.context, driver='ibmc')
|
||||
with task_manager.acquire(self.context, node.id) as task:
|
||||
self.assertIsInstance(task.driver.management,
|
||||
ibmc_mgmt.IBMCManagement)
|
||||
self.assertIsInstance(task.driver.power,
|
||||
ibmc_power.IBMCPower)
|
||||
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)
|
||||
self.assertIsInstance(task.driver.vendor, ibmc_vendor.IBMCVendor)
|
|
@ -170,3 +170,10 @@ XCLARITY_STATES_SPEC = (
|
|||
'STATE_POWER_OFF',
|
||||
'STATE_POWER_ON',
|
||||
)
|
||||
|
||||
# python-ibmcclient
|
||||
IBMCCLIENT_SPEC = (
|
||||
'connect',
|
||||
'exceptions',
|
||||
'constants',
|
||||
)
|
||||
|
|
|
@ -26,6 +26,7 @@ Current list of mocked libraries:
|
|||
- pysnmp
|
||||
- scciclient
|
||||
- python-dracclient
|
||||
- python-ibmcclient
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
@ -263,3 +264,45 @@ if not xclarity_client:
|
|||
xclarity_client.exceptions.XClarityException = type('XClarityException',
|
||||
(Exception,), {})
|
||||
sys.modules['xclarity_client.models'] = xclarity_client.models
|
||||
|
||||
|
||||
# python-ibmcclient mocks for HUAWEI rack server driver
|
||||
ibmc_client = importutils.try_import('ibmc_client')
|
||||
if not ibmc_client:
|
||||
ibmc_client = mock.MagicMock(spec_set=mock_specs.IBMCCLIENT_SPEC)
|
||||
sys.modules['ibmc_client'] = ibmc_client
|
||||
|
||||
# Mock iBMC client exceptions
|
||||
exceptions = mock.MagicMock()
|
||||
exceptions.ConnectionError = (
|
||||
type('ConnectionError', (MockKwargsException,), {}))
|
||||
exceptions.IBMCClientError = (
|
||||
type('IBMCClientError', (MockKwargsException,), {}))
|
||||
sys.modules['ibmc_client.exceptions'] = exceptions
|
||||
|
||||
# Mock iIBMC client constants
|
||||
constants = mock.MagicMock(
|
||||
SYSTEM_POWER_STATE_ON='On',
|
||||
SYSTEM_POWER_STATE_OFF='Off',
|
||||
BOOT_SOURCE_TARGET_NONE='None',
|
||||
BOOT_SOURCE_TARGET_PXE='Pxe',
|
||||
BOOT_SOURCE_TARGET_FLOPPY='Floppy',
|
||||
BOOT_SOURCE_TARGET_CD='Cd',
|
||||
BOOT_SOURCE_TARGET_HDD='Hdd',
|
||||
BOOT_SOURCE_TARGET_BIOS_SETUP='BiosSetup',
|
||||
BOOT_SOURCE_MODE_BIOS='Legacy',
|
||||
BOOT_SOURCE_MODE_UEFI='UEFI',
|
||||
BOOT_SOURCE_ENABLED_ONCE='Once',
|
||||
BOOT_SOURCE_ENABLED_CONTINUOUS='Continuous',
|
||||
BOOT_SOURCE_ENABLED_DISABLED='Disabled',
|
||||
RESET_NMI='Nmi',
|
||||
RESET_ON='On',
|
||||
RESET_FORCE_OFF='ForceOff',
|
||||
RESET_GRACEFUL_SHUTDOWN='GracefulShutdown',
|
||||
RESET_FORCE_RESTART='ForceRestart',
|
||||
RESET_FORCE_POWER_CYCLE='ForcePowerCycle')
|
||||
sys.modules['ibmc_client.constants'] = constants
|
||||
|
||||
if 'ironic.drivers.modules.ibmc' in sys.modules:
|
||||
six.moves.reload_module(
|
||||
sys.modules['ironic.drivers.modules.ibmc'])
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Adds a new hardware type ``ibmc`` for HUAWEI 2288H V5, CH121 V5 series
|
||||
servers. This hardware type supports PXE based boot using HUAWEI iBMC
|
||||
RESTful APIs. The following driver interfaces are supported:
|
||||
|
||||
* management: ``ibmc``
|
||||
* power: ``ibmc``
|
||||
* vendor: ``ibmc``
|
|
@ -95,6 +95,7 @@ ironic.hardware.interfaces.inspect =
|
|||
ironic.hardware.interfaces.management =
|
||||
cimc = ironic.drivers.modules.cimc.management:CIMCManagement
|
||||
fake = ironic.drivers.modules.fake:FakeManagement
|
||||
ibmc = ironic.drivers.modules.ibmc.management:IBMCManagement
|
||||
idrac = ironic.drivers.modules.drac.management:DracManagement
|
||||
ilo = ironic.drivers.modules.ilo.management:IloManagement
|
||||
ipmitool = ironic.drivers.modules.ipmitool:IPMIManagement
|
||||
|
@ -112,6 +113,7 @@ ironic.hardware.interfaces.network =
|
|||
ironic.hardware.interfaces.power =
|
||||
cimc = ironic.drivers.modules.cimc.power:Power
|
||||
fake = ironic.drivers.modules.fake:FakePower
|
||||
ibmc = ironic.drivers.modules.ibmc.power:IBMCPower
|
||||
idrac = ironic.drivers.modules.drac.power:DracPower
|
||||
ilo = ironic.drivers.modules.ilo.power:IloPower
|
||||
ipmitool = ironic.drivers.modules.ipmitool:IPMIPower
|
||||
|
@ -142,6 +144,7 @@ ironic.hardware.interfaces.storage =
|
|||
|
||||
ironic.hardware.interfaces.vendor =
|
||||
fake = ironic.drivers.modules.fake:FakeVendorB
|
||||
ibmc = ironic.drivers.modules.ibmc.vendor:IBMCVendor
|
||||
idrac = ironic.drivers.modules.drac.vendor_passthru:DracVendorPassthru
|
||||
ilo = ironic.drivers.modules.ilo.vendor:VendorPassthru
|
||||
ipmitool = ironic.drivers.modules.ipmitool:VendorPassthru
|
||||
|
@ -151,6 +154,7 @@ ironic.hardware.types =
|
|||
cisco-ucs-managed = ironic.drivers.cisco_ucs:CiscoUCSManaged
|
||||
cisco-ucs-standalone = ironic.drivers.cisco_ucs:CiscoUCSStandalone
|
||||
fake-hardware = ironic.drivers.fake_hardware:FakeHardware
|
||||
ibmc = ironic.drivers.ibmc:IBMCHardware
|
||||
idrac = ironic.drivers.drac:IDRACHardware
|
||||
ilo = ironic.drivers.ilo:IloHardware
|
||||
ilo5 = ironic.drivers.ilo:Ilo5Hardware
|
||||
|
|
Loading…
Reference in New Issue