diff --git a/doc/source/deploy/drivers.rst b/doc/source/deploy/drivers.rst old mode 100644 new mode 100755 index 9a2eb8390c..588b13cd7c --- a/doc/source/deploy/drivers.rst +++ b/doc/source/deploy/drivers.rst @@ -84,3 +84,44 @@ SeaMicro driver :maxdepth: 1 ../drivers/seamicro + +iRMC +---- + +The iRMC driver enables PXE Deploy to control power via ServerView Common +Command Interface (SCCI). + + +Software Requirements +^^^^^^^^^^^^^^^^^^^^^ + +- Install `python-scciclient package `_ + +Enabling the iRMC Driver +^^^^^^^^^^^^^^^^^^^^^^^^ + +- Add ``pxe_irmc`` to the list of ``enabled_drivers in`` + ``/etc/ironic/ironic.conf`` +- Ironic Conductor must be restarted for the new driver to be loaded. + +Ironic Node Configuration +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Nodes are configured for iRMC with PXE Deploy by setting the Ironic node +object's ``driver`` property to be ``pxe_irmc``. Further configuration values +are added to ``driver_info``: + +- ``irmc_address``: hostname or IP of iRMC +- ``irmc_username``: username for iRMC with administrator privileges +- ``irmc_password``: password for irmc_username +- ``irmc_port``: port number of iRMC (optional, either 80 or 443. defalut 443) +- ``irmc_auth_method``: authentication method for iRMC (optional, either + 'basic' or 'digest'. default is 'basic') + +Supported Platforms +^^^^^^^^^^^^^^^^^^^ +This driver supports FUJITSU PRIMERGY BX S4 or RX S8 servers and above. + +- PRIMERGY BX920 S4 +- PRIMERGY BX924 S4 +- PRIMERGY RX300 S8 diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index 6f2302524e..2f9af966f7 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -900,6 +900,24 @@ #min_command_interval=5 +[irmc] + +# +# Options defined in ironic.drivers.modules.irmc.common +# + +# Port to be used for iRMC operations, either 80 or 443 +# (integer value) +#port=443 + +# Authentication method to be used for iRMC operations, either +# "basic" or "digest" (string value) +#auth_method=basic + +# Timeout (in seconds) for iRMC operations (integer value) +#client_timeout=60 + + [keystone_authtoken] # diff --git a/ironic/common/exception.py b/ironic/common/exception.py old mode 100644 new mode 100755 index 08e3629ab8..bc2d361275 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -501,3 +501,7 @@ class SNMPFailure(IronicException): class FileSystemNotSupported(IronicException): message = _("Failed to create a file system. " "File system %(fs)s is not supported.") + + +class IRMCOperationError(IronicException): + message = _('iRMC %(operation)s failed. Reason: %(error)s') diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py old mode 100644 new mode 100755 index 678db4f78f..c92de86b8d --- a/ironic/drivers/fake.py +++ b/ironic/drivers/fake.py @@ -31,6 +31,7 @@ from ironic.drivers.modules.ilo import management as ilo_management from ironic.drivers.modules.ilo import power as ilo_power from ironic.drivers.modules import ipminative from ironic.drivers.modules import ipmitool +from ironic.drivers.modules.irmc import power as irmc_power from ironic.drivers.modules import pxe from ironic.drivers.modules import seamicro from ironic.drivers.modules import snmp @@ -171,3 +172,15 @@ class FakeSNMPDriver(base.BaseDriver): reason=_("Unable to import pysnmp library")) self.power = snmp.SNMPPower() self.deploy = fake.FakeDeploy() + + +class FakeIRMCDriver(base.BaseDriver): + """Fake iRMC driver.""" + + def __init__(self): + if not importutils.try_import('scciclient'): + raise exception.DriverLoadError( + driver=self.__class__.__name__, + reason=_("Unable to import python-scciclient library")) + self.power = irmc_power.IRMCPower() + self.deploy = fake.FakeDeploy() diff --git a/ironic/drivers/modules/irmc/__init__.py b/ironic/drivers/modules/irmc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ironic/drivers/modules/irmc/common.py b/ironic/drivers/modules/irmc/common.py new file mode 100644 index 0000000000..bc33208088 --- /dev/null +++ b/ironic/drivers/modules/irmc/common.py @@ -0,0 +1,148 @@ +# +# 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. + +""" +Common functionalities shared between different iRMC modules. +""" + +from oslo.config import cfg +from oslo.utils import importutils + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.openstack.common import log as logging + +scci = importutils.try_import('scciclient.irmc.scci') + +opts = [ + cfg.IntOpt('port', + default=443, + help='Port to be used for iRMC operations, either 80 or 443'), + cfg.StrOpt('auth_method', + default='basic', + help='Authentication method to be used for iRMC operations, ' + + 'either "basic" or "digest"'), + cfg.IntOpt('client_timeout', + default=60, + help='Timeout (in seconds) for iRMC operations'), +] + +CONF = cfg.CONF +CONF.register_opts(opts, group='irmc') + +LOG = logging.getLogger(__name__) + +REQUIRED_PROPERTIES = { + 'irmc_address': _("IP address or hostname of the iRMC. Required."), + 'irmc_username': _("Username for the iRMC with administrator privileges. " + "Required."), + 'irmc_password': _("Password for irmc_username. Required.") +} +OPTIONAL_PROPERTIES = { + 'port': _("Port to be used for irmc operations either 80 or 443. " + "Optional. The default value is 443"), + 'auth_method': _("Authentication method for iRMC operations " + "either 'basic' or 'digest'. " + "Optional. The default value is 'digest'"), + 'client_timeout': _("Timeout (in seconds) for iRMC operations. " + "Optional. The default value is 60.") +} + +COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() +COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) + + +def parse_driver_info(node): + """Gets the specific Node driver info. + + This method validates whether the 'driver_info' property of the + supplied node contains the required information for this driver. + + :param node: an ironic node object. + :returns: a dict containing information from driver_info + and default values. + :raises: InvalidParameterValue if invalid value is contained + in the 'driver_info' property. + :raises: MissingParameterValue if some mandatory key is missing + in the 'driver_info' property. + """ + info = node.driver_info + missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)] + if missing_info: + raise exception.MissingParameterValue(_( + "Missing the following iRMC parameters in node's" + " driver_info: %s.") % missing_info) + + req = {key: value for key, value in info.iteritems() + if key in REQUIRED_PROPERTIES} + opt = {'irmc_' + param: info.get('irmc_' + param, CONF.irmc.get(param)) + for param in OPTIONAL_PROPERTIES} + d_info = dict(list(req.items()) + list(opt.items())) + + error_msgs = [] + if (d_info['irmc_auth_method'].lower() not in ('basic', 'digest')): + error_msgs.append( + _("'%s' has unsupported value.") % 'irmc_auth_method') + if d_info['irmc_port'] not in (80, 443): + error_msgs.append( + _("'%s' has unsupported value.") % 'irmc_port') + if not isinstance(d_info['irmc_client_timeout'], int): + error_msgs.append( + _("'%s' is not integer type.") % 'irmc_client_timeout') + if error_msgs: + msg = (_("The following type errors were encountered while parsing " + "driver_info:\n%s") % "\n".join(error_msgs)) + raise exception.InvalidParameterValue(msg) + + return d_info + + +def get_irmc_client(node): + """Gets an iRMC SCCI client. + + Given an ironic node object, this method gives back a iRMC SCCI client + to do operations on the iRMC. + + :param node: an ironic node object. + :returns: scci_cmd partial function which takes a SCCI command param. + :raises: InvalidParameterValue on invalid inputs. + :raises: MissingParameterValue if some mandatory information + is missing on the node + """ + driver_info = parse_driver_info(node) + + scci_client = scci.get_client( + driver_info['irmc_address'], + driver_info['irmc_username'], + driver_info['irmc_password'], + port=driver_info['irmc_port'], + auth_method=driver_info['irmc_auth_method'], + client_timeout=driver_info['irmc_client_timeout']) + return scci_client + + +def update_ipmi_properties(task): + """Update ipmi properties to node driver_info + + :param task: a task from TaskManager. + """ + node = task.node + info = node.driver_info + + # updating ipmi credentials + info['ipmi_address'] = info.get('irmc_address') + info['ipmi_username'] = info.get('irmc_username') + info['ipmi_password'] = info.get('irmc_password') + + # saving ipmi credentials to task object + task.node.driver_info = info diff --git a/ironic/drivers/modules/irmc/power.py b/ironic/drivers/modules/irmc/power.py new file mode 100644 index 0000000000..75b6651c3c --- /dev/null +++ b/ironic/drivers/modules/irmc/power.py @@ -0,0 +1,133 @@ +# +# 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. + +""" +iRMC Power Driver using the Base Server Profile +""" +from oslo.config import cfg +from oslo.utils import importutils + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.common.i18n import _LE +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers import base +from ironic.drivers.modules import ipmitool +from ironic.drivers.modules.irmc import common as irmc_common +from ironic.openstack.common import log as logging + +scci = importutils.try_import('scciclient.irmc.scci') + +CONF = cfg.CONF + +LOG = logging.getLogger(__name__) + +if scci: + STATES_MAP = {states.POWER_OFF: scci.POWER_OFF, + states.POWER_ON: scci.POWER_ON, + states.REBOOT: scci.POWER_RESET} + + +def _set_power_state(task, target_state): + """Turns the server power on/off or do a reboot. + + :param task: a TaskManager instance containing the node to act on. + :param target_state: target state of the node. + :raises: InvalidParameterValue if an invalid power state was specified. + :raises: MissingParameterValue if some mandatory information + is missing on the node + :raises: IRMCOperationError on an error from SCCI + """ + + node = task.node + irmc_client = irmc_common.get_irmc_client(node) + + try: + irmc_client(STATES_MAP[target_state]) + + except KeyError: + msg = _("_set_power_state called with invalid power state " + "'%s'") % target_state + raise exception.InvalidParameterValue(msg) + + except scci.SCCIClientError as irmc_exception: + LOG.error(_LE("iRMC set_power_state failed to set state to %(tstate)s " + " for node %(node_id)s with error: %(error)s"), + {'tstate': target_state, 'node_id': node.uuid, + 'error': irmc_exception}) + operation = _('iRMC set_power_state') + raise exception.IRMCOperationError(operation=operation, + error=irmc_exception) + + +class IRMCPower(base.PowerInterface): + """Interface for power-related actions.""" + + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + return irmc_common.COMMON_PROPERTIES + + def validate(self, task): + """Validate the driver-specific Node power info. + + This method validates whether the 'driver_info' property of the + supplied node contains the required information for this driver to + manage the power state of the node. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if required driver_info attribute + is missing or invalid on the node. + :raises: MissingParameterValue if a required parameter is missing. + """ + irmc_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. + :returns: a power state. One of :mod:`ironic.common.states`. + :raises: InvalidParameterValue if required ipmi parameters are missing. + :raises: MissingParameterValue if a required parameter is missing. + :raises: IPMIFailure on an error from ipmitool (from _power_status + call). + """ + irmc_common.update_ipmi_properties(task) + ipmi_power = ipmitool.IPMIPower() + return ipmi_power.get_power_state(task) + + @task_manager.require_exclusive_lock + def set_power_state(self, task, power_state): + """Set the power state of the task's node. + + :param task: a TaskManager instance containing the node to act on. + :param power_state: Any power state from :mod:`ironic.common.states`. + :raises: InvalidParameterValue if an invalid power state was specified. + :raises: MissingParameterValue if some mandatory information + is missing on the node + :raises: IRMCOperationError if failed to set the power state. + """ + _set_power_state(task, power_state) + + @task_manager.require_exclusive_lock + def reboot(self, task): + """Perform a hard reboot of the task's node. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if an invalid power state was specified. + :raises: IRMCOperationError if failed to set the power state. + """ + _set_power_state(task, states.REBOOT) diff --git a/ironic/drivers/pxe.py b/ironic/drivers/pxe.py index c843877528..877de5a503 100644 --- a/ironic/drivers/pxe.py +++ b/ironic/drivers/pxe.py @@ -28,6 +28,7 @@ from ironic.drivers.modules.ilo import management as ilo_management from ironic.drivers.modules.ilo import power as ilo_power from ironic.drivers.modules import ipminative from ironic.drivers.modules import ipmitool +from ironic.drivers.modules.irmc import power as irmc_power from ironic.drivers.modules import pxe from ironic.drivers.modules import seamicro from ironic.drivers.modules import snmp @@ -187,3 +188,24 @@ class PXEAndSNMPDriver(base.BaseDriver): # PDUs have no boot device management capability. # Only PXE as a boot device is supported. self.management = None + + +class PXEAndIRMCDriver(base.BaseDriver): + """PXE + iRMC driver using SCCI. + + This driver implements the `core` functionality using + :class:`ironic.drivers.modules.irmc.power.IRMCPower` for power management + :class:`ironic.drivers.modules.pxe.PXEDeploy` for image deployment. + + """ + + def __init__(self): + if not importutils.try_import('scciclient'): + raise exception.DriverLoadError( + driver=self.__class__.__name__, + reason=_("Unable to import python-scciclient library")) + self.power = irmc_power.IRMCPower() + self.console = ipmitool.IPMIShellinaboxConsole() + self.deploy = pxe.PXEDeploy() + self.management = ipmitool.IPMIManagement() + self.vendor = pxe.VendorPassthru() diff --git a/ironic/tests/db/utils.py b/ironic/tests/db/utils.py index bf62ca74a5..911ef5fcc6 100644 --- a/ironic/tests/db/utils.py +++ b/ironic/tests/db/utils.py @@ -104,6 +104,16 @@ def get_test_drac_info(): } +def get_test_irmc_info(): + return { + "irmc_address": "1.2.3.4", + "irmc_username": "admin0", + "irmc_password": "fake0", + "irmc_port": 80, + "irmc_auth_method": "digest", + } + + def get_test_agent_instance_info(): return { 'image_source': 'fake-image', diff --git a/ironic/tests/drivers/irmc/__init__.py b/ironic/tests/drivers/irmc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ironic/tests/drivers/irmc/test_common.py b/ironic/tests/drivers/irmc/test_common.py new file mode 100644 index 0000000000..288c4e7a8f --- /dev/null +++ b/ironic/tests/drivers/irmc/test_common.py @@ -0,0 +1,132 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test class for common methods used by iRMC modules. +""" + +import mock +from oslo.config import cfg + +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.irmc import common as irmc_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 + + +CONF = cfg.CONF + + +class IRMCValidateParametersTestCase(db_base.DbTestCase): + + def setUp(self): + super(IRMCValidateParametersTestCase, self).setUp() + self.node = obj_utils.create_test_node( + self.context, + driver='fake_irmc', + driver_info=db_utils.get_test_irmc_info()) + + def test_parse_driver_info(self): + info = irmc_common.parse_driver_info(self.node) + + self.assertIsNotNone(info.get('irmc_address')) + self.assertIsNotNone(info.get('irmc_username')) + self.assertIsNotNone(info.get('irmc_password')) + self.assertIsNotNone(info.get('irmc_client_timeout')) + self.assertIsNotNone(info.get('irmc_port')) + self.assertIsNotNone(info.get('irmc_auth_method')) + + def test_parse_driver_info_missing_address(self): + del self.node.driver_info['irmc_address'] + self.assertRaises(exception.MissingParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_username(self): + del self.node.driver_info['irmc_username'] + self.assertRaises(exception.MissingParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_password(self): + del self.node.driver_info['irmc_password'] + self.assertRaises(exception.MissingParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_timeout(self): + self.node.driver_info['irmc_client_timeout'] = 'qwe' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_port(self): + self.node.driver_info['irmc_port'] = 'qwe' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_auth_method(self): + self.node.driver_info['irmc_auth_method'] = 'qwe' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_multiple_params(self): + del self.node.driver_info['irmc_password'] + del self.node.driver_info['irmc_address'] + try: + irmc_common.parse_driver_info(self.node) + self.fail("parse_driver_info did not throw exception.") + except exception.MissingParameterValue as e: + self.assertIn('irmc_password', str(e)) + self.assertIn('irmc_address', str(e)) + + +class IRMCCommonMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(IRMCCommonMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_irmc") + self.info = db_utils.get_test_irmc_info() + self.node = obj_utils.create_test_node( + self.context, + driver='fake_irmc', + driver_info=self.info) + + @mock.patch.object(irmc_common, 'scci') + def test_get_irmc_client(self, mock_scci): + self.info['irmc_port'] = 80 + self.info['irmc_auth_method'] = 'digest' + self.info['irmc_client_timeout'] = 60 + mock_scci.get_client.return_value = 'get_client' + returned_mock_scci_get_client = irmc_common.get_irmc_client(self.node) + mock_scci.get_client.assert_called_with( + self.info['irmc_address'], + self.info['irmc_username'], + self.info['irmc_password'], + port=self.info['irmc_port'], + auth_method=self.info['irmc_auth_method'], + client_timeout=self.info['irmc_client_timeout']) + self.assertEqual('get_client', returned_mock_scci_get_client) + + def test_update_ipmi_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ipmi_info = { + "ipmi_address": "1.2.3.4", + "ipmi_username": "admin0", + "ipmi_password": "fake0", + } + task.node.driver_info = self.info + irmc_common.update_ipmi_properties(task) + actual_info = task.node.driver_info + expected_info = dict(self.info, **ipmi_info) + self.assertEqual(expected_info, actual_info) diff --git a/ironic/tests/drivers/irmc/test_power.py b/ironic/tests/drivers/irmc/test_power.py new file mode 100644 index 0000000000..85bf10f1bc --- /dev/null +++ b/ironic/tests/drivers/irmc/test_power.py @@ -0,0 +1,154 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test class for iRMC Power Driver +""" + +import mock +from oslo.config import cfg + +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules.irmc import power as irmc_power +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 + +INFO_DICT = db_utils.get_test_irmc_info() +CONF = cfg.CONF + + +@mock.patch.object(irmc_common, 'get_irmc_client') +class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(IRMCPowerInternalMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_irmc') + driver_info = INFO_DICT + self.node = db_utils.create_test_node( + driver='fake_irmc', + driver_info=driver_info, + instance_uuid='instance_uuid_123') + + def test__set_power_state_power_on_ok(self, + get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + target_state = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + irmc_power._set_power_state(task, target_state) + irmc_client.assert_called_once_with(irmc_power.scci.POWER_ON) + + def test__set_power_state_power_off_ok(self, + get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + target_state = states.POWER_OFF + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + irmc_power._set_power_state(task, target_state) + irmc_client.assert_called_once_with(irmc_power.scci.POWER_OFF) + + def test__set_power_state_power_reboot_ok(self, + get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + target_state = states.REBOOT + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + irmc_power._set_power_state(task, target_state) + irmc_client.assert_called_once_with(irmc_power.scci.POWER_RESET) + + def test__set_power_state_invalid_target_state(self, + get_irmc_client_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + irmc_power._set_power_state, + task, + states.ERROR) + + def test__set_power_state_scci_exception(self, + get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_client.side_effect = Exception() + irmc_power.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + irmc_power._set_power_state, + task, + states.POWER_ON) + + +class IRMCPowerTestCase(db_base.DbTestCase): + def setUp(self): + super(IRMCPowerTestCase, self).setUp() + driver_info = INFO_DICT + mgr_utils.mock_the_extension_manager(driver="fake_irmc") + self.node = obj_utils.create_test_node(self.context, + driver='fake_irmc', + driver_info=driver_info) + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + properties = task.driver.get_properties() + for prop in irmc_common.COMMON_PROPERTIES: + self.assertIn(prop, properties) + + @mock.patch.object(irmc_common, 'parse_driver_info') + def test_validate(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.power.validate(task) + mock_drvinfo.assert_called_once_with(task.node) + + @mock.patch.object(irmc_common, 'parse_driver_info') + def test_validate_fail(self, mock_drvinfo): + side_effect = exception.InvalidParameterValue("Invalid Input") + mock_drvinfo.side_effect = side_effect + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.validate, + task) + + @mock.patch('ironic.drivers.modules.irmc.power.ipmitool.IPMIPower') + def test_get_power_state(self, mock_IPMIPower): + ipmi_power = mock_IPMIPower.return_value + ipmi_power.get_power_state.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(states.POWER_ON, + task.driver.power.get_power_state(task)) + ipmi_power.get_power_state.assert_called_once_with(task) + + @mock.patch.object(irmc_power, '_set_power_state') + def test_set_power_state(self, mock_set_power): + mock_set_power.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.set_power_state(task, states.POWER_ON) + mock_set_power.assert_called_once_with(task, states.POWER_ON) + + @mock.patch.object(irmc_power, '_set_power_state') + @mock.patch.object(irmc_power.IRMCPower, 'get_power_state') + def test_reboot(self, mock_get_power, mock_set_power): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.reboot(task) + mock_set_power.assert_called_once_with(task, states.REBOOT) diff --git a/ironic/tests/drivers/third_party_driver_mocks.py b/ironic/tests/drivers/third_party_driver_mocks.py old mode 100644 new mode 100755 index 404d7fe07d..40cda34829 --- a/ironic/tests/drivers/third_party_driver_mocks.py +++ b/ironic/tests/drivers/third_party_driver_mocks.py @@ -26,6 +26,7 @@ Current list of mocked libraries: - ipminative - proliantutils - pysnmp +- scciclient """ import sys @@ -141,3 +142,22 @@ if not pysnmp: # external library has been mocked if 'ironic.drivers.modules.snmp' in sys.modules: reload(sys.modules['ironic.drivers.modules.snmp']) + + +# attempt to load the external 'scciclient' library, which is required by +# the optional drivers.modules.irmc module +scciclient = importutils.try_import('scciclient') +if not scciclient: + mock_scciclient = mock.MagicMock() + sys.modules['scciclient'] = mock_scciclient + sys.modules['scciclient.irmc'] = mock_scciclient.irmc + sys.modules['scciclient.irmc.scci'] = mock.MagicMock( + POWER_OFF=mock.sentinel.POWER_OFF, + POWER_ON=mock.sentinel.POWER_ON, + POWER_RESET=mock.sentinel.POWER_RESET) + + +# if anything has loaded the iRMC driver yet, reload it now that the +# external library has been mocked +if 'ironic.drivers.modules.irmc' in sys.modules: + reload(sys.modules['ironic.drivers.modules.irmc']) diff --git a/setup.cfg b/setup.cfg old mode 100644 new mode 100755 index 539fd2a574..174b5a231d --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,7 @@ ironic.drivers = fake_ilo = ironic.drivers.fake:FakeIloDriver fake_drac = ironic.drivers.fake:FakeDracDriver fake_snmp = ironic.drivers.fake:FakeSNMPDriver + fake_irmc = ironic.drivers.fake:FakeIRMCDriver iscsi_ilo = ironic.drivers.ilo:IloVirtualMediaIscsiDriver pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver @@ -58,6 +59,7 @@ ironic.drivers = pxe_ilo = ironic.drivers.pxe:PXEAndIloDriver pxe_drac = ironic.drivers.drac:PXEDracDriver pxe_snmp = ironic.drivers.pxe:PXEAndSNMPDriver + pxe_irmc = ironic.drivers.pxe:PXEAndIRMCDriver ironic.database.migration_backend = sqlalchemy = ironic.db.sqlalchemy.migration