diff options
author | linggao <linggao@us.ibm.com> | 2013-09-17 19:45:35 +0000 |
---|---|---|
committer | linggao <linggao@us.ibm.com> | 2013-09-25 20:43:44 +0000 |
commit | 2089d3c01b4168e07d303e14da9a05608e921f28 (patch) | |
tree | 397536d13fd9c3a4da6cf48cf52d47e08e067d57 | |
parent | 98670162c74c245cb671ca53c934f0533b3d570a (diff) | |
download | ironic-2089d3c01b4168e07d303e14da9a05608e921f28.tar.gz |
Add native ipmi driver
Implemented a power driver for baremetal node that uses the native
python ipmi driver called pyghmi.
Change-Id: I41954ebba7c8fa2873a7f1a1f73a4511b0afa301
Implements: blueprint native-ipmi
-rw-r--r-- | ironic/common/exception.py | 2 | ||||
-rw-r--r-- | ironic/drivers/fake.py | 10 | ||||
-rw-r--r-- | ironic/drivers/modules/ipminative.py | 294 | ||||
-rw-r--r-- | ironic/drivers/pxe.py | 19 | ||||
-rw-r--r-- | ironic/tests/drivers/test_ipminative.py | 274 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | setup.cfg | 2 |
7 files changed, 601 insertions, 1 deletions
diff --git a/ironic/common/exception.py b/ironic/common/exception.py index 10b74fe33..1ea8d327d 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -307,7 +307,7 @@ class ChassisNotEmpty(IronicException): class IPMIFailure(IronicException): - message = _("IPMI command failed: %(cmd)s.") + message = _("IPMI call failed: %(cmd)s.") class SSHConnectFailed(IronicException): diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py index 3479d2eb7..73b0c260c 100644 --- a/ironic/drivers/fake.py +++ b/ironic/drivers/fake.py @@ -20,6 +20,7 @@ Fake drivers used in testing. from ironic.drivers import base from ironic.drivers.modules import fake +from ironic.drivers.modules import ipminative from ironic.drivers.modules import ipmitool from ironic.drivers.modules import pxe from ironic.drivers.modules import ssh @@ -58,3 +59,12 @@ class FakeSSHDriver(base.BaseDriver): def __init__(self): self.power = ssh.SSHPower() self.deploy = fake.FakeDeploy() + + +class FakeIPMINativeDriver(base.BaseDriver): + """Example implementation of a Driver.""" + + def __init__(self): + self.power = ipminative.NativeIPMIPower() + self.deploy = fake.FakeDeploy() + self.vendor = self.power diff --git a/ironic/drivers/modules/ipminative.py b/ironic/drivers/modules/ipminative.py new file mode 100644 index 000000000..ec1170e39 --- /dev/null +++ b/ironic/drivers/modules/ipminative.py @@ -0,0 +1,294 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# coding=utf-8 + +# Copyright 2013 International Business Machines Corporation +# All Rights Reserved. +# +# 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. + +""" +Ironic Native IPMI power manager. +""" + +from oslo.config import cfg + +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers import base +from ironic.openstack.common import log as logging +from pyghmi import exceptions as pyghmi_exception +from pyghmi.ipmi import command as ipmi_command + +opts = [ + cfg.IntOpt('native_ipmi_waiting_time', + default=300, + help='Waiting time for a native ipmi command in seconds'), + ] + +CONF = cfg.CONF +CONF.register_opts(opts) + +LOG = logging.getLogger(__name__) + + +def _parse_driver_info(node): + """Gets the bmc access info for the given node. + :raises: InvalidParameterValue when required ipmi credentials + are missing. + """ + + info = node.get('driver_info', '') + ipmi_info = info.get('ipmi') + bmc_info = {} + bmc_info['address'] = ipmi_info.get('address') + bmc_info['username'] = ipmi_info.get('username') + bmc_info['password'] = ipmi_info.get('password') + + # address, username and password must be present + missing_info = [key for key in bmc_info if not bmc_info[key]] + if missing_info: + raise exception.InvalidParameterValue(_( + "The following IPMI credentials are not supplied" + " to IPMI driver: %s." + ) % missing_info) + + # get additonal info + bmc_info['uuid'] = node.get('uuid') + + return bmc_info + + +def _power_on(driver_info): + """Turn the power on for this node. + + :param driver_info: the bmc access info for a node. + :returns: power state POWER_ON, one of :class:`ironic.common.states`. + :raises: IPMIFailure when the native ipmi call fails. + :raises: PowerStateFailure when invalid power state is returned + from ipmi. + """ + + msg = _("IPMI power on failed for node %(node_id)s with the " + "following error: %(error)s") + try: + ipmicmd = ipmi_command.Command(bmc=driver_info['address'], + userid=driver_info['username'], + password=driver_info['password']) + wait = CONF.native_ipmi_waiting_time + ret = ipmicmd.set_power('on', wait) + except pyghmi_exception.IpmiException as e: + LOG.warning(msg % {'node_id': driver_info['uuid'], 'error': str(e)}) + raise exception.IPMIFailure(cmd=str(e)) + + state = ret.get('powerstate') + if state == 'on': + return states.POWER_ON + else: + LOG.warning(msg % {'node_id': driver_info['uuid'], 'error': ret}) + raise exception.PowerStateFailure(pstate=state) + + +def _power_off(driver_info): + """Turn the power off for this node. + + :param driver_info: the bmc access info for a node. + :returns: power state POWER_OFF, one of :class:`ironic.common.states`. + :raises: IPMIFailure when the native ipmi call fails. + :raises: PowerStateFailure when invalid power state is returned + from ipmi. + """ + + msg = _("IPMI power off failed for node %(node_id)s with the " + "following error: %(error)s") + try: + ipmicmd = ipmi_command.Command(bmc=driver_info['address'], + userid=driver_info['username'], + password=driver_info['password']) + wait = CONF.native_ipmi_waiting_time + ret = ipmicmd.set_power('off', wait) + except pyghmi_exception.IpmiException as e: + LOG.warning(msg % {'node_id': driver_info['uuid'], 'error': str(e)}) + raise exception.IPMIFailure(cmd=str(e)) + + state = ret.get('powerstate') + if state == 'off': + return states.POWER_OFF + else: + LOG.warning(msg % {'node_id': driver_info['uuid'], 'error': ret}) + raise exception.PowerStateFailure(pstate=state) + + +def _reboot(driver_info): + """Reboot this node. + + If the power is off, turn it on. If the power is on, reset it. + + :param driver_info: the bmc access info for a node. + :returns: power state POWER_ON, one of :class:`ironic.common.states`. + :raises: IPMIFailure when the native ipmi call fails. + :raises: PowerStateFailure when invalid power state is returned + from ipmi. + """ + + msg = _("IPMI power reboot failed for node %(node_id)s with the " + "following error: %(error)s") + try: + ipmicmd = ipmi_command.Command(bmc=driver_info['address'], + userid=driver_info['username'], + password=driver_info['password']) + wait = CONF.native_ipmi_waiting_time + ret = ipmicmd.set_power('boot', wait) + except pyghmi_exception.IpmiException as e: + LOG.warning(msg % {'node_id': driver_info['uuid'], 'error': str(e)}) + raise exception.IPMIFailure(cmd=str(e)) + + state = ret.get('powerstate') + if state == 'on': + return states.POWER_ON + else: + LOG.warning(msg % {'node_id': driver_info['uuid'], 'error': ret}) + raise exception.PowerStateFailure(pstate=state) + + +def _power_status(driver_info): + """Get the power status for this node. + + :param driver_info: the bmc access info for a node. + :returns: power state POWER_ON, POWER_OFF or ERROR defined in + :class:`ironic.common.states`. + :raises: IPMIFailure when the native ipmi call fails. + """ + + try: + ipmicmd = ipmi_command.Command(bmc=driver_info['address'], + userid=driver_info['username'], + password=driver_info['password']) + ret = ipmicmd.get_power() + except pyghmi_exception.IpmiException as e: + LOG.warning(_("IPMI get power state failed for node %(node_id)s " + "with the following error: %(error)s") + % {'node_id': driver_info['uuid'], 'error': str(e)}) + raise exception.IPMIFailure(cmd=str(e)) + + state = ret.get('powerstate') + if state == 'on': + return states.POWER_ON + elif state == 'off': + return states.POWER_OFF + else: + # NOTE(linggao): Do not throw an exception here because it might + # return other valid values. It is up to the caller to decide + # what to do. + LOG.warning(_("IPMI get power state for node %(node_id)s returns the " + "following details: %(detail)s") + % {'node_id': driver_info['uuid'], 'detail': ret}) + return states.ERROR + + +class NativeIPMIPower(base.PowerInterface): + """The power driver using native python-ipmi library.""" + + def validate(self, node): + """Check that node['driver_info'] contains IPMI credentials. + + :param node: a single node to validate. + :raises: InvalidParameterValue when required ipmi credentials + are missing. + """ + _parse_driver_info(node) + + def get_power_state(self, task, node): + """Get the current power state. + + :param task: a TaskManager instance. + :param node: the node info. + :returns: power state POWER_ON, POWER_OFF or ERROR defined in + :class:`ironic.common.states`. + :raises: InvalidParameterValue when required ipmi credentials + are missing. + :raises: IPMIFailure when the native ipmi call fails. + """ + driver_info = _parse_driver_info(node) + return _power_status(driver_info) + + @task_manager.require_exclusive_lock + def set_power_state(self, task, node, pstate): + """Turn the power on or off. + + :param task: a TaskManager instance. + :param node: the node info. + :param pstate: a power state that will be set on the given node. + :raises: IPMIFailure when the native ipmi call fails. + :raises: InvalidParameterValue when an invalid power state + is specified or required ipmi credentials are missing. + :raises: PowerStateFailure when invalid power state is returned + from ipmi. + """ + + driver_info = _parse_driver_info(node) + + if pstate == states.POWER_ON: + _power_on(driver_info) + elif pstate == states.POWER_OFF: + _power_off(driver_info) + else: + raise exception.InvalidParameterValue(_( + "set_power_state called with an invalid power state: %s." + ) % pstate) + + @task_manager.require_exclusive_lock + def reboot(self, task, node): + """Cycles the power to a node. + + :param task: a TaskManager instance. + :param node: the node info. + :raises: IPMIFailure when the native ipmi call fails. + :raises: InvalidParameterValue when required ipmi credentials + are missing. + :raises: PowerStateFailure when invalid power state is returned + from ipmi. + """ + + driver_info = _parse_driver_info(node) + _reboot(driver_info) + + @task_manager.require_exclusive_lock + def _set_boot_device(self, task, node, device, persistent=False): + """Set the boot device for a node. + + :param task: a TaskManager instance. + :param node: The Node. + :param device: Boot device. One of [net, network, pxe, hd, cd, + cdrom, dvd, floppy, default, setup, f1] + :param persistent: Whether to set next-boot, or make the change + permanent. Default: False. + :raises: InvalidParameterValue if an invalid boot device is specified + or required ipmi credentials are missing. + :raises: IPMIFailure when the native ipmi call fails. + """ + + if device not in ipmi_command.boot_devices: + raise exception.InvalidParameterValue(_( + "Invalid boot device %s specified.") % device) + driver_info = _parse_driver_info(node) + try: + ipmicmd = ipmi_command.Command(bmc=driver_info['address'], + userid=driver_info['username'], + password=driver_info['password']) + ipmicmd.set_bootdev(device) + except pyghmi_exception.IpmiException as e: + LOG.warning(_("IPMI set boot device failed for node %(node_id)s " + "with the following error: %(error)s") + % {'node_id': driver_info['uuid'], 'error': str(e)}) + raise exception.IPMIFailure(cmd=str(e)) diff --git a/ironic/drivers/pxe.py b/ironic/drivers/pxe.py index 5b2b45a2d..262d085bf 100644 --- a/ironic/drivers/pxe.py +++ b/ironic/drivers/pxe.py @@ -19,6 +19,7 @@ PXE Driver and supporting meta-classes. """ from ironic.drivers import base +from ironic.drivers.modules import ipminative from ironic.drivers.modules import ipmitool from ironic.drivers.modules import pxe from ironic.drivers.modules import ssh @@ -57,3 +58,21 @@ class PXEAndSSHDriver(base.BaseDriver): self.deploy = pxe.PXEDeploy() self.rescue = self.deploy self.vendor = None + + +class PXEAndIPMINativeDriver(base.BaseDriver): + """PXE + Native IPMI driver. + + This driver implements the `core` functionality, combining + :class:ironic.drivers.modules.ipminative.NativeIPMIPower for power + on/off and reboot with + :class:ironic.driver.modules.pxe.PXE for image deployment. + Implementations are in those respective classes; + this class is merely the glue between them. + """ + + def __init__(self): + self.power = ipminative.NativeIPMIPower() + self.deploy = pxe.PXEDeploy() + self.rescue = self.deploy + self.vendor = pxe.IPMIVendorPassthru() diff --git a/ironic/tests/drivers/test_ipminative.py b/ironic/tests/drivers/test_ipminative.py new file mode 100644 index 000000000..31388ed7b --- /dev/null +++ b/ironic/tests/drivers/test_ipminative.py @@ -0,0 +1,274 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# coding=utf-8 + +# Copyright 2013 International Business Machines Corporation +# All Rights Reserved. +# +# 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 Native IPMI power driver module. +""" + +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.db import api as db_api +from ironic.drivers.modules import ipminative +from ironic.tests import base +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 oslo.config import cfg +from pyghmi.ipmi import command as ipmi_command + + +CONF = cfg.CONF + + +class IPMINativePrivateMethodTestCase(base.TestCase): + """Test cases for ipminative private methods.""" + + def setUp(self): + super(IPMINativePrivateMethodTestCase, self).setUp() + n = db_utils.get_test_node( + driver='fake_ipminative', + driver_info=db_utils.ipmi_info) + self.dbapi = db_api.get_instance() + self.node = self.dbapi.create_node(n) + self.info = ipminative._parse_driver_info(self.node) + self.mox.StubOutWithMock(ipmi_command.Command, '__init__') + ipmi_command.Command.__init__(bmc=self.info.get('address'), + userid=self.info.get('username'), + password=self.info.get('password')).AndReturn(None) + + def test__parse_driver_info(self): + # make sure we get back the expected things + self.assertIsNotNone(self.info.get('address')) + self.assertIsNotNone(self.info.get('username')) + self.assertIsNotNone(self.info.get('password')) + self.assertIsNotNone(self.info.get('uuid')) + + self.mox.ReplayAll() + ipmi_command.Command(bmc=self.info.get('address'), + userid=self.info.get('username'), + password=self.info.get('password')) + self.mox.VerifyAll() + + # make sure error is raised when info, eg. username, is missing + _driver_info = { + 'ipmi': { + "address": "2.2.3.4", + "password": "fake", + } + } + node = db_utils.get_test_node(driver_info=_driver_info) + self.assertRaises(exception.InvalidParameterValue, + ipminative._parse_driver_info, + node) + + def test__power_status_on(self): + self.mox.StubOutWithMock(ipmi_command.Command, 'get_power') + ipmi_command.Command.get_power().AndReturn({'powerstate': 'on'}) + self.mox.ReplayAll() + + state = ipminative._power_status(self.info) + self.mox.VerifyAll() + self.assertEqual(state, states.POWER_ON) + + def test__power_status_off(self): + self.mox.StubOutWithMock(ipmi_command.Command, 'get_power') + ipmi_command.Command.get_power().AndReturn({'powerstate': 'off'}) + self.mox.ReplayAll() + + state = ipminative._power_status(self.info) + self.mox.VerifyAll() + self.assertEqual(state, states.POWER_OFF) + + def test__power_status_error(self): + self.mox.StubOutWithMock(ipmi_command.Command, 'get_power') + ipmi_command.Command.get_power().AndReturn({'powerstate': 'Error'}) + self.mox.ReplayAll() + + state = ipminative._power_status(self.info) + self.mox.VerifyAll() + self.assertEqual(state, states.ERROR) + + def test__power_on(self): + self.mox.StubOutWithMock(ipmi_command.Command, 'set_power') + ipmi_command.Command.set_power('on', 300).AndReturn( + {'powerstate': 'on'}) + self.mox.ReplayAll() + + state = ipminative._power_on(self.info) + self.mox.VerifyAll() + self.assertEqual(state, states.POWER_ON) + + def test__power_off(self): + self.mox.StubOutWithMock(ipmi_command.Command, 'set_power') + ipmi_command.Command.set_power('off', 300).AndReturn( + {'powerstate': 'off'}) + self.mox.ReplayAll() + + state = ipminative._power_off(self.info) + self.mox.VerifyAll() + self.assertEqual(state, states.POWER_OFF) + + def test__reboot(self): + self.mox.StubOutWithMock(ipmi_command.Command, 'set_power') + ipmi_command.Command.set_power('boot', 300).AndReturn( + {'powerstate': 'on'}) + self.mox.ReplayAll() + + state = ipminative._reboot(self.info) + self.mox.VerifyAll() + self.assertEqual(state, states.POWER_ON) + + +class IPMINativeDriverTestCase(db_base.DbTestCase): + """Test cases for ipminative.NativeIPMIPower class functions. + """ + + def setUp(self): + super(IPMINativeDriverTestCase, self).setUp() + self.dbapi = db_api.get_instance() + self.driver = mgr_utils.get_mocked_node_manager( + driver='fake_ipminative') + + n = db_utils.get_test_node( + driver='fake_ipminative', + driver_info=db_utils.ipmi_info) + self.dbapi = db_api.get_instance() + self.node = self.dbapi.create_node(n) + self.info = ipminative._parse_driver_info(self.node) + + def test_get_power_state(self): + + self.mox.StubOutWithMock(ipmi_command.Command, 'get_power') + self.mox.StubOutWithMock(ipmi_command.Command, '__init__') + ipmi_command.Command.__init__(bmc=self.info.get('address'), + userid=self.info.get('username'), + password=self.info.get('password')).AndReturn(None) + ipmi_command.Command.get_power().AndReturn({'powerstate': 'off'}) + ipmi_command.Command.__init__(bmc=self.info.get('address'), + userid=self.info.get('username'), + password=self.info.get('password')).AndReturn(None) + ipmi_command.Command.get_power().AndReturn({'powerstate': 'on'}) + ipmi_command.Command.__init__(bmc=self.info.get('address'), + userid=self.info.get('username'), + password=self.info.get('password')).AndReturn(None) + ipmi_command.Command.get_power().AndReturn({'powerstate': 'error'}) + self.mox.ReplayAll() + + pstate = self.driver.power.get_power_state(None, self.node) + self.assertEqual(pstate, states.POWER_OFF) + + pstate = self.driver.power.get_power_state(None, self.node) + self.assertEqual(pstate, states.POWER_ON) + + pstate = self.driver.power.get_power_state(None, self.node) + self.assertEqual(pstate, states.ERROR) + + self.mox.VerifyAll() + + def test_set_power_on_ok(self): + self.mox.StubOutWithMock(ipminative, '_power_on') + self.mox.StubOutWithMock(ipminative, '_power_off') + + ipminative._power_on(self.info).AndReturn(states.POWER_ON) + self.mox.ReplayAll() + + with task_manager.acquire([self.node['uuid']]) as task: + self.driver.power.set_power_state( + task, self.node, states.POWER_ON) + self.mox.VerifyAll() + + def test_set_power_off_ok(self): + self.mox.StubOutWithMock(ipminative, '_power_on') + self.mox.StubOutWithMock(ipminative, '_power_off') + + ipminative._power_off(self.info).AndReturn(states.POWER_OFF) + self.mox.ReplayAll() + + with task_manager.acquire([self.node['uuid']]) as task: + self.driver.power.set_power_state( + task, self.node, states.POWER_OFF) + self.mox.VerifyAll() + + def test_set_power_on_fail(self): + self.mox.StubOutWithMock(ipmi_command.Command, 'set_power') + self.mox.StubOutWithMock(ipmi_command.Command, '__init__') + ipmi_command.Command.__init__(bmc=self.info.get('address'), + userid=self.info.get('username'), + password=self.info.get('password')).AndReturn(None) + ipmi_command.Command.set_power('on', 300).AndReturn( + {'powerstate': 'error'}) + self.mox.ReplayAll() + + with task_manager.acquire([self.node['uuid']]) as task: + self.assertRaises(exception.PowerStateFailure, + self.driver.power.set_power_state, + task, + self.node, + states.POWER_ON) + self.mox.VerifyAll() + + def test_set_boot_device_ok(self): + self.mox.StubOutWithMock(ipmi_command.Command, 'set_bootdev') + self.mox.StubOutWithMock(ipmi_command.Command, '__init__') + ipmi_command.Command.__init__(bmc=self.info.get('address'), + userid=self.info.get('username'), + password=self.info.get('password')).AndReturn(None) + ipmi_command.Command.set_bootdev('pxe').AndReturn(None) + self.mox.ReplayAll() + + with task_manager.acquire([self.node['uuid']]) as task: + self.driver.power._set_boot_device(task, self.node, 'pxe') + self.mox.VerifyAll() + + def test_set_boot_device_bad_device(self): + with task_manager.acquire([self.node['uuid']]) as task: + self.assertRaises(exception.InvalidParameterValue, + self.driver.power._set_boot_device, + task, + self.node, + 'fake-device') + + def test_reboot_ok(self): + self.mox.StubOutWithMock(ipminative, '_reboot') + + ipminative._reboot(self.info).AndReturn(None) + self.mox.ReplayAll() + + with task_manager.acquire([self.node['uuid']]) as task: + self.driver.power.reboot(task, self.node) + + self.mox.VerifyAll() + + def test_reboot_fail(self): + self.mox.StubOutWithMock(ipmi_command.Command, 'set_power') + self.mox.StubOutWithMock(ipmi_command.Command, '__init__') + ipmi_command.Command.__init__(bmc=self.info.get('address'), + userid=self.info.get('username'), + password=self.info.get('password')).AndReturn(None) + ipmi_command.Command.set_power('boot', 300).AndReturn( + {'powerstate': 'error'}) + self.mox.ReplayAll() + + with task_manager.acquire([self.node['uuid']]) as task: + self.assertRaises(exception.PowerStateFailure, + self.driver.power.reboot, + task, + self.node) + + self.mox.VerifyAll() diff --git a/requirements.txt b/requirements.txt index d72aaa035..2c4c1bed6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,3 +24,4 @@ six<1.4.0 jsonpatch>=1.1 WSME>=0.5b2 Cheetah>=2.4.4 +pyghmi @@ -36,9 +36,11 @@ console_scripts = ironic.drivers = fake = ironic.drivers.fake:FakeDriver fake_ipmitool = ironic.drivers.fake:FakeIPMIToolDriver + fake_ipminative = ironic.drivers.fake:FakeIPMINativeDriver fake_ssh = ironic.drivers.fake:FakeSSHDriver fake_pxe = ironic.drivers.fake:FakePXEDriver pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver + pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver [pbr] |