Add socat console support

This adds another serial console type socat support.

Change-Id: Ic443d050790aee2ea2e27daf165310f8e292fda7
Implements: bp socat-console
This commit is contained in:
Zhenguo Niu 2017-08-15 19:20:06 +08:00
parent 30a16bc608
commit dead07444c
28 changed files with 612 additions and 206 deletions

View File

@ -10,7 +10,7 @@ Baremetal Compute API V1 (CURRENT)
.. include:: servers.inc
.. include:: server_states.inc
.. include:: server_networks.inc
.. include:: server_serial_console.inc
.. include:: server_remote_consoles.inc
.. include:: flavors.inc
.. include:: flavor_access.inc
.. include:: availability_zones.inc

View File

@ -592,6 +592,25 @@ provision_state:
in: body
required: true
type: string
remote_console_protocol:
description: |
The protocol of remote console. The valid value is ``serial`` now.
in: body
required: true
type: string
remote_console_type:
description: |
The type of remote console. The valid values are ``socat``, and
``shellinabox``.
in: body
required: true
type: string
remote_console_url:
description: |
The URL is used to connect the console.
in: body
required: true
type: string
scheduler_hints:
description: |
The dictionary of data send to the scheduler, it represents scheduling

View File

@ -0,0 +1,4 @@
{
"protocol": "serial",
"type": "shellinabox"
}

View File

@ -0,0 +1,5 @@
{
"protocol": "serial",
"type": "shellinabox",
"url": "http://127.0.0.1:8866/?token=b4f5cb4a-8b01-40ea-ae46-67f0db4969b3"
}

View File

@ -1,5 +0,0 @@
{
"console": {
"url": "http://127.0.0.1:8866/?token=b4f5cb4a-8b01-40ea-ae46-67f0db4969b3"
}
}

View File

@ -0,0 +1,51 @@
.. -*- rst -*-
========================
Server Remote Consoles
========================
Create server remote console.
Create Remote Console
=====================
.. rest_method:: POST /v1/servers/{server_uuid}/remote_consoles
The API provides a unified request for creating a remote console. The user can
get a URL to connect the console from this API. The URL includes the token
which is used to get permission to access the console. Servers may support
different console protocols. To return a remote console using a specific
protocol, such as serial, set the ``protocol`` parameter to ``serial``. For the
same protocol, there may be different connection types such as ``serial protocal
and socat type`` or ``serial protocol and shellinabox type``.
Normal response code: 200
Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404),
conflict(409), notImplemented(501)
Request
-------
.. rest_parameters:: parameters.yaml
- server_uuid: server_ident
- protocol: remote_console_protocol
- type: remote_console_type
**Example Create Remote Socat Console: JSON request**
.. literalinclude:: samples/remote_consoles/create-shellinabox-console-req.json
Response
--------
.. rest_parameters:: parameters.yaml
- protocol: remote_console_protocol
- type: remote_console_type
- url: remote_console_url
**Example Create Remote Socat Console: JSON response**
.. literalinclude:: samples/remote_consoles/create-shellinabox-console-resp.json

View File

@ -1,36 +0,0 @@
.. -*- rst -*-
========================
Server Serial Console
========================
Servers Serial Console can be managed through serial_console sub-resource.
Server Serial Console Summary
===============================
.. rest_method:: GET /v1/servers/{server_uuid}/serial_console
Get the console url info of the Server.
Normal response code: 200
Request
-------
.. rest_parameters:: parameters.yaml
- server_uuid: server_ident
Response
--------
.. rest_parameters:: parameters.yaml
- console: console_info
- url: console_url
**Example server network:**
.. literalinclude:: samples/server_console/server-serial-console-get.json

View File

@ -0,0 +1,31 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
create_console = {
'type': 'object',
'properties': {
'protocol': {
'type': 'string',
'enum': ['serial'],
},
'type': {
'type': 'string',
'enum': ['shellinabox', 'socat'],
},
},
'required': ['protocol', 'type'],
'additionalProperties': False
}

View File

