Merge "Interactive console support for ipminative driver"

This commit is contained in:
Jenkins 2014-09-04 21:00:59 +00:00 committed by Gerrit Code Review
commit 823bd59d61
6 changed files with 185 additions and 4 deletions

View File

@ -59,6 +59,7 @@ class AgentAndIPMINativeDriver(base.BaseDriver):
self.power = ipminative.NativeIPMIPower()
self.deploy = agent.AgentDeploy()
self.management = ipminative.NativeIPMIManagement()
self.console = ipminative.NativeIPMIShellinaboxConsole()
self.agent_vendor = agent.AgentVendorInterface()
self.mapping = {'heartbeat': self.agent_vendor}
self.dl_mapping = {'lookup': self.agent_vendor}

View File

@ -87,6 +87,7 @@ class FakeIPMINativeDriver(base.BaseDriver):
def __init__(self):
self.power = ipminative.NativeIPMIPower()
self.console = ipminative.NativeIPMIShellinaboxConsole()
self.deploy = fake.FakeDeploy()
self.management = ipminative.NativeIPMIManagement()

View File

@ -19,7 +19,11 @@
Ironic Native IPMI power manager.
"""
import os
import tempfile
from oslo.config import cfg
from oslo.utils import excutils
from oslo.utils import importutils
from ironic.common import boot_devices
@ -27,8 +31,10 @@ from ironic.common import exception
from ironic.common import i18n
from ironic.common.i18n import _
from ironic.common import states
from ironic.common import utils
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.drivers.modules import console_utils
from ironic.openstack.common import log as logging
pyghmi = importutils.try_import('pyghmi')
@ -59,6 +65,10 @@ REQUIRED_PROPERTIES = {'ipmi_address': _("IP of the node's BMC. Required."),
'ipmi_password': _("IPMI password. Required."),
'ipmi_username': _("IPMI username. Required.")}
COMMON_PROPERTIES = REQUIRED_PROPERTIES
CONSOLE_PROPERTIES = {
'ipmi_terminal_port': _("node's UDP port to connect to. Only required for "
"console access.")
}
_BOOT_DEVICES_MAP = {
boot_devices.DISK: 'hd',
@ -71,7 +81,9 @@ _BOOT_DEVICES_MAP = {
def _parse_driver_info(node):
"""Gets the bmc access info for the given node.
:raises: MissingParameterValue when required ipmi credentials
are missing.
are missing.
:raises: InvalidParameterValue when the IPMI terminal port is not an
integer.
"""
info = node.driver_info or {}
@ -89,10 +101,27 @@ def _parse_driver_info(node):
# get additional info
bmc_info['uuid'] = node.uuid
bmc_info['port'] = info.get('ipmi_terminal_port')
# terminal port must be an integer
port = bmc_info.get('port')
if port is not None:
try:
port = int(port)
except ValueError:
raise exception.InvalidParameterValue(_(
"IPMI terminal port is not an integer."))
bmc_info['port'] = port
return bmc_info
def _console_pwfile_path(uuid):
"""Return the file path for storing the ipmi password."""
file_name = "%(uuid)s.pw" % {'uuid': uuid}
return os.path.join(tempfile.gettempdir(), file_name)
def _power_on(driver_info):
"""Turn the power on for this node.
@ -443,3 +472,76 @@ class NativeIPMIManagement(base.ManagementInterface):
"""
driver_info = _parse_driver_info(task.node)
return _get_sensors_data(driver_info)
class NativeIPMIShellinaboxConsole(base.ConsoleInterface):
"""A ConsoleInterface that uses pyghmi and shellinabox."""
def get_properties(self):
d = COMMON_PROPERTIES.copy()
d.update(CONSOLE_PROPERTIES)
return d
def validate(self, task):
"""Validate the Node console info.
:param task: a TaskManager instance containing the node to act on.
:raises: MissingParameterValue when required IPMI credentials or
the IPMI terminal port are missing
:raises: InvalidParameterValue when the IPMI terminal port is not
an integer.
"""
driver_info = _parse_driver_info(task.node)
if not driver_info['port']:
raise exception.MissingParameterValue(_(
"IPMI terminal port not supplied to the IPMI driver."))
def start_console(self, task):
"""Start a remote console for the node.
:param task: a TaskManager instance containing the node to act on.
:raises: ConsoleError if unable to start the console process.
"""
driver_info = _parse_driver_info(task.node)
path = _console_pwfile_path(driver_info['uuid'])
pw_file = console_utils.make_persistent_password_file(
path, driver_info['password'])
console_cmd = "/:%(uid)s:%(gid)s:HOME:pyghmicons %(bmc)s" \
" %(user)s" \
" %(passwd_file)s" \
% {'uid': os.getuid(),
'gid': os.getgid(),
'bmc': driver_info['address'],
'user': driver_info['username'],
'passwd_file': pw_file}
try:
console_utils.start_shellinabox_console(driver_info['uuid'],
driver_info['port'],
console_cmd)
except exception.ConsoleError:
with excutils.save_and_reraise_exception():
utils.unlink_without_raise(path)
def stop_console(self, task):
"""Stop the remote console session for the node.
:param task: a TaskManager instance containing the node to act on.
:raises: ConsoleError if unable to stop the console process.
"""
driver_info = _parse_driver_info(task.node)
try:
console_utils.stop_shellinabox_console(driver_info['uuid'])
finally:
password_file = _console_pwfile_path(driver_info['uuid'])
utils.unlink_without_raise(password_file)
def get_console(self, task):
"""Get the type and connection information about the console.
:param task: a TaskManager instance containing the node to act on.
"""
driver_info = _parse_driver_info(task.node)
url = console_utils.get_shellinabox_console_url(driver_info['port'])
return {'type': 'shellinabox', 'url': url}

View File

@ -86,6 +86,7 @@ class PXEAndIPMINativeDriver(base.BaseDriver):
driver=self.__class__.__name__,
reason=_("Unable to import pyghmi library"))
self.power = ipminative.NativeIPMIPower()
self.console = ipminative.NativeIPMIShellinaboxConsole()
self.deploy = pxe.PXEDeploy()
self.management = ipminative.NativeIPMIManagement()
self.vendor = pxe.VendorPassthru()

View File

@ -2195,7 +2195,8 @@ class ManagerTestProperties(tests_db_base.DbTestCase):
self._check_driver_properties("fake_ipmitool", expected)
def test_driver_properties_fake_ipminative(self):
expected = ['ipmi_address', 'ipmi_password', 'ipmi_username']
expected = ['ipmi_address', 'ipmi_password', 'ipmi_username',
'ipmi_terminal_port']
self._check_driver_properties("fake_ipminative", expected)
def test_driver_properties_fake_ssh(self):
@ -2231,7 +2232,8 @@ class ManagerTestProperties(tests_db_base.DbTestCase):
def test_driver_properties_pxe_ipminative(self):
expected = ['ipmi_address', 'ipmi_password', 'ipmi_username',
'pxe_deploy_kernel', 'pxe_deploy_ramdisk']
'pxe_deploy_kernel', 'pxe_deploy_ramdisk',
'ipmi_terminal_port']
self._check_driver_properties("pxe_ipminative", expected)
def test_driver_properties_pxe_ssh(self):

View File

@ -31,6 +31,7 @@ from ironic.common import states
from ironic.common import utils
from ironic.conductor import task_manager
from ironic.db import api as db_api
from ironic.drivers.modules import console_utils
from ironic.drivers.modules import ipminative
from ironic.openstack.common import context
from ironic.tests import base
@ -228,9 +229,16 @@ class IPMINativeDriverTestCase(db_base.DbTestCase):
def test_get_properties(self):
expected = ipminative.COMMON_PROPERTIES
self.assertEqual(expected, self.driver.get_properties())
self.assertEqual(expected, self.driver.power.get_properties())
self.assertEqual(expected, self.driver.management.get_properties())
expected = ipminative.COMMON_PROPERTIES.keys()
expected += ipminative.CONSOLE_PROPERTIES.keys()
self.assertEqual(sorted(expected),
sorted(self.driver.console.get_properties().keys()))
self.assertEqual(sorted(expected),
sorted(self.driver.get_properties().keys()))
@mock.patch('pyghmi.ipmi.command.Command')
def test_get_power_state(self, ipmi_mock):
# Getting the mocked command.
@ -404,3 +412,69 @@ class IPMINativeDriverTestCase(db_base.DbTestCase):
self.node.uuid) as task:
self.driver.management.get_sensors_data(task)
ipmicmd.get_sensor_data.assert_called_once_with()
@mock.patch.object(console_utils, 'start_shellinabox_console',
autospec=True)
def test_start_console(self, mock_exec):
mock_exec.return_value = None
with task_manager.acquire(self.context,
self.node['uuid']) as task:
self.driver.console.start_console(task)
mock_exec.assert_called_once_with(self.info['uuid'],
self.info['port'],
mock.ANY)
self.assertTrue(mock_exec.called)
@mock.patch.object(console_utils, 'start_shellinabox_console',
autospec=True)
def test_start_console_fail(self, mock_exec):
mock_exec.side_effect = exception.ConsoleSubprocessFailed(
error='error')
with task_manager.acquire(self.context,
self.node['uuid']) as task:
self.assertRaises(exception.ConsoleSubprocessFailed,
self.driver.console.start_console,
task)
@mock.patch.object(console_utils, 'stop_shellinabox_console',
autospec=True)
def test_stop_console(self, mock_exec):
mock_exec.return_value = None
with task_manager.acquire(self.context,
self.node['uuid']) as task:
self.driver.console.stop_console(task)
mock_exec.assert_called_once_with(self.info['uuid'])
self.assertTrue(mock_exec.called)
@mock.patch.object(console_utils, 'stop_shellinabox_console',
autospec=True)
def test_stop_console_fail(self, mock_stop):
mock_stop.side_effect = exception.ConsoleError()
with task_manager.acquire(self.context,
self.node.uuid) as task:
self.assertRaises(exception.ConsoleError,
self.driver.console.stop_console,
task)
mock_stop.assert_called_once_with(self.node.uuid)
@mock.patch.object(console_utils, 'get_shellinabox_console_url',
utospec=True)
def test_get_console(self, mock_exec):
url = 'http://localhost:4201'
mock_exec.return_value = url
expected = {'type': 'shellinabox', 'url': url}
with task_manager.acquire(self.context,
self.node['uuid']) as task:
console_info = self.driver.console.get_console(task)
self.assertEqual(expected, console_info)
mock_exec.assert_called_once_with(self.info['port'])
self.assertTrue(mock_exec.called)