|
| 1 | +# Copyright (c) Jupyter Development Team. |
| 2 | +# Distributed under the terms of the Modified BSD License. |
| 3 | + |
| 4 | +import json |
| 5 | +import os |
| 6 | + |
| 7 | +from socket import gaierror |
| 8 | +from tornado import web |
| 9 | +from tornado.httpclient import AsyncHTTPClient, HTTPError |
| 10 | +from traitlets import Unicode, Int, Float, Bool, default, validate, TraitError |
| 11 | +from traitlets.config import SingletonConfigurable |
| 12 | + |
| 13 | + |
| 14 | +class GatewayClient(SingletonConfigurable): |
| 15 | + """This class manages the configuration. It's its own singleton class so that we |
| 16 | + can share these values across all objects. It also contains some helper methods |
| 17 | + to build request arguments out of the various config options. |
| 18 | +
|
| 19 | + """ |
| 20 | + |
| 21 | + url = Unicode(default_value=None, allow_none=True, config=True, |
| 22 | + help="""The url of the Kernel or Enterprise Gateway server where |
| 23 | + kernel specifications are defined and kernel management takes place. |
| 24 | + If defined, this Notebook server acts as a proxy for all kernel |
| 25 | + management and kernel specification retrieval. (JUPYTER_GATEWAY_URL env var) |
| 26 | + """ |
| 27 | + ) |
| 28 | + |
| 29 | + url_env = 'JUPYTER_GATEWAY_URL' |
| 30 | + |
| 31 | + @default('url') |
| 32 | + def _url_default(self): |
| 33 | + return os.environ.get(self.url_env) |
| 34 | + |
| 35 | + @validate('url') |
| 36 | + def _url_validate(self, proposal): |
| 37 | + value = proposal['value'] |
| 38 | + # Ensure value, if present, starts with 'http' |
| 39 | + if value is not None and len(value) > 0: |
| 40 | + if not str(value).lower().startswith('http'): |
| 41 | + raise TraitError("GatewayClient url must start with 'http': '%r'" % value) |
| 42 | + return value |
| 43 | + |
| 44 | + ws_url = Unicode(default_value=None, allow_none=True, config=True, |
| 45 | + help="""The websocket url of the Kernel or Enterprise Gateway server. If not provided, this value |
| 46 | + will correspond to the value of the Gateway url with 'ws' in place of 'http'. (JUPYTER_GATEWAY_WS_URL env var) |
| 47 | + """ |
| 48 | + ) |
| 49 | + |
| 50 | + ws_url_env = 'JUPYTER_GATEWAY_WS_URL' |
| 51 | + |
| 52 | + @default('ws_url') |
| 53 | + def _ws_url_default(self): |
| 54 | + default_value = os.environ.get(self.ws_url_env) |
| 55 | + if default_value is None: |
| 56 | + if self.gateway_enabled: |
| 57 | + default_value = self.url.lower().replace('http', 'ws') |
| 58 | + return default_value |
| 59 | + |
| 60 | + @validate('ws_url') |
| 61 | + def _ws_url_validate(self, proposal): |
| 62 | + value = proposal['value'] |
| 63 | + # Ensure value, if present, starts with 'ws' |
| 64 | + if value is not None and len(value) > 0: |
| 65 | + if not str(value).lower().startswith('ws'): |
| 66 | + raise TraitError("GatewayClient ws_url must start with 'ws': '%r'" % value) |
| 67 | + return value |
| 68 | + |
| 69 | + kernels_endpoint_default_value = '/api/kernels' |
| 70 | + kernels_endpoint_env = 'JUPYTER_GATEWAY_KERNELS_ENDPOINT' |
| 71 | + kernels_endpoint = Unicode(default_value=kernels_endpoint_default_value, config=True, |
| 72 | + help="""The gateway API endpoint for accessing kernel resources (JUPYTER_GATEWAY_KERNELS_ENDPOINT env var)""") |
| 73 | + |
| 74 | + @default('kernels_endpoint') |
| 75 | + def _kernels_endpoint_default(self): |
| 76 | + return os.environ.get(self.kernels_endpoint_env, self.kernels_endpoint_default_value) |
| 77 | + |
| 78 | + kernelspecs_endpoint_default_value = '/api/kernelspecs' |
| 79 | + kernelspecs_endpoint_env = 'JUPYTER_GATEWAY_KERNELSPECS_ENDPOINT' |
| 80 | + kernelspecs_endpoint = Unicode(default_value=kernelspecs_endpoint_default_value, config=True, |
| 81 | + help="""The gateway API endpoint for accessing kernelspecs (JUPYTER_GATEWAY_KERNELSPECS_ENDPOINT env var)""") |
| 82 | + |
| 83 | + @default('kernelspecs_endpoint') |
| 84 | + def _kernelspecs_endpoint_default(self): |
| 85 | + return os.environ.get(self.kernelspecs_endpoint_env, self.kernelspecs_endpoint_default_value) |
| 86 | + |
| 87 | + kernelspecs_resource_endpoint_default_value = '/kernelspecs' |
| 88 | + kernelspecs_resource_endpoint_env = 'JUPYTER_GATEWAY_KERNELSPECS_RESOURCE_ENDPOINT' |
| 89 | + kernelspecs_resource_endpoint = Unicode(default_value=kernelspecs_resource_endpoint_default_value, config=True, |
| 90 | + help="""The gateway endpoint for accessing kernelspecs resources |
| 91 | + (JUPYTER_GATEWAY_KERNELSPECS_RESOURCE_ENDPOINT env var)""") |
| 92 | + |
| 93 | + @default('kernelspecs_resource_endpoint') |
| 94 | + def _kernelspecs_resource_endpoint_default(self): |
| 95 | + return os.environ.get(self.kernelspecs_resource_endpoint_env, self.kernelspecs_resource_endpoint_default_value) |
| 96 | + |
| 97 | + connect_timeout_default_value = 40.0 |
| 98 | + connect_timeout_env = 'JUPYTER_GATEWAY_CONNECT_TIMEOUT' |
| 99 | + connect_timeout = Float(default_value=connect_timeout_default_value, config=True, |
| 100 | + help="""The time allowed for HTTP connection establishment with the Gateway server. |
| 101 | + (JUPYTER_GATEWAY_CONNECT_TIMEOUT env var)""") |
| 102 | + |
| 103 | + @default('connect_timeout') |
| 104 | + def connect_timeout_default(self): |
| 105 | + return float(os.environ.get('JUPYTER_GATEWAY_CONNECT_TIMEOUT', self.connect_timeout_default_value)) |
| 106 | + |
| 107 | + request_timeout_default_value = 40.0 |
| 108 | + request_timeout_env = 'JUPYTER_GATEWAY_REQUEST_TIMEOUT' |
| 109 | + request_timeout = Float(default_value=request_timeout_default_value, config=True, |
| 110 | + help="""The time allowed for HTTP request completion. (JUPYTER_GATEWAY_REQUEST_TIMEOUT env var)""") |
| 111 | + |
| 112 | + @default('request_timeout') |
| 113 | + def request_timeout_default(self): |
| 114 | + return float(os.environ.get('JUPYTER_GATEWAY_REQUEST_TIMEOUT', self.request_timeout_default_value)) |
| 115 | + |
| 116 | + client_key = Unicode(default_value=None, allow_none=True, config=True, |
| 117 | + help="""The filename for client SSL key, if any. (JUPYTER_GATEWAY_CLIENT_KEY env var) |
| 118 | + """ |
| 119 | + ) |
| 120 | + client_key_env = 'JUPYTER_GATEWAY_CLIENT_KEY' |
| 121 | + |
| 122 | + @default('client_key') |
| 123 | + def _client_key_default(self): |
| 124 | + return os.environ.get(self.client_key_env) |
| 125 | + |
| 126 | + client_cert = Unicode(default_value=None, allow_none=True, config=True, |
| 127 | + help="""The filename for client SSL certificate, if any. (JUPYTER_GATEWAY_CLIENT_CERT env var) |
| 128 | + """ |
| 129 | + ) |
| 130 | + client_cert_env = 'JUPYTER_GATEWAY_CLIENT_CERT' |
| 131 | + |
| 132 | + @default('client_cert') |
| 133 | + def _client_cert_default(self): |
| 134 | + return os.environ.get(self.client_cert_env) |
| 135 | + |
| 136 | + ca_certs = Unicode(default_value=None, allow_none=True, config=True, |
| 137 | + help="""The filename of CA certificates or None to use defaults. (JUPYTER_GATEWAY_CA_CERTS env var) |
| 138 | + """ |
| 139 | + ) |
| 140 | + ca_certs_env = 'JUPYTER_GATEWAY_CA_CERTS' |
| 141 | + |
| 142 | + @default('ca_certs') |
| 143 | + def _ca_certs_default(self): |
| 144 | + return os.environ.get(self.ca_certs_env) |
| 145 | + |
| 146 | + http_user = Unicode(default_value=None, allow_none=True, config=True, |
| 147 | + help="""The username for HTTP authentication. (JUPYTER_GATEWAY_HTTP_USER env var) |
| 148 | + """ |
| 149 | + ) |
| 150 | + http_user_env = 'JUPYTER_GATEWAY_HTTP_USER' |
| 151 | + |
| 152 | + @default('http_user') |
| 153 | + def _http_user_default(self): |
| 154 | + return os.environ.get(self.http_user_env) |
| 155 | + |
| 156 | + http_pwd = Unicode(default_value=None, allow_none=True, config=True, |
| 157 | + help="""The password for HTTP authentication. (JUPYTER_GATEWAY_HTTP_PWD env var) |
| 158 | + """ |
| 159 | + ) |
| 160 | + http_pwd_env = 'JUPYTER_GATEWAY_HTTP_PWD' |
| 161 | + |
| 162 | + @default('http_pwd') |
| 163 | + def _http_pwd_default(self): |
| 164 | + return os.environ.get(self.http_pwd_env) |
| 165 | + |
| 166 | + headers_default_value = '{}' |
| 167 | + headers_env = 'JUPYTER_GATEWAY_HEADERS' |
| 168 | + headers = Unicode(default_value=headers_default_value, allow_none=True, config=True, |
| 169 | + help="""Additional HTTP headers to pass on the request. This value will be converted to a dict. |
| 170 | + (JUPYTER_GATEWAY_HEADERS env var) |
| 171 | + """ |
| 172 | + ) |
| 173 | + |
| 174 | + @default('headers') |
| 175 | + def _headers_default(self): |
| 176 | + return os.environ.get(self.headers_env, self.headers_default_value) |
| 177 | + |
| 178 | + auth_token = Unicode(default_value=None, allow_none=True, config=True, |
| 179 | + help="""The authorization token used in the HTTP headers. (JUPYTER_GATEWAY_AUTH_TOKEN env var) |
| 180 | + """ |
| 181 | + ) |
| 182 | + auth_token_env = 'JUPYTER_GATEWAY_AUTH_TOKEN' |
| 183 | + |
| 184 | + @default('auth_token') |
| 185 | + def _auth_token_default(self): |
| 186 | + return os.environ.get(self.auth_token_env, '') |
| 187 | + |
| 188 | + validate_cert_default_value = True |
| 189 | + validate_cert_env = 'JUPYTER_GATEWAY_VALIDATE_CERT' |
| 190 | + validate_cert = Bool(default_value=validate_cert_default_value, config=True, |
| 191 | + help="""For HTTPS requests, determines if server's certificate should be validated or not. |
| 192 | + (JUPYTER_GATEWAY_VALIDATE_CERT env var)""" |
| 193 | + ) |
| 194 | + |
| 195 | + @default('validate_cert') |
| 196 | + def validate_cert_default(self): |
| 197 | + return bool(os.environ.get(self.validate_cert_env, str(self.validate_cert_default_value)) not in ['no', 'false']) |
| 198 | + |
| 199 | + def __init__(self, **kwargs): |
| 200 | + super(GatewayClient, self).__init__(**kwargs) |
| 201 | + self._static_args = {} # initialized on first use |
| 202 | + |
| 203 | + env_whitelist_default_value = '' |
| 204 | + env_whitelist_env = 'JUPYTER_GATEWAY_ENV_WHITELIST' |
| 205 | + env_whitelist = Unicode(default_value=env_whitelist_default_value, config=True, |
| 206 | + help="""A comma-separated list of environment variable names that will be included, along with |
| 207 | + their values, in the kernel startup request. The corresponding `env_whitelist` configuration |
| 208 | + value must also be set on the Gateway server - since that configuration value indicates which |
| 209 | + environmental values to make available to the kernel. (JUPYTER_GATEWAY_ENV_WHITELIST env var)""") |
| 210 | + |
| 211 | + @default('env_whitelist') |
| 212 | + def _env_whitelist_default(self): |
| 213 | + return os.environ.get(self.env_whitelist_env, self.env_whitelist_default_value) |
| 214 | + |
| 215 | + gateway_retry_interval_default_value = 1.0 |
| 216 | + gateway_retry_interval_env = 'JUPYTER_GATEWAY_RETRY_INTERVAL' |
| 217 | + gateway_retry_interval = Float(default_value=gateway_retry_interval_default_value, config=True, |
| 218 | + help="""The time allowed for HTTP reconnection with the Gateway server for the first time. |
| 219 | + Next will be JUPYTER_GATEWAY_RETRY_INTERVAL multiplied by two in factor of numbers of retries |
| 220 | + but less than JUPYTER_GATEWAY_RETRY_INTERVAL_MAX. |
| 221 | + (JUPYTER_GATEWAY_RETRY_INTERVAL env var)""") |
| 222 | + |
| 223 | + @default('gateway_retry_interval') |
| 224 | + def gateway_retry_interval_default(self): |
| 225 | + return float(os.environ.get('JUPYTER_GATEWAY_RETRY_INTERVAL', self.gateway_retry_interval_default_value)) |
| 226 | + |
| 227 | + gateway_retry_interval_max_default_value = 30.0 |
| 228 | + gateway_retry_interval_max_env = 'JUPYTER_GATEWAY_RETRY_INTERVAL_MAX' |
| 229 | + gateway_retry_interval_max = Float(default_value=gateway_retry_interval_max_default_value, config=True, |
| 230 | + help="""The maximum time allowed for HTTP reconnection retry with the Gateway server. |
| 231 | + (JUPYTER_GATEWAY_RETRY_INTERVAL_MAX env var)""") |
| 232 | + |
| 233 | + @default('gateway_retry_interval_max') |
| 234 | + def gateway_retry_interval_max_default(self): |
| 235 | + return float(os.environ.get('JUPYTER_GATEWAY_RETRY_INTERVAL_MAX', self.gateway_retry_interval_max_default_value)) |
| 236 | + |
| 237 | + gateway_retry_max_default_value = 5 |
| 238 | + gateway_retry_max_env = 'JUPYTER_GATEWAY_RETRY_MAX' |
| 239 | + gateway_retry_max = Int(default_value=gateway_retry_max_default_value, config=True, |
| 240 | + help="""The maximum retries allowed for HTTP reconnection with the Gateway server. |
| 241 | + (JUPYTER_GATEWAY_RETRY_MAX env var)""") |
| 242 | + |
| 243 | + @default('gateway_retry_max') |
| 244 | + def gateway_retry_max_default(self): |
| 245 | + return int(os.environ.get('JUPYTER_GATEWAY_RETRY_MAX', self.gateway_retry_max_default_value)) |
| 246 | + |
| 247 | + @property |
| 248 | + def gateway_enabled(self): |
| 249 | + return bool(self.url is not None and len(self.url) > 0) |
| 250 | + |
| 251 | + # Ensure KERNEL_LAUNCH_TIMEOUT has a default value. |
| 252 | + KERNEL_LAUNCH_TIMEOUT = int(os.environ.get('KERNEL_LAUNCH_TIMEOUT', 40)) |
| 253 | + |
| 254 | + def init_static_args(self): |
| 255 | + """Initialize arguments used on every request. Since these are static values, we'll |
| 256 | + perform this operation once. |
| 257 | +
|
| 258 | + """ |
| 259 | + # Ensure that request timeout and KERNEL_LAUNCH_TIMEOUT are the same, taking the |
| 260 | + # greater value of the two. |
| 261 | + if self.request_timeout < float(GatewayClient.KERNEL_LAUNCH_TIMEOUT): |
| 262 | + self.request_timeout = float(GatewayClient.KERNEL_LAUNCH_TIMEOUT) |
| 263 | + elif self.request_timeout > float(GatewayClient.KERNEL_LAUNCH_TIMEOUT): |
| 264 | + GatewayClient.KERNEL_LAUNCH_TIMEOUT = int(self.request_timeout) |
| 265 | + # Ensure any adjustments are reflected in env. |
| 266 | + os.environ['KERNEL_LAUNCH_TIMEOUT'] = str(GatewayClient.KERNEL_LAUNCH_TIMEOUT) |
| 267 | + |
| 268 | + self._static_args['headers'] = json.loads(self.headers) |
| 269 | + if 'Authorization' not in self._static_args['headers'].keys(): |
| 270 | + self._static_args['headers'].update({ |
| 271 | + 'Authorization': 'token {}'.format(self.auth_token) |
| 272 | + }) |
| 273 | + self._static_args['connect_timeout'] = self.connect_timeout |
| 274 | + self._static_args['request_timeout'] = self.request_timeout |
| 275 | + self._static_args['validate_cert'] = self.validate_cert |
| 276 | + if self.client_cert: |
| 277 | + self._static_args['client_cert'] = self.client_cert |
| 278 | + self._static_args['client_key'] = self.client_key |
| 279 | + if self.ca_certs: |
| 280 | + self._static_args['ca_certs'] = self.ca_certs |
| 281 | + if self.http_user: |
| 282 | + self._static_args['auth_username'] = self.http_user |
| 283 | + if self.http_pwd: |
| 284 | + self._static_args['auth_password'] = self.http_pwd |
| 285 | + |
| 286 | + def load_connection_args(self, **kwargs): |
| 287 | + """Merges the static args relative to the connection, with the given keyword arguments. If statics |
| 288 | + have yet to be initialized, we'll do that here. |
| 289 | +
|
| 290 | + """ |
| 291 | + if len(self._static_args) == 0: |
| 292 | + self.init_static_args() |
| 293 | + |
| 294 | + kwargs.update(self._static_args) |
| 295 | + return kwargs |
| 296 | + |
| 297 | + |
| 298 | +async def gateway_request(endpoint, **kwargs): |
| 299 | + """Make an async request to kernel gateway endpoint, returns a response """ |
| 300 | + client = AsyncHTTPClient() |
| 301 | + kwargs = GatewayClient.instance().load_connection_args(**kwargs) |
| 302 | + try: |
| 303 | + response = await client.fetch(endpoint, **kwargs) |
| 304 | + # Trap a set of common exceptions so that we can inform the user that their Gateway url is incorrect |
| 305 | + # or the server is not running. |
| 306 | + # NOTE: We do this here since this handler is called during the Notebook's startup and subsequent refreshes |
| 307 | + # of the tree view. |
| 308 | + except ConnectionRefusedError as e: |
| 309 | + raise web.HTTPError(503, "Connection refused from Gateway server url '{}'. " |
| 310 | + "Check to be sure the Gateway instance is running.".format(GatewayClient.instance().url)) from e |
| 311 | + except HTTPError as e: |
| 312 | + # This can occur if the host is valid (e.g., foo.com) but there's nothing there. |
| 313 | + raise web.HTTPError(e.code, "Error attempting to connect to Gateway server url '{}'. " |
| 314 | + "Ensure gateway url is valid and the Gateway instance is running.". |
| 315 | + format(GatewayClient.instance().url)) from e |
| 316 | + except gaierror as e: |
| 317 | + raise web.HTTPError(404, "The Gateway server specified in the gateway_url '{}' doesn't appear to be valid. " |
| 318 | + "Ensure gateway url is valid and the Gateway instance is running.". |
| 319 | + format(GatewayClient.instance().url)) from e |
| 320 | + |
| 321 | + return response |
| 322 | + |
0 commit comments