diff options
author | Sam Betts <sam@code-smash.net> | 2015-09-01 12:32:23 +0100 |
---|---|---|
committer | Sam Betts <sam@code-smash.net> | 2015-09-21 18:29:43 +0100 |
commit | 363c9c38df001ba0d9499c132c13ef19b7080a9f (patch) | |
tree | f3032a135f55ccc777577172b12e4eb497a3a007 /ironic | |
parent | 4b5d69ffcf58cd8e044e2738b05791034da2e34a (diff) | |
download | ironic-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.py | 4 | ||||
-rw-r--r-- | ironic/drivers/agent.py | 25 | ||||
-rw-r--r-- | ironic/drivers/fake.py | 15 | ||||
-rw-r--r-- | ironic/drivers/modules/cimc/__init__.py | 0 | ||||
-rw-r--r-- | ironic/drivers/modules/cimc/common.py | 87 | ||||
-rw-r--r-- | ironic/drivers/modules/cimc/management.py | 166 | ||||
-rw-r--r-- | ironic/drivers/modules/cimc/power.py | 184 | ||||
-rw-r--r-- | ironic/drivers/pxe.py | 24 | ||||
-rw-r--r-- | ironic/tests/db/utils.py | 8 | ||||
-rw-r--r-- | ironic/tests/drivers/cimc/__init__.py | 0 | ||||
-rw-r--r-- | ironic/tests/drivers/cimc/test_common.py | 125 | ||||
-rw-r--r-- | ironic/tests/drivers/cimc/test_management.py | 126 | ||||
-rw-r--r-- | ironic/tests/drivers/cimc/test_power.py | 302 | ||||
-rw-r--r-- | ironic/tests/drivers/third_party_driver_mocks.py | 9 |
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']) |