Convert websocketproxy to use db for token validation
Now we can use the ConsoleAuthToken object to do token validation. This change converts websocketproxy to use the ConsoleAuthToken object for token validation. Tha ConsoleAuthToken object is prepared to work with cells v2. We use consoleauth if using cells v1. A new config option: [workarounds]/enable_consoleauth has been added to aid in transitioning to the database backend if resetting already existing consoles would be problematic for an operator. Co-Authored-By: melanie witt <melwittt@gmail.com> partially-implements: blueprint convert-consoles-to-objects Depends-On: I67894a31b887a93de26f3d2d8a1fa84be5b9ea89 Change-Id: If1b6e5f20d2ea82d94f5f0550f13189fc9bc16c4
This commit is contained in:
parent
93f4364a4e
commit
969239029d
@ -27,12 +27,14 @@ from oslo_reports import opts as gmr_opts
|
||||
import nova.conf
|
||||
from nova.conf import novnc
|
||||
from nova.console import websocketproxy
|
||||
from nova import objects
|
||||
from nova import version
|
||||
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
novnc.register_cli_opts(CONF)
|
||||
gmr_opts.set_defaults(CONF)
|
||||
objects.register_all()
|
||||
|
||||
|
||||
def exit_with_error(msg, errno=-1):
|
||||
|
@ -32,6 +32,10 @@ The lifetime of a console auth token (in seconds).
|
||||
A console auth token is used in authorizing console access for a user.
|
||||
Once the auth token time to live count has elapsed, the token is
|
||||
considered expired. Expired tokens are then deleted.
|
||||
|
||||
Related options:
|
||||
|
||||
* ``[workarounds]/enable_consoleauth``
|
||||
""")
|
||||
]
|
||||
|
||||
|
@ -146,6 +146,35 @@ Related options:
|
||||
|
||||
* [filter_scheduler]/track_instance_changes also relies on upcalls from the
|
||||
compute service to the scheduler service.
|
||||
"""),
|
||||
|
||||
cfg.BoolOpt(
|
||||
'enable_consoleauth',
|
||||
default=False,
|
||||
deprecated_for_removal=True,
|
||||
deprecated_since="18.0.0",
|
||||
deprecated_reason="""
|
||||
Enable the consoleauth service to avoid resetting unexpired consoles.
|
||||
|
||||
Console token authorizations have moved from the ``nova-consoleauth`` service
|
||||
to the database, so all new consoles will be supported by the database backend.
|
||||
With this, consoles that existed before database backend support will be reset.
|
||||
For most operators, this should be a minimal disruption as the default TTL of a
|
||||
console token is 10 minutes.
|
||||
|
||||
Operators that have much longer token TTL configured or otherwise wish to avoid
|
||||
immediately resetting all existing consoles can enable this flag to continue
|
||||
using the ``nova-consoleauth`` service in addition to the database backend.
|
||||
Once all of the old ``nova-consoleauth`` supported console tokens have expired,
|
||||
this flag should be disabled and it will be no longer necessary to run the
|
||||
``nova-consoleauth`` service. For example, if a deployment has configured a
|
||||
token TTL of one hour, the operator may disable the flag and stop running the
|
||||
``nova-consoleauth`` service one hour after deploying the new code during an
|
||||
upgrade.
|
||||
|
||||
Related options:
|
||||
|
||||
* ``[consoleauth]/token_ttl``
|
||||
"""),
|
||||
]
|
||||
|
||||
|
@ -28,11 +28,13 @@ from six.moves import http_cookies as Cookie
|
||||
import six.moves.urllib.parse as urlparse
|
||||
import websockify
|
||||
|
||||
from nova.compute import rpcapi as compute_rpcapi
|
||||
import nova.conf
|
||||
from nova.consoleauth import rpcapi as consoleauth_rpcapi
|
||||
from nova import context
|
||||
from nova import exception
|
||||
from nova.i18n import _
|
||||
from nova import objects
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@ -111,6 +113,86 @@ class NovaProxyRequestHandlerBase(object):
|
||||
|
||||
return origin_proto in expected_protos
|
||||
|
||||
@staticmethod
|
||||
def _console_auth_token_obj_to_dict(obj):
|
||||
"""Convert to a dict representation."""
|
||||
# NOTE(PaulMurray) For compatibility while there is code that
|
||||
# expects the dict representation returned by consoleauth.
|
||||
# TODO(PaulMurray) Remove this function when the code no
|
||||
# longer expects the consoleauth dict representation
|
||||
connect_info = {}
|
||||
connect_info['token'] = obj.token,
|
||||
connect_info['instance_uuid'] = obj.instance_uuid
|
||||
connect_info['console_type'] = obj.console_type
|
||||
connect_info['host'] = obj.host
|
||||
connect_info['port'] = obj.port
|
||||
if 'internal_access_path' in obj:
|
||||
connect_info['internal_access_path'] = obj.internal_access_path
|
||||
if 'access_url_base' in obj:
|
||||
connect_info['access_url'] = obj.access_url
|
||||
return connect_info
|
||||
|
||||
def _check_console_port(self, ctxt, instance_uuid, port, console_type):
|
||||
|
||||
try:
|
||||
instance = objects.Instance.get_by_uuid(ctxt, instance_uuid)
|
||||
except exception.InstanceNotFound:
|
||||
return
|
||||
|
||||
# NOTE(melwitt): The port is expected to be a str for validation.
|
||||
return self.compute_rpcapi.validate_console_port(ctxt, instance,
|
||||
str(port),
|
||||
console_type)
|
||||
|
||||
def _get_connect_info_consoleauth(self, ctxt, token):
|
||||
# NOTE(PaulMurray) consoleauth check_token() validates the token
|
||||
# and does an rpc to compute manager to check the console port
|
||||
# is correct.
|
||||
rpcapi = consoleauth_rpcapi.ConsoleAuthAPI()
|
||||
return rpcapi.check_token(ctxt, token=token)
|
||||
|
||||
def _get_connect_info_database(self, ctxt, token):
|
||||
# NOTE(PaulMurray) ConsoleAuthToken.validate validates the token.
|
||||
# We call the compute manager directly to check the console port
|
||||
# is correct.
|
||||
connect_info = self._console_auth_token_obj_to_dict(
|
||||
objects.ConsoleAuthToken.validate(ctxt, token))
|
||||
|
||||
valid_port = self._check_console_port(
|
||||
ctxt, connect_info['instance_uuid'], connect_info['port'],
|
||||
connect_info['console_type'])
|
||||
|
||||
if not valid_port:
|
||||
raise exception.InvalidToken(token='***')
|
||||
|
||||
return connect_info
|
||||
|
||||
def _get_connect_info(self, ctxt, token):
|
||||
"""Validate the token and get the connect info."""
|
||||
connect_info = None
|
||||
# NOTE(PaulMurray) if we are using cells v1, we use the old consoleauth
|
||||
# way of doing things. The database backend is not supported for cells
|
||||
# v1.
|
||||
if CONF.cells.enable:
|
||||
connect_info = self._get_connect_info_consoleauth(ctxt, token)
|
||||
if not connect_info:
|
||||
raise exception.InvalidToken(token='***')
|
||||
else:
|
||||
# NOTE(melwitt): If consoleauth is enabled to aid in transitioning
|
||||
# to the database backend, check it first before falling back to
|
||||
# the database. Tokens that existed pre-database-backend will
|
||||
# reside in the consoleauth service storage.
|
||||
if CONF.workarounds.enable_consoleauth:
|
||||
connect_info = self._get_connect_info_consoleauth(ctxt, token)
|
||||
# If consoleauth is enabled to aid in transitioning to the database
|
||||
# backend and we didn't find a token in the consoleauth service
|
||||
# storage, check the database for a token because it's probably a
|
||||
# post-database-backend token, which are stored in the database.
|
||||
if not connect_info:
|
||||
connect_info = self._get_connect_info_database(ctxt, token)
|
||||
|
||||
return connect_info
|
||||
|
||||
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
|
||||
@ -151,11 +233,7 @@ class NovaProxyRequestHandlerBase(object):
|
||||
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)
|
||||
connect_info = self._get_connect_info(ctxt, token)
|
||||
|
||||
# Verify Origin
|
||||
expected_origin_hostname = self.headers.get('Host')
|
||||
@ -239,6 +317,10 @@ class NovaProxyRequestHandlerBase(object):
|
||||
class NovaProxyRequestHandler(NovaProxyRequestHandlerBase,
|
||||
websockify.ProxyRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Order matters here. ProxyRequestHandler.__init__() will eventually
|
||||
# call new_websocket_client() and we need self.compute_rpcapi set
|
||||
# before then.
|
||||
self.compute_rpcapi = compute_rpcapi.ComputeAPI()
|
||||
websockify.ProxyRequestHandler.__init__(self, *args, **kwargs)
|
||||
|
||||
def socket(self, *args, **kwargs):
|
||||
|
@ -14,13 +14,117 @@
|
||||
|
||||
"""Tests for nova websocketproxy."""
|
||||
|
||||
import mock
|
||||
import copy
|
||||
import socket
|
||||
|
||||
import mock
|
||||
|
||||
import nova.conf
|
||||
from nova.console.securityproxy import base
|
||||
from nova.console import websocketproxy
|
||||
from nova import context as nova_context
|
||||
from nova import exception
|
||||
from nova import objects
|
||||
from nova import test
|
||||
from nova.tests.unit import fake_console_auth_token as fake_ca
|
||||
from nova.tests import uuidsentinel as uuids
|
||||
from nova import utils
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
|
||||
|
||||
class NovaProxyRequestHandlerDBTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(NovaProxyRequestHandlerDBTestCase, self).setUp()
|
||||
|
||||
self.flags(console_allowed_origins=['allowed-origin-example-1.net',
|
||||
'allowed-origin-example-2.net'])
|
||||
with mock.patch('websockify.ProxyRequestHandler'):
|
||||
self.wh = websocketproxy.NovaProxyRequestHandler()
|
||||
self.wh.server = websocketproxy.NovaWebSocketProxy()
|
||||
self.wh.socket = mock.MagicMock()
|
||||
self.wh.msg = mock.MagicMock()
|
||||
self.wh.do_proxy = mock.MagicMock()
|
||||
self.wh.headers = mock.MagicMock()
|
||||
|
||||
def _fake_console_db(self, **updates):
|
||||
console_db = copy.deepcopy(fake_ca.fake_token_dict)
|
||||
console_db['token_hash'] = utils.get_sha256_str('123-456-789')
|
||||
if updates:
|
||||
console_db.update(updates)
|
||||
return console_db
|
||||
|
||||
fake_header = {
|
||||
'cookie': 'token="123-456-789"',
|
||||
'Origin': 'https://example.net:6080',
|
||||
'Host': 'example.net:6080',
|
||||
}
|
||||
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
@mock.patch('nova.objects.Instance.get_by_uuid')
|
||||
@mock.patch('nova.compute.rpcapi.ComputeAPI.validate_console_port')
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_db(
|
||||
self, mock_ca_check, mock_validate_port, mock_inst_get,
|
||||
mock_validate, internal_access_path=None,
|
||||
instance_not_found=False):
|
||||
|
||||
db_obj = self._fake_console_db(
|
||||
host='node1',
|
||||
port=10000,
|
||||
console_type='novnc',
|
||||
access_url_base='https://example.net:6080',
|
||||
internal_access_path=internal_access_path,
|
||||
instance_uuid=uuids.instance,
|
||||
# This is set by ConsoleAuthToken.validate
|
||||
token='123-456-789'
|
||||
)
|
||||
ctxt = nova_context.get_context()
|
||||
obj = nova.objects.ConsoleAuthToken._from_db_object(
|
||||
ctxt, nova.objects.ConsoleAuthToken(), db_obj)
|
||||
mock_validate.return_value = obj
|
||||
|
||||
if instance_not_found:
|
||||
mock_inst_get.side_effect = exception.InstanceNotFound(
|
||||
instance_id=uuids.instance)
|
||||
|
||||
if internal_access_path is None:
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
else:
|
||||
tsock = mock.MagicMock()
|
||||
tsock.recv.return_value = "HTTP/1.1 200 OK\r\n\r\n"
|
||||
self.wh.socket.return_value = tsock
|
||||
|
||||
self.wh.path = "http://127.0.0.1/?token=123-456-789"
|
||||
self.wh.headers = self.fake_header
|
||||
|
||||
if instance_not_found:
|
||||
self.assertRaises(exception.InvalidToken,
|
||||
self.wh.new_websocket_client)
|
||||
else:
|
||||
with mock.patch('nova.context.get_admin_context',
|
||||
return_value=ctxt):
|
||||
self.wh.new_websocket_client()
|
||||
|
||||
mock_validate.called_once_with(ctxt, '123-456-789')
|
||||
mock_validate_port.assert_called_once_with(
|
||||
ctxt, mock_inst_get.return_value, str(db_obj['port']),
|
||||
db_obj['console_type'])
|
||||
mock_ca_check.assert_not_called()
|
||||
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
|
||||
if internal_access_path is None:
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
else:
|
||||
self.wh.do_proxy.assert_called_with(tsock)
|
||||
|
||||
def test_new_websocket_client_db_internal_access_path(self):
|
||||
self.test_new_websocket_client_db(internal_access_path='vmid')
|
||||
|
||||
def test_new_websocket_client_db_instance_not_found(self):
|
||||
self.test_new_websocket_client_db(instance_not_found=True)
|
||||
|
||||
|
||||
class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
@ -32,7 +136,8 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
'allowed-origin-example-2.net'],
|
||||
group='console')
|
||||
self.server = websocketproxy.NovaWebSocketProxy()
|
||||
self.wh = websocketproxy.NovaProxyRequestHandlerBase()
|
||||
with mock.patch('websockify.ProxyRequestHandler'):
|
||||
self.wh = websocketproxy.NovaProxyRequestHandler()
|
||||
self.wh.server = self.server
|
||||
self.wh.socket = mock.MagicMock()
|
||||
self.wh.msg = mock.MagicMock()
|
||||
@ -93,7 +198,9 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
}
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client(self, check_token):
|
||||
def test_new_websocket_client_with_server_with_cells(self, check_token):
|
||||
# this test cells enabled, so consoleauth should be used
|
||||
CONF.set_override('enable', True, group='cells')
|
||||
check_token.return_value = {
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
@ -111,16 +218,18 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_ipv6_url(self, check_token):
|
||||
def test_new_websocket_client_enable_consoleauth(self, check_token):
|
||||
self.flags(enable_consoleauth=True, group='workarounds')
|
||||
|
||||
check_token.return_value = {
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'access_url': 'https://[2001:db8::1]:6080'
|
||||
'access_url': 'https://example.net:6080'
|
||||
}
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
self.wh.path = "http://[2001:db8::1]/?token=123-456-789"
|
||||
self.wh.headers = self.fake_header_ipv6
|
||||
self.wh.path = "http://127.0.0.1/?token=123-456-789"
|
||||
self.wh.headers = self.fake_header
|
||||
|
||||
self.wh.new_websocket_client()
|
||||
|
||||
@ -128,26 +237,117 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_token_invalid(self, check_token):
|
||||
check_token.return_value = False
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token',
|
||||
return_value=None)
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_enable_consoleauth_fallback(self, validate,
|
||||
check_port,
|
||||
check_token):
|
||||
# Since consoleauth is enabled, it should be called first before
|
||||
# falling back to the database.
|
||||
self.flags(enable_consoleauth=True, group='workarounds')
|
||||
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
self.wh.path = "http://127.0.0.1/?token=123-456-789"
|
||||
self.wh.headers = self.fake_header
|
||||
|
||||
self.wh.new_websocket_client()
|
||||
|
||||
check_token.assert_called_with(mock.ANY, token="123-456-789")
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client(self, validate, check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
self.wh.path = "http://127.0.0.1/?token=123-456-789"
|
||||
self.wh.headers = self.fake_header
|
||||
|
||||
self.wh.new_websocket_client()
|
||||
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_ipv6_url(self, validate, check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'access_url_base': 'https://[2001:db8::1]:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
self.wh.path = "http://[2001:db8::1]/?token=123-456-789"
|
||||
self.wh.headers = self.fake_header_ipv6
|
||||
|
||||
self.wh.new_websocket_client()
|
||||
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_token_invalid(self, validate):
|
||||
validate.side_effect = exception.InvalidToken(token='XXX')
|
||||
|
||||
self.wh.path = "http://127.0.0.1/?token=XXX"
|
||||
self.wh.headers = self.fake_header_bad_token
|
||||
|
||||
self.assertRaises(exception.InvalidToken,
|
||||
self.wh.new_websocket_client)
|
||||
check_token.assert_called_with(mock.ANY, token="XXX")
|
||||
validate.assert_called_with(mock.ANY, "XXX")
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_internal_access_path(self, check_token):
|
||||
check_token.return_value = {
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_internal_access_path(self, validate,
|
||||
check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'internal_access_path': 'vmid',
|
||||
'console_type': 'novnc',
|
||||
'access_url': 'https://example.net:6080'
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
tsock = mock.MagicMock()
|
||||
tsock.recv.return_value = "HTTP/1.1 200 OK\r\n\r\n"
|
||||
@ -158,20 +358,29 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
|
||||
self.wh.new_websocket_client()
|
||||
|
||||
check_token.assert_called_with(mock.ANY, token="123-456-789")
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
tsock.send.assert_called_with(test.MatchType(bytes))
|
||||
self.wh.do_proxy.assert_called_with(tsock)
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_internal_access_path_err(self, check_token):
|
||||
check_token.return_value = {
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_internal_access_path_err(self, validate,
|
||||
check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'internal_access_path': 'xxx',
|
||||
'console_type': 'novnc',
|
||||
'access_url': 'https://example.net:6080'
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
tsock = mock.MagicMock()
|
||||
tsock.recv.return_value = "HTTP/1.1 500 Internal Server Error\r\n\r\n"
|
||||
@ -182,17 +391,24 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
|
||||
self.assertRaises(exception.InvalidConnectionInfo,
|
||||
self.wh.new_websocket_client)
|
||||
check_token.assert_called_with(mock.ANY, token="123-456-789")
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_internal_access_path_rfb(self, check_token):
|
||||
check_token.return_value = {
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_internal_access_path_rfb(self, validate,
|
||||
check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'internal_access_path': 'vmid',
|
||||
'console_type': 'novnc',
|
||||
'access_url': 'https://example.net:6080'
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
tsock = mock.MagicMock()
|
||||
HTTP_RESP = "HTTP/1.1 200 OK\r\n\r\n"
|
||||
@ -207,30 +423,37 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
|
||||
self.wh.new_websocket_client()
|
||||
|
||||
check_token.assert_called_with(mock.ANY, token="123-456-789")
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
tsock.recv.assert_has_calls([mock.call(4096, socket.MSG_PEEK),
|
||||
mock.call(len(HTTP_RESP))])
|
||||
self.wh.do_proxy.assert_called_with(tsock)
|
||||
|
||||
@mock.patch.object(websocketproxy, 'sys')
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_py273_good_scheme(
|
||||
self, check_token, mock_sys):
|
||||
self, validate, check_port, mock_sys):
|
||||
mock_sys.version_info.return_value = (2, 7, 3)
|
||||
check_token.return_value = {
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'access_url': 'https://example.net:6080'
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
self.wh.path = "http://127.0.0.1/?token=123-456-789"
|
||||
self.wh.headers = self.fake_header
|
||||
|
||||
self.wh.new_websocket_client()
|
||||
|
||||
check_token.assert_called_with(mock.ANY, token="123-456-789")
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
|
||||
@ -268,13 +491,20 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
self.assertFalse(getfqdn.called) # no reverse dns look up
|
||||
self.assertEqual(handler.address_string(), '8.8.8.8') # plain address
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_novnc_bad_origin_header(self, check_token):
|
||||
check_token.return_value = {
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_novnc_bad_origin_header(self, validate,
|
||||
check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.path = "http://127.0.0.1/"
|
||||
self.wh.headers = self.fake_header_bad_origin
|
||||
@ -282,32 +512,46 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
self.assertRaises(exception.ValidationError,
|
||||
self.wh.new_websocket_client)
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_novnc_allowed_origin_header(self,
|
||||
check_token):
|
||||
check_token.return_value = {
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_novnc_allowed_origin_header(self, validate,
|
||||
check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'access_url': 'https://example.net:6080'
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
self.wh.path = "http://127.0.0.1/"
|
||||
self.wh.headers = self.fake_header_allowed_origin
|
||||
|
||||
self.wh.new_websocket_client()
|
||||
|
||||
check_token.assert_called_with(mock.ANY, token="123-456-789")
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_novnc_blank_origin_header(self, check_token):
|
||||
check_token.return_value = {
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_novnc_blank_origin_header(self, validate,
|
||||
check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.path = "http://127.0.0.1/"
|
||||
self.wh.headers = self.fake_header_blank_origin
|
||||
@ -315,32 +559,46 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
self.assertRaises(exception.ValidationError,
|
||||
self.wh.new_websocket_client)
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_novnc_no_origin_header(self, check_token):
|
||||
check_token.return_value = {
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_novnc_no_origin_header(self, validate,
|
||||
check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
self.wh.path = "http://127.0.0.1/"
|
||||
self.wh.headers = self.fake_header_no_origin
|
||||
|
||||
self.wh.new_websocket_client()
|
||||
|
||||
check_token.assert_called_with(mock.ANY, token="123-456-789")
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_novnc_https_origin_proto_http(self,
|
||||
check_token):
|
||||
check_token.return_value = {
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_novnc_https_origin_proto_http(
|
||||
self, validate, check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'access_url': 'http://example.net:6080'
|
||||
'access_url_base': 'http://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.path = "https://127.0.0.1/"
|
||||
self.wh.headers = self.fake_header
|
||||
@ -348,15 +606,21 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
self.assertRaises(exception.ValidationError,
|
||||
self.wh.new_websocket_client)
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_novnc_https_origin_proto_ws(self,
|
||||
check_token):
|
||||
check_token.return_value = {
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_novnc_https_origin_proto_ws(self, validate,
|
||||
check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'serial',
|
||||
'access_url': 'ws://example.net:6080'
|
||||
'access_url_base': 'ws://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.path = "https://127.0.0.1/"
|
||||
self.wh.headers = self.fake_header
|
||||
@ -364,13 +628,20 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
self.assertRaises(exception.ValidationError,
|
||||
self.wh.new_websocket_client)
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_new_websocket_client_novnc_bad_console_type(self, check_token):
|
||||
check_token.return_value = {
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_novnc_bad_console_type(self, validate,
|
||||
check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'bad-console-type'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.path = "http://127.0.0.1/"
|
||||
self.wh.headers = self.fake_header
|
||||
@ -378,21 +649,28 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase):
|
||||
self.assertRaises(exception.ValidationError,
|
||||
self.wh.new_websocket_client)
|
||||
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
|
||||
def test_malformed_cookie(self, check_token):
|
||||
check_token.return_value = {
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandlerBase.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_malformed_cookie(self, validate, check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'access_url': 'https://example.net:6080'
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
self.wh.path = "http://127.0.0.1/"
|
||||
self.wh.headers = self.fake_header_malformed_cookie
|
||||
|
||||
self.wh.new_websocket_client()
|
||||
|
||||
check_token.assert_called_with(mock.ANY, token="123-456-789")
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
|
||||
@ -411,7 +689,8 @@ class NovaWebsocketSecurityProxyTestCase(test.NoDBTestCase):
|
||||
spec=base.SecurityProxy)
|
||||
)
|
||||
|
||||
self.wh = websocketproxy.NovaProxyRequestHandlerBase()
|
||||
with mock.patch('websockify.ProxyRequestHandler'):
|
||||
self.wh = websocketproxy.NovaProxyRequestHandler()
|
||||
self.wh.server = self.server
|
||||
self.wh.path = "http://127.0.0.1/?token=123-456-789"
|
||||
self.wh.socket = mock.MagicMock()
|
||||
@ -431,17 +710,20 @@ class NovaWebsocketSecurityProxyTestCase(test.NoDBTestCase):
|
||||
|
||||
self.wh.headers.get = get_header
|
||||
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
@mock.patch('nova.objects.Instance.get_by_uuid')
|
||||
@mock.patch('nova.compute.rpcapi.ComputeAPI.validate_console_port')
|
||||
@mock.patch('nova.console.websocketproxy.TenantSock.close')
|
||||
@mock.patch('nova.console.websocketproxy.TenantSock.finish_up')
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token',
|
||||
return_value=True)
|
||||
def test_proxy_connect_ok(self, check_token, mock_finish, mock_close):
|
||||
check_token.return_value = {
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'access_url': 'https://example.net:6080'
|
||||
}
|
||||
def test_proxy_connect_ok(self, mock_finish, mock_close,
|
||||
mock_port_validate, mock_get,
|
||||
mock_token_validate):
|
||||
mock_token_validate.return_value = nova.objects.ConsoleAuthToken(
|
||||
instance_uuid=uuids.instance, host='node1', port='10000',
|
||||
console_type='novnc', access_url_base='https://example.net:6080')
|
||||
# The token and id attributes are set by the validate() method.
|
||||
mock_token_validate.return_value.token = '123-456-789'
|
||||
mock_token_validate.return_value.id = 1
|
||||
|
||||
sock = mock.MagicMock(
|
||||
spec=websocketproxy.TenantSock)
|
||||
@ -453,17 +735,20 @@ class NovaWebsocketSecurityProxyTestCase(test.NoDBTestCase):
|
||||
mock_finish.assert_called_with()
|
||||
self.assertEqual(len(mock_close.calls), 0)
|
||||
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
@mock.patch('nova.objects.Instance.get_by_uuid')
|
||||
@mock.patch('nova.compute.rpcapi.ComputeAPI.validate_console_port')
|
||||
@mock.patch('nova.console.websocketproxy.TenantSock.close')
|
||||
@mock.patch('nova.console.websocketproxy.TenantSock.finish_up')
|
||||
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token',
|
||||
return_value=True)
|
||||
def test_proxy_connect_err(self, check_token, mock_finish, mock_close):
|
||||
check_token.return_value = {
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'access_url': 'https://example.net:6080'
|
||||
}
|
||||
def test_proxy_connect_err(self, mock_finish, mock_close,
|
||||
mock_port_validate, mock_get,
|
||||
mock_token_validate):
|
||||
mock_token_validate.return_value = nova.objects.ConsoleAuthToken(
|
||||
instance_uuid=uuids.instance, host='node1', port='10000',
|
||||
console_type='novnc', access_url_base='https://example.net:6080')
|
||||
# The token attribute is set by the validate() method.
|
||||
mock_token_validate.return_value.token = '123-456-789'
|
||||
mock_token_validate.return_value.id = 1
|
||||
|
||||
ex = exception.SecurityProxyNegotiationFailed("Wibble")
|
||||
self.server.security_proxy.connect.side_effect = ex
|
||||
|
@ -0,0 +1,24 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
The ``nova-consoleauth`` service has been deprecated and new consoles will
|
||||
have their token authorizations stored in cell databases instead of in the
|
||||
``nova-consoleauth`` service backend. With this, console proxies are
|
||||
required to be deployed per cell. All existing consoles will be reset. For
|
||||
most operators, this should be a minimal disruption as the default TTL of a
|
||||
console token is 10 minutes.
|
||||
|
||||
Operators that have configured a much longer token TTL or otherwise wish to
|
||||
avoid immediately resetting all existing consoles can use the new
|
||||
configuration option ``[workarounds]/enable_consoleauth`` to fall back on
|
||||
the ``nova-consoleauth`` service for locating existing console
|
||||
authorizations. The option defaults to False. Once all of the existing
|
||||
consoles have naturally expired, operators may unset the configuration
|
||||
option and discontinue running the consoleauth service. For example, if
|
||||
a deployment has configured a token TTL of one hour, the operator may
|
||||
disable the ``[workarounds]/enable_consoleauth`` option and stop running
|
||||
the ``nova-consoleauth`` service one hour after deploying the new code.
|
||||
|
||||
Operators who do not need to use the ``[workarounds]/enable_consoleauth``
|
||||
configuration option may discontinue running the consoleauth service
|
||||
immediately.
|
Loading…
Reference in New Issue
Block a user