diff --git a/ironic/drivers/agent.py b/ironic/drivers/agent.py index 5b98da5b47..de916feba7 100644 --- a/ironic/drivers/agent.py +++ b/ironic/drivers/agent.py @@ -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} diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py index 246cc11780..7956792e8d 100644 --- a/ironic/drivers/fake.py +++ b/ironic/drivers/fake.py @@ -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() diff --git a/ironic/drivers/modules/ipminative.py b/ironic/drivers/modules/ipminative.py index 2ef1b491c4..a26e203ce8 100644 --- a/ironic/drivers/modules/ipminative.py +++ b/ironic/drivers/modules/ipminative.py @@ -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} diff --git a/ironic/drivers/pxe.py b/ironic/drivers/pxe.py index 630e6a2534..b6cea52633 100644 --- a/ironic/drivers/pxe.py +++ b/ironic/drivers/pxe.py @@ -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() diff --git a/ironic/tests/conductor/test_manager.py b/ironic/tests/conductor/test_manager.py index 06025e53a4..c27d5813f1 100644 --- a/ironic/tests/conductor/test_manager.py +++ b/ironic/tests/conductor/test_manager.py @@ -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): diff --git a/ironic/tests/drivers/test_ipminative.py b/ironic/tests/drivers/test_ipminative.py index 86a81fd592..a8fcb5b26f 100644 --- a/ironic/tests/drivers/test_ipminative.py +++ b/ironic/tests/drivers/test_ipminative.py @@ -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)