From da25d7a50696e9ff37e143ce5546e0610915be18 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Tue, 4 Dec 2018 14:19:56 -0800 Subject: [PATCH] Move environment variables to SingletonConfigurable Created a singleton class `Gateway` to store all configuration options for a Gateway. This class also holds some help methods to make it easier to use the options and determine if the gateway option is enabled. Updated the NotebookTestBase class to allow for subclasses to infuence the patched environment as well as command line options via argv. Added a test to ensure various gateway configuration items can be set via the environment or command-line. --- notebook/gateway/handlers.py | 63 ++----- notebook/gateway/managers.py | 284 ++++++++++++++++++++++++------- notebook/notebookapp.py | 42 ++--- notebook/tests/launchnotebook.py | 28 +-- notebook/tests/test_gateway.py | 32 +++- 5 files changed, 293 insertions(+), 156 deletions(-) diff --git a/notebook/gateway/handlers.py b/notebook/gateway/handlers.py index 30098787e41..1e28bfa779f 100644 --- a/notebook/gateway/handlers.py +++ b/notebook/gateway/handlers.py @@ -21,27 +21,7 @@ from jupyter_client.session import Session from traitlets.config.configurable import LoggingConfigurable -# Note: Although some of these are available via NotebookApp (command line), we will -# take the approach of using environment variables to enable separate sets of values -# for use with the local Notebook server (via command-line) and remote Gateway server -# (via environment variables). - -GATEWAY_HEADERS = json.loads(os.getenv('GATEWAY_HEADERS', '{}')) -GATEWAY_HEADERS.update({ - 'Authorization': 'token {}'.format(os.getenv('GATEWAY_AUTH_TOKEN', '')) -}) -VALIDATE_GATEWAY_CERT = os.getenv('VALIDATE_GATEWAY_CERT') not in ['no', 'false'] - -GATEWAY_CLIENT_KEY = os.getenv('GATEWAY_CLIENT_KEY') -GATEWAY_CLIENT_CERT = os.getenv('GATEWAY_CLIENT_CERT') -GATEWAY_CLIENT_CA = os.getenv('GATEWAY_CLIENT_CA') - -GATEWAY_HTTP_USER = os.getenv('GATEWAY_HTTP_USER') -GATEWAY_HTTP_PASS = os.getenv('GATEWAY_HTTP_PASS') - -# Get env variables to handle timeout of request and connection -GATEWAY_CONNECT_TIMEOUT = float(os.getenv('GATEWAY_CONNECT_TIMEOUT', 20.0)) -GATEWAY_REQUEST_TIMEOUT = float(os.getenv('GATEWAY_REQUEST_TIMEOUT', 20.0)) +from .managers import Gateway class WebSocketChannelsHandler(WebSocketHandler, IPythonHandler): @@ -77,7 +57,7 @@ def authenticate(self): def initialize(self): self.log.debug("Initializing websocket connection %s", self.request.path) self.session = Session(config=self.config) - self.gateway = GatewayWebSocketClient(gateway_url=self.kernel_manager.parent.gateway_url) + self.gateway = GatewayWebSocketClient(gateway_url=Gateway.instance().url) @gen.coroutine def get(self, kernel_id, *args, **kwargs): @@ -135,7 +115,6 @@ class GatewayWebSocketClient(LoggingConfigurable): def __init__(self, **kwargs): super(GatewayWebSocketClient, self).__init__(**kwargs) - self.gateway_url = kwargs['gateway_url'] self.kernel_id = None self.ws = None self.ws_future = Future() @@ -145,29 +124,14 @@ def __init__(self, **kwargs): def _connect(self, kernel_id): self.kernel_id = kernel_id ws_url = url_path_join( - os.getenv('GATEWAY_WS_URL', self.gateway_url.replace('http', 'ws')), - '/api/kernels', - url_escape(kernel_id), - 'channels' + Gateway.instance().ws_url, + Gateway.instance().kernels_endpoint, url_escape(kernel_id), 'channels' ) self.log.info('Connecting to {}'.format(ws_url)) - parameters = { - "headers": GATEWAY_HEADERS, - "validate_cert": VALIDATE_GATEWAY_CERT, - "connect_timeout": GATEWAY_CONNECT_TIMEOUT, - "request_timeout": GATEWAY_REQUEST_TIMEOUT - } - if GATEWAY_HTTP_USER: - parameters["auth_username"] = GATEWAY_HTTP_USER - if GATEWAY_HTTP_PASS: - parameters["auth_password"] = GATEWAY_HTTP_PASS - if GATEWAY_CLIENT_KEY: - parameters["client_key"] = GATEWAY_CLIENT_KEY - parameters["client_cert"] = GATEWAY_CLIENT_CERT - if GATEWAY_CLIENT_CA: - parameters["ca_certs"] = GATEWAY_CLIENT_CA - - request = HTTPRequest(ws_url, **parameters) + kwargs = {} + kwargs = Gateway.instance().load_connection_args(**kwargs) + + request = HTTPRequest(ws_url, **kwargs) self.ws_future = websocket_connect(request) self.ws_future.add_done_callback(self._connection_done) @@ -178,7 +142,7 @@ def _connection_done(self, fut): else: self.log.warning("Websocket connection has been cancelled via client disconnect before its establishment. " "Kernel with ID '{}' may not be terminated on Gateway: {}". - format(self.kernel_id, self.gateway_url)) + format(self.kernel_id, Gateway.instance().url)) def _disconnect(self): if self.ws is not None: @@ -343,18 +307,15 @@ def get(self): # NOTE: We do this here since this handler is called during the Notebook's startup and subsequent refreshes # of the tree view. except ConnectionRefusedError: - gateway_url = ksm.parent.gateway_url self.log.error("Connection refused from Gateway server url '{}'. " - "Check to be sure the Gateway instance is running.".format(gateway_url)) + "Check to be sure the Gateway instance is running.".format(Gateway.instance().url)) except HTTPTimeoutError: # This can occur if the host is valid (e.g., foo.com) but there's nothing there. - gateway_url = ksm.parent.gateway_url self.log.error("Timeout error attempting to connect to Gateway server url '{}'. " - "Ensure gateway_url is valid and the Gateway instance is running.".format(gateway_url)) + "Ensure gateway url is valid and the Gateway instance is running.".format(Gateway.instance().url)) except gaierror as e: - gateway_url = ksm.parent.gateway_url self.log.error("The Gateway server specified in the gateway_url '{}' doesn't appear to be valid. " - "Ensure gateway_url is valid and the Gateway instance is running.".format(gateway_url)) + "Ensure gateway url is valid and the Gateway instance is running.".format(Gateway.instance().url)) self.finish() diff --git a/notebook/gateway/managers.py b/notebook/gateway/managers.py index 31dcba9cdce..f8624cbeb3f 100644 --- a/notebook/gateway/managers.py +++ b/notebook/gateway/managers.py @@ -14,78 +14,243 @@ from jupyter_client.kernelspec import KernelSpecManager from ..utils import url_path_join -from traitlets import Instance, Unicode, default +from traitlets import Instance, Unicode, Float, Bool, default, validate, TraitError +from traitlets.config import SingletonConfigurable -# Note: Although some of these are available via NotebookApp (command line), we will -# take the approach of using environment variables to enable separate sets of values -# for use with the local Notebook server (via command-line) and remote Gateway server -# (via environment variables). -GATEWAY_HEADERS = json.loads(os.getenv('GATEWAY_HEADERS', '{}')) -GATEWAY_HEADERS.update({ - 'Authorization': 'token {}'.format(os.getenv('GATEWAY_AUTH_TOKEN', '')) -}) -VALIDATE_GATEWAY_CERT = os.getenv('VALIDATE_GATEWAY_CERT') not in ['no', 'false'] +@gen.coroutine +def fetch_gateway(endpoint, **kwargs): + """Make an async request to kernel gateway endpoint.""" + client = AsyncHTTPClient() -GATEWAY_CLIENT_KEY = os.getenv('GATEWAY_CLIENT_KEY') -GATEWAY_CLIENT_CERT = os.getenv('GATEWAY_CLIENT_CERT') -GATEWAY_CLIENT_CA = os.getenv('GATEWAY_CLIENT_CA') + kwargs = Gateway.instance().load_connection_args(**kwargs) -GATEWAY_HTTP_USER = os.getenv('GATEWAY_HTTP_USER') -GATEWAY_HTTP_PASS = os.getenv('GATEWAY_HTTP_PASS') + response = yield client.fetch(endpoint, **kwargs) + raise gen.Return(response) -GATEWAY_CONNECT_TIMEOUT = float(os.getenv('GATEWAY_CONNECT_TIMEOUT', 20.0)) -GATEWAY_REQUEST_TIMEOUT = float(os.getenv('GATEWAY_REQUEST_TIMEOUT', 20.0)) +class Gateway(SingletonConfigurable): + """This class manages the configuration. It's its own class so that we can avoid having command + line options of the likes `--GatewayKernelManager.connect_timeout` and use the shorter and more + applicable `--Gateway.connect_timeout`, etc. It also contains some helper methods to build + request arguments out of the various config options. -def load_connection_args(**kwargs): + """ - if GATEWAY_CLIENT_CERT: - kwargs["client_key"] = kwargs.get("client_key", GATEWAY_CLIENT_KEY) - kwargs["client_cert"] = kwargs.get("client_cert", GATEWAY_CLIENT_CERT) - if GATEWAY_CLIENT_CA: - kwargs["ca_certs"] = kwargs.get("ca_certs", GATEWAY_CLIENT_CA) - kwargs['connect_timeout'] = kwargs.get('connect_timeout', GATEWAY_CONNECT_TIMEOUT) - kwargs['request_timeout'] = kwargs.get('request_timeout', GATEWAY_REQUEST_TIMEOUT) - kwargs['headers'] = kwargs.get('headers', GATEWAY_HEADERS) - kwargs['validate_cert'] = kwargs.get('validate_cert', VALIDATE_GATEWAY_CERT) - if GATEWAY_HTTP_USER: - kwargs['auth_username'] = kwargs.get('auth_username', GATEWAY_HTTP_USER) - if GATEWAY_HTTP_PASS: - kwargs['auth_password'] = kwargs.get('auth_password', GATEWAY_HTTP_PASS) + url = Unicode(default_value=None, allow_none=True, config=True, + help="""The url of the Kernel or Enterprise Gateway server where + kernel specifications are defined and kernel management takes place. + If defined, this Notebook server acts as a proxy for all kernel + management and kernel specification retrieval. (JUPYTER_GATEWAY_URL env var) + """ + ) + + url_env = 'JUPYTER_GATEWAY_URL' + @default('url') + def _url_default(self): + return os.environ.get(self.url_env) + + @validate('url') + def _url_validate(self, proposal): + value = proposal['value'] + # Ensure value, if present, starts with 'http' + if value is not None and len(value) > 0: + if not str(value).lower().startswith('http'): + raise TraitError("Gateway url must start with 'http': '%r'" % value) + return value + + ws_url = Unicode(default_value=None, allow_none=True, config=True, + help="""The websocket url of the Kernel or Enterprise Gateway server. If not provided, this value + will correspond to the value of the Gateway url with 'ws' in place of 'http'. (JUPYTER_GATEWAY_WS_URL env var) + """ + ) + + ws_url_env = 'JUPYTER_GATEWAY_WS_URL' + @default('ws_url') + def _ws_url_default(self): + default_value = os.getenv(self.ws_url_env) + if default_value is None: + if self.gateway_enabled: + default_value = self.url.lower().replace('http', 'ws') + return default_value + + @validate('ws_url') + def _ws_url_validate(self, proposal): + value = proposal['value'] + # Ensure value, if present, starts with 'ws' + if value is not None and len(value) > 0: + if not str(value).lower().startswith('ws'): + raise TraitError("Gateway ws_url must start with 'ws': '%r'" % value) + return value + + kernels_endpoint_default_value = '/api/kernels' + kernels_endpoint_env = 'JUPYTER_GATEWAY_KERNELS_ENDPOINT' + kernels_endpoint = Unicode(default_value=kernels_endpoint_default_value, config=True, + help="""The gateway API endpoint for accessing kernel resources (JUPYTER_GATEWAY_KERNELS_ENDPOINT env var)""") - return kwargs + @default('kernels_endpoint') + def _kernels_endpoint_default(self): + return os.getenv(self.kernels_endpoint_env, self.kernels_endpoint_default_value) + kernelspecs_endpoint_default_value = '/api/kernelspecs' + kernelspecs_endpoint_env = 'JUPYTER_GATEWAY_KERNELSPECS_ENDPOINT' + kernelspecs_endpoint = Unicode(default_value=kernelspecs_endpoint_default_value, config=True, + help="""The gateway API endpoint for accessing kernelspecs (JUPYTER_GATEWAY_KERNELSPECS_ENDPOINT env var)""") -@gen.coroutine -def fetch_gateway(endpoint, **kwargs): - """Make an async request to kernel gateway endpoint.""" - client = AsyncHTTPClient() + @default('kernelspecs_endpoint') + def _kernelspecs_endpoint_default(self): + return os.getenv(self.kernelspecs_endpoint_env, self.kernelspecs_endpoint_default_value) + + connect_timeout_default_value = 20.0 + connect_timeout_env = 'JUPYTER_GATEWAY_CONNECT_TIMEOUT' + connect_timeout = Float(default_value=connect_timeout_default_value, config=True, + help="""The time allowed for HTTP connection establishment with the Gateway server. + (JUPYTER_GATEWAY_CONNECT_TIMEOUT env var)""") + + @default('connect_timeout') + def connect_timeout_default(self): + return float(os.getenv('JUPYTER_GATEWAY_CONNECT_TIMEOUT', self.connect_timeout_default_value)) + + request_timeout_default_value = 20.0 + request_timeout_env = 'JUPYTER_GATEWAY_REQUEST_TIMEOUT' + request_timeout = Float(default_value=request_timeout_default_value, config=True, + help="""The time allowed for HTTP request completion. (JUPYTER_GATEWAY_REQUEST_TIMEOUT env var)""") + + @default('request_timeout') + def request_timeout_default(self): + return float(os.getenv('JUPYTER_GATEWAY_REQUEST_TIMEOUT', self.request_timeout_default_value)) + + client_key = Unicode(default_value=None, allow_none=True, config=True, + help="""The filename for client SSL key, if any. (JUPYTER_GATEWAY_CLIENT_KEY env var) + """ + ) + client_key_env = 'JUPYTER_GATEWAY_CLIENT_KEY' - kwargs = load_connection_args(**kwargs) + @default('client_key') + def _client_key_default(self): + return os.getenv(self.client_key_env) - response = yield client.fetch(endpoint, **kwargs) - raise gen.Return(response) + client_cert = Unicode(default_value=None, allow_none=True, config=True, + help="""The filename for client SSL certificate, if any. (JUPYTER_GATEWAY_CLIENT_CERT env var) + """ + ) + client_cert_env = 'JUPYTER_GATEWAY_CLIENT_CERT' + @default('client_cert') + def _client_cert_default(self): + return os.getenv(self.client_cert_env) -class GatewayKernelManager(MappingKernelManager): - """Kernel manager that supports remote kernels hosted by Jupyter - kernel gateway.""" + ca_certs = Unicode(default_value=None, allow_none=True, config=True, + help="""The filename of CA certificates or None to use defaults. (JUPYTER_GATEWAY_CA_CERTS env var) + """ + ) + ca_certs_env = 'JUPYTER_GATEWAY_CA_CERTS' - kernels_endpoint_env = 'GATEWAY_KERNELS_ENDPOINT' - kernels_endpoint = Unicode(config=True, - help="""The gateway API endpoint for accessing kernel resources (GATEWAY_KERNELS_ENDPOINT env var)""") + @default('ca_certs') + def _ca_certs_default(self): + return os.getenv(self.ca_certs_env) - @default('kernels_endpoint') - def kernels_endpoint_default(self): - return os.getenv(self.kernels_endpoint_env, '/api/kernels') + http_user = Unicode(default_value=None, allow_none=True, config=True, + help="""The username for HTTP authentication. (JUPYTER_GATEWAY_HTTP_USER env var) + """ + ) + http_user_env = 'JUPYTER_GATEWAY_HTTP_USER' + + @default('http_user') + def _http_user_default(self): + return os.getenv(self.http_user_env) + + http_pwd = Unicode(default_value=None, allow_none=True, config=True, + help="""The password for HTTP authentication. (JUPYTER_GATEWAY_HTTP_PWD env var) + """ + ) + http_pwd_env = 'JUPYTER_GATEWAY_HTTP_PWD' + + @default('http_pwd') + def _http_pwd_default(self): + return os.getenv(self.http_pwd_env) + + headers_default_value = '{}' + headers_env = 'JUPYTER_GATEWAY_HEADERS' + headers = Unicode(default_value=headers_default_value, allow_none=True,config=True, + help="""Additional HTTP headers to pass on the request. This value will be converted to a dict. + (JUPYTER_GATEWAY_HEADERS env var) + """ + ) + + @default('headers') + def _headers_default(self): + return os.getenv(self.headers_env, self.headers_default_value) + + auth_token = Unicode(default_value=None, allow_none=True, config=True, + help="""The authorization token used in the HTTP headers. (JUPYTER_GATEWAY_AUTH_TOKEN env var) + """ + ) + auth_token_env = 'JUPYTER_GATEWAY_AUTH_TOKEN' + + @default('auth_token') + def _auth_token_default(self): + return os.getenv(self.auth_token_env) + + validate_cert_default_value = True + validate_cert_env = 'JUPYTER_GATEWAY_VALIDATE_CERT' + validate_cert = Bool(default_value=validate_cert_default_value, config=True, + help="""For HTTPS requests, determines if server's certificate should be validated or not. + (JUPYTER_GATEWAY_VALIDATE_CERT env var)""" + ) + + @default('validate_cert') + def validate_cert_default(self): + return bool(os.getenv(self.validate_cert_env, str(self.validate_cert_default_value)) not in ['no', 'false']) + + def __init__(self, **kwargs): + super(Gateway, self).__init__(**kwargs) + self._static_args = {} # initialized on first use + + @property + def gateway_enabled(self): + return bool(self.url is not None and len(self.url) > 0) + + def init_static_args(self): + """Initialize arguments used on every request. Since these are static values, we'll + perform this operation once. + + """ + self._static_args['headers'] = json.loads(self.headers) + self._static_args['headers'].update({'Authorization': 'token {}'.format(self.auth_token)}) + self._static_args['connect_timeout'] = self.connect_timeout + self._static_args['request_timeout'] = self.request_timeout + self._static_args['validate_cert'] = self.validate_cert + if self.client_cert: + self._static_args['client_cert'] = self.client_cert + self._static_args['client_key'] = self.client_key + if self.ca_certs: + self._static_args['ca_certs'] = self.ca_certs + if self.http_user: + self._static_args['auth_username'] = self.http_user + if self.http_pwd: + self._static_args['auth_password'] = self.http_pwd + + def load_connection_args(self, **kwargs): + """Merges the static args relative to the connection, with the given keyword arguments. If statics + have yet to be initialized, we'll do that here. + + """ + if len(self._static_args) == 0: + self.init_static_args() + + kwargs.update(self._static_args) + return kwargs + +class GatewayKernelManager(MappingKernelManager): + """Kernel manager that supports remote kernels hosted by Jupyter Kernel or Enterprise Gateway.""" # We'll maintain our own set of kernel ids _kernels = {} def __init__(self, **kwargs): super(GatewayKernelManager, self).__init__(**kwargs) - self.gateway_url = self.parent.gateway_url + self.base_endpoint = url_path_join(Gateway.instance().url, Gateway.instance().kernels_endpoint) def __contains__(self, kernel_id): return kernel_id in self._kernels @@ -105,9 +270,9 @@ def _get_kernel_endpoint_url(self, kernel_id=None): kernel_id: kernel UUID (optional) """ if kernel_id: - return url_path_join(self.gateway_url, self.kernels_endpoint, url_escape(str(kernel_id))) + return url_path_join(self.base_endpoint, url_escape(str(kernel_id))) - return url_path_join(self.gateway_url, self.kernels_endpoint) + return self.base_endpoint @gen.coroutine def start_kernel(self, kernel_id=None, path=None, **kwargs): @@ -243,7 +408,7 @@ def shutdown_all(self): """Shutdown all kernels.""" # Note: We have to make this sync because the NotebookApp does not wait for async. kwargs = {'method': 'DELETE'} - kwargs = load_connection_args(**kwargs) + kwargs = Gateway.instance().load_connection_args(**kwargs) client = HTTPClient() for kernel_id in self._kernels.keys(): kernel_url = self._get_kernel_endpoint_url(kernel_id) @@ -259,18 +424,9 @@ def shutdown_all(self): class GatewayKernelSpecManager(KernelSpecManager): - kernelspecs_endpoint_env = 'GATEWAY_KERNELSPECS_ENDPOINT' - kernelspecs_endpoint = Unicode(config=True, - help="""The kernel gateway API endpoint for accessing kernelspecs - (GATEWAY_KERNELSPECS_ENDPOINT env var)""") - - @default('kernelspecs_endpoint') - def kernelspecs_endpoint_default(self): - return os.getenv(self.kernelspecs_endpoint_env, '/api/kernelspecs') - def __init__(self, **kwargs): super(GatewayKernelSpecManager, self).__init__(**kwargs) - self.gateway_url = self.parent.gateway_url + self.base_endpoint = url_path_join(Gateway.instance().url, Gateway.instance().kernelspecs_endpoint) def _get_kernelspecs_endpoint_url(self, kernel_name=None): """Builds a url for the kernels endpoint @@ -280,9 +436,9 @@ def _get_kernelspecs_endpoint_url(self, kernel_name=None): kernel_name: kernel name (optional) """ if kernel_name: - return url_path_join(self.gateway_url, self.kernelspecs_endpoint, url_escape(kernel_name)) + return url_path_join(self.base_endpoint, url_escape(kernel_name)) - return url_path_join(self.gateway_url, self.kernelspecs_endpoint) + return self.base_endpoint @gen.coroutine def list_kernel_specs(self): diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 9371e6a932b..f27bf2ed060 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -83,9 +83,7 @@ from .services.contents.filemanager import FileContentsManager from .services.contents.largefilemanager import LargeFileManager from .services.sessions.sessionmanager import SessionManager -from .gateway.managers import GatewayKernelManager -from .gateway.managers import GatewayKernelSpecManager -from .gateway.managers import GatewaySessionManager +from .gateway.managers import GatewayKernelManager, GatewayKernelSpecManager, GatewaySessionManager, Gateway from .auth.login import LoginHandler from .auth.logout import LogoutHandler @@ -152,14 +150,13 @@ class NotebookWebApplication(web.Application): def __init__(self, jupyter_app, kernel_manager, contents_manager, session_manager, kernel_spec_manager, config_manager, extra_services, log, - base_url, default_url, settings_overrides, jinja_env_options, - gateway_url): + base_url, default_url, settings_overrides, jinja_env_options): settings = self.init_settings( jupyter_app, kernel_manager, contents_manager, session_manager, kernel_spec_manager, config_manager, extra_services, log, base_url, - default_url, settings_overrides, jinja_env_options, gateway_url) + default_url, settings_overrides, jinja_env_options) handlers = self.init_handlers(settings) super(NotebookWebApplication, self).__init__(handlers, **settings) @@ -168,7 +165,7 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager, session_manager, kernel_spec_manager, config_manager, extra_services, log, base_url, default_url, settings_overrides, - jinja_env_options=None, gateway_url=None): + jinja_env_options=None): _template_path = settings_overrides.get( "template_path", @@ -282,7 +279,6 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager, server_root_dir=root_dir, jinja2_env=env, terminals_available=False, # Set later if terminals are available - gateway_url=gateway_url, ) # allow custom overrides for the tornado web app. @@ -316,7 +312,7 @@ def init_handlers(self, settings): handlers.extend(load_handlers('notebook.services.shutdown')) # If gateway server is configured, replace appropriate handlers to perform redirection - if settings['gateway_url']: + if Gateway.instance().gateway_enabled: handlers.extend(load_handlers('notebook.gateway.handlers')) else: handlers.extend(load_handlers('notebook.services.kernels.handlers')) @@ -557,7 +553,7 @@ def start(self): 'notebook-dir': 'NotebookApp.notebook_dir', 'browser': 'NotebookApp.browser', 'pylab': 'NotebookApp.pylab', - 'gateway-url': 'NotebookApp.gateway_url', + 'gateway-url': 'Gateway.url', }) #----------------------------------------------------------------------------- @@ -578,7 +574,7 @@ class NotebookApp(JupyterApp): classes = [ KernelManager, Session, MappingKernelManager, KernelSpecManager, ContentsManager, FileContentsManager, NotebookNotary, - GatewayKernelManager, GatewayKernelSpecManager, GatewaySessionManager, + GatewayKernelManager, GatewayKernelSpecManager, GatewaySessionManager, Gateway, ] flags = Dict(flags) aliases = Dict(aliases) @@ -1305,20 +1301,6 @@ def _update_server_extensions(self, change): is not available. """)) - gateway_url_env = 'GATEWAY_URL' - - @default('gateway_url') - def gateway_url_default(self): - return os.getenv(self.gateway_url_env) - - gateway_url = Unicode(default_value=None, allow_none=True, config=True, - help="""The url of the Kernel or Enterprise Gateway server where - kernel specifications are defined and kernel management takes place. - If defined, this Notebook server acts as a proxy for all kernel - management and kernel specification retrieval. (GATEWAY_URL env var) - """ - ) - def parse_command_line(self, argv=None): super(NotebookApp, self).parse_command_line(argv) @@ -1342,7 +1324,9 @@ def parse_command_line(self, argv=None): def init_configurables(self): # If gateway server is configured, replace appropriate managers to perform redirection - if self.gateway_url: + self.gateway_config = Gateway.instance(parent=self) + + if self.gateway_config.gateway_enabled: self.kernel_manager_class = 'notebook.gateway.managers.GatewayKernelManager' self.session_manager_class = 'notebook.gateway.managers.GatewaySessionManager' self.kernel_spec_manager_class = 'notebook.gateway.managers.GatewayKernelSpecManager' @@ -1415,7 +1399,7 @@ def init_webapp(self): self.session_manager, self.kernel_spec_manager, self.config_manager, self.extra_services, self.log, self.base_url, self.default_url, self.tornado_settings, - self.jinja_environment_options, self.gateway_url, + self.jinja_environment_options, ) ssl_options = self.ssl_options if self.certfile: @@ -1695,8 +1679,8 @@ def notebook_info(self, kernel_count=True): info += "\n" # Format the info so that the URL fits on a single line in 80 char display info += _("The Jupyter Notebook is running at:\n%s") % self.display_url - if self.gateway_url: - info += _("\nKernels will be managed by the Gateway server running at:\n%s") % self.gateway_url + if self.gateway_config.gateway_enabled: + info += _("\nKernels will be managed by the Gateway server running at:\n%s") % self.gateway_config.url return info def server_info(self): diff --git a/notebook/tests/launchnotebook.py b/notebook/tests/launchnotebook.py index 1b685df0ca4..9e84a5964ba 100644 --- a/notebook/tests/launchnotebook.py +++ b/notebook/tests/launchnotebook.py @@ -91,6 +91,22 @@ def request(cls, verb, path, **kwargs): url_path_join(cls.base_url(), path), **kwargs) return response + + @classmethod + def get_patch_env(cls): + return { + 'HOME': cls.home_dir, + 'PYTHONPATH': os.pathsep.join(sys.path), + 'IPYTHONDIR': pjoin(cls.home_dir, '.ipython'), + 'JUPYTER_NO_CONFIG': '1', # needed in the future + 'JUPYTER_CONFIG_DIR' : cls.config_dir, + 'JUPYTER_DATA_DIR' : cls.data_dir, + 'JUPYTER_RUNTIME_DIR': cls.runtime_dir, + } + + @classmethod + def get_argv(cls): + return [] @classmethod def setup_class(cls): @@ -109,15 +125,7 @@ def tmp(*parts): config_dir = cls.config_dir = tmp('config') runtime_dir = cls.runtime_dir = tmp('runtime') cls.notebook_dir = tmp('notebooks') - cls.env_patch = patch.dict('os.environ', { - 'HOME': cls.home_dir, - 'PYTHONPATH': os.pathsep.join(sys.path), - 'IPYTHONDIR': pjoin(cls.home_dir, '.ipython'), - 'JUPYTER_NO_CONFIG': '1', # needed in the future - 'JUPYTER_CONFIG_DIR' : config_dir, - 'JUPYTER_DATA_DIR' : data_dir, - 'JUPYTER_RUNTIME_DIR': runtime_dir, - }) + cls.env_patch = patch.dict('os.environ', cls.get_patch_env()) cls.env_patch.start() cls.path_patch = patch.multiple( jupyter_core.paths, @@ -157,7 +165,7 @@ def start_thread(): # needs to be redone after initialize, which reconfigures logging app.log.propagate = True app.log.handlers = [] - app.initialize(argv=[]) + app.initialize(argv=cls.get_argv()) app.log.propagate = True app.log.handlers = [] loop = IOLoop.current() diff --git a/notebook/tests/test_gateway.py b/notebook/tests/test_gateway.py index 385c1f1cd55..a007046966c 100644 --- a/notebook/tests/test_gateway.py +++ b/notebook/tests/test_gateway.py @@ -7,6 +7,7 @@ from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError from traitlets.config import Config from .launchnotebook import NotebookTestBase +from notebook.gateway.managers import Gateway try: from unittest.mock import patch, Mock @@ -137,12 +138,39 @@ def mock_fetch_gateway(url, **kwargs): class TestGateway(NotebookTestBase): + mock_gateway_url = 'http://mock-gateway-server:8889' + mock_http_user = 'alice' + @classmethod def setup_class(cls): - cls.config = Config() - cls.config.NotebookApp.gateway_url = 'http://mock-gateway-server:8889' + Gateway.clear_instance() super(TestGateway, cls).setup_class() + @classmethod + def teardown_class(cls): + Gateway.clear_instance() + super(TestGateway, cls).teardown_class() + + @classmethod + def get_patch_env(cls): + test_env = super(TestGateway, cls).get_patch_env() + test_env.update({'JUPYTER_GATEWAY_URL': TestGateway.mock_gateway_url, + 'JUPYTER_GATEWAY_REQUEST_TIMEOUT': '44.4'}) + return test_env + + @classmethod + def get_argv(cls): + argv = super(TestGateway, cls).get_argv() + argv.extend(['--Gateway.connect_timeout=44.4', '--Gateway.http_user=' + TestGateway.mock_http_user]) + return argv + + def test_gateway_options(self): + nt.assert_equal(self.notebook.gateway_config.gateway_enabled, True) + nt.assert_equal(self.notebook.gateway_config.url, TestGateway.mock_gateway_url) + nt.assert_equal(self.notebook.gateway_config.http_user, TestGateway.mock_http_user) + nt.assert_equal(self.notebook.gateway_config.connect_timeout, self.notebook.gateway_config.connect_timeout) + nt.assert_equal(self.notebook.gateway_config.connect_timeout, 44.4) + def test_gateway_class_mappings(self): # Ensure appropriate class mappings are in place. nt.assert_equal(self.notebook.kernel_manager_class.__name__, 'GatewayKernelManager')