summaryrefslogtreecommitdiff
path: root/ironic
diff options
context:
space:
mode:
authorSam Betts <sam@code-smash.net>2015-09-01 12:32:23 +0100
committerSam Betts <sam@code-smash.net>2015-09-21 18:29:43 +0100
commit363c9c38df001ba0d9499c132c13ef19b7080a9f (patch)
treef3032a135f55ccc777577172b12e4eb497a3a007 /ironic
parent4b5d69ffcf58cd8e044e2738b05791034da2e34a (diff)
downloadironic-363c9c38df001ba0d9499c132c13ef19b7080a9f.tar.gz
Add Cisco IMC PXE Driver
Current drivers only allow for control of UCS servers via either IPMI or UCSM, the Cisco UCS C-Series operating in standalone mode can also be controlled via CIMC using its http/s XML API. This provides finer control over the server than IPMI can, and doesn't require the extra infrastructure that UCSM needs. Change-Id: Ibd39040e3d7e82a87960d33150750433beb2453b Implements: blueprint cisco-imc-pxe-driver
Diffstat (limited to 'ironic')
-rw-r--r--ironic/common/exception.py4
-rw-r--r--ironic/drivers/agent.py25
-rw-r--r--ironic/drivers/fake.py15
-rw-r--r--ironic/drivers/modules/cimc/__init__.py0
-rw-r--r--ironic/drivers/modules/cimc/common.py87
-rw-r--r--ironic/drivers/modules/cimc/management.py166
-rw-r--r--ironic/drivers/modules/cimc/power.py184
-rw-r--r--ironic/drivers/pxe.py24
-rw-r--r--ironic/tests/db/utils.py8
-rw-r--r--ironic/tests/drivers/cimc/__init__.py0
-rw-r--r--ironic/tests/drivers/cimc/test_common.py125
-rw-r--r--ironic/tests/drivers/cimc/test_management.py126
-rw-r--r--ironic/tests/drivers/cimc/test_power.py302
-rw-r--r--ironic/tests/drivers/third_party_driver_mocks.py9
14 files changed, 1075 insertions, 0 deletions
diff --git a/ironic/common/exception.py b/ironic/common/exception.py
index afa140dda..3eb83cbaa 100644
--- a/ironic/common/exception.py
+++ b/ironic/common/exception.py
@@ -590,3 +590,7 @@ class WolOperationError(IronicException):
class ImageUploadFailed(IronicException):
message = _("Failed to upload %(image_name)s image to web server "
"%(web_server)s, reason: %(reason)s")
+
+
+class CIMCException(IronicException):
+ message = _("Cisco IMC exception occured for node %(node)s: %(error)s")
diff --git a/ironic/drivers/agent.py b/ironic/drivers/agent.py
index 088c2e118..34446ed6f 100644
--- a/ironic/drivers/agent.py
+++ b/ironic/drivers/agent.py
@@ -18,6 +18,8 @@ from ironic.common import exception
from ironic.common.i18n import _
from ironic.drivers import base
from ironic.drivers.modules import agent
+from ironic.drivers.modules.cimc import management as cimc_mgmt
+from ironic.drivers.modules.cimc import power as cimc_power
from ironic.drivers.modules import ipminative
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules import pxe
@@ -157,3 +159,26 @@ class AgentAndUcsDriver(base.BaseDriver):
self.deploy = agent.AgentDeploy()
self.management = ucs_mgmt.UcsManagement()
self.vendor = agent.AgentVendorInterface()
+
+
+class AgentAndCIMCDriver(base.BaseDriver):
+ """Agent + Cisco CIMC driver.
+
+ This driver implements the `core` functionality, combining
+ :class:ironic.drivers.modules.cimc.power.Power for power
+ on/off and reboot with
+ :class:'ironic.driver.modules.agent.AgentDeploy' (for image deployment.)
+ Implementations are in those respective classes;
+ this class is merely the glue between them.
+ """
+
+ def __init__(self):
+ if not importutils.try_import('ImcSdk'):
+ raise exception.DriverLoadError(
+ driver=self.__class__.__name__,
+ reason=_("Unable to import ImcSdk library"))
+ self.power = cimc_power.Power()
+ self.boot = pxe.PXEBoot()
+ self.deploy = agent.AgentDeploy()
+ self.management = cimc_mgmt.CIMCManagement()
+ self.vendor = agent.AgentVendorInterface()
diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py
index da4644935..b13914896 100644
--- a/ironic/drivers/fake.py
+++ b/ironic/drivers/fake.py
@@ -25,6 +25,8 @@ from ironic.drivers import base
from ironic.drivers.modules import agent
from ironic.drivers.modules.amt import management as amt_mgmt
from ironic.drivers.modules.amt import power as amt_power
+from ironic.drivers.modules.cimc import management as cimc_mgmt
+from ironic.drivers.modules.cimc import power as cimc_power
from ironic.drivers.modules.drac import management as drac_mgmt
from ironic.drivers.modules.drac import power as drac_power
from ironic.drivers.modules import fake
@@ -270,6 +272,19 @@ class FakeUcsDriver(base.BaseDriver):
self.management = ucs_mgmt.UcsManagement()
+class FakeCIMCDriver(base.BaseDriver):
+ """Fake CIMC driver."""
+
+ def __init__(self):
+ if not importutils.try_import('ImcSdk'):
+ raise exception.DriverLoadError(
+ driver=self.__class__.__name__,
+ reason=_("Unable to import ImcSdk library"))
+ self.power = cimc_power.Power()
+ self.deploy = fake.FakeDeploy()
+ self.management = cimc_mgmt.CIMCManagement()
+
+
class FakeWakeOnLanDriver(base.BaseDriver):
"""Fake Wake-On-Lan driver."""
diff --git a/ironic/drivers/modules/cimc/__init__.py b/ironic/drivers/modules/cimc/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ironic/drivers/modules/cimc/__init__.py
diff --git a/ironic/drivers/modules/cimc/common.py b/ironic/drivers/modules/cimc/common.py
new file mode 100644
index 000000000..1340477d4
--- /dev/null
+++ b/ironic/drivers/modules/cimc/common.py
@@ -0,0 +1,87 @@
+# Copyright 2015, Cisco Systems.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from contextlib import contextmanager
+
+from oslo_log import log as logging
+from oslo_utils import importutils
+
+from ironic.common import exception
+from ironic.drivers.modules import deploy_utils
+
+REQUIRED_PROPERTIES = {
+ 'cimc_address': _('IP or Hostname of the CIMC. Required.'),
+ 'cimc_username': _('CIMC Manager admin username. Required.'),
+ 'cimc_password': _('CIMC Manager password. Required.'),
+}
+
+COMMON_PROPERTIES = REQUIRED_PROPERTIES
+
+imcsdk = importutils.try_import('ImcSdk')
+
+LOG = logging.getLogger(__name__)
+
+
+def parse_driver_info(node):
+ """Parses and creates Cisco driver info
+
+ :param node: An Ironic node object.
+ :returns: dictionary that contains node.driver_info parameter/values.
+ :raises: MissingParameterValue if any required parameters are missing.
+ """
+
+ info = {}
+ for param in REQUIRED_PROPERTIES:
+ info[param] = node.driver_info.get(param)
+ error_msg = (_("%s driver requires these parameters to be set in the "
+ "node's driver_info.") %
+ node.driver)
+ deploy_utils.check_for_missing_params(info, error_msg)
+ return info
+
+
+def handle_login(task, handle, info):
+ """Login to the CIMC handle.
+
+ Run login on the CIMC handle, catching any ImcException and reraising
+ it as an ironic CIMCException.
+
+ :param handle: A CIMC handle.
+ :param info: A list of driver info as produced by parse_driver_info.
+ :raises: CIMCException if there error logging in.
+ """
+ try:
+ handle.login(info['cimc_address'],
+ info['cimc_username'],
+ info['cimc_password'])
+ except imcsdk.ImcException as e:
+ raise exception.CIMCException(node=task.node.uuid, error=e)
+
+
+@contextmanager
+def cimc_handle(task):
+ """Context manager for creating a CIMC handle and logging into it
+
+ :param task: The current task object.
+ :raises: CIMCException if login fails
+ :yields: A CIMC Handle for the node in the task.
+ """
+ info = parse_driver_info(task.node)
+ handle = imcsdk.ImcHandle()
+
+ handle_login(task, handle, info)
+ try:
+ yield handle
+ finally:
+ handle.logout()
diff --git a/ironic/drivers/modules/cimc/management.py b/ironic/drivers/modules/cimc/management.py
new file mode 100644
index 000000000..fc8dfcee8
--- /dev/null
+++ b/ironic/drivers/modules/cimc/management.py
@@ -0,0 +1,166 @@
+# Copyright 2015, Cisco Systems.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from oslo_log import log as logging
+from oslo_utils import importutils
+
+from ironic.common import boot_devices
+from ironic.common import exception
+from ironic.drivers import base
+from ironic.drivers.modules.cimc import common
+
+imcsdk = importutils.try_import('ImcSdk')
+
+LOG = logging.getLogger(__name__)
+
+CIMC_TO_IRONIC_BOOT_DEVICE = {
+ 'storage-read-write': boot_devices.DISK,
+ 'lan-read-only': boot_devices.PXE,
+ 'vm-read-only': boot_devices.CDROM
+}
+
+IRONIC_TO_CIMC_BOOT_DEVICE = {
+ boot_devices.DISK: ('lsbootStorage', 'storage-read-write',
+ 'storage', 'read-write'),
+ boot_devices.PXE: ('lsbootLan', 'lan-read-only',
+ 'lan', 'read-only'),
+ boot_devices.CDROM: ('lsbootVirtualMedia', 'vm-read-only',
+ 'virtual-media', 'read-only')
+}
+
+
+class CIMCManagement(base.ManagementInterface):
+
+ def get_properties(self):
+ """Return the properties of the interface.
+
+ :returns: dictionary of <property name>:<property description> entries.
+ """
+ return common.COMMON_PROPERTIES
+
+ def validate(self, task):
+ """Check if node.driver_info contains the required CIMC credentials.
+
+ :param task: a TaskManager instance.
+ :raises: InvalidParameterValue if required CIMC credentials are
+ missing.
+ """
+ common.parse_driver_info(task.node)
+
+ def get_supported_boot_devices(self, task):
+ """Get a list of the supported boot devices.
+
+ :param task: a task from TaskManager.
+ :returns: A list with the supported boot devices defined
+ in :mod:`ironic.common.boot_devices`.
+ """
+ return list(CIMC_TO_IRONIC_BOOT_DEVICE.values())
+
+ def get_boot_device(self, task):
+ """Get the current boot device for a node.
+
+ Provides the current boot device of the node. Be aware that not
+ all drivers support this.
+
+ :param task: a task from TaskManager.
+ :raises: MissingParameterValue if a required parameter is missing
+ :raises: CIMCException if there is an error from CIMC
+ :returns: a dictionary containing:
+
+ :boot_device:
+ the boot device, one of :mod:`ironic.common.boot_devices` or
+ None if it is unknown.
+ :persistent:
+ Whether the boot device will persist to all future boots or
+ not, None if it is unknown.
+ """
+
+ with common.cimc_handle(task) as handle:
+ method = imcsdk.ImcCore.ExternalMethod("ConfigResolveClass")
+ method.Cookie = handle.cookie
+ method.InDn = "sys/rack-unit-1"
+ method.InHierarchical = "true"
+ method.ClassId = "lsbootDef"
+
+ try:
+ resp = handle.xml_query(method, imcsdk.WriteXmlOption.DIRTY)
+ except imcsdk.ImcException as e:
+ raise exception.CIMCException(node=task.node.uuid, error=e)
+ error = getattr(resp, 'error_code', None)
+ if error:
+ raise exception.CIMCException(node=task.node.uuid, error=error)
+
+ bootDevs = resp.OutConfigs.child[0].child
+
+ first_device = None
+ for dev in bootDevs:
+ try:
+ if int(dev.Order) == 1:
+ first_device = dev
+ break
+ except (ValueError, AttributeError):
+ pass
+
+ boot_device = (CIMC_TO_IRONIC_BOOT_DEVICE.get(
+ first_device.Rn) if first_device else None)
+
+ # Every boot device in CIMC is persistent right now
+ persistent = True if boot_device else None
+ return {'boot_device': boot_device, 'persistent': persistent}
+
+ def set_boot_device(self, task, device, persistent=True):
+ """Set the boot device for a node.
+
+ Set the boot device to use on next reboot of the node.
+
+ :param task: a task from TaskManager.
+ :param device: the boot device, one of
+ :mod:`ironic.common.boot_devices`.
+ :param persistent: Every boot device in CIMC is persistent right now,
+ so this value is ignored.
+ :raises: InvalidParameterValue if an invalid boot device is
+ specified.
+ :raises: MissingParameterValue if a required parameter is missing
+ :raises: CIMCException if there is an error from CIMC
+ """
+
+ with common.cimc_handle(task) as handle:
+ dev = IRONIC_TO_CIMC_BOOT_DEVICE[device]
+
+ method = imcsdk.ImcCore.ExternalMethod("ConfigConfMo")
+ method.Cookie = handle.cookie
+ method.Dn = "sys/rack-unit-1/boot-policy"
+ method.InHierarchical = "true"
+
+ config = imcsdk.Imc.ConfigConfig()
+
+ bootMode = imcsdk.ImcCore.ManagedObject(dev[0])
+ bootMode.set_attr("access", dev[3])
+ bootMode.set_attr("type", dev[2])
+ bootMode.set_attr("Rn", dev[1])
+ bootMode.set_attr("order", "1")
+
+ config.add_child(bootMode)
+ method.InConfig = config
+
+ try:
+ resp = handle.xml_query(method, imcsdk.WriteXmlOption.DIRTY)
+ except imcsdk.ImcException as e:
+ raise exception.CIMCException(node=task.node.uuid, error=e)
+ error = getattr(resp, 'error_code')
+ if error:
+ raise exception.CIMCException(node=task.node.uuid, error=error)
+
+ def get_sensors_data(self, task):
+ raise NotImplementedError()
diff --git a/ironic/drivers/modules/cimc/power.py b/ironic/drivers/modules/cimc/power.py
new file mode 100644
index 000000000..a6a40240a
--- /dev/null
+++ b/ironic/drivers/modules/cimc/power.py
@@ -0,0 +1,184 @@
+# Copyright 2015, Cisco Systems.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_service import loopingcall
+from oslo_utils import importutils
+
+from ironic.common import exception
+from ironic.common.i18n import _
+from ironic.common import states
+from ironic.conductor import task_manager
+from ironic.drivers import base
+from ironic.drivers.modules.cimc import common
+
+imcsdk = importutils.try_import('ImcSdk')
+
+opts = [
+ cfg.IntOpt('max_retry',
+ default=6,
+ help=_('Number of times a power operation needs to be '
+ 'retried')),
+ cfg.IntOpt('action_interval',
+ default=10,
+ help=_('Amount of time in seconds to wait in between power '
+ 'operations')),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(opts, group='cimc')
+
+LOG = logging.getLogger(__name__)
+
+if imcsdk:
+ CIMC_TO_IRONIC_POWER_STATE = {
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON: states.POWER_ON,
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_OFF: states.POWER_OFF,
+ }
+
+ IRONIC_TO_CIMC_POWER_STATE = {
+ states.POWER_ON: imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_UP,
+ states.POWER_OFF: imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_DOWN,
+ states.REBOOT:
+ imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_HARD_RESET_IMMEDIATE
+ }
+
+
+def _wait_for_state_change(target_state, task):
+ """Wait and check for the power state change
+
+ :param target_state: The target state we are waiting for.
+ :param task: a TaskManager instance containing the node to act on.
+ :raises: CIMCException if there is an error communicating with CIMC
+ """
+ store = {'state': None, 'retries': CONF.cimc.max_retry}
+
+ def _wait(store):
+
+ current_power_state = None
+ with common.cimc_handle(task) as handle:
+ try:
+ rack_unit = handle.get_imc_managedobject(
+ None, None, params={"Dn": "sys/rack-unit-1"}
+ )
+ except imcsdk.ImcException as e:
+ raise exception.CIMCException(node=task.node.uuid, error=e)
+ else:
+ current_power_state = rack_unit[0].get_attr("OperPower")
+ store['state'] = CIMC_TO_IRONIC_POWER_STATE.get(current_power_state)
+
+ if store['state'] == target_state:
+ raise loopingcall.LoopingCallDone()
+
+ store['retries'] -= 1
+ if store['retries'] <= 0:
+ store['state'] = states.ERROR
+ raise loopingcall.LoopingCallDone()
+
+ timer = loopingcall.FixedIntervalLoopingCall(_wait, store)
+ timer.start(interval=CONF.cimc.action_interval).wait()
+ return store['state']
+
+
+class Power(base.PowerInterface):
+
+ def get_properties(self):
+ """Return the properties of the interface.
+
+ :returns: dictionary of <property name>:<property description> entries.
+ """
+ return common.COMMON_PROPERTIES
+
+ def validate(self, task):
+ """Check if node.driver_info contains the required CIMC credentials.
+
+ :param task: a TaskManager instance.
+ :raises: InvalidParameterValue if required CIMC credentials are
+ missing.
+ """
+ common.parse_driver_info(task.node)
+
+ def get_power_state(self, task):
+ """Return the power state of the task's node.
+
+ :param task: a TaskManager instance containing the node to act on.
+ :raises: MissingParameterValue if a required parameter is missing.
+ :returns: a power state. One of :mod:`ironic.common.states`.
+ :raises: CIMCException if there is an error communicating with CIMC
+ """
+ current_power_state = None
+ with common.cimc_handle(task) as handle:
+ try:
+ rack_unit = handle.get_imc_managedobject(
+ None, None, params={"Dn": "sys/rack-unit-1"}
+ )
+ except imcsdk.ImcException as e:
+ raise exception.CIMCException(node=task.node.uuid, error=e)
+ else:
+ current_power_state = rack_unit[0].get_attr("OperPower")
+ return CIMC_TO_IRONIC_POWER_STATE.get(current_power_state,
+ states.ERROR)
+
+ @task_manager.require_exclusive_lock
+ def set_power_state(self, task, pstate):
+ """Set the power state of the task's node.
+
+ :param task: a TaskManager instance containing the node to act on.
+ :param pstate: Any power state from :mod:`ironic.common.states`.
+ :raises: MissingParameterValue if a required parameter is missing.
+ :raises: InvalidParameterValue if an invalid power state is passed
+ :raises: CIMCException if there is an error communicating with CIMC
+ """
+ if pstate not in IRONIC_TO_CIMC_POWER_STATE:
+ msg = _("set_power_state called for %(node)s with "
+ "invalid state %(state)s")
+ raise exception.InvalidParameterValue(
+ msg % {"node": task.node.uuid, "state": pstate})
+ with common.cimc_handle(task) as handle:
+ try:
+ handle.set_imc_managedobject(
+ None, class_id="ComputeRackUnit",
+ params={
+ imcsdk.ComputeRackUnit.ADMIN_POWER:
+ IRONIC_TO_CIMC_POWER_STATE[pstate],
+ imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1"
+ })
+ except imcsdk.ImcException as e:
+ raise exception.CIMCException(node=task.node.uuid, error=e)
+
+ if pstate is states.REBOOT:
+ pstate = states.POWER_ON
+
+ state = _wait_for_state_change(pstate, task)
+ if state != pstate:
+ raise exception.PowerStateFailure(pstate=pstate)
+
+ @task_manager.require_exclusive_lock
+ def reboot(self, task):
+ """Perform a hard reboot of the task's node.
+
+ If the node is already powered on then it shall reboot the node, if
+ its off then the node will just be turned on.
+
+ :param task: a TaskManager instance containing the node to act on.
+ :raises: MissingParameterValue if a required parameter is missing.
+ :raises: CIMCException if there is an error communicating with CIMC
+ """
+ current_power_state = self.get_power_state(task)
+
+ if current_power_state == states.POWER_ON:
+ self.set_power_state(task, states.REBOOT)
+ elif current_power_state == states.POWER_OFF:
+ self.set_power_state(task, states.POWER_ON)
diff --git a/ironic/drivers/pxe.py b/ironic/drivers/pxe.py
index 5cfde84e9..257be8cf1 100644
--- a/ironic/drivers/pxe.py
+++ b/ironic/drivers/pxe.py
@@ -25,6 +25,8 @@ from ironic.drivers import base
from ironic.drivers.modules.amt import management as amt_management
from ironic.drivers.modules.amt import power as amt_power
from ironic.drivers.modules.amt import vendor as amt_vendor
+from ironic.drivers.modules.cimc import management as cimc_mgmt
+from ironic.drivers.modules.cimc import power as cimc_power
from ironic.drivers.modules import iboot
from ironic.drivers.modules.ilo import deploy as ilo_deploy
from ironic.drivers.modules.ilo import inspect as ilo_inspect
@@ -340,6 +342,28 @@ class PXEAndUcsDriver(base.BaseDriver):
self.vendor = iscsi_deploy.VendorPassthru()
+class PXEAndCIMCDriver(base.BaseDriver):
+ """PXE + Cisco IMC driver.
+
+ This driver implements the 'core' functionality, combining
+ :class:`ironic.drivers.modules.cimc.Power` for power on/off and reboot with
+ :class:`ironic.drivers.modules.pxe.PXEBoot` for booting the node and
+ :class:`ironic.drivers.modules.iscsi_deploy.ISCSIDeploy` for image
+ deployment. Implentations are in those respective classes; this
+ class is merely the glue between them.
+ """
+ def __init__(self):
+ if not importutils.try_import('ImcSdk'):
+ raise exception.DriverLoadError(
+ driver=self.__class__.__name__,
+ reason=_("Unable to import ImcSdk library"))
+ self.power = cimc_power.Power()
+ self.boot = pxe.PXEBoot()
+ self.deploy = iscsi_deploy.ISCSIDeploy()
+ self.management = cimc_mgmt.CIMCManagement()
+ self.vendor = iscsi_deploy.VendorPassthru()
+
+
class PXEAndWakeOnLanDriver(base.BaseDriver):
"""PXE + WakeOnLan driver.
diff --git a/ironic/tests/db/utils.py b/ironic/tests/db/utils.py
index 39c7cb17d..7ea0c5811 100644
--- a/ironic/tests/db/utils.py
+++ b/ironic/tests/db/utils.py
@@ -318,3 +318,11 @@ def get_test_ucs_info():
"ucs_service_profile": "org-root/ls-devstack",
"ucs_address": "ucs-b",
}
+
+
+def get_test_cimc_info():
+ return {
+ "cimc_username": "admin",
+ "cimc_password": "password",
+ "cimc_address": "1.2.3.4",
+ }
diff --git a/ironic/tests/drivers/cimc/__init__.py b/ironic/tests/drivers/cimc/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ironic/tests/drivers/cimc/__init__.py
diff --git a/ironic/tests/drivers/cimc/test_common.py b/ironic/tests/drivers/cimc/test_common.py
new file mode 100644
index 000000000..84478cd97
--- /dev/null
+++ b/ironic/tests/drivers/cimc/test_common.py
@@ -0,0 +1,125 @@
+# Copyright 2015, Cisco Systems.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mock
+
+from oslo_config import cfg
+from oslo_utils import importutils
+
+from ironic.common import exception
+from ironic.conductor import task_manager
+from ironic.drivers.modules.cimc import common as cimc_common
+from ironic.tests.conductor import utils as mgr_utils
+from ironic.tests.db import base as db_base
+from ironic.tests.db import utils as db_utils
+from ironic.tests.objects import utils as obj_utils
+
+imcsdk = importutils.try_import('ImcSdk')
+
+CONF = cfg.CONF
+
+
+class CIMCBaseTestCase(db_base.DbTestCase):
+
+ def setUp(self):
+ super(CIMCBaseTestCase, self).setUp()
+ mgr_utils.mock_the_extension_manager(driver="fake_cimc")
+ self.node = obj_utils.create_test_node(
+ self.context,
+ driver='fake_cimc',
+ driver_info=db_utils.get_test_cimc_info(),
+ instance_uuid="fake_uuid")
+ CONF.set_override('max_retry', 2, 'cimc')
+ CONF.set_override('action_interval', 0, 'cimc')
+
+
+class ParseDriverInfoTestCase(CIMCBaseTestCase):
+
+ def test_parse_driver_info(self):
+ info = cimc_common.parse_driver_info(self.node)
+
+ self.assertIsNotNone(info.get('cimc_address'))
+ self.assertIsNotNone(info.get('cimc_username'))
+ self.assertIsNotNone(info.get('cimc_password'))
+
+ def test_parse_driver_info_missing_address(self):
+ del self.node.driver_info['cimc_address']
+ self.assertRaises(exception.MissingParameterValue,
+ cimc_common.parse_driver_info, self.node)
+
+ def test_parse_driver_info_missing_username(self):
+ del self.node.driver_info['cimc_username']
+ self.assertRaises(exception.MissingParameterValue,
+ cimc_common.parse_driver_info, self.node)
+
+ def test_parse_driver_info_missing_password(self):
+ del self.node.driver_info['cimc_password']
+ self.assertRaises(exception.MissingParameterValue,
+ cimc_common.parse_driver_info, self.node)
+
+
+@mock.patch.object(cimc_common, 'cimc_handle', autospec=True)
+class CIMCHandleLogin(CIMCBaseTestCase):
+
+ def test_cimc_handle_login(self, mock_handle):
+ info = cimc_common.parse_driver_info(self.node)
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ cimc_common.handle_login(task, handle, info)
+
+ handle.login.assert_called_once_with(
+ self.node.driver_info['cimc_address'],
+ self.node.driver_info['cimc_username'],
+ self.node.driver_info['cimc_password'])
+
+ def test_cimc_handle_login_exception(self, mock_handle):
+ info = cimc_common.parse_driver_info(self.node)
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ handle.login.side_effect = imcsdk.ImcException('Boom')
+
+ self.assertRaises(exception.CIMCException,
+ cimc_common.handle_login,
+ task, handle, info)
+
+ handle.login.assert_called_once_with(
+ self.node.driver_info['cimc_address'],
+ self.node.driver_info['cimc_username'],
+ self.node.driver_info['cimc_password'])
+
+
+class CIMCHandleTestCase(CIMCBaseTestCase):
+
+ @mock.patch.object(imcsdk, 'ImcHandle', autospec=True)
+ @mock.patch.object(cimc_common, 'handle_login', autospec=True)
+ def test_cimc_handle(self, mock_login, mock_handle):
+ mo_hand = mock.MagicMock()
+ mo_hand.username = self.node.driver_info.get('cimc_username')
+ mo_hand.password = self.node.driver_info.get('cimc_password')
+ mo_hand.name = self.node.driver_info.get('cimc_address')
+ mock_handle.return_value = mo_hand
+ info = cimc_common.parse_driver_info(self.node)
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with cimc_common.cimc_handle(task) as handle:
+ self.assertEqual(handle, mock_handle.return_value)
+
+ mock_login.assert_called_once_with(task, mock_handle.return_value,
+ info)
+ mock_handle.return_value.logout.assert_called_once_with()
diff --git a/ironic/tests/drivers/cimc/test_management.py b/ironic/tests/drivers/cimc/test_management.py
new file mode 100644
index 000000000..dc3bf917a
--- /dev/null
+++ b/ironic/tests/drivers/cimc/test_management.py
@@ -0,0 +1,126 @@
+# Copyright 2015, Cisco Systems.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mock
+
+from oslo_utils import importutils
+
+from ironic.common import boot_devices
+from ironic.common import exception
+from ironic.conductor import task_manager
+from ironic.drivers.modules.cimc import common
+from ironic.tests.drivers.cimc import test_common
+
+imcsdk = importutils.try_import('ImcSdk')
+
+
+@mock.patch.object(common, 'cimc_handle', autospec=True)
+class CIMCManagementTestCase(test_common.CIMCBaseTestCase):
+
+ def test_get_properties(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ self.assertEqual(common.COMMON_PROPERTIES,
+ task.driver.management.get_properties())
+
+ @mock.patch.object(common, "parse_driver_info", autospec=True)
+ def test_validate(self, mock_driver_info, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ task.driver.management.validate(task)
+ mock_driver_info.assert_called_once_with(task.node)
+
+ def test_get_supported_boot_devices(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ expected = [boot_devices.PXE, boot_devices.DISK,
+ boot_devices.CDROM]
+ result = task.driver.management.get_supported_boot_devices(task)
+ self.assertEqual(sorted(expected), sorted(result))
+
+ def test_get_boot_device(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ handle.xml_query.return_value.error_code = None
+ mock_dev = mock.MagicMock()
+ mock_dev.Order = 1
+ mock_dev.Rn = 'storage-read-write'
+ handle.xml_query().OutConfigs.child[0].child = [mock_dev]
+
+ device = task.driver.management.get_boot_device(task)
+ self.assertEqual(
+ {'boot_device': boot_devices.DISK, 'persistent': True},
+ device)
+
+ def test_get_boot_device_fail(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ handle.xml_query.return_value.error_code = None
+ mock_dev = mock.MagicMock()
+ mock_dev.Order = 1
+ mock_dev.Rn = 'storage-read-write'
+ handle.xml_query().OutConfigs.child[0].child = [mock_dev]
+
+ device = task.driver.management.get_boot_device(task)
+
+ self.assertEqual(
+ {'boot_device': boot_devices.DISK, 'persistent': True},
+ device)
+
+ def test_set_boot_device(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ handle.xml_query.return_value.error_code = None
+ task.driver.management.set_boot_device(task, boot_devices.DISK)
+ method = imcsdk.ImcCore.ExternalMethod("ConfigConfMo")
+ method.Cookie = handle.cookie
+ method.Dn = "sys/rack-unit-1/boot-policy"
+ method.InHierarchical = "true"
+
+ config = imcsdk.Imc.ConfigConfig()
+
+ bootMode = imcsdk.ImcCore.ManagedObject('lsbootStorage')
+ bootMode.set_attr("access", 'read-write')
+ bootMode.set_attr("type", 'storage')
+ bootMode.set_attr("Rn", 'storage-read-write')
+ bootMode.set_attr("order", "1")
+
+ config.add_child(bootMode)
+ method.InConfig = config
+
+ handle.xml_query.assert_called_once_with(
+ method, imcsdk.WriteXmlOption.DIRTY)
+
+ def test_set_boot_device_fail(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ method = imcsdk.ImcCore.ExternalMethod("ConfigConfMo")
+ handle.xml_query.return_value.error_code = "404"
+
+ self.assertRaises(exception.CIMCException,
+ task.driver.management.set_boot_device,
+ task, boot_devices.DISK)
+
+ handle.xml_query.assert_called_once_with(
+ method, imcsdk.WriteXmlOption.DIRTY)
+
+ def test_get_sensors_data(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ self.assertRaises(NotImplementedError,
+ task.driver.management.get_sensors_data, task)
diff --git a/ironic/tests/drivers/cimc/test_power.py b/ironic/tests/drivers/cimc/test_power.py
new file mode 100644
index 000000000..d82c71990
--- /dev/null
+++ b/ironic/tests/drivers/cimc/test_power.py
@@ -0,0 +1,302 @@
+# Copyright 2015, Cisco Systems.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mock
+
+from oslo_config import cfg
+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.cimc import common
+from ironic.drivers.modules.cimc import power
+from ironic.tests.drivers.cimc import test_common
+
+imcsdk = importutils.try_import('ImcSdk')
+
+CONF = cfg.CONF
+
+
+@mock.patch.object(common, 'cimc_handle', autospec=True)
+class WaitForStateChangeTestCase(test_common.CIMCBaseTestCase):
+
+ def setUp(self):
+ super(WaitForStateChangeTestCase, self).setUp()
+ CONF.set_override('max_retry', 2, 'cimc')
+ CONF.set_override('action_interval', 0, 'cimc')
+
+ def test__wait_for_state_change(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ mock_rack_unit = mock.MagicMock()
+ mock_rack_unit.get_attr.return_value = (
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON)
+
+ handle.get_imc_managedobject.return_value = [mock_rack_unit]
+
+ state = power._wait_for_state_change(states.POWER_ON, task)
+
+ handle.get_imc_managedobject.assert_called_once_with(
+ None, None, params={"Dn": "sys/rack-unit-1"})
+
+ self.assertEqual(state, states.POWER_ON)
+
+ def test__wait_for_state_change_fail(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ mock_rack_unit = mock.MagicMock()
+ mock_rack_unit.get_attr.return_value = (
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_OFF)
+
+ handle.get_imc_managedobject.return_value = [mock_rack_unit]
+
+ state = power._wait_for_state_change(states.POWER_ON, task)
+
+ calls = [
+ mock.call(None, None, params={"Dn": "sys/rack-unit-1"}),
+ mock.call(None, None, params={"Dn": "sys/rack-unit-1"})
+ ]
+ handle.get_imc_managedobject.assert_has_calls(calls)
+ self.assertEqual(state, states.ERROR)
+
+ def test__wait_for_state_change_imc_exception(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ handle.get_imc_managedobject.side_effect = (
+ imcsdk.ImcException('Boom'))
+
+ self.assertRaises(
+ exception.CIMCException,
+ power._wait_for_state_change, states.POWER_ON, task)
+
+ handle.get_imc_managedobject.assert_called_once_with(
+ None, None, params={"Dn": "sys/rack-unit-1"})
+
+
+@mock.patch.object(common, 'cimc_handle', autospec=True)
+class PowerTestCase(test_common.CIMCBaseTestCase):
+
+ def test_get_properties(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ self.assertEqual(common.COMMON_PROPERTIES,
+ task.driver.power.get_properties())
+
+ @mock.patch.object(common, "parse_driver_info", autospec=True)
+ def test_validate(self, mock_driver_info, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ task.driver.power.validate(task)
+ mock_driver_info.assert_called_once_with(task.node)
+
+ def test_get_power_state(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ mock_rack_unit = mock.MagicMock()
+ mock_rack_unit.get_attr.return_value = (
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON)
+
+ handle.get_imc_managedobject.return_value = [mock_rack_unit]
+
+ state = task.driver.power.get_power_state(task)
+
+ handle.get_imc_managedobject.assert_called_once_with(
+ None, None, params={"Dn": "sys/rack-unit-1"})
+ self.assertEqual(states.POWER_ON, state)
+
+ def test_get_power_state_fail(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ mock_rack_unit = mock.MagicMock()
+ mock_rack_unit.get_attr.return_value = (
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON)
+
+ handle.get_imc_managedobject.side_effect = (
+ imcsdk.ImcException("boom"))
+
+ self.assertRaises(exception.CIMCException,
+ task.driver.power.get_power_state, task)
+
+ handle.get_imc_managedobject.assert_called_once_with(
+ None, None, params={"Dn": "sys/rack-unit-1"})
+
+ def test_set_power_state_invalid_state(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ self.assertRaises(exception.InvalidParameterValue,
+ task.driver.power.set_power_state,
+ task, states.ERROR)
+
+ def test_set_power_state_reboot_ok(self, mock_handle):
+ hri = imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_HARD_RESET_IMMEDIATE
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ mock_rack_unit = mock.MagicMock()
+ mock_rack_unit.get_attr.side_effect = [
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_OFF,
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON
+ ]
+ handle.get_imc_managedobject.return_value = [mock_rack_unit]
+
+ task.driver.power.set_power_state(task, states.REBOOT)
+
+ handle.set_imc_managedobject.assert_called_once_with(
+ None, class_id="ComputeRackUnit",
+ params={
+ imcsdk.ComputeRackUnit.ADMIN_POWER: hri,
+ imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1"
+ })
+
+ handle.get_imc_managedobject.assert_called_with(
+ None, None, params={"Dn": "sys/rack-unit-1"})
+
+ def test_set_power_state_reboot_fail(self, mock_handle):
+ hri = imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_HARD_RESET_IMMEDIATE
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ handle.get_imc_managedobject.side_effect = (
+ imcsdk.ImcException("boom"))
+
+ self.assertRaises(exception.CIMCException,
+ task.driver.power.set_power_state,
+ task, states.REBOOT)
+
+ handle.set_imc_managedobject.assert_called_once_with(
+ None, class_id="ComputeRackUnit",
+ params={
+ imcsdk.ComputeRackUnit.ADMIN_POWER: hri,
+ imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1"
+ })
+
+ handle.get_imc_managedobject.assert_called_with(
+ None, None, params={"Dn": "sys/rack-unit-1"})
+
+ def test_set_power_state_on_ok(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ mock_rack_unit = mock.MagicMock()
+ mock_rack_unit.get_attr.side_effect = [
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_OFF,
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON
+ ]
+ handle.get_imc_managedobject.return_value = [mock_rack_unit]
+
+ task.driver.power.set_power_state(task, states.POWER_ON)
+
+ handle.set_imc_managedobject.assert_called_once_with(
+ None, class_id="ComputeRackUnit",
+ params={
+ imcsdk.ComputeRackUnit.ADMIN_POWER:
+ imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_UP,
+ imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1"
+ })
+
+ handle.get_imc_managedobject.assert_called_with(
+ None, None, params={"Dn": "sys/rack-unit-1"})
+
+ def test_set_power_state_on_fail(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ handle.get_imc_managedobject.side_effect = (
+ imcsdk.ImcException("boom"))
+
+ self.assertRaises(exception.CIMCException,
+ task.driver.power.set_power_state,
+ task, states.POWER_ON)
+
+ handle.set_imc_managedobject.assert_called_once_with(
+ None, class_id="ComputeRackUnit",
+ params={
+ imcsdk.ComputeRackUnit.ADMIN_POWER:
+ imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_UP,
+ imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1"
+ })
+
+ handle.get_imc_managedobject.assert_called_with(
+ None, None, params={"Dn": "sys/rack-unit-1"})
+
+ def test_set_power_state_off_ok(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ mock_rack_unit = mock.MagicMock()
+ mock_rack_unit.get_attr.side_effect = [
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON,
+ imcsdk.ComputeRackUnit.CONST_OPER_POWER_OFF
+ ]
+ handle.get_imc_managedobject.return_value = [mock_rack_unit]
+
+ task.driver.power.set_power_state(task, states.POWER_OFF)
+
+ handle.set_imc_managedobject.assert_called_once_with(
+ None, class_id="ComputeRackUnit",
+ params={
+ imcsdk.ComputeRackUnit.ADMIN_POWER:
+ imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_DOWN,
+ imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1"
+ })
+
+ handle.get_imc_managedobject.assert_called_with(
+ None, None, params={"Dn": "sys/rack-unit-1"})
+
+ def test_set_power_state_off_fail(self, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ with mock_handle(task) as handle:
+ handle.get_imc_managedobject.side_effect = (
+ imcsdk.ImcException("boom"))
+
+ self.assertRaises(exception.CIMCException,
+ task.driver.power.set_power_state,
+ task, states.POWER_OFF)
+
+ handle.set_imc_managedobject.assert_called_once_with(
+ None, class_id="ComputeRackUnit",
+ params={
+ imcsdk.ComputeRackUnit.ADMIN_POWER:
+ imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_DOWN,
+ imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1"
+ })
+
+ handle.get_imc_managedobject.assert_called_with(
+ None, None, params={"Dn": "sys/rack-unit-1"})
+
+ @mock.patch.object(power.Power, "set_power_state", autospec=True)
+ @mock.patch.object(power.Power, "get_power_state", autospec=True)
+ def test_reboot_on(self, mock_get_state, mock_set_state, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ mock_get_state.return_value = states.POWER_ON
+ task.driver.power.reboot(task)
+ mock_set_state.assert_called_with(mock.ANY, task, states.REBOOT)
+
+ @mock.patch.object(power.Power, "set_power_state", autospec=True)
+ @mock.patch.object(power.Power, "get_power_state", autospec=True)
+ def test_reboot_off(self, mock_get_state, mock_set_state, mock_handle):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ mock_get_state.return_value = states.POWER_OFF
+ task.driver.power.reboot(task)
+ mock_set_state.assert_called_with(mock.ANY, task, states.POWER_ON)
diff --git a/ironic/tests/drivers/third_party_driver_mocks.py b/ironic/tests/drivers/third_party_driver_mocks.py
index 909ea0a0e..6389c6382 100644
--- a/ironic/tests/drivers/third_party_driver_mocks.py
+++ b/ironic/tests/drivers/third_party_driver_mocks.py
@@ -232,3 +232,12 @@ if not ucssdk:
if 'ironic.drivers.modules.ucs' in sys.modules:
six.moves.reload_module(
sys.modules['ironic.drivers.modules.ucs'])
+
+imcsdk = importutils.try_import('ImcSdk')
+if not imcsdk:
+ imcsdk = mock.MagicMock()
+ imcsdk.ImcException = Exception
+ sys.modules['ImcSdk'] = imcsdk
+ if 'ironic.drivers.modules.cimc' in sys.modules:
+ six.moves.reload_module(
+ sys.modules['ironic.drivers.modules.cimc'])