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:
Paul Murray 2016-06-24 17:32:03 +01:00 committed by Matt Riedemann
parent 93f4364a4e
commit 969239029d
6 changed files with 515 additions and 89 deletions

View File

@ -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):

View File

@ -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``
""")
]

View File

@ -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``
"""),
]

View File

@ -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):

View File

@ -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

View File

@ -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.