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
This commit is contained in:
parent
0279d846ab
commit
f1bb1a213b
etc/nova/rootwrap.d
nova
9
etc/nova/rootwrap.d/baremetal_compute_ipmi.filters
Normal file
9
etc/nova/rootwrap.d/baremetal_compute_ipmi.filters
Normal file
@ -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
|
228
nova/tests/baremetal/test_ipmi.py
Normal file
228
nova/tests/baremetal/test_ipmi.py
Normal file
@ -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)
|
257
nova/virt/baremetal/ipmi.py
Normal file
257
nova/virt/baremetal/ipmi.py
Normal file
@ -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))
|
Loading…
x
Reference in New Issue
Block a user