@ -29,6 +29,7 @@ from mogan.api.controllers import base
from mogan.api.controllers import link
from mogan.api.controllers.v1.schemas import floating_ips as fip_schemas
from mogan.api.controllers.v1.schemas import interfaces as interface_schemas
from mogan.api.controllers.v1.schemas import remote_consoles as console_schemas
from mogan.api.controllers.v1.schemas import servers as server_schemas
from mogan.api.controllers.v1 import types
from mogan.api.controllers.v1 import utils as api_utils
@ -527,24 +528,49 @@ class ServerCollection(base.APIBase):
class ServerConsole(base.APIBase):
"""API representation of the console of a server."""
console = {wtypes.text: types.jsontype}
"""The console information of the server"""
protocol = wtypes.text
"""The protocol of the console"""
type = wtypes.text
"""The type of the console"""
url = wtypes.text
"""The url of the console"""
@classmethod
def sample(cls):
sample = cls(
protocol='serial', type='shellinabox',
url='http://example.com/?token='
'b4f5cb4a-8b01-40ea-ae46-67f0db4969b3')
return sample
class ServerSerialConsoleController(ServerControllerBase):
class ServerRemoteConsoleController(ServerControllerBase):
"""REST controller for Server."""
@policy.authorize_wsgi("mogan:server", "get_serial_console")
@expose.expose(ServerConsole, types.uuid)
def get(self, server_uuid):
@policy.authorize_wsgi("mogan:server", "create_remote_console")
@expose.expose(ServerConsole, types.uuid, body=types.jsontype,
status_code=http_client.CREATED)
def post(self, server_uuid, remote_console):
"""Get the serial console info of the server.
:param server_uuid: the UUID of a server.
:param remote_console: request body includes console type and protocol.
"""
validation.check_schema(
remote_console, console_schemas.create_console)
server_obj = self._resource or self._get_resource(server_uuid)
console = pecan.request.engine_api.get_serial_console(
pecan.request.context, server_obj)
return ServerConsole(console=console)
protocol = remote_console['protocol']
console_type = remote_console['type']
# Only serial console is supported now
if protocol == 'serial':
console_url = pecan.request.engine_api.get_serial_console(
pecan.request.context, server_obj, console_type)
return ServerConsole(protocol=protocol, type=console_type,
url=console_url['url'])
class ServerController(ServerControllerBase):
@ -556,7 +582,7 @@ class ServerController(ServerControllerBase):
networks = ServerNetworksController()
"""Expose the network controller action as a sub-element of servers"""
serial_console = ServerSerialConsoleController()
remote_consoles = ServerRemoteConsoleController()
"""Expose the console controller of servers"""
_custom_actions = {

View File

@ -107,10 +107,11 @@ class BaseEngineDriver(object):
"""
raise NotImplementedError()
def get_serial_console_by_server(self, context, server):
def get_serial_console(self, context, server, console_type):
"""Get console info by server.
:param server: server to get its console info.
:param console_type: the type of the console.
"""
raise NotImplementedError()

View File

@ -19,6 +19,7 @@ from oslo_log import log as logging
from oslo_service import loopingcall
from oslo_utils import excutils
import six
import six.moves.urllib.parse as urlparse
from mogan.baremetal import driver as base_driver
from mogan.baremetal.ironic import ironic_states
@ -545,7 +546,19 @@ class IronicDriver(base_driver.BaseEngineDriver):
timer.start(interval=CONF.ironic.api_retry_interval).wait()
LOG.info('Server was successfully rebuilt', server=server)
def get_serial_console_by_server(self, context, server):
def _get_node_console_with_reset(self, server):
"""Acquire console information for a server.
If the console is enabled, the console will be re-enabled
before returning.
:param server: mogan server object
:return: a dictionary with below values
{ 'node': ironic node
'console_info': node console info }
:raise ConsoleNotAvailable: if console is unavailable
for the server
"""
node = self._validate_server_and_node(server)
node_uuid = node.uuid
@ -620,16 +633,41 @@ class IronicDriver(base_driver.BaseEngineDriver):
console = _enable_console(True)
if console['console_enabled']:
if console['console_info']['type'] != 'shellinabox':
raise exception.ConsoleTypeUnavailable(
console_type=console['console_info']['type'])
return {'node': node,
'console_info': console['console_info']}
else:
LOG.debug('Console is disabled for node %s', node_uuid)
raise exception.ConsoleNotAvailable()
def get_serial_console(self, context, server, console_type):
"""Acquire serial console information.
:param context: request context
:param server: mogan server object
:return: console url
:raise ConsoleTypeUnavailable: if serial console is unavailable
for the server
"""
LOG.debug('Getting serial console for server %s', server.uuid)
try:
result = self._get_node_console_with_reset(server)
except exception.ConsoleNotAvailable:
raise exception.ConsoleTypeUnavailable(console_type=console_type)
node = result['node']
console_info = result['console_info']
if console_info["type"] != console_type:
LOG.warning('Console type "%(type)s" (of ironic node '
'%(node)s) does not match with the given type',
{'type': console_info["type"],
'node': node.uuid},
server=server)
raise exception.ConsoleTypeUnavailable(console_type=console_type)
# Parse and check the console url
return urlparse.urlparse(console_info["url"])
def get_available_nodes(self):
"""Helper function to return the list of all nodes.

View File

@ -16,17 +16,16 @@ import sys
from mogan.common import service as mogan_service
from mogan.conf import CONF
from mogan.conf import shellinabox
from mogan.conf import serial_console
from mogan.console import shellinabox as shellinabox_console
shellinabox.register_cli_opts(CONF)
serial_console.register_cli_opts(CONF)
def main():
mogan_service.prepare_service(sys.argv)
server_address = (CONF.shellinabox_console.shellinaboxproxy_host,
CONF.shellinabox_console.shellinaboxproxy_port)
server_address = (CONF.serial_console.shellinaboxproxy_host,
CONF.serial_console.shellinaboxproxy_port)
httpd = shellinabox_console.ThreadingHTTPServer(
server_address,

32
mogan/cmd/socatproxy.py Normal file
View File

@ -0,0 +1,32 @@
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import sys
from mogan.common import service as mogan_service
from mogan.conf import CONF
from mogan.conf import serial_console
from mogan.console import websocketproxy
serial_console.register_cli_opts(CONF)
def main():
mogan_service.prepare_service(sys.argv)
websocketproxy.MoganWebSocketProxy(
listen_host=CONF.serial_console.socatproxy_host,
listen_port=CONF.serial_console.socatproxy_port,
file_only=True,
RequestHandlerClass=websocketproxy.MoganProxyRequestHandler
).start_server()

View File

@ -398,6 +398,14 @@ class ConsoleTypeUnavailable(Invalid):
_msg_fmt = _("Unavailable console type %(console_type)s.")
class ConsoleTypeInvalid(Invalid):
_msg_fmt = _("Invalid console type %(console_type)s")
class ConsoleProtocolInvalid(Invalid):
_msg_fmt = _("Invalid console protocol %(protocol)s")
class ConfigDriveMountFailed(MoganException):
_msg_fmt = _("Could not mount vfat config drive. %(operation)s failed. "
"Error: %(error)s")

View File

@ -111,9 +111,9 @@ server_policies = [
policy.RuleDefault('mogan:server:set_provision_state',
'rule:default',
description='Set the provision state of a server'),
policy.RuleDefault('mogan:server:get_serial_console',
policy.RuleDefault('mogan:server:create_remote_console',
'rule:default',
description='Get serial console for a server'),
description='Create remote console for a server'),
policy.RuleDefault('mogan:availability_zone:get_all',
'rule:default',
description='Get the availability zone list'),

View File

@ -27,7 +27,7 @@ from mogan.conf import neutron
from mogan.conf import placement
from mogan.conf import quota
from mogan.conf import scheduler
from mogan.conf import shellinabox
from mogan.conf import serial_console
CONF = cfg.CONF
@ -41,6 +41,6 @@ keystone.register_opts(CONF)
neutron.register_opts(CONF)
quota.register_opts(CONF)
scheduler.register_opts(CONF)
shellinabox.register_opts(CONF)
serial_console.register_opts(CONF)
cache.register_opts(CONF)
placement.register_opts(CONF)

View File

@ -23,7 +23,7 @@ import mogan.conf.neutron
import mogan.conf.placement
import mogan.conf.quota
import mogan.conf.scheduler
import mogan.conf.shellinabox
import mogan.conf.serial_console
_default_opt_lists = [
mogan.conf.default.api_opts,
@ -45,7 +45,7 @@ _opts = [
('placement', mogan.conf.placement.list_opts()),
('quota', mogan.conf.quota.quota_opts),
('scheduler', mogan.conf.scheduler.opts),
('shellinabox_console', mogan.conf.shellinabox.shellinabox_opts),
('serial_console', mogan.conf.serial_console.opts),
]

View File

@ -0,0 +1,182 @@
# Copyright 2015 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
serial_console_group = cfg.OptGroup(
"serial_console",
title="The serial console options",
help="""
The serial console feature allows you to connect to a guest.""")
opts = [
cfg.URIOpt('shellinabox_base_url',
default='http://127.0.0.1:8866/',
help="""
The URL an end user would use to connect to the ``mogan-shellinaboxproxy``
service.
The ``mogan-shellinaboxproxy`` service is called with this token enriched URL
and establishes the connection to the proper server.
Possible values:
* <scheme><IP-address><port-number>
Services which consume this:
* ``mogan-engine``
Interdependencies to other options:
* The IP address must be identical to the address to which the
``mogan-shellinaboxproxy`` service is listening (see option
``shellinaboxproxy_host``in this section).
* The port must be the same as in the option ``shellinaboxproxy_port`` of this
section.
"""),
# TODO(macsz) check if WS protocol is still being used
cfg.URIOpt('socat_base_url',
default='ws://127.0.0.1:8867/',
help="""
The URL an end user would use to connect to the ``mogan-socatproxy`` service.
The ``mogan-socatproxy`` service is called with this token enriched URL
and establishes the connection to the proper server.
Related options:
* The IP address must be identical to the address to which the
``mogan-socatproxy`` service is listening (see option ``socatproxy_host``
in this section).
* The port must be the same as in the option ``socatproxy_port`` of this
section.
* If you choose to use a secured websocket connection, then start this option
with ``wss://`` instead of the unsecured ``ws://``. The options ``cert``
and ``key`` in the ``[DEFAULT]`` section have to be set for that.
"""),
]
cli_opts = [
cfg.IPOpt('shellinaboxproxy_host',
default='0.0.0.0',
help="""
The IP address which is used by the ``mogan-shellinaboxproxy`` service to
listen for incoming requests.
The ``mogan-shellinaboxproxy`` service listens on this IP address for incoming
connection requests to servers which expose shellinabox serial console.
Possible values:
* An IP address
Services which consume this:
* ``mogan-shellinaboxproxy``
Interdependencies to other options:
* Ensure that this is the same IP address which is defined in the option
``shellinabox_base_url`` of this section or use ``0.0.0.0`` to listen on
all addresses.
"""),
cfg.PortOpt('shellinaboxproxy_port',
default=8866,
min=1,
max=65535,
help="""
The port number which is used by the ``mogan-shellinaboxproxy`` service to
listen for incoming requests.
The ``mogan-shellinaboxproxy`` service listens on this port number for incoming
connection requests to servers which expose shellinabox serial console.
Possible values:
* A port number
Services which consume this:
* ``mogan-shellinaboxproxy``
Interdependencies to other options:
* Ensure that this is the same port number which is defined in the option
``shellinabox_base_url`` of this section.
"""),
cfg.IPOpt('socatproxy_host',
default='0.0.0.0',
help="""
The IP address which is used by the ``mogan-socatproxy`` service to
listen for incoming requests.
The ``mogan-socatproxy`` service listens on this IP address for incoming
connection requests to servers which expose socat serial console.
Possible values:
* An IP address
Services which consume this:
* ``mogan-socatproxy``
Interdependencies to other options:
* Ensure that this is the same IP address which is defined in the option
``socat_base_url`` of this section or use ``0.0.0.0`` to listen on
all addresses.
"""),
cfg.PortOpt('socatproxy_port',
default=8867,
min=1,
max=65535,
help="""
The port number which is used by the ``mogan-socatproxy`` service to
listen for incoming requests.
The ``mogan-socatproxy`` service listens on this port number for incoming
connection requests to servers which expose socat serial console.
Possible values:
* A port number
Services which consume this:
* ``mogan-socatproxy``
Interdependencies to other options:
* Ensure that this is the same port number which is defined in the option
``socat_base_url`` of this section.
"""),
]
opts.extend(cli_opts)
def register_opts(conf):
conf.register_group(serial_console_group)
conf.register_opts(opts, group=serial_console_group)
def register_cli_opts(conf):
conf.register_cli_opts(cli_opts, group=serial_console_group)

View File

@ -1,111 +0,0 @@
# Copyright 2015 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
shellinabox_opt_group = cfg.OptGroup("shellinabox_console",
title="The shellinabox console feature",
help="""
The shellinabox console feature allows you to connect to a guest in case a
graphical console like VNC, RDP or SPICE is not available. This is only
currently supported for the Ironic driver.""")
shellinaboxproxy_host_opt = cfg.IPOpt('shellinaboxproxy_host',
default='0.0.0.0',
help="""
The IP address which is used by the ``mogan-shellinaboxproxy`` service to
listen for incoming requests.
The ``mogan-shellinaboxproxy`` service listens on this IP address for incoming
connection requests to servers which expose shellinabox serial console.
Possible values:
* An IP address
Services which consume this:
* ``mogan-shellinaboxproxy``
Interdependencies to other options:
* Ensure that this is the same IP address which is defined in the option
``shellinabox_base_url`` of this section or use ``0.0.0.0`` to listen on
all addresses.
""")
shellinaboxproxy_port_opt = cfg.PortOpt('shellinaboxproxy_port',
default=8866,
min=1,
max=65535,
help="""
The port number which is used by the ``mogan-shellinaboxproxy`` service to
listen for incoming requests.
The ``mogan-shellinaboxproxy`` service listens on this port number for incoming
connection requests to servers which expose shellinabox serial console.
Possible values:
* A port number
Services which consume this:
* ``mogan-shellinaboxproxy``
Interdependencies to other options:
* Ensure that this is the same port number which is defined in the option
``shellinabox_base_url`` of this section.
""")
shellinabox_base_url_opt = cfg.URIOpt('shellinabox_base_url',
default='http://127.0.0.1:8866/',
help="""
The URL an end user would use to connect to the ``mogan-shellinaboxproxy``
service.
The ``mogan-shellinaboxproxy`` service is called with this token enriched URL
and establishes the connection to the proper server.
Possible values:
* <scheme><IP-address><port-number>
Services which consume this:
* ``mogan-engine``
Interdependencies to other options:
* The IP address must be identical to the address to which the
``mogan-shellinaboxproxy`` service is listening (see option
``shellinaboxproxy_host``in this section).
* The port must be the same as in the option ``shellinaboxproxy_port`` of this
section.
""")
shellinabox_opts = [shellinaboxproxy_host_opt, shellinaboxproxy_port_opt,
shellinabox_base_url_opt]
def register_opts(conf):
conf.register_group(shellinabox_opt_group)
conf.register_opts(shellinabox_opts, group=shellinabox_opt_group)
def register_cli_opts(conf):
conf.register_group(shellinabox_opt_group)
conf.register_cli_opts(shellinabox_opts, shellinabox_opt_group)

View File

@ -0,0 +1,147 @@
# Copyright (c) 2012 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
'''
Websocket proxy that is compatible with OpenStack Mogan.
Leverages websockify.py by Joel Martin
'''
import socket
import sys
from oslo_context import context
from oslo_log import log
from six.moves import http_cookies as Cookie
import six.moves.urllib.parse as urlparse
import websockify
from mogan.common import exception
from mogan.common.i18n import _
from mogan.consoleauth import rpcapi as consoleauth_rpcapi
LOG = log.getLogger(__name__)
class MoganProxyRequestHandler(websockify.ProxyRequestHandler):
def socket(self, *args, **kwargs):
return websockify.WebSocketServer.socket(*args, **kwargs)
def address_string(self):
# NOTE(rpodolyaka): override the superclass implementation here and
# explicitly disable the reverse DNS lookup, which might fail on some
# deployments due to DNS configuration and break VNC access completely
return str(self.client_address[0])
def verify_origin_proto(self, connection_info, origin_proto):
access_url = connection_info.get('access_url')
if not access_url:
detail = _("No access_url in connection_info. "
"Cannot validate protocol")
raise exception.ValidationError(detail=detail)
expected_protos = [urlparse.urlparse(access_url).scheme]
# NOTE: For serial consoles the expected protocol could be ws or
# wss which correspond to http and https respectively in terms of
# security.
if 'ws' in expected_protos:
expected_protos.append('http')
if 'wss' in expected_protos:
expected_protos.append('https')
return origin_proto in expected_protos
def new_websocket_client(self):
"""Called after a new WebSocket connection has been established."""
# Reopen the eventlet hub to make sure we don't share an epoll
# fd with parent and/or siblings, which would be bad
from eventlet import hubs
hubs.use_hub()
# The expected behavior is to have token
# passed to the method GET of the request
parse = urlparse.urlparse(self.path)
if parse.scheme not in ('http', 'https'):
# From a bug in urlparse in Python < 2.7.4 we cannot support
# special schemes (cf: http://bugs.python.org/issue9374)
if sys.version_info < (2, 7, 4):
raise exception.NovaException(
_("We do not support scheme '%s' under Python < 2.7.4, "
"please use http or https") % parse.scheme)
query = parse.query
token = urlparse.parse_qs(query).get("token", [""]).pop()
if not token:
# NoVNC uses it's own convention that forward token
# from the request to a cookie header, we should check
# also for this behavior
hcookie = self.headers.get('cookie')
if hcookie:
cookie = Cookie.SimpleCookie()
for hcookie_part in hcookie.split(';'):
hcookie_part = hcookie_part.lstrip()
try:
cookie.load(hcookie_part)
except Cookie.CookieError:
# NOTE(stgleb): Do not print out cookie content
# for security reasons.
LOG.warning('Found malformed cookie')
else:
if 'token' in cookie:
token = cookie['token'].value
ctxt = context.get_admin_context()
rpcapi = consoleauth_rpcapi.ConsoleAuthAPI()
connect_info = rpcapi.check_token(ctxt, token=token)
if not connect_info:
raise exception.InvalidToken(token=token)
self.msg(_('connect info: %s'), str(connect_info))
host = connect_info['host']
port = int(connect_info['port'])
# Connect to the target
self.msg(_("connecting to: %(host)s:%(port)s") % {'host': host,
'port': port})
tsock = self.socket(host, port, connect=True)
# Handshake as necessary
if connect_info.get('internal_access_path'):
tsock.send("CONNECT %s HTTP/1.1\r\n\r\n" %
connect_info['internal_access_path'])
while True:
data = tsock.recv(4096, socket.MSG_PEEK)
if data.find("\r\n\r\n") != -1:
if data.split("\r\n")[0].find("200") == -1:
raise exception.InvalidConnectionInfo()
tsock.recv(len(data))
break
# Start proxying
try:
self.do_proxy(tsock)
except Exception:
if tsock:
tsock.shutdown(socket.SHUT_RDWR)
tsock.close()
self.vmsg(_("%(host)s:%(port)s: "
"Websocket client or target closed") %
{'host': host, 'port': port})
raise
class MoganWebSocketProxy(websockify.WebSocketProxy):
@staticmethod
def get_logger():
return LOG

View File

@ -442,17 +442,17 @@ class API(object):
return False
return True
def get_serial_console(self, context, server):
def get_serial_console(self, context, server, console_type):
"""Get a url to a server Console."""
connect_info = self.engine_rpcapi.get_serial_console(
context, server=server)
context, server=server, console_type=console_type)
self.consoleauth_rpcapi.authorize_console(
context,
connect_info['token'], 'serial',
connect_info['token'], console_type,
connect_info['host'], connect_info['port'],
connect_info['internal_access_path'], server.uuid,
access_url=connect_info['access_url'])
return {'url': connect_info['access_url']}
def _validate_new_key_pair(self, context, user_id, key_name, key_type):

View File

@ -23,7 +23,6 @@ from oslo_utils import excutils
from oslo_utils import timeutils
from oslo_utils import uuidutils
import six
import six.moves.urllib.parse as urlparse
from mogan.common import exception
from mogan.common import flow_utils
@ -516,18 +515,27 @@ class EngineManager(base_manager.BaseEngineManager):
utils.process_event(fsm, server, event='done')
LOG.info('Server was successfully rebuilt')
def get_serial_console(self, context, server):
node_console_info = self.driver.get_serial_console_by_server(
context, server)
def get_serial_console(self, context, server, console_type):
"""Returns connection information for a serial console."""
LOG.debug("Getting serial console for server %s", server.uuid)
token = uuidutils.generate_uuid()
if console_type == 'shellinabox':
access_url = '%s?token=%s' % (
CONF.shellinabox_console.shellinabox_base_url, token)
console_url = node_console_info['console_info']['url']
parsed_url = urlparse.urlparse(console_url)
CONF.serial_console.shellinabox_base_url, token)
elif console_type == 'socat':
access_url = '%s?token=%s' % (
CONF.serial_console.socat_base_url, token)
else:
raise exception.ConsoleTypeInvalid(console_type=console_type)
console_url = self.driver.get_serial_console(
context, server, console_type)
return {'access_url': access_url,
'token': token,
'host': parsed_url.hostname,
'port': parsed_url.port,
'host': console_url.hostname,
'port': console_url.port,
'internal_access_path': None}
def attach_interface(self, context, server, net_id=None):

View File

@ -78,10 +78,10 @@ class EngineAPI(object):
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
return cctxt.cast(context, 'rebuild_server', server=server)
def get_serial_console(self, context, server):
def get_serial_console(self, context, server, console_type):
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
return cctxt.call(context, 'get_serial_console',
server=server)
server=server, console_type=console_type)
def attach_interface(self, context, server, net_id):
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)

