summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDevananda van der Veen <devananda.vdv@gmail.com>2012-12-21 20:15:38 -0800
committerDevananda van der Veen <devananda.vdv@gmail.com>2012-12-28 14:53:55 -0800
commitf1bb1a213b63df050e98dbdeb1e7fb5ea8f3e05c (patch)
treea2a3425e17750f84e001bd601f1f75c2cb61a5f4
parent0279d846abd20904fa5a1d357fac6727005a2943 (diff)
downloadnova-f1bb1a213b63df050e98dbdeb1e7fb5ea8f3e05c.tar.gz
nova-f1bb1a213b63df050e98dbdeb1e7fb5ea8f3e05c.tar.xz
nova-f1bb1a213b63df050e98dbdeb1e7fb5ea8f3e05c.zip
Implement IPMI sub-driver for baremetal compute
This patch implements only the IPMI power manager for baremetal nova compute. Documentation will come in a separate patch. blueprint general-bare-metal-provisioning-framework Change-Id: I60ccfbf963d7bbf6f840e627396601b7bba80e7f
-rw-r--r--etc/nova/rootwrap.d/baremetal_compute_ipmi.filters9
-rw-r--r--nova/tests/baremetal/test_ipmi.py228
-rw-r--r--nova/virt/baremetal/ipmi.py257
3 files changed, 494 insertions, 0 deletions
diff --git a/etc/nova/rootwrap.d/baremetal_compute_ipmi.filters b/etc/nova/rootwrap.d/baremetal_compute_ipmi.filters
new file mode 100644
index 000000000..a2858cd11
--- /dev/null
+++ b/etc/nova/rootwrap.d/baremetal_compute_ipmi.filters
@@ -0,0 +1,9 @@
+# nova-rootwrap command filters for compute nodes
+# This file should be owned by (and only-writeable by) the root user
+
+[Filters]
+# nova/virt/baremetal/ipmi.py: 'ipmitool', ..
+ipmitool: CommandFilter, /usr/bin/ipmitool, root
+
+# nova/virt/baremetal/ipmi.py: 'kill', '-TERM', str(console_pid)
+kill_shellinaboxd: KillFilter, root, /usr/local/bin/shellinaboxd, -15, -TERM
diff --git a/nova/tests/baremetal/test_ipmi.py b/nova/tests/baremetal/test_ipmi.py
new file mode 100644
index 000000000..40ec43abd
--- /dev/null
+++ b/nova/tests/baremetal/test_ipmi.py
@@ -0,0 +1,228 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# coding=utf-8
+
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+# Copyright (c) 2012 NTT DOCOMO, INC.
+# 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 baremetal IPMI power manager.
+"""
+
+import os
+import stat
+import tempfile
+import time
+
+import mox
+
+from nova.openstack.common import cfg
+from nova import test
+from nova.tests.baremetal.db import utils as bm_db_utils
+from nova import utils
+from nova.virt.baremetal import baremetal_states
+from nova.virt.baremetal import ipmi
+from nova.virt.baremetal import utils as bm_utils
+from nova.virt.libvirt import utils as libvirt_utils
+
+CONF = cfg.CONF
+
+
+class BareMetalIPMITestCase(test.TestCase):
+
+ def setUp(self):
+ super(BareMetalIPMITestCase, self).setUp()
+ self.node = bm_db_utils.new_bm_node(
+ id=123,
+ pm_address='fake-address',
+ pm_user='fake-user',
+ pm_password='fake-password')
+ self.ipmi = ipmi.IPMI(self.node)
+
+ def test_construct(self):
+ self.assertEqual(self.ipmi.node_id, 123)
+ self.assertEqual(self.ipmi.address, 'fake-address')
+ self.assertEqual(self.ipmi.user, 'fake-user')
+ self.assertEqual(self.ipmi.password, 'fake-password')
+
+ def test_make_password_file(self):
+ pw_file = ipmi._make_password_file(self.node['pm_password'])
+ try:
+ self.assertTrue(os.path.isfile(pw_file))
+ self.assertEqual(os.stat(pw_file)[stat.ST_MODE] & 0777, 0600)
+ with open(pw_file, "r") as f:
+ pm_password = f.read()
+ self.assertEqual(pm_password, self.node['pm_password'])
+ finally:
+ os.unlink(pw_file)
+
+ def test_exec_ipmitool(self):
+ pw_file = '/tmp/password_file'
+
+ self.mox.StubOutWithMock(ipmi, '_make_password_file')
+ self.mox.StubOutWithMock(utils, 'execute')
+ self.mox.StubOutWithMock(bm_utils, 'unlink_without_raise')
+ ipmi._make_password_file(self.ipmi.password).AndReturn(pw_file)
+ args = [
+ 'ipmitool',
+ '-I', 'lanplus',
+ '-H', self.ipmi.address,
+ '-U', self.ipmi.user,
+ '-f', pw_file,
+ 'A', 'B', 'C',
+ ]
+ utils.execute(*args, attempts=3).AndReturn(('', ''))
+ bm_utils.unlink_without_raise(pw_file).AndReturn(None)
+ self.mox.ReplayAll()
+
+ self.ipmi._exec_ipmitool('A B C')
+ self.mox.VerifyAll()
+
+ def test_is_power(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.mox.VerifyAll()
+
+ def test_power_already_on(self):
+ self.flags(ipmi_power_retry=0, group='baremetal')
+ self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool')
+
+ self.ipmi._exec_ipmitool("power status").AndReturn(
+ ["Chassis Power is on\n"])
+ self.mox.ReplayAll()
+
+ self.ipmi.state = baremetal_states.DELETED
+ self.ipmi._power_on()
+ self.mox.VerifyAll()
+ self.assertEqual(self.ipmi.state, baremetal_states.ACTIVE)
+
+ def test_power_on_ok(self):
+ self.flags(ipmi_power_retry=0, group='baremetal')
+ self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool')
+
+ self.ipmi._exec_ipmitool("power status").AndReturn(
+ ["Chassis Power is off\n"])
+ self.ipmi._exec_ipmitool("power on").AndReturn([])
+ self.ipmi._exec_ipmitool("power status").AndReturn(
+ ["Chassis Power is on\n"])
+ self.mox.ReplayAll()
+
+ self.ipmi.state = baremetal_states.DELETED
+ self.ipmi._power_on()
+ self.mox.VerifyAll()
+ self.assertEqual(self.ipmi.state, baremetal_states.ACTIVE)
+
+ def test_power_on_fail(self):
+ self.flags(ipmi_power_retry=0, group='baremetal')
+ self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool')
+
+ self.ipmi._exec_ipmitool("power status").AndReturn(
+ ["Chassis Power is off\n"])
+ self.ipmi._exec_ipmitool("power on").AndReturn([])
+ self.ipmi._exec_ipmitool("power status").AndReturn(
+ ["Chassis Power is off\n"])
+ self.mox.ReplayAll()
+
+ self.ipmi.state = baremetal_states.DELETED
+ self.ipmi._power_on()
+ self.mox.VerifyAll()
+ self.assertEqual(self.ipmi.state, baremetal_states.ERROR)
+
+ def test_power_on_max_retries(self):
+ self.flags(ipmi_power_retry=2, group='baremetal')
+ self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool')
+
+ self.ipmi._exec_ipmitool("power status").AndReturn(
+ ["Chassis Power is off\n"])
+ self.ipmi._exec_ipmitool("power on").AndReturn([])
+ self.ipmi._exec_ipmitool("power status").AndReturn(
+ ["Chassis Power is off\n"])
+ self.ipmi._exec_ipmitool("power on").AndReturn([])
+ self.ipmi._exec_ipmitool("power status").AndReturn(
+ ["Chassis Power is off\n"])
+ self.ipmi._exec_ipmitool("power on").AndReturn([])
+ self.ipmi._exec_ipmitool("power status").AndReturn(
+ ["Chassis Power is off\n"])
+ self.mox.ReplayAll()
+
+ self.ipmi.state = baremetal_states.DELETED
+ self.ipmi._power_on()
+ self.mox.VerifyAll()
+ self.assertEqual(self.ipmi.state, baremetal_states.ERROR)
+ self.assertEqual(self.ipmi.retries, 3)
+
+ def test_power_off_ok(self):
+ self.flags(ipmi_power_retry=0, group='baremetal')
+ 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 = baremetal_states.ACTIVE
+ self.ipmi._power_off()
+ self.mox.VerifyAll()
+ self.assertEqual(self.ipmi.state, baremetal_states.DELETED)
+
+ def test_get_console_pid_path(self):
+ self.flags(terminal_pid_dir='/tmp', group='baremetal')
+ path = ipmi._get_console_pid_path(self.ipmi.node_id)
+ self.assertEqual(path, '/tmp/%s.pid' % self.ipmi.node_id)
+
+ def test_console_pid(self):
+ fd, path = tempfile.mkstemp()
+ with os.fdopen(fd, 'w') as f:
+ f.write("12345\n")
+
+ self.mox.StubOutWithMock(ipmi, '_get_console_pid_path')
+ ipmi._get_console_pid_path(self.ipmi.node_id).AndReturn(path)
+ self.mox.ReplayAll()
+
+ pid = ipmi._get_console_pid(self.ipmi.node_id)
+ bm_utils.unlink_without_raise(path)
+ self.mox.VerifyAll()
+ self.assertEqual(pid, 12345)
+
+ def test_console_pid_nan(self):
+ fd, path = tempfile.mkstemp()
+ with os.fdopen(fd, 'w') as f:
+ f.write("hello world\n")
+
+ self.mox.StubOutWithMock(ipmi, '_get_console_pid_path')
+ ipmi._get_console_pid_path(self.ipmi.node_id).AndReturn(path)
+ self.mox.ReplayAll()
+
+ pid = ipmi._get_console_pid(self.ipmi.node_id)
+ bm_utils.unlink_without_raise(path)
+ self.mox.VerifyAll()
+ self.assertTrue(pid is None)
+
+ def test_console_pid_file_not_found(self):
+ pid_path = ipmi._get_console_pid_path(self.ipmi.node_id)
+
+ self.mox.StubOutWithMock(os.path, 'exists')
+ os.path.exists(pid_path).AndReturn(False)
+ self.mox.ReplayAll()
+
+ pid = ipmi._get_console_pid(self.ipmi.node_id)
+ self.mox.VerifyAll()
+ self.assertTrue(pid is None)
diff --git a/nova/virt/baremetal/ipmi.py b/nova/virt/baremetal/ipmi.py
new file mode 100644
index 000000000..cc1704c7c
--- /dev/null
+++ b/nova/virt/baremetal/ipmi.py
@@ -0,0 +1,257 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# coding=utf-8
+
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+# Copyright (c) 2012 NTT DOCOMO, INC.
+# 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.
+
+"""
+Baremetal IPMI power manager.
+"""
+
+import os
+import stat
+import tempfile
+import time
+
+from nova.exception import InvalidParameterValue
+from nova.openstack.common import cfg
+from nova.openstack.common import log as logging
+from nova import utils
+from nova.virt.baremetal import baremetal_states
+from nova.virt.baremetal import base
+from nova.virt.baremetal import utils as bm_utils
+
+opts = [
+ cfg.StrOpt('terminal',
+ default='shellinaboxd',
+ help='path to baremetal terminal program'),
+ cfg.StrOpt('terminal_cert_dir',
+ default=None,
+ help='path to baremetal terminal SSL cert(PEM)'),
+ cfg.StrOpt('terminal_pid_dir',
+ default='$state_path/baremetal/console',
+ help='path to directory stores pidfiles of baremetal_terminal'),
+ cfg.IntOpt('ipmi_power_retry',
+ default=5,
+ help='maximal number of retries for IPMI operations'),
+ ]
+
+baremetal_group = cfg.OptGroup(name='baremetal',
+ title='Baremetal Options')
+
+CONF = cfg.CONF
+CONF.register_group(baremetal_group)
+CONF.register_opts(opts, baremetal_group)
+
+LOG = logging.getLogger(__name__)
+
+
+def _make_password_file(password):
+ fd, path = tempfile.mkstemp()
+ os.fchmod(fd, stat.S_IRUSR | stat.S_IWUSR)
+ with os.fdopen(fd, "w") as f:
+ f.write(password)
+ return path
+
+
+def _get_console_pid_path(node_id):
+ name = "%s.pid" % node_id
+ path = os.path.join(CONF.baremetal.terminal_pid_dir, name)
+ return path
+
+
+def _get_console_pid(node_id):
+ pid_path = _get_console_pid_path(node_id)
+ if os.path.exists(pid_path):
+ with open(pid_path, 'r') as f:
+ pid_str = f.read()
+ try:
+ return int(pid_str)
+ except ValueError:
+ LOG.warn(_("pid file %s does not contain any pid"), pid_path)
+ 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.
+
+ """
+
+ def __init__(self, node, **kwargs):
+ self.state = None
+ self.retries = None
+ self.node_id = node['id']
+ self.address = node['pm_address']
+ self.user = node['pm_user']
+ self.password = node['pm_password']
+ self.port = node['terminal_port']
+
+ if self.node_id == None:
+ raise InvalidParameterValue(_("Node id not supplied to IPMI"))
+ if self.address == None:
+ raise InvalidParameterValue(_("Address not supplied to IPMI"))
+ if self.user == None:
+ raise InvalidParameterValue(_("User not supplied to IPMI"))
+ if self.password == None:
+ raise InvalidParameterValue(_("Password not supplied to IPMI"))
+
+ def _exec_ipmitool(self, command):
+ args = ['ipmitool',
+ '-I',
+ 'lanplus',
+ '-H',
+ self.address,
+ '-U',
+ self.user,
+ '-f']
+ pwfile = _make_password_file(self.password)
+ try:
+ args.append(pwfile)
+ args.extend(command.split(" "))
+ out, err = utils.execute(*args, attempts=3)
+ LOG.debug(_("ipmitool stdout: '%(out)s', stderr: '%(err)%s'"),
+ locals())
+ return out, err
+ finally:
+ bm_utils.unlink_without_raise(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 = baremetal_states.ACTIVE
+ raise utils.LoopingCallDone()
+ if self.retries > CONF.baremetal.ipmi_power_retry:
+ self.state = baremetal_states.ERROR
+ raise utils.LoopingCallDone()
+ try:
+ self.retries += 1
+ self._exec_ipmitool("power on")
+ except Exception:
+ LOG.exception(_("IPMI power on failed"))
+
+ self.retries = 0
+ timer = utils.LoopingCall(_wait_for_power_on)
+ timer.start(interval=0.5).wait()
+
+ def _power_off(self):
+ """Turn the power to this node OFF"""
+
+ def _wait_for_power_off():
+ """Called at an interval until the node's power is off"""
+
+ if self._is_power("off"):
+ self.state = baremetal_states.DELETED
+ raise utils.LoopingCallDone()
+ if self.retries > CONF.baremetal.ipmi_power_retry:
+ self.state = baremetal_states.ERROR
+ raise utils.LoopingCallDone()
+ try:
+ self.retries += 1
+ self._exec_ipmitool("power off")
+ except Exception:
+ LOG.exception(_("IPMI power off failed"))
+
+ self.retries = 0
+ timer = utils.LoopingCall(_wait_for_power_off)
+ timer.start(interval=0.5).wait()
+
+ def _set_pxe_for_next_boot(self):
+ 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 == baremetal_states.ACTIVE:
+ LOG.warning(_("Activate node called, but node %s "
+ "is already active") % self.address)
+ self._set_pxe_for_next_boot()
+ self._power_on()
+ return self.state
+
+ def reboot_node(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")
+
+ def start_console(self):
+ if not self.port:
+ return
+ args = []
+ args.append(CONF.baremetal.terminal)
+ if CONF.baremetal.terminal_cert_dir:
+ args.append("-c")
+ args.append(CONF.baremetal.terminal_cert_dir)
+ else:
+ args.append("-t")
+ args.append("-p")
+ args.append(str(self.port))
+ args.append("--background=%s" % _get_console_pid_path(self.node_id))
+ args.append("-s")
+
+ try:
+ pwfile = _make_password_file(self.password)
+ ipmi_args = "/:%(uid)s:%(gid)s:HOME:ipmitool -H %(address)s" \
+ " -I lanplus -U %(user)s -f %(pwfile)s sol activate" \
+ % {'uid': os.getuid(),
+ 'gid': os.getgid(),
+ 'address': self.address,
+ 'user': self.user,
+ 'pwfile': pwfile,
+ }
+
+ args.append(ipmi_args)
+ # Run shellinaboxd without pipes. Otherwise utils.execute() waits
+ # infinitely since shellinaboxd does not close passed fds.
+ x = ["'" + arg.replace("'", "'\\''") + "'" for arg in args]
+ x.append('</dev/null')
+ x.append('>/dev/null')
+ x.append('2>&1')
+ utils.execute(' '.join(x), shell=True)
+ finally:
+ bm_utils.unlink_without_raise(pwfile)
+
+ def stop_console(self):
+ console_pid = _get_console_pid(self.node_id)
+ if console_pid:
+ # Allow exitcode 99 (RC_UNAUTHORIZED)
+ utils.execute('kill', '-TERM', str(console_pid),
+ run_as_root=True,
+ check_exit_code=[0, 99])
+ bm_utils.unlink_without_raise(_get_console_pid_path(self.node_id))