Add serial console feature to seamicro driver
Implements a console driver -- "ShellinaboxConsole" -- that uses telnet+shellinabox to connect to serial console of physical servers configured within the Seamicro Fabric Compute system. SeaMicro System provides telnet facility to connect to any of it's physical server's serial console. Port to which we telnet depends on server-id. Change-Id: I2e60f6402a984dbbb6e0f749729c5a7006310076 Implements: blueprint seamicro-serial-console
This commit is contained in:
parent
9aebde3981
commit
5126dafe59
@ -109,6 +109,7 @@ class FakeSeaMicroDriver(base.BaseDriver):
|
||||
self.deploy = fake.FakeDeploy()
|
||||
self.management = seamicro.Management()
|
||||
self.vendor = seamicro.VendorPassthru()
|
||||
self.console = seamicro.ShellinaboxConsole()
|
||||
|
||||
|
||||
class FakeAgentDriver(base.BaseDriver):
|
||||
|
@ -18,9 +18,12 @@ python-seamicroclient.
|
||||
|
||||
Provides vendor passthru methods for SeaMicro specific functionality.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
from oslo.config import cfg
|
||||
from oslo.utils import importutils
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from ironic.common import boot_devices
|
||||
from ironic.common import exception
|
||||
@ -30,6 +33,7 @@ from ironic.common.i18n import _LW
|
||||
from ironic.common import states
|
||||
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
|
||||
from ironic.openstack.common import loopingcall
|
||||
|
||||
@ -72,6 +76,11 @@ OPTIONAL_PROPERTIES = {
|
||||
}
|
||||
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
|
||||
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
|
||||
CONSOLE_PROPERTIES = {
|
||||
'seamicro_terminal_port': _("node's UDP port to connect to. "
|
||||
"Only required for console access.")
|
||||
}
|
||||
PORT_BASE = 2000
|
||||
|
||||
|
||||
def _get_client(*args, **kwargs):
|
||||
@ -99,6 +108,7 @@ def _parse_driver_info(node):
|
||||
:param node: An Ironic node object.
|
||||
:returns: SeaMicro driver info.
|
||||
:raises: MissingParameterValue if any required parameters are missing.
|
||||
:raises: InvalidParameterValue if required parameter are invalid.
|
||||
"""
|
||||
|
||||
info = node.driver_info or {}
|
||||
@ -113,13 +123,35 @@ def _parse_driver_info(node):
|
||||
password = info.get('seamicro_password')
|
||||
server_id = info.get('seamicro_server_id')
|
||||
api_version = info.get('seamicro_api_version', "2")
|
||||
port = info.get('seamicro_terminal_port')
|
||||
|
||||
if port:
|
||||
try:
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"SeaMicro terminal port is not an integer."))
|
||||
|
||||
r = re.compile(r"(^[0-9]+)/([0-9]+$)")
|
||||
if not r.match(server_id):
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"Invalid 'seamicro_server_id' parameter in node's "
|
||||
"driver_info. Expected format of 'seamicro_server_id' "
|
||||
"is <int>/<int>"))
|
||||
|
||||
url = urlparse.urlparse(api_endpoint)
|
||||
if (not (url.scheme == "http") or not url.netloc):
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"Invalid 'seamicro_api_endpoint' parameter in node's "
|
||||
"driver_info."))
|
||||
|
||||
res = {'username': username,
|
||||
'password': password,
|
||||
'api_endpoint': api_endpoint,
|
||||
'server_id': server_id,
|
||||
'api_version': api_version,
|
||||
'uuid': node.uuid}
|
||||
'uuid': node.uuid,
|
||||
'port': port}
|
||||
|
||||
return res
|
||||
|
||||
@ -329,6 +361,12 @@ def _create_volume(driver_info, volume_size):
|
||||
least_used_pool)
|
||||
|
||||
|
||||
def get_telnet_port(driver_info):
|
||||
"""Get SeaMicro telnet port to listen."""
|
||||
server_id = int(driver_info['server_id'].split("/")[0])
|
||||
return PORT_BASE + (10 * server_id)
|
||||
|
||||
|
||||
class Power(base.PowerInterface):
|
||||
"""SeaMicro Power Interface.
|
||||
|
||||
@ -572,3 +610,77 @@ class Management(base.ManagementInterface):
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ShellinaboxConsole(base.ConsoleInterface):
|
||||
"""A ConsoleInterface that uses telnet 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 task from TaskManager.
|
||||
:raises: MissingParameterValue if required seamicro parameters are
|
||||
missing
|
||||
:raises: InvalidParameterValue if required parameter are invalid.
|
||||
"""
|
||||
driver_info = _parse_driver_info(task.node)
|
||||
if not driver_info['port']:
|
||||
raise exception.MissingParameterValue(_(
|
||||
"Missing 'seamicro_terminal_port' parameter in node's "
|
||||
"driver_info"))
|
||||
|
||||
def start_console(self, task):
|
||||
"""Start a remote console for the node.
|
||||
|
||||
:param task: a task from TaskManager
|
||||
:raises: MissingParameterValue if required seamicro parameters are
|
||||
missing
|
||||
:raises: ConsoleError if the directory for the PID file cannot be
|
||||
created
|
||||
:raises: ConsoleSubprocessFailed when invoking the subprocess failed
|
||||
:raises: InvalidParameterValue if required parameter are invalid.
|
||||
"""
|
||||
|
||||
driver_info = _parse_driver_info(task.node)
|
||||
telnet_port = get_telnet_port(driver_info)
|
||||
chassis_ip = urlparse.urlparse(driver_info['api_endpoint']).netloc
|
||||
|
||||
seamicro_cmd = ("/:%(uid)s:%(gid)s:HOME:telnet %(chassis)s %(port)s"
|
||||
% {'uid': os.getuid(),
|
||||
'gid': os.getgid(),
|
||||
'chassis': chassis_ip,
|
||||
'port': telnet_port})
|
||||
|
||||
console_utils.start_shellinabox_console(driver_info['uuid'],
|
||||
driver_info['port'],
|
||||
seamicro_cmd)
|
||||
|
||||
def stop_console(self, task):
|
||||
"""Stop the remote console session for the node.
|
||||
|
||||
:param task: a task from TaskManager
|
||||
:raises: MissingParameterValue if required seamicro parameters are
|
||||
missing
|
||||
:raises: ConsoleError if unable to stop the console
|
||||
:raises: InvalidParameterValue if required parameter are invalid.
|
||||
"""
|
||||
|
||||
driver_info = _parse_driver_info(task.node)
|
||||
console_utils.stop_shellinabox_console(driver_info['uuid'])
|
||||
|
||||
def get_console(self, task):
|
||||
"""Get the type and connection information about the console.
|
||||
|
||||
:raises: MissingParameterValue if required seamicro parameters are
|
||||
missing
|
||||
:raises: InvalidParameterValue if required parameter are invalid.
|
||||
"""
|
||||
|
||||
driver_info = _parse_driver_info(task.node)
|
||||
url = console_utils.get_shellinabox_console_url(driver_info['port'])
|
||||
return {'type': 'shellinabox', 'url': url}
|
||||
|
@ -118,6 +118,7 @@ class PXEAndSeaMicroDriver(base.BaseDriver):
|
||||
'attach_volume': self.seamicro_vendor,
|
||||
'set_node_vlan_id': self.seamicro_vendor}
|
||||
self.vendor = utils.MixinVendorInterface(self.mapping)
|
||||
self.console = seamicro.ShellinaboxConsole()
|
||||
|
||||
|
||||
class PXEAndIBootDriver(base.BaseDriver):
|
||||
|
@ -2399,7 +2399,7 @@ class ManagerTestProperties(tests_db_base.DbTestCase):
|
||||
def test_driver_properties_fake_seamicro(self):
|
||||
expected = ['seamicro_api_endpoint', 'seamicro_password',
|
||||
'seamicro_server_id', 'seamicro_username',
|
||||
'seamicro_api_version']
|
||||
'seamicro_api_version', 'seamicro_terminal_port']
|
||||
self._check_driver_properties("fake_seamicro", expected)
|
||||
|
||||
def test_driver_properties_fake_snmp(self):
|
||||
@ -2434,7 +2434,7 @@ class ManagerTestProperties(tests_db_base.DbTestCase):
|
||||
expected = ['pxe_deploy_kernel', 'pxe_deploy_ramdisk',
|
||||
'seamicro_api_endpoint', 'seamicro_password',
|
||||
'seamicro_server_id', 'seamicro_username',
|
||||
'seamicro_api_version']
|
||||
'seamicro_api_version', 'seamicro_terminal_port']
|
||||
self._check_driver_properties("pxe_seamicro", expected)
|
||||
|
||||
def test_driver_properties_pxe_snmp(self):
|
||||
|
@ -24,6 +24,7 @@ from ironic.common import exception
|
||||
from ironic.common import states
|
||||
from ironic.common import utils
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.drivers.modules import console_utils
|
||||
from ironic.drivers.modules import seamicro
|
||||
from ironic.tests.conductor import utils as mgr_utils
|
||||
from ironic.tests.db import base as db_base
|
||||
@ -291,7 +292,14 @@ class SeaMicroPowerDriverTestCase(db_base.DbTestCase):
|
||||
expected = seamicro.COMMON_PROPERTIES
|
||||
with task_manager.acquire(self.context, self.node['uuid'],
|
||||
shared=True) as task:
|
||||
self.assertEqual(expected, task.driver.get_properties())
|
||||
self.assertEqual(expected, task.driver.power.get_properties())
|
||||
|
||||
expected = (list(seamicro.COMMON_PROPERTIES) +
|
||||
list(seamicro.CONSOLE_PROPERTIES))
|
||||
console_properties = task.driver.console.get_properties().keys()
|
||||
self.assertEqual(sorted(expected), sorted(console_properties))
|
||||
self.assertEqual(sorted(expected),
|
||||
sorted(task.driver.get_properties().keys()))
|
||||
|
||||
def test_vendor_routes(self):
|
||||
expected = ['set_node_vlan_id', 'attach_volume']
|
||||
@ -601,3 +609,87 @@ class SeaMicroPowerDriverTestCase(db_base.DbTestCase):
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
self.assertRaises(exception.MissingParameterValue,
|
||||
task.driver.management.validate, task)
|
||||
|
||||
|
||||
class SeaMicroDriverTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(SeaMicroDriverTestCase, self).setUp()
|
||||
mgr_utils.mock_the_extension_manager(driver='fake_seamicro')
|
||||
self.driver = driver_factory.get_driver('fake_seamicro')
|
||||
self.node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_seamicro',
|
||||
driver_info=INFO_DICT)
|
||||
self.get_server_patcher = mock.patch.object(seamicro, '_get_server')
|
||||
|
||||
self.get_server_mock = None
|
||||
self.Server = Fake_Server
|
||||
self.Volume = Fake_Volume
|
||||
self.info = seamicro._parse_driver_info(self.node)
|
||||
|
||||
@mock.patch.object(console_utils, 'start_shellinabox_console')
|
||||
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)
|
||||
|
||||
@mock.patch.object(console_utils, 'start_shellinabox_console')
|
||||
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')
|
||||
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'])
|
||||
|
||||
@mock.patch.object(console_utils, 'stop_shellinabox_console')
|
||||
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, 'start_shellinabox_console')
|
||||
def test_start_console_fail_nodir(self, mock_exec):
|
||||
mock_exec.side_effect = exception.ConsoleError()
|
||||
|
||||
with task_manager.acquire(self.context,
|
||||
self.node.uuid) as task:
|
||||
self.assertRaises(exception.ConsoleError,
|
||||
self.driver.console.start_console,
|
||||
task)
|
||||
mock_exec.assert_called_once_with(self.node.uuid, mock.ANY, mock.ANY)
|
||||
|
||||
@mock.patch.object(console_utils, 'get_shellinabox_console_url')
|
||||
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'])
|
||||
|
Loading…
x
Reference in New Issue
Block a user