Merge "Interactive console support for ipminative driver"
This commit is contained in:
commit
823bd59d61
@ -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}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user