From 969239029d4a13956747e6e0b850d6c6ab4035f0 Mon Sep 17 00:00:00 2001 From: Paul Murray Date: Fri, 24 Jun 2016 17:32:03 +0100 Subject: [PATCH] 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 partially-implements: blueprint convert-consoles-to-objects Depends-On: I67894a31b887a93de26f3d2d8a1fa84be5b9ea89 Change-Id: If1b6e5f20d2ea82d94f5f0550f13189fc9bc16c4 --- nova/cmd/baseproxy.py | 2 + nova/conf/consoleauth.py | 4 + nova/conf/workarounds.py | 29 ++ nova/console/websocketproxy.py | 92 +++- .../tests/unit/console/test_websocketproxy.py | 453 ++++++++++++++---- ...s-enable-consoleauth-71d68c3879dc2c8a.yaml | 24 + 6 files changed, 515 insertions(+), 89 deletions(-) create mode 100644 releasenotes/notes/workarounds-enable-consoleauth-71d68c3879dc2c8a.yaml diff --git a/nova/cmd/baseproxy.py b/nova/cmd/baseproxy.py index bed9c70abf35..020d0aadf788 100644 --- a/nova/cmd/baseproxy.py +++ b/nova/cmd/baseproxy.py @@ -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): diff --git a/nova/conf/consoleauth.py b/nova/conf/consoleauth.py index 0ebadbb2415f..3f4480ee380f 100644 --- a/nova/conf/consoleauth.py +++ b/nova/conf/consoleauth.py @@ -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`` """) ] diff --git a/nova/conf/workarounds.py b/nova/conf/workarounds.py index a53247759019..62cc1ab4bea1 100644 --- a/nova/conf/workarounds.py +++ b/nova/conf/workarounds.py @@ -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`` """), ] diff --git a/nova/console/websocketproxy.py b/nova/console/websocketproxy.py index d93d61ac6b69..105765ebedbc 100644 --- a/nova/console/websocketproxy.py +++ b/nova/console/websocketproxy.py @@ -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): diff --git a/nova/tests/unit/console/test_websocketproxy.py b/nova/tests/unit/console/test_websocketproxy.py index 154013b4775e..60ccbc16174d 100644 --- a/nova/tests/unit/console/test_websocketproxy.py +++ b/nova/tests/unit/console/test_websocketproxy.py @@ -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 = '' + 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('') + 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('') @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 = '' - 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('') - @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 = '' + 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('') + + @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 = '' + 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('') + + @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 = '' + 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('') + + @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 = '' 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('') @@ -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 = '' 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('') - @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 = '' 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('') - @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 = '' 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('') @@ -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 diff --git a/releasenotes/notes/workarounds-enable-consoleauth-71d68c3879dc2c8a.yaml b/releasenotes/notes/workarounds-enable-consoleauth-71d68c3879dc2c8a.yaml new file mode 100644 index 000000000000..a6f21497ecdd --- /dev/null +++ b/releasenotes/notes/workarounds-enable-consoleauth-71d68c3879dc2c8a.yaml @@ -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.