diff --git a/notebook/gateway/handlers.py b/notebook/gateway/handlers.py index 8e09b10861..0fbbfb18b9 100644 --- a/notebook/gateway/handlers.py +++ b/notebook/gateway/handlers.py @@ -3,8 +3,9 @@ import os import logging +import mimetypes -from ..base.handlers import IPythonHandler +from ..base.handlers import APIHandler, IPythonHandler from ..utils import url_path_join from tornado import gen, web @@ -200,8 +201,26 @@ def on_close(self): self._disconnect() +class GatewayResourceHandler(APIHandler): + """Retrieves resources for specific kernelspec definitions from kernel/enterprise gateway.""" + + @web.authenticated + @gen.coroutine + def get(self, kernel_name, path, include_body=True): + ksm = self.kernel_spec_manager + kernel_spec_res = yield ksm.get_kernel_spec_resource(kernel_name, path) + if kernel_spec_res is None: + self.log.warning("Kernelspec resource '{}' for '{}' not found. Gateway may not support" + " resource serving.".format(path, kernel_name)) + else: + self.set_header("Content-Type", mimetypes.guess_type(path)[0]) + self.finish(kernel_spec_res) + + from ..services.kernels.handlers import _kernel_id_regex +from ..services.kernelspecs.handlers import kernel_name_regex default_handlers = [ (r"/api/kernels/%s/channels" % _kernel_id_regex, WebSocketChannelsHandler), + (r"/kernelspecs/%s/(?P.*)" % kernel_name_regex, GatewayResourceHandler), ] diff --git a/notebook/gateway/managers.py b/notebook/gateway/managers.py index 4a8fe31bf8..6c97e57a9a 100644 --- a/notebook/gateway/managers.py +++ b/notebook/gateway/managers.py @@ -90,6 +90,16 @@ def _kernels_endpoint_default(self): def _kernelspecs_endpoint_default(self): return os.environ.get(self.kernelspecs_endpoint_env, self.kernelspecs_endpoint_default_value) + kernelspecs_resource_endpoint_default_value = '/kernelspecs' + kernelspecs_resource_endpoint_env = 'JUPYTER_GATEWAY_KERNELSPECS_RESOURCE_ENDPOINT' + kernelspecs_resource_endpoint = Unicode(default_value=kernelspecs_resource_endpoint_default_value, config=True, + help="""The gateway endpoint for accessing kernelspecs resources + (JUPYTER_GATEWAY_KERNELSPECS_RESOURCE_ENDPOINT env var)""") + + @default('kernelspecs_resource_endpoint') + def _kernelspecs_resource_endpoint_default(self): + return os.environ.get(self.kernelspecs_resource_endpoint_env, self.kernelspecs_resource_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, @@ -332,6 +342,11 @@ def start_kernel(self, kernel_id=None, path=None, **kwargs): kernel_env = {k: v for (k, v) in dict(os.environ).items() if k.startswith('KERNEL_') or k in GatewayClient.instance().env_whitelist.split(",")} + + # Convey the full path to where this notebook file is located. + if path is not None and kernel_env.get('KERNEL_WORKING_DIR') is None: + kernel_env['KERNEL_WORKING_DIR'] = kwargs['cwd'] + json_body = json_encode({'name': kernel_name, 'env': kernel_env}) response = yield gateway_request(kernel_url, method='POST', body=json_body) @@ -466,7 +481,10 @@ class GatewayKernelSpecManager(KernelSpecManager): def __init__(self, **kwargs): super(GatewayKernelSpecManager, self).__init__(**kwargs) - self.base_endpoint = url_path_join(GatewayClient.instance().url, GatewayClient.instance().kernelspecs_endpoint) + self.base_endpoint = url_path_join(GatewayClient.instance().url, + GatewayClient.instance().kernelspecs_endpoint) + self.base_resource_endpoint = url_path_join(GatewayClient.instance().url, + GatewayClient.instance().kernelspecs_resource_endpoint) def _get_kernelspecs_endpoint_url(self, kernel_name=None): """Builds a url for the kernels endpoint @@ -497,14 +515,7 @@ def get_all_specs(self): notebook_default=km.default_kernel_name)) km.default_kernel_name = remote_default_kernel_name - # gateway doesn't support resources (requires transfer for use by NB client) - # so add `resource_dir` to each kernelspec and value of 'not supported in gateway mode' remote_kspecs = fetched_kspecs.get('kernelspecs') - for kernel_name, kspec_info in remote_kspecs.items(): - if not kspec_info.get('resource_dir'): - kspec_info['resource_dir'] = 'not supported in gateway mode' - remote_kspecs[kernel_name].update(kspec_info) - raise gen.Return(remote_kspecs) @gen.coroutine @@ -539,9 +550,32 @@ def get_kernel_spec(self, kernel_name, **kwargs): raise else: kernel_spec = json_decode(response.body) - # Convert to instance of Kernelspec - kspec_instance = self.kernel_spec_class(resource_dir=u'', **kernel_spec['spec']) - raise gen.Return(kspec_instance) + + raise gen.Return(kernel_spec) + + @gen.coroutine + def get_kernel_spec_resource(self, kernel_name, path): + """Get kernel spec for kernel_name. + + Parameters + ---------- + kernel_name : str + The name of the kernel. + path : str + The name of the desired resource + """ + kernel_spec_resource_url = url_path_join(self.base_resource_endpoint, str(kernel_name), str(path)) + self.log.debug("Request kernel spec resource '{}' at: {}".format(path, kernel_spec_resource_url)) + try: + response = yield gateway_request(kernel_spec_resource_url, method='GET') + except HTTPError as error: + if error.code == 404: + kernel_spec_resource = None + else: + raise + else: + kernel_spec_resource = response.body + raise gen.Return(kernel_spec_resource) class GatewaySessionManager(SessionManager): diff --git a/notebook/services/kernelspecs/handlers.py b/notebook/services/kernelspecs/handlers.py index c0157e4c57..061c907e2b 100644 --- a/notebook/services/kernelspecs/handlers.py +++ b/notebook/services/kernelspecs/handlers.py @@ -16,6 +16,7 @@ from ...base.handlers import APIHandler from ...utils import url_path_join, url_unescape + def kernelspec_model(handler, name, spec_dict, resource_dir): """Load a KernelSpec by name and return the REST API model""" d = { @@ -45,6 +46,12 @@ def kernelspec_model(handler, name, spec_dict, resource_dir): ) return d + +def is_kernelspec_model(spec_dict): + """Returns True if spec_dict is already in proper form. This will occur when using a gateway.""" + return isinstance(spec_dict, dict) and 'name' in spec_dict and 'spec' in spec_dict and 'resources' in spec_dict + + class MainKernelSpecHandler(APIHandler): @web.authenticated @@ -58,8 +65,10 @@ def get(self): kspecs = yield gen.maybe_future(ksm.get_all_specs()) for kernel_name, kernel_info in kspecs.items(): try: - d = kernelspec_model(self, kernel_name, kernel_info['spec'], - kernel_info['resource_dir']) + if is_kernelspec_model(kernel_info): + d = kernel_info + else: + d = kernelspec_model(self, kernel_name, kernel_info['spec'], kernel_info['resource_dir']) except Exception: self.log.error("Failed to load kernel spec: '%s'", kernel_name, exc_info=True) continue @@ -79,7 +88,10 @@ def get(self, kernel_name): spec = yield gen.maybe_future(ksm.get_kernel_spec(kernel_name)) except KeyError: raise web.HTTPError(404, u'Kernel spec %s not found' % kernel_name) - model = kernelspec_model(self, kernel_name, spec.to_dict(), spec.resource_dir) + if is_kernelspec_model(spec): + model = spec + else: + model = kernelspec_model(self, kernel_name, spec.to_dict(), spec.resource_dir) self.set_header("Content-Type", 'application/json') self.finish(json.dumps(model))