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
This commit is contained in:
Sam Betts 2015-09-01 12:32:23 +01:00
parent 4b5d69ffcf
commit 363c9c38df
19 changed files with 1200 additions and 0 deletions

View File

@ -105,3 +105,12 @@ iBoot driver
:maxdepth: 1
../drivers/iboot
CIMC driver
------------
.. toctree::
:maxdepth: 1
../drivers/cimc

View File

@ -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 <pxe_cimc/agent_cimc> -i cimc_address=<CIMC hostname/ip-address> -i cimc_username=<cimc_username> -i cimc_password=<cimc_password> -i deploy_kernel=<glance_uuid_of_deploy_kernel> -i deploy_ramdisk=<glance_uuid_of_deploy_ramdisk> -p cpus=<number_of_cpus> -p memory_mb=<memory_size_in_MB> -p local_gb=<local_disk_size_in_GB> -p cpu_arch=<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 <MAC_address_of_Ucs_server's_NIC>
For more information about enrolling nodes see "Enrolling a node" in the :ref:`install-guide`

View File

@ -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

View File

@ -453,6 +453,21 @@
#public_endpoint=<None>
[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]
#

View File

@ -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")

View File

@ -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()

View File

@ -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."""

View File

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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.

View File

@ -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",
}

View File

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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'])

View File

@ -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