Merge "Add serial console feature to seamicro driver"
This commit is contained in:
commit
cb4bc1e54a
@ -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):
|
||||
|
@ -2416,7 +2416,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):
|
||||
@ -2451,7 +2451,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