From e1109c9b99c420918b2ada987c1da8d6827cc3bc Mon Sep 17 00:00:00 2001 From: Devananda van der Veen Date: Mon, 13 May 2013 00:51:36 -0700 Subject: [PATCH] Update IPMI driver for new base class. Fix IPMI unit tests, and add missing power states and exceptions. --- ironic/common/exception.py | 6 ++- ironic/common/states.py | 6 +++ ironic/drivers/ipmi.py | 83 +++++++++++++++++------------ ironic/tests/drivers/__init__.py | 16 ++++++ ironic/tests/drivers/test_ipmi.py | 86 +++++++++++++++++++++---------- 5 files changed, 136 insertions(+), 61 deletions(-) create mode 100644 ironic/tests/drivers/__init__.py diff --git a/ironic/common/exception.py b/ironic/common/exception.py index 40a56f9501..6c3ad17891 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -31,7 +31,7 @@ from oslo.config import cfg from ironic.common import safe_utils from ironic.openstack.common import excutils from ironic.openstack.common import log as logging - +from ironic.openstack.common.gettextutils import _ LOG = logging.getLogger(__name__) @@ -234,3 +234,7 @@ class NodeNotFound(NotFound): class InterfaceNotFound(NotFound): message = _("Interface %(iface)s could not be found.") + + +class PowerStateFailure(IronicException): + message = _("Failed to set node power state to %(pstate)s.") diff --git a/ironic/common/states.py b/ironic/common/states.py index c669b4116b..8c1785c630 100644 --- a/ironic/common/states.py +++ b/ironic/common/states.py @@ -26,6 +26,7 @@ health. """ +NOSTATE = None NULL = None INIT = 'initializing' ACTIVE = 'active' @@ -35,3 +36,8 @@ DEPLOYFAIL = 'deploy failed' DEPLOYDONE = 'deploy complete' DELETED = 'deleted' ERROR = 'error' + +POWER_ON = 'power on' +POWER_OFF = 'power off' +REBOOTING = 'rebooting' +SUSPEND = 'suspended' diff --git a/ironic/drivers/ipmi.py b/ironic/drivers/ipmi.py index cfa4e01ef9..4ac0f4ca2f 100644 --- a/ironic/drivers/ipmi.py +++ b/ironic/drivers/ipmi.py @@ -31,7 +31,7 @@ from ironic.common import exception from ironic.common import paths from ironic.common import states from ironic.common import utils -from ironic.manager import base +from ironic.drivers import base from ironic.openstack.common import jsonutils as json from ironic.openstack.common import log as logging from ironic.openstack.common import loopingcall @@ -48,7 +48,7 @@ opts = [ help='path to directory stores pidfiles of baremetal_terminal'), cfg.IntOpt('ipmi_power_retry', default=5, - help='maximal number of retries for IPMI operations'), + help='Maximum seconds to retry IPMI operations'), ] CONF = cfg.CONF @@ -83,13 +83,12 @@ def _get_console_pid(node_id): return None -class IPMI(base.PowerManager): - """IPMI Power Driver for Baremetal Nova Compute - - This PowerManager class provides mechanism for controlling the power state - of physical hardware via IPMI calls. It also provides serial console access - where available. +class IPMIPowerDriver(base.BMCDriver): + """Generic IPMI Power Driver + This BMCDriver class provides mechanism for controlling the power state + of physical hardware via IPMI calls. It also provides console access + for some supported hardware. """ def __init__(self, node, **kwargs): @@ -135,19 +134,16 @@ class IPMI(base.PowerManager): finally: utils.delete_if_exists(pwfile) - def _is_power(self, state): - out_err = self._exec_ipmitool("power status") - return out_err[0] == ("Chassis Power is %s\n" % state) - def _power_on(self): """Turn the power to this node ON.""" def _wait_for_power_on(): """Called at an interval until the node's power is on.""" - if self._is_power("on"): - self.state = states.ACTIVE + self._update_state() + if self.state == states.POWER_ON: raise loopingcall.LoopingCallDone() + if self.retries > CONF.ipmi_power_retry: self.state = states.ERROR raise loopingcall.LoopingCallDone() @@ -159,7 +155,7 @@ class IPMI(base.PowerManager): self.retries = 0 timer = loopingcall.FixedIntervalLoopingCall(_wait_for_power_on) - timer.start(interval=0.5).wait() + timer.start(interval=1).wait() def _power_off(self): """Turn the power to this node OFF.""" @@ -167,9 +163,10 @@ class IPMI(base.PowerManager): def _wait_for_power_off(): """Called at an interval until the node's power is off.""" - if self._is_power("off"): - self.state = states.DELETED + self._update_state() + if self.state == states.POWER_OFF: raise loopingcall.LoopingCallDone() + if self.retries > CONF.ipmi_power_retry: self.state = states.ERROR raise loopingcall.LoopingCallDone() @@ -181,37 +178,55 @@ class IPMI(base.PowerManager): self.retries = 0 timer = loopingcall.FixedIntervalLoopingCall(_wait_for_power_off) - timer.start(interval=0.5).wait() + timer.start(interval=1).wait() def _set_pxe_for_next_boot(self): + # FIXME: raise exception, not just log + # FIXME: make into a public set-boot function try: self._exec_ipmitool("chassis bootdev pxe") except Exception: LOG.exception(_("IPMI set next bootdev failed")) - def activate_node(self): - """Turns the power to node ON.""" - if self._is_power("on") and self.state == states.ACTIVE: - LOG.warning(_("Activate node called, but node %s " - "is already active") % self.address) - self._set_pxe_for_next_boot() - self._power_on() + def _update_state(self): + # FIXME: better error and other-state handling + out_err = self._exec_ipmitool("power status") + if out_err[0] == "Chassis Power is on\n": + self.state = states.POWER_ON + elif out_err[0] == "Chassis Power is off\n": + self.state = states.POWER_OFF + else: + self.state = states.ERROR + + def get_power_state(self): + """Checks and returns current power state.""" + self._update_state() return self.state - def reboot_node(self): + def set_power_state(self, pstate): + """Turn the power on or off.""" + if self.state and self.state == pstate: + LOG.warning(_("set_power_state called with current state.")) + + if pstate == states.POWER_ON: + self._set_pxe_for_next_boot() + self._power_on() + elif pstate == states.POWER_OFF: + self._power_off() + else: + LOG.error(_("set_power_state called with invalid pstate.")) + + if self.state != pstate: + raise exception.PowerStateFailure(pstate=pstate) + + def reboot(self): """Cycles the power to a node.""" self._power_off() self._set_pxe_for_next_boot() self._power_on() - return self.state - def deactivate_node(self): - """Turns the power to node OFF, regardless of current state.""" - self._power_off() - return self.state - - def is_power_on(self): - return self._is_power("on") + if self.state != states.POWER_ON: + raise exception.PowerStateFailure(pstate=states.POWER_ON) def start_console(self): if not self.port: diff --git a/ironic/tests/drivers/__init__.py b/ironic/tests/drivers/__init__.py new file mode 100644 index 0000000000..56425d0fce --- /dev/null +++ b/ironic/tests/drivers/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# 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. diff --git a/ironic/tests/drivers/test_ipmi.py b/ironic/tests/drivers/test_ipmi.py index d6a36cf5ba..383ce207b0 100644 --- a/ironic/tests/drivers/test_ipmi.py +++ b/ironic/tests/drivers/test_ipmi.py @@ -29,7 +29,7 @@ from ironic import test from ironic.common import states from ironic.common import utils from ironic.tests.db import utils as db_utils -from ironic.manager import ipmi +from ironic.drivers import ipmi CONF = cfg.CONF @@ -39,7 +39,7 @@ class BareMetalIPMITestCase(test.TestCase): def setUp(self): super(BareMetalIPMITestCase, self).setUp() self.node = db_utils.get_test_node() - self.ipmi = ipmi.IPMI(self.node) + self.ipmi = ipmi.IPMIPowerDriver(self.node) def test_construct(self): self.assertEqual(self.ipmi.node_id, 123) @@ -81,32 +81,44 @@ class BareMetalIPMITestCase(test.TestCase): self.ipmi._exec_ipmitool('A B C') self.mox.VerifyAll() - def test_is_power(self): + def test_update_state_on(self): self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') self.ipmi._exec_ipmitool("power status").AndReturn( ["Chassis Power is on\n"]) self.mox.ReplayAll() - self.ipmi._is_power("on") + self.ipmi.state = states.NOSTATE + self.ipmi._update_state() self.mox.VerifyAll() + self.assertEqual(self.ipmi.state, states.POWER_ON) - def test_power_already_on(self): - self.flags(ipmi_power_retry=0) + def test_update_state_off(self): self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') - self.ipmi._exec_ipmitool("power status").AndReturn( - ["Chassis Power is on\n"]) + ["Chassis Power is off\n"]) self.mox.ReplayAll() - self.ipmi.state = states.DELETED - self.ipmi._power_on() + self.ipmi.state = states.NOSTATE + self.ipmi._update_state() self.mox.VerifyAll() - self.assertEqual(self.ipmi.state, states.ACTIVE) + self.assertEqual(self.ipmi.state, states.POWER_OFF) - def test_power_on_ok(self): + def test_update_state_error(self): + self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is foobar\n"]) + self.mox.ReplayAll() + + self.ipmi.state = states.NOSTATE + self.ipmi._update_state() + self.mox.VerifyAll() + self.assertEqual(self.ipmi.state, states.ERROR) + + def test_set_power_on_ok(self): self.flags(ipmi_power_retry=0) self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') + self.ipmi._exec_ipmitool("set bootdev pxe").AndReturn([]) self.ipmi._exec_ipmitool("power status").AndReturn( ["Chassis Power is off\n"]) self.ipmi._exec_ipmitool("power on").AndReturn([]) @@ -114,10 +126,26 @@ class BareMetalIPMITestCase(test.TestCase): ["Chassis Power is on\n"]) self.mox.ReplayAll() - self.ipmi.state = states.DELETED - self.ipmi._power_on() + self.ipmi.state = states.POWER_OFF + self.ipmi.set_power_state(states.POWER_ON) self.mox.VerifyAll() - self.assertEqual(self.ipmi.state, states.ACTIVE) + self.assertEqual(self.ipmi.state, states.POWER_ON) + + def test_set_power_off_ok(self): + self.flags(ipmi_power_retry=0) + self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') + + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is on\n"]) + self.ipmi._exec_ipmitool("power off").AndReturn([]) + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is off\n"]) + self.mox.ReplayAll() + + self.ipmi.state = states.POWER_ON + self.ipmi._power_off() + self.mox.VerifyAll() + self.assertEqual(self.ipmi.state, states.POWER_OFF) def test_power_on_fail(self): self.flags(ipmi_power_retry=0) @@ -130,7 +158,7 @@ class BareMetalIPMITestCase(test.TestCase): ["Chassis Power is off\n"]) self.mox.ReplayAll() - self.ipmi.state = states.DELETED + self.ipmi.state = states.POWER_OFF self.ipmi._power_on() self.mox.VerifyAll() self.assertEqual(self.ipmi.state, states.ERROR) @@ -152,27 +180,33 @@ class BareMetalIPMITestCase(test.TestCase): ["Chassis Power is off\n"]) self.mox.ReplayAll() - self.ipmi.state = states.DELETED + self.ipmi.state = states.POWER_OFF self.ipmi._power_on() self.mox.VerifyAll() self.assertEqual(self.ipmi.state, states.ERROR) self.assertEqual(self.ipmi.retries, 3) - def test_power_off_ok(self): - self.flags(ipmi_power_retry=0) + def test_get_power_state(self): self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') - - self.ipmi._exec_ipmitool("power status").AndReturn( - ["Chassis Power is on\n"]) - self.ipmi._exec_ipmitool("power off").AndReturn([]) self.ipmi._exec_ipmitool("power status").AndReturn( ["Chassis Power is off\n"]) + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is on\n"]) self.mox.ReplayAll() - self.ipmi.state = states.ACTIVE - self.ipmi._power_off() + self.ipmi.state = states.POWER_OFF + pstate = self.ipmi.get_power_state() + self.assertEqual(pstate, states.POWER_OFF) + + self.ipmi.state = states.POWER_ON + pstate = self.ipmi.get_power_state() + self.assertEqual(pstate, states.POWER_ON) + self.mox.VerifyAll() - self.assertEqual(self.ipmi.state, states.DELETED) + + def test_reboot(self): + # TODO + pass def test_get_console_pid_path(self): self.flags(terminal_pid_dir='/tmp')