View File

@ -142,16 +142,18 @@ class BaremetalComputeAPIServersTest(base.BaseBaremetalComputeTest):
'Failed to acquire console information for node: %s' %
node)
def test_server_get_console(self):
def test_server_create_shellinabox_console(self):
self._ensure_states_before_test()
node = self.baremetal_node_client.show_bm_node(
service_id=self.server_ids[0])
self.baremetal_node_client.bm_node_set_console_port(node['uuid'], 4321)
self.baremetal_node_client.set_node_console_state(node['uuid'], True)
self._wait_for_console(node['uuid'], True)
console = self.baremetal_compute_client.server_get_serial_console(
self.server_ids[0])
console = self.baremetal_compute_client.\
server_create_shellinabox_console(self.server_ids[0])
self.assertIn('url', console)
self.assertEqual('serial', console['protocol'])
self.assertEqual('shellinabox', console['type'])
def test_server_get_nics(self):
nics = self.baremetal_compute_client.server_get_networks(

View File

@ -177,11 +177,14 @@ class BaremetalComputeClient(rest_client.RestClient):
body = self.deserialize(body)['nodes']
return rest_client.ResponseBodyList(resp, body)
def server_get_serial_console(self, server_id):
uri = '%s/servers/%s/serial_console' % (self.uri_prefix, server_id)
resp, body = self.get(uri)
self.expected_success(200, resp.status)
body = self.deserialize(body)['console']
def server_create_shellinabox_console(self, server_id):
uri = '%s/servers/%s/remote_consoles' % (self.uri_prefix, server_id)
target_body = {'protocol': 'serial', 'type': 'shellinabox'}
target_body = self.serialize(target_body)
resp, body = self.post(uri, target_body)
self.expected_success(201, resp.status)
if body:
body = self.deserialize(body)
return rest_client.ResponseBody(resp, body)
def server_get_networks(self, server_id):

View File

@ -138,7 +138,8 @@ class ManageServerTestCase(mgr_utils.ServiceSetUpMixin,
{"console_enabled": True, "console_info": fake_console_url}]
server = obj_utils.create_test_server(self.context)
self._start_service()
console = self.service.get_serial_console(self.context, server)
console = self.service.get_serial_console(
self.context, server, 'shellinabox')
self._stop_service()
self.assertEqual(4321, console['port'])
self.assertTrue(

View File

@ -34,5 +34,5 @@ WSME>=0.8 # MIT
keystonemiddleware>=4.12.0 # Apache-2.0
stevedore>=1.20.0 # Apache-2.0
automaton>=0.5.0 # Apache-2.0
websockify>=0.8.0 # LGPLv3
python-memcached>=1.56 # PSF

View File

@ -38,6 +38,7 @@ console_scripts =
mogan-scheduler = mogan.cmd.scheduler:main
mogan-consoleauth = mogan.cmd.consoleauth:main
mogan-shellinaboxproxy = mogan.cmd.shellinaboxproxy:main
mogan-socatproxy = mogan.cmd.socatproxy:main
mogan.database.migration_backend =
sqlalchemy = mogan.db.sqlalchemy.migration