From 363c9c38df001ba0d9499c132c13ef19b7080a9f Mon Sep 17 00:00:00 2001 From: Sam Betts Date: Tue, 1 Sep 2015 12:32:23 +0100 Subject: [PATCH] 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 --- doc/source/deploy/drivers.rst | 9 + doc/source/drivers/cimc.rst | 95 ++++++ driver-requirements.txt | 3 + etc/ironic/ironic.conf.sample | 15 + ironic/common/exception.py | 4 + ironic/drivers/agent.py | 25 ++ ironic/drivers/fake.py | 15 + ironic/drivers/modules/cimc/__init__.py | 0 ironic/drivers/modules/cimc/common.py | 87 +++++ ironic/drivers/modules/cimc/management.py | 166 ++++++++++ ironic/drivers/modules/cimc/power.py | 184 +++++++++++ ironic/drivers/pxe.py | 24 ++ ironic/tests/db/utils.py | 8 + ironic/tests/drivers/cimc/__init__.py | 0 ironic/tests/drivers/cimc/test_common.py | 125 ++++++++ ironic/tests/drivers/cimc/test_management.py | 126 ++++++++ ironic/tests/drivers/cimc/test_power.py | 302 ++++++++++++++++++ .../tests/drivers/third_party_driver_mocks.py | 9 + setup.cfg | 3 + 19 files changed, 1200 insertions(+) create mode 100644 doc/source/drivers/cimc.rst create mode 100644 ironic/drivers/modules/cimc/__init__.py create mode 100644 ironic/drivers/modules/cimc/common.py create mode 100644 ironic/drivers/modules/cimc/management.py create mode 100644 ironic/drivers/modules/cimc/power.py create mode 100644 ironic/tests/drivers/cimc/__init__.py create mode 100644 ironic/tests/drivers/cimc/test_common.py create mode 100644 ironic/tests/drivers/cimc/test_management.py create mode 100644 ironic/tests/drivers/cimc/test_power.py diff --git a/doc/source/deploy/drivers.rst b/doc/source/deploy/drivers.rst index 67ea119703..ef78f3221d 100644 --- a/doc/source/deploy/drivers.rst +++ b/doc/source/deploy/drivers.rst @@ -105,3 +105,12 @@ iBoot driver :maxdepth: 1 ../drivers/iboot + + +CIMC driver +------------ + +.. toctree:: + :maxdepth: 1 + + ../drivers/cimc diff --git a/doc/source/drivers/cimc.rst b/doc/source/drivers/cimc.rst new file mode 100644 index 0000000000..b7c22efa9a --- /dev/null +++ b/doc/source/drivers/cimc.rst @@ -0,0 +1,95 @@ +.. _CIMC: + +============ +CIMC drivers +============ + +Overview +======== +The CIMC drivers are targeted for standalone Cisco UCS C series servers. +These drivers enable you to take advantage of CIMC by using the +python SDK. + +``pxe_iscsi_cimc`` driver uses PXE boot + iSCSI deploy (just like ``pxe_ipmitool`` +driver) to deploy the image and uses CIMC to do all management operations on +the baremetal node (instead of using IPMI). + +``pxe_agent_cimc`` driver uses PXE boot + Agent deploy (just like ``agent_ipmitool`` +and ``agent_ipminative`` drivers.) to deploy the image and uses CIMC to do all +management operations on the baremetal node (instead of using IPMI). Unlike with +iSCSI deploy in Agent deploy, the ramdisk is responsible for writing the image to +the disk, instead of the conductor. + +Prerequisites +============= + +* ``ImcSdk`` is a python SDK for the CIMC HTTP/HTTPS XML API used to control + CIMC. + +Install the ``ImcSdk`` module +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + + Install the ``ImcSdk`` module on the Ironic conductor node. Required version is + 0.7.1. + +#. Download the tar.gz from: https://communities.cisco.com/docs/DOC-56257 + +#. Unpack it:: + + $ tar xvf ImcSdk-0.7.1.tar.gz + +#. Install it:: + + $ cd ImcSdk-0.7.1 + $ python setup.py install + +Tested Platforms +~~~~~~~~~~~~~~~~ +This driver works with UCS C-Series servers and has been tested with: + +* UCS C240M3S + +Configuring and Enabling the driver +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1. Add ``pxe_cimc`` and/or ``agent_cimc`` to the list of ``enabled_drivers`` in + ``/etc/ironic/ironic.conf``. For example:: + + enabled_drivers = pxe_ipmitool,pxe_cimc,agent_cimc + +2. Restart the Ironic conductor service: + + For Ubuntu/Debian systems:: + + $ sudo service ironic-conductor restart + + or for RHEL/CentOS/Fedora:: + + $ sudo systemctl restart openstack-ironic-conductor + +Registering Standalone UCS node in Ironic +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Nodes configured for CIMC driver should have the ``driver`` property set to +``pxe_iscsi_cimc`` or ``pxe_agent_cimc``. The following configuration values are +also required in ``driver_info``: + +- ``cimc_address``: IP address or hostname for CIMC +- ``cimc_username``: CIMC login user name +- ``cimc_password``: CIMC login password for the above CIMC user. +- ``deploy_kernel``: The Glance UUID of the deployment kernel. +- ``deploy_ramdisk``: The Glance UUID of the deployment ramdisk. + +The following sequence of commands can be used to enroll a UCS Standalone node. + + Create Node:: + + ironic node-create -d -i cimc_address= -i cimc_username= -i cimc_password= -i deploy_kernel= -i deploy_ramdisk= -p cpus= -p memory_mb= -p local_gb= -p cpu_arch= + + The above command 'ironic node-create' will return UUID of the node, which is the value of $NODE in the following command. + + Associate port with the node created:: + + ironic port-create -n $NODE -a + +For more information about enrolling nodes see "Enrolling a node" in the :ref:`install-guide` diff --git a/driver-requirements.txt b/driver-requirements.txt index 108e92fc01..692c7e062a 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -26,3 +26,6 @@ UcsSdk==0.8.2.2 # Refer documentation on how to install and configure this: # http://docs.openstack.org/developer/ironic/drivers/vbox.html pyremotevbox>=0.5.0 + +# The CIMC drivers use the Cisco IMC SDK version 0.7.1, which is avaliable from +# https://communities.cisco.com/docs/DOC-37174 diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index dbbf766cc5..932888570f 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -453,6 +453,21 @@ #public_endpoint= +[cimc] + +# +# Options defined in ironic.drivers.modules.cimc.power +# + +# Number of times a power operation needs to be retried +# (integer value) +#max_retry=6 + +# Amount of time in seconds to wait in between power +# operations (integer value) +#action_interval=10 + + [cisco_ucs] # diff --git a/ironic/common/exception.py b/ironic/common/exception.py index afa140ddaf..3eb83cbaaf 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 088c2e1181..34446ed6f8 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 da4644935d..b139148961 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 0000000000..e69de29bb2 diff --git a/ironic/drivers/modules/cimc/common.py b/ironic/drivers/modules/cimc/common.py new file mode 100644 index 0000000000..1340477d4c --- /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 0000000000..fc8dfcee80 --- /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 : 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 0000000000..a6a40240a3 --- /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 : 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 5cfde84e9e..257be8cf12 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 39c7cb17d5..7ea0c58112 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 0000000000..e69de29bb2 diff --git a/ironic/tests/drivers/cimc/test_common.py b/ironic/tests/drivers/cimc/test_common.py new file mode 100644 index 0000000000..84478cd97f --- /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 0000000000..dc3bf917af --- /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 0000000000..d82c71990c --- /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 909ea0a0e8..6389c63828 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']) diff --git a/setup.cfg b/setup.cfg index e8483a00c4..45c08aac1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,6 +56,7 @@ ironic.drivers = fake_amt = ironic.drivers.fake:FakeAMTDriver fake_msftocs = ironic.drivers.fake:FakeMSFTOCSDriver fake_ucs = ironic.drivers.fake:FakeUcsDriver + fake_cimc = ironic.drivers.fake:FakeCIMCDriver fake_wol = ironic.drivers.fake:FakeWakeOnLanDriver iscsi_ilo = ironic.drivers.ilo:IloVirtualMediaIscsiDriver iscsi_irmc = ironic.drivers.irmc:IRMCVirtualMediaIscsiDriver @@ -73,6 +74,8 @@ ironic.drivers = pxe_msftocs = ironic.drivers.pxe:PXEAndMSFTOCSDriver pxe_ucs = ironic.drivers.pxe:PXEAndUcsDriver pxe_wol = ironic.drivers.pxe:PXEAndWakeOnLanDriver + pxe_iscsi_cimc = ironic.drivers.pxe:PXEAndCIMCDriver + pxe_agent_cimc = ironic.drivers.agent:AgentAndCIMCDriver ironic.database.migration_backend = sqlalchemy = ironic.db.sqlalchemy.migration