From 09edf38a354d9c062aa35ecd7d29c3f72ed13a26 Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 3 May 2019 16:16:19 +0200 Subject: [PATCH 001/541] back to dev --- jupyterhub/_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterhub/_version.py b/jupyterhub/_version.py index 240bab39..6ca01de4 100644 --- a/jupyterhub/_version.py +++ b/jupyterhub/_version.py @@ -5,9 +5,9 @@ version_info = ( 1, 0, - 0, + 1, # "b2", # release (b1, rc1, or "" for final or dev) - # "dev", # dev or nothing + "dev", # dev or nothing ) # pep 440 version: no dot before beta/rc, but before .dev From bfbf2c05211d75c8f2287cf53d66dfb6383ee7ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20L=C3=A9one?= Date: Fri, 3 May 2019 16:41:43 +0200 Subject: [PATCH 002/541] Dict rewritten as literal --- jupyterhub/tests/mocking.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index 9e2a928d..5ac2406d 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -166,8 +166,7 @@ class FormSpawner(MockSpawner): options_form = "IMAFORM" def options_from_form(self, form_data): - options = {} - options['notspecified'] = 5 + options = {'notspecified': 5} if 'bounds' in form_data: options['bounds'] = [int(i) for i in form_data['bounds']] if 'energy' in form_data: @@ -379,9 +378,9 @@ class MockHub(JupyterHub): class MockSingleUserServer(SingleUserNotebookApp): """Mock-out problematic parts of single-user server when run in a thread - + Currently: - + - disable signal handler """ From 83af28c137a0e1d9d46957074a3167572da33bb7 Mon Sep 17 00:00:00 2001 From: viditagarwal Date: Mon, 6 May 2019 16:49:41 +0530 Subject: [PATCH 003/541] Adding the use case of the Elucidata: How Jupyter Notebook is used inside the Elucidata with Jupyterhub --- docs/source/gallery-jhub-deployments.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/gallery-jhub-deployments.md b/docs/source/gallery-jhub-deployments.md index ef4e7967..84f03e97 100644 --- a/docs/source/gallery-jhub-deployments.md +++ b/docs/source/gallery-jhub-deployments.md @@ -142,7 +142,10 @@ easy to do with RStudio too. - Kristen Thyng - Oceanography - [Teaching with JupyterHub and nbgrader](http://kristenthyng.com/blog/2016/09/07/jupyterhub+nbgrader/) - +### Elucidata + - What's new in Jupyter Notebooks @[Elucidata](https://elucidata.io/): + - Using Jupyter Notebooks with Jupyterhub on GCP, managed by GKE + - https://medium.com/elucidata/why-you-should-be-using-a-jupyter-notebook-8385a4ccd93d ## Service Providers From 9e7b0c0bfd419faf93b1d9ac96e4d4e591fef41e Mon Sep 17 00:00:00 2001 From: NikeNano Date: Fri, 10 May 2019 09:52:17 +0200 Subject: [PATCH 004/541] update to simplyfi the language related to spawner options --- share/jupyterhub/templates/spawn.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/share/jupyterhub/templates/spawn.html b/share/jupyterhub/templates/spawn.html index 97511990..f1330b1f 100644 --- a/share/jupyterhub/templates/spawn.html +++ b/share/jupyterhub/templates/spawn.html @@ -8,7 +8,7 @@
{% block heading %}
-

Spawner Options

+

Server Options

{% endblock %}
@@ -23,7 +23,7 @@
{{spawner_options_form | safe}}
- +
From aaad55e0762cd268cb6aa075e53d489bc87593a3 Mon Sep 17 00:00:00 2001 From: Alejandro Del Castillo Date: Fri, 10 May 2019 16:17:29 -0500 Subject: [PATCH 005/541] Jupyterhub: use previous exit strategy for Windows Windows doesn't have support for signal handling so it can't use the signal handling capabilities of asyncio. Use the previous atexit strategy on the Windows case instead. Signed-off-by: Alejandro Del Castillo --- jupyterhub/app.py | 46 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 815097a8..a0c65fb0 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2420,22 +2420,30 @@ class JupyterHub(Application): pc.start() self.log.info("JupyterHub is now running at %s", self.proxy.public_url) + # Use atexit for Windows, it doesn't have signal handling support + if _mswindows: + atexit.register(self.atexit) # register cleanup on both TERM and INT self.init_signal() def init_signal(self): loop = asyncio.get_event_loop() for s in (signal.SIGTERM, signal.SIGINT): - loop.add_signal_handler( - s, lambda s=s: asyncio.ensure_future(self.shutdown_cancel_tasks(s)) - ) - infosignals = [signal.SIGUSR1] - if hasattr(signal, 'SIGINFO'): - infosignals.append(signal.SIGINFO) - for s in infosignals: - loop.add_signal_handler( - s, lambda s=s: asyncio.ensure_future(self.log_status(s)) - ) + if not _mswindows: + loop.add_signal_handler( + s, lambda s=s: asyncio.ensure_future(self.shutdown_cancel_tasks(s)) + ) + else: + signal.signal(s, self.win_shutdown_cancel_tasks) + + if not _mswindows: + infosignals = [signal.SIGUSR1] + if hasattr(signal, 'SIGINFO'): + infosignals.append(signal.SIGINFO) + for s in infosignals: + loop.add_signal_handler( + s, lambda s=s: asyncio.ensure_future(self.log_status(s)) + ) async def log_status(self, sig): """Log current status, triggered by SIGINFO (^T in many terminals)""" @@ -2443,6 +2451,24 @@ class JupyterHub(Application): print_ps_info() print_stacks() + def win_shutdown_cancel_tasks(self, signum, frame): + self.log.critical("Received signalnum %s, , initiating shutdown...", signum) + raise SystemExit(128 + signum) + + _atexit_ran = False + + def atexit(self): + """atexit callback""" + if self._atexit_ran: + return + self._atexit_ran = True + # run the cleanup step (in a new loop, because the interrupted one is unclean) + asyncio.set_event_loop(asyncio.new_event_loop()) + IOLoop.clear_current() + loop = IOLoop() + loop.make_current() + loop.run_sync(self.cleanup) + async def shutdown_cancel_tasks(self, sig): """Cancel all other tasks of the event loop and initiate cleanup""" self.log.critical("Received signal %s, initiating shutdown...", sig.name) From c83777ccdc12e8e71be6c3dd1dd60a06d11c08db Mon Sep 17 00:00:00 2001 From: Julien Chastang Date: Tue, 14 May 2019 16:54:08 -0600 Subject: [PATCH 006/541] typo --- docs/source/reference/templates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/reference/templates.md b/docs/source/reference/templates.md index 61784403..d820c099 100644 --- a/docs/source/reference/templates.md +++ b/docs/source/reference/templates.md @@ -70,7 +70,7 @@ To add announcements to be displayed on a page, you have two options: ### Announcement Configuration Variables If you set the configuration variable `JupyterHub.template_vars = -{'announcement': 'some_text}`, the given `some_text` will be placed on +{'announcement': 'some_text'}`, the given `some_text` will be placed on the top of all pages. The more specific variables `announcement_login`, `announcement_spawn`, `announcement_home`, and `announcement_logout` are more specific and only show on their From 9e35ba5bef7175ee475e398199e921aff001e1a7 Mon Sep 17 00:00:00 2001 From: Iblis Lin Date: Wed, 15 May 2019 11:29:35 +0800 Subject: [PATCH 007/541] Update link of `changelog` --- docs/source/admin/upgrading.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/admin/upgrading.rst b/docs/source/admin/upgrading.rst index 18404955..874aae97 100644 --- a/docs/source/admin/upgrading.rst +++ b/docs/source/admin/upgrading.rst @@ -18,7 +18,7 @@ JupyterHub is painless, quick and with minimal user interruption. Read the Changelog ================== -The `changelog `_ contains information on what has +The `changelog <../changelog.html>`_ contains information on what has changed with the new JupyterHub release, and any deprecation warnings. Read these notes to familiarize yourself with the coming changes. There might be new releases of authenticators & spawners you are using, so From da460064ae303b0fe17b1cd32fe276761db9bbc8 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 16 May 2019 18:31:10 +0200 Subject: [PATCH 008/541] Make announcements visible without custom HTML Fixes https://github.com/jupyterhub/jupyterhub/issues/2566 to some degree by making the announcement stand out using twitter-bootstrap classes `alert` and `alert-warning`. Perhaps we could theme twitter bootstrap or this alert specifically with jupyter related colors as well though? --- share/jupyterhub/templates/page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index 5677eb93..70c733e5 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -148,7 +148,7 @@ {% block announcement %} {% if announcement %} -
+
{{ announcement | safe }}
{% endif %} From 38cf95523f2ec7ce553136b630c915b0395cbca2 Mon Sep 17 00:00:00 2001 From: Aaron Huang Date: Tue, 21 May 2019 17:12:01 +0800 Subject: [PATCH 009/541] Update script --- share/jupyterhub/static/js/home.js | 1 + 1 file changed, 1 insertion(+) diff --git a/share/jupyterhub/static/js/home.js b/share/jupyterhub/static/js/home.js index 616170a9..e81b6690 100644 --- a/share/jupyterhub/static/js/home.js +++ b/share/jupyterhub/static/js/home.js @@ -90,6 +90,7 @@ require(["jquery", "moment", "jhapi", "utils"], function( }); api.stop_server(user, { success: function() { + $("#stop").hide(); $("#start") .text("Start My Server") .attr("title", "Start your default server") From b624116be79168f37af728195af663498f3c55c0 Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 24 May 2019 13:29:34 +0200 Subject: [PATCH 010/541] re-raise exceptions in launch_instance_async avoids asyncio tracebacks in e.g. `jupyterhub --version` --- jupyterhub/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 815097a8..63537af7 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2487,12 +2487,15 @@ class JupyterHub(Application): self = cls.instance() AsyncIOMainLoop().install() loop = IOLoop.current() - loop.add_callback(self.launch_instance_async, argv) + task = asyncio.ensure_future(self.launch_instance_async(argv)) try: loop.start() except KeyboardInterrupt: print("\nInterrupted") finally: + if task.done(): + # re-raise exceptions in launch_instance_async + task.result() loop.stop() loop.close() From e9c78422b57e47b682d31c4faae6083d53ccaeac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Mon, 27 May 2019 14:08:04 -0400 Subject: [PATCH 011/541] Define default values for HubAuth ssl traitlets The default values are taken from environment variables defined by Spawner.get_env. --- jupyterhub/services/auth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jupyterhub/services/auth.py b/jupyterhub/services/auth.py index c6d8b83c..9c9028a6 100644 --- a/jupyterhub/services/auth.py +++ b/jupyterhub/services/auth.py @@ -216,7 +216,7 @@ class HubAuth(SingletonConfigurable): return self.hub_host + url_path_join(self.hub_prefix, 'login') keyfile = Unicode( - '', + os.getenv('JUPYTERHUB_SSL_KEYFILE', ''), help="""The ssl key to use for requests Use with certfile @@ -224,7 +224,7 @@ class HubAuth(SingletonConfigurable): ).tag(config=True) certfile = Unicode( - '', + os.getenv('JUPYTERHUB_SSL_CERTFILE', ''), help="""The ssl cert to use for requests Use with keyfile @@ -232,7 +232,7 @@ class HubAuth(SingletonConfigurable): ).tag(config=True) client_ca = Unicode( - '', + os.getenv('JUPYTERHUB_SSL_CLIENT_CA', ''), help="""The ssl certificate authority to use to verify requests Use with keyfile and certfile From 30cfdcaa83886c3a9cc775d2632ca98867652c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Brunk?= Date: Tue, 28 May 2019 18:04:17 +0200 Subject: [PATCH 012/541] Change API description to a valid OpenAPI spec * Add missing responses (doesn't include all possible responses yet) * Refactor invalid multi in body parameters into a single parameter * Change form type into valid formData * Fix use of required fields * Apply a few other minor fixes --- docs/rest-api.yml | 184 +++++++++++++++++++++++++++++----------------- 1 file changed, 117 insertions(+), 67 deletions(-) diff --git a/docs/rest-api.yml b/docs/rest-api.yml index 573e886b..0835686e 100644 --- a/docs/rest-api.yml +++ b/docs/rest-api.yml @@ -1,4 +1,4 @@ -# see me at: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/master/docs/rest-api.yml#/default +# see me at: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#/default swagger: '2.0' info: title: JupyterHub @@ -7,7 +7,7 @@ info: license: name: BSD-3-Clause schemes: - - [http, https] + [http, https] securityDefinitions: token: type: apiKey @@ -190,7 +190,7 @@ paths: in: path required: true type: string - - body: + - name: body in: body schema: type: object @@ -202,34 +202,37 @@ paths: Timestamp of last-seen activity for this user. Only needed if this is not activity associated with using a given server. - required: false servers: description: | Register activity for specific servers by name. The keys of this dict are the names of servers. The default server has an empty name (''). - required: false type: object properties: '': description: | Activity for a single server. type: object + required: + - last_activity properties: last_activity: - required: true type: string format: date-time description: | Timestamp of last-seen activity on this server. - example: - last_activity: '2019-02-06T12:54:14Z' - servers: - '': - last_activity: '2019-02-06T12:54:14Z' - gpu: - last_activity: '2019-02-06T12:54:14Z' - + example: + last_activity: '2019-02-06T12:54:14Z' + servers: + '': + last_activity: '2019-02-06T12:54:14Z' + gpu: + last_activity: '2019-02-06T12:54:14Z' + responses: + '401': + $ref: '#/responses/Unauthorized' + '404': + description: No such user /users/{name}/server: post: summary: Start a user's single-user notebook server @@ -239,7 +242,7 @@ paths: in: path required: true type: string - - options: + - name: options description: | Spawn options can be passed as a JSON body when spawning via the API instead of spawn form. @@ -247,7 +250,8 @@ paths: will depend on the Spawner's configuration. in: body required: false - type: object + schema: + type: object responses: '201': description: The user's notebook server has started @@ -280,7 +284,7 @@ paths: in: path required: true type: string - - options: + - name: options description: | Spawn options can be passed as a JSON body when spawning via the API instead of spawn form. @@ -288,7 +292,8 @@ paths: will depend on the Spawner's configuration. in: body required: false - type: object + schema: + type: object responses: '201': description: The user's notebook named-server has started @@ -313,13 +318,20 @@ paths: Removing a server deletes things like the state of the stopped server. in: body required: false - type: boolean + schema: + type: boolean responses: '204': description: The user's notebook named-server has stopped '202': description: The user's notebook named-server has not yet stopped as it is taking a while to stop /users/{name}/tokens: + parameters: + - name: name + description: username + in: path + required: true + type: string get: summary: List tokens for the user responses: @@ -329,25 +341,43 @@ paths: type: array items: $ref: '#/definitions/Token' + '401': + $ref: '#/responses/Unauthorized' + '404': + description: No such user post: summary: Create a new token for the user parameters: - - name: expires_in - type: number - required: false + - name: token_params in: body - description: lifetime (in seconds) after which the requested token will expire. - - name: note - type: string required: false - in: body - description: A note attached to the token for future bookkeeping + schema: + type: object + properties: + expires_in: + type: number + description: lifetime (in seconds) after which the requested token will expire. + note: + type: string + description: A note attached to the token for future bookkeeping responses: '201': description: The newly created token schema: $ref: '#/definitions/Token' + '400': + description: Body must be a JSON dict or empty /users/{name}/tokens/{token_id}: + parameters: + - name: name + description: username + in: path + required: true + type: string + - name: token_id + in: path + required: true + type: string get: summary: Get the model for a token by id responses: @@ -361,12 +391,13 @@ paths: '204': description: The token has been deleted /user: - summary: Return authenticated user's model - description: - parameters: - responses: - '200': - description: The authenticated user's model is returned. + get: + summary: Return authenticated user's model + responses: + '200': + description: The authenticated user's model is returned. + schema: + $ref: '#/definitions/User' /groups: get: summary: List groups @@ -539,14 +570,15 @@ paths: Logging in via this method is only available when the active Authenticator accepts passwords (e.g. not OAuth). parameters: - - name: username + - name: credentials in: body - required: false - type: string - - name: password - in: body - required: false - type: string + schema: + type: object + properties: + username: + type: string + password: + type: string responses: '200': description: The new API token @@ -562,10 +594,10 @@ paths: get: summary: Identify a user or service from an API token parameters: - - name: token - in: path - required: true - type: string + - name: token + in: path + required: true + type: string responses: '200': description: The user or service identified by the API token @@ -576,14 +608,14 @@ paths: summary: Identify a user from a cookie description: Used by single-user notebook servers to hand off cookie authentication to the Hub parameters: - - name: cookie_name - in: path - required: true - type: string - - name: cookie_value - in: path - required: true - type: string + - name: cookie_name + in: path + required: true + type: string + - name: cookie_value + in: path + required: true + type: string responses: '200': description: The user identified by the cookie @@ -618,6 +650,11 @@ paths: in: query required: true type: string + responses: + '200': + description: Success + '400': + description: OAuth2Error /oauth2/token: post: summary: Request an OAuth2 token @@ -629,27 +666,27 @@ paths: parameters: - name: client_id description: The client id - in: form + in: formData required: true type: string - name: client_secret description: The client secret - in: form + in: formData required: true type: string - name: grant_type description: The grant type (always 'authorization_code') - in: form + in: formData required: true type: string - name: code description: The code provided by the authorization redirect - in: form + in: formData required: true type: string - name: redirect_uri description: The redirect url - in: form + in: formData required: true type: string responses: @@ -668,14 +705,28 @@ paths: post: summary: Shutdown the Hub parameters: - - name: proxy + - name: body in: body - type: boolean - description: Whether the proxy should be shutdown as well (default from Hub config) - - name: servers - in: body - type: boolean - description: Whether users' notebook servers should be shutdown as well (default from Hub config) + schema: + type: object + properties: + proxy: + type: boolean + description: Whether the proxy should be shutdown as well (default from Hub config) + servers: + type: boolean + description: Whether users' notebook servers should be shutdown as well (default from Hub config) + responses: + '202': + description: Shutdown successful + '400': + description: Unexpeced value for proxy or servers +# Descriptions of common responses +responses: + NotFound: + description: The specified resource was not found + Unauthorized: + description: Authentication/Authorization error definitions: User: type: object @@ -703,11 +754,10 @@ definitions: format: date-time description: Timestamp of last-seen activity from the user servers: - type: object + type: array description: The active servers for this user. items: - schema: - $ref: '#/definitions/Server' + $ref: '#/definitions/Server' Server: type: object properties: From 19da170435e487aefa14cde4c2e17b5d8d53a99b Mon Sep 17 00:00:00 2001 From: Will Starms Date: Fri, 31 May 2019 17:49:24 -0500 Subject: [PATCH 013/541] Correct empty string redirect to default --- share/jupyterhub/static/js/home.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/share/jupyterhub/static/js/home.js b/share/jupyterhub/static/js/home.js index 616170a9..f4013ab2 100644 --- a/share/jupyterhub/static/js/home.js +++ b/share/jupyterhub/static/js/home.js @@ -102,6 +102,10 @@ require(["jquery", "moment", "jhapi", "utils"], function( $(".new-server-btn").click(function() { var row = getRow($(this)); var serverName = row.find(".new-server-name").val(); + if (serverName === "") { + // ../spawn/user/ causes a 404, ../spawn/user redirects correctly to the default server + window.location.href = "../spawn/" + user; + } window.location.href = "../spawn/" + user + "/" + serverName; }); From 5eb7a14a3398f57e98d68aad390c14a0cf5fb1e4 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Tue, 4 Jun 2019 13:30:28 +0200 Subject: [PATCH 014/541] [WIP] Add support for Jupyter Server --- .travis.yml | 7 +++++++ jupyterhub/singleuser.py | 27 ++++++++++++++++++--------- jupyterhub/spawner.py | 2 +- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 788bfa4f..3980dd87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,6 +61,13 @@ script: make html popd fi + - | + if [[ "$TEST" == "jupyter_server" ]]; then + pip uninstall notebook + pip install jupyter_server + export USE_JUPYTER_SERVER=True + pytest -v --maxfail=2 --cov=jupyterhub jupyterhub/tests + fi after_success: - codecov after_failure: diff --git a/jupyterhub/singleuser.py b/jupyterhub/singleuser.py index 6fa4f350..0ab557a5 100755 --- a/jupyterhub/singleuser.py +++ b/jupyterhub/singleuser.py @@ -6,6 +6,7 @@ import asyncio import json import os import random +import importlib from datetime import datetime from datetime import timezone from textwrap import dedent @@ -20,10 +21,13 @@ from tornado.httpclient import HTTPRequest from tornado.web import HTTPError from tornado.web import RequestHandler +use_serverapp = os.environ.get('USE_JUPYTER_SERVER', 'False') == 'True' + +required_package = 'jupyter_server' if use_serverapp else 'notebook' try: - import notebook + parent_module = importlib.import_module(required_package) except ImportError: - raise ImportError("JupyterHub single-user server requires notebook >= 4.0") + raise ImportError("JupyterHub single-user server requires {}".format(required_package)) from traitlets import ( Any, @@ -38,14 +42,19 @@ from traitlets import ( TraitError, ) -from notebook.notebookapp import ( - NotebookApp, - aliases as notebook_aliases, - flags as notebook_flags, +app_name = 'jupyter_server.serverapp' if use_serverapp else 'notebook.notebookapp' +app_module = importlib.import_module(app_name) + +NotebookApp = getattr(app_module, 'ServerApp' if use_serverapp else 'NotebookApp') +notebook_aliases = app_module.aliases +notebook_flags = app_module.flags + +LoginHandler = getattr(importlib.import_module(required_package + '.auth.login'), 'LoginHandler') +LogoutHandler = getattr(importlib.import_module(required_package + '.auth.logout'), 'LogoutHandler') +IPythonHandler = getattr( + importlib.import_module(required_package + '.base.handlers'), + 'JupyterHandler' if use_serverapp else 'IPythonHandler' ) -from notebook.auth.login import LoginHandler -from notebook.auth.logout import LogoutHandler -from notebook.base.handlers import IPythonHandler from ._version import __version__, _check_version from .log import log_request diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index af84c122..348eb8d5 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -931,7 +931,7 @@ class Spawner(LoggingConfigurable): args.append('--notebook-dir=%s' % _quote_safe(notebook_dir)) if self.default_url: default_url = self.format_string(self.default_url) - args.append('--NotebookApp.default_url=%s' % _quote_safe(default_url)) + args.append('--SingleUserNotebookApp.default_url=%s' % _quote_safe(default_url)) if self.debug: args.append('--debug') From cab1bca6fbe7c5b87e38072499c41f26619c9f52 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Tue, 4 Jun 2019 13:42:52 +0200 Subject: [PATCH 015/541] Use jupyter_server if notebook package isn't available --- jupyterhub/singleuser.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/jupyterhub/singleuser.py b/jupyterhub/singleuser.py index 0ab557a5..deff5b01 100755 --- a/jupyterhub/singleuser.py +++ b/jupyterhub/singleuser.py @@ -21,13 +21,19 @@ from tornado.httpclient import HTTPRequest from tornado.web import HTTPError from tornado.web import RequestHandler -use_serverapp = os.environ.get('USE_JUPYTER_SERVER', 'False') == 'True' - -required_package = 'jupyter_server' if use_serverapp else 'notebook' try: - parent_module = importlib.import_module(required_package) + import notebook + use_serverapp = False + server_package = 'notebook' + app_name = 'notebook.notebookapp' except ImportError: - raise ImportError("JupyterHub single-user server requires {}".format(required_package)) + try: + import jupyter_server + use_serverapp = True + server_package = 'jupyter_server' + app_name = 'jupyter_server.serverapp' + except ImportError: + raise ImportError("JupyterHub single-user server requires notebook or jupyter_server packages") from traitlets import ( Any, @@ -42,17 +48,15 @@ from traitlets import ( TraitError, ) -app_name = 'jupyter_server.serverapp' if use_serverapp else 'notebook.notebookapp' app_module = importlib.import_module(app_name) - NotebookApp = getattr(app_module, 'ServerApp' if use_serverapp else 'NotebookApp') notebook_aliases = app_module.aliases notebook_flags = app_module.flags -LoginHandler = getattr(importlib.import_module(required_package + '.auth.login'), 'LoginHandler') -LogoutHandler = getattr(importlib.import_module(required_package + '.auth.logout'), 'LogoutHandler') +LoginHandler = getattr(importlib.import_module(server_package + '.auth.login'), 'LoginHandler') +LogoutHandler = getattr(importlib.import_module(server_package + '.auth.logout'), 'LogoutHandler') IPythonHandler = getattr( - importlib.import_module(required_package + '.base.handlers'), + importlib.import_module(server_package + '.base.handlers'), 'JupyterHandler' if use_serverapp else 'IPythonHandler' ) From f830b2a41798b1936639f5b98ce5434dc4c7a135 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Tue, 4 Jun 2019 13:45:57 +0200 Subject: [PATCH 016/541] Try to test notebook package is still uninstalled --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3980dd87..c2d83296 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,8 +65,10 @@ script: if [[ "$TEST" == "jupyter_server" ]]; then pip uninstall notebook pip install jupyter_server - export USE_JUPYTER_SERVER=True pytest -v --maxfail=2 --cov=jupyterhub jupyterhub/tests + # Make sure that the notebook package is still not installed + pip list | grep notebook + test $? -eq 1 fi after_success: - codecov From 399def182b639171d1fbf526fb0af43ab47fdff2 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Tue, 4 Jun 2019 13:57:26 +0200 Subject: [PATCH 017/541] Actually run jupyter_server test on Travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index c2d83296..d279947d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -90,6 +90,8 @@ matrix: include: - python: 3.6 env: TEST=lint + - python: 3.6 + env: TEST=jupyter_server - python: 3.6 env: TEST=docs - python: 3.6 From 8f7e25f9a1897d1db1370338380df89b27e06c09 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Tue, 4 Jun 2019 14:24:30 +0200 Subject: [PATCH 018/541] Don't make pip uninstall wait for human input --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d279947d..02c0645c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,7 +63,7 @@ script: fi - | if [[ "$TEST" == "jupyter_server" ]]; then - pip uninstall notebook + pip uninstall notebook --yes pip install jupyter_server pytest -v --maxfail=2 --cov=jupyterhub jupyterhub/tests # Make sure that the notebook package is still not installed From e9bc25cce0616b6cf1d14c4aab61b87b13617711 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Tue, 4 Jun 2019 14:42:49 +0200 Subject: [PATCH 019/541] Run all tests for jupyter_server regardless of failure --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 02c0645c..19f05529 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,7 +65,8 @@ script: if [[ "$TEST" == "jupyter_server" ]]; then pip uninstall notebook --yes pip install jupyter_server - pytest -v --maxfail=2 --cov=jupyterhub jupyterhub/tests + # Run all tests regardless of failure, so we know what's up + pytest -v --cov=jupyterhub jupyterhub/tests # Make sure that the notebook package is still not installed pip list | grep notebook test $? -eq 1 From 1cea503292e343d5373c5d8763b1fa4d80a784bb Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 7 Jun 2019 11:44:57 +0200 Subject: [PATCH 020/541] add activity_resolution config limits last_activity update interval to 30 seconds by default to avoid a db commit on every authenticated request --- jupyterhub/app.py | 13 +++++++++++ jupyterhub/handlers/base.py | 45 +++++++++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 63537af7..ff8ead30 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -325,6 +325,18 @@ class JupyterHub(Application): redirect_to_server = Bool( True, help="Redirect user to server (if running), instead of control panel." ).tag(config=True) + activity_resolution = Integer( + 30, + help=""" + Resolution (in seconds) for updating activity + + If activity is registered that is less than activity_resolution seconds + more recent than the current value, + the new value will be ignored. + + This avoids too many writes to the Hub database. + """, + ).tag(config=True) last_activity_interval = Integer( 300, help="Interval (in seconds) at which to update last-activity timestamps." ).tag(config=True) @@ -2011,6 +2023,7 @@ class JupyterHub(Application): db=self.db, proxy=self.proxy, hub=self.hub, + activity_resolution=self.activity_resolution, admin_users=self.authenticator.admin_users, admin_access=self.admin_access, authenticator=self.authenticator, diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 83449750..35501a09 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -248,10 +248,40 @@ class BaseHandler(RequestHandler): orm_token = orm.OAuthAccessToken.find(self.db, token) if orm_token is None: return None - orm_token.last_activity = orm_token.user.last_activity = datetime.utcnow() - self.db.commit() + + now = datetime.utcnow() + recorded = self._record_activity(orm_token, now) + if self._record_activity(orm_token.user, now) or recorded: + self.db.commit() return self._user_from_orm(orm_token.user) + def _record_activity(self, obj, timestamp=None): + """record activity on an ORM object + + If last_activity was more recent than self.activity_resolution seconds ago, + do nothing to avoid unnecessarily frequent database commits. + + Args: + obj: an ORM object with a last_activity attribute + timestamp (datetime, optional): the timestamp of activity to register. + Returns: + recorded (bool): True if activity was recorded, False if not. + """ + if timestamp is None: + timestamp = datetime.utcnow() + resolution = self.settings.get("activity_resolution", 0) + if not obj.last_activity or resolution == 0: + self.log.debug("Recording first activity for %s", obj) + obj.last_activity = timestamp + return True + if (timestamp - obj.last_activity).total_seconds() > resolution: + # this debug line will happen just too often + # uncomment to debug last_activity updates + # self.log.debug("Recording activity for %s", obj) + obj.last_activity = timestamp + return True + return False + async def refresh_auth(self, user, force=False): """Refresh user authentication info @@ -322,14 +352,15 @@ class BaseHandler(RequestHandler): # record token activity now = datetime.utcnow() - orm_token.last_activity = now + recorded = self._record_activity(orm_token, now) if orm_token.user: # FIXME: scopes should give us better control than this # don't consider API requests originating from a server # to be activity from the user if not orm_token.note.startswith("Server at "): - orm_token.user.last_activity = now - self.db.commit() + recorded = self._record_activity(orm_token.user, now) or recorded + if recorded: + self.db.commit() if orm_token.service: return orm_token.service @@ -359,8 +390,8 @@ class BaseHandler(RequestHandler): clear() return # update user activity - user.last_activity = datetime.utcnow() - self.db.commit() + if self._record_activity(user): + self.db.commit() return user def _user_from_orm(self, orm_user): From 5e5a976ea6a97a4a38ec14e84ace17027964e4a2 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Wed, 12 Jun 2019 15:27:23 +1200 Subject: [PATCH 021/541] Replace existing redirect code by Tornado's addslash decorator --- jupyterhub/handlers/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 83449750..8eff2714 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -26,6 +26,7 @@ from tornado.httputil import HTTPHeaders from tornado.httputil import url_concat from tornado.ioloop import IOLoop from tornado.log import app_log +from tornado.web import addslash from tornado.web import MissingArgumentError from tornado.web import RequestHandler @@ -1450,10 +1451,9 @@ class CSPReportHandler(BaseHandler): class AddSlashHandler(BaseHandler): """Handler for adding trailing slash to URLs that need them""" - def get(self, *args): - src = urlparse(self.request.uri) - dest = src._replace(path=src.path + '/') - self.redirect(urlunparse(dest)) + @addslash + def get(self): + pass default_handlers = [ From a6b7e303df03865d6420f6bccdf627b39f1d0dc1 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Sun, 16 Jun 2019 20:00:35 +0300 Subject: [PATCH 022/541] cull-idle: Include a hint on how to add custom culling logic - cull_idle_servers.py gets the full server state, so is capable of doing any kind of arbitrary logic on the profile in order to be more flexible in culling. - This patch does not change anything, but gives an embedded (commented out) example of how you can easily add custom logic to the script. - This was added as a tempate/demo for #2598. --- examples/cull-idle/cull_idle_servers.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/examples/cull-idle/cull_idle_servers.py b/examples/cull-idle/cull_idle_servers.py index 5740063c..8b2b3c2a 100755 --- a/examples/cull-idle/cull_idle_servers.py +++ b/examples/cull-idle/cull_idle_servers.py @@ -120,6 +120,8 @@ def cull_idle( def handle_server(user, server_name, server): """Handle (maybe) culling a single server + "server" is the entire server model from the API. + Returns True if server is now stopped (user removable), False otherwise. """ @@ -162,6 +164,20 @@ def cull_idle( # for running servers inactive = age + # CUSTOM CULLING TEST CODE HERE + # Add in additional server tests here. Return False to mean "don't + # cull", True means "cull immediately", or, for example, update some + # other variables like inactive_limit. + # + # Here, server['state'] is the result of the get_state method + # on the spawner. This does *not* contain the below by + # default, you may have to modify your spawner to make this + # work. The `user` variable is the user model from the API. + # + # if server['state']['profile_name'] == 'unlimited' + # return False + # inactive_limit = server['state']['culltime'] + should_cull = ( inactive is not None and inactive.total_seconds() >= inactive_limit ) From d5d315df08b980c26d07e0608c97f48e0a0aa48e Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Mon, 1 Jul 2019 20:40:02 -0400 Subject: [PATCH 023/541] Add missing words Copied from https://jupyterhub.readthedocs.io/en/latest/reference/services.html --- docs/source/getting-started/services-basics.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/getting-started/services-basics.md b/docs/source/getting-started/services-basics.md index 7e716a63..9014fb9b 100644 --- a/docs/source/getting-started/services-basics.md +++ b/docs/source/getting-started/services-basics.md @@ -3,9 +3,9 @@ When working with JupyterHub, a **Service** is defined as a process that interacts with the Hub's REST API. A Service may perform a specific or action or task. For example, shutting down individuals' single user -notebook servers that have been is a good example of a task that could -be automated by a Service. Let's look at how the [cull_idle_servers][] -script can be used as a Service. +notebook servers that have been idle for some time is a good example of +a task that could be automated by a Service. Let's look at how the +[cull_idle_servers][] script can be used as a Service. ## Real-world example to cull idle servers From 2f15d5128eb318900eca4f911faaa3ac2cff06d8 Mon Sep 17 00:00:00 2001 From: lumbric Date: Wed, 3 Jul 2019 12:05:41 +0200 Subject: [PATCH 024/541] Update doc: do not suggest depricated config key According to changelog JupyterHub.bind_url has been added in 0.9.0. --- docs/source/reference/config-proxy.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/reference/config-proxy.md b/docs/source/reference/config-proxy.md index 5decd697..564f2efc 100644 --- a/docs/source/reference/config-proxy.md +++ b/docs/source/reference/config-proxy.md @@ -17,10 +17,12 @@ satisfy the following: Let's start out with needed JupyterHub configuration in `jupyterhub_config.py`: ```python -# Force the proxy to only listen to connections to 127.0.0.1 -c.JupyterHub.ip = '127.0.0.1' +# Force the proxy to only listen to connections to 127.0.0.1 (on port 8000) +c.JupyterHub.bind_url = 'http://127.0.0.1:8000' ``` +(For Jupyterhub < 0.9 use `c.JupyterHub.ip = '127.0.0.1'`.) + For high-quality SSL configuration, we also generate Diffie-Helman parameters. This can take a few minutes: From de11909a04e8335d5b484b94e4c1569c4cd1c4f1 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Thu, 4 Jul 2019 11:56:34 +0200 Subject: [PATCH 025/541] Update config used for testing --- testing/jupyterhub_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/jupyterhub_config.py b/testing/jupyterhub_config.py index 56aea917..bd70ed97 100644 --- a/testing/jupyterhub_config.py +++ b/testing/jupyterhub_config.py @@ -13,6 +13,6 @@ c.JupyterHub.authenticator_class = DummyAuthenticator # Optionally set a global password that all users must use # c.DummyAuthenticator.password = "your_password" -from jupyterhub.spawners import SimpleSpawner +from jupyterhub.spawner import SimpleLocalProcessSpawner -c.JupyterHub.spawner_class = SimpleSpawner +c.JupyterHub.spawner_class = SimpleLocalProcessSpawner From 97c27774b1c9dd7987eb080ba01c7855133f4330 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 4 Jul 2019 13:23:32 +0300 Subject: [PATCH 026/541] fixed running_servers count --- jupyterhub/handlers/base.py | 4 ++-- jupyterhub/metrics.py | 4 +--- jupyterhub/user.py | 2 ++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index c9eb5274..f9c8485b 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -759,6 +759,7 @@ class BaseHandler(RequestHandler): active_counts['spawn_pending'] + active_counts['proxy_pending'] ) active_count = active_counts['active'] + RUNNING_SERVERS.set(active_count) concurrent_spawn_limit = self.concurrent_spawn_limit active_server_limit = self.active_server_limit @@ -842,7 +843,6 @@ class BaseHandler(RequestHandler): "User %s took %.3f seconds to start", user_server_name, toc - tic ) self.statsd.timing('spawner.success', (toc - tic) * 1000) - RUNNING_SERVERS.inc() SERVER_SPAWN_DURATION_SECONDS.labels( status=ServerSpawnStatus.success ).observe(time.perf_counter() - spawn_start_time) @@ -854,6 +854,7 @@ class BaseHandler(RequestHandler): PROXY_ADD_DURATION_SECONDS.labels(status='success').observe( time.perf_counter() - proxy_add_start_time ) + RUNNING_SERVERS.inc() except Exception: self.log.exception("Failed to add %s to proxy!", user_server_name) self.log.error( @@ -1022,7 +1023,6 @@ class BaseHandler(RequestHandler): "User %s server took %.3f seconds to stop", user.name, toc - tic ) self.statsd.timing('spawner.stop', (toc - tic) * 1000) - RUNNING_SERVERS.dec() SERVER_STOP_DURATION_SECONDS.labels( status=ServerStopStatus.success ).observe(toc - tic) diff --git a/jupyterhub/metrics.py b/jupyterhub/metrics.py index 558167ff..27de7be6 100644 --- a/jupyterhub/metrics.py +++ b/jupyterhub/metrics.py @@ -39,9 +39,7 @@ RUNNING_SERVERS = Gauge( 'running_servers', 'the number of user servers currently running' ) -RUNNING_SERVERS.set(0) - -TOTAL_USERS = Gauge('total_users', 'toal number of users') +TOTAL_USERS = Gauge('total_users', 'total number of users') TOTAL_USERS.set(0) diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 42dbcdf5..333175bb 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -21,6 +21,7 @@ from .crypto import decrypt from .crypto import encrypt from .crypto import EncryptionUnavailable from .crypto import InvalidToken +from .metrics import RUNNING_SERVERS from .metrics import TOTAL_USERS from .objects import Server from .spawner import LocalProcessSpawner @@ -753,6 +754,7 @@ class User: self.db.delete(oauth_client) self.db.commit() self.log.debug("Finished stopping %s", spawner._log_name) + RUNNING_SERVERS.dec() finally: spawner.orm_spawner.started = None self.db.commit() From 49a5f3a65415d0f06b93220c432f0625ef2d06c7 Mon Sep 17 00:00:00 2001 From: Dan Lester Date: Fri, 5 Jul 2019 10:53:47 +0100 Subject: [PATCH 027/541] Fixed docs and testing code to use refactored SimpleLocalProcessSpawner --- docs/source/contributing/setup.rst | 16 +++++++--------- testing/jupyterhub_config.py | 4 ++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/docs/source/contributing/setup.rst b/docs/source/contributing/setup.rst index 73ffada6..aa0f0595 100644 --- a/docs/source/contributing/setup.rst +++ b/docs/source/contributing/setup.rst @@ -112,13 +112,12 @@ happen. Happy developing! -Using DummyAuthenticator & SimpleSpawner -======================================== +Using DummyAuthenticator & SimpleLocalProcessSpawner +==================================================== To simplify testing of JupyterHub, it’s helpful to use :class:`~jupyterhub.auth.DummyAuthenticator` instead of the default JupyterHub -authenticator and `SimpleSpawner `_ -instead of the default spawner. +authenticator and SimpleLocalProcessSpawner instead of the default spawner. There is a sample configuration file that does this in ``testing/jupyterhub_config.py``. To launch jupyterhub with this @@ -126,7 +125,6 @@ configuration: .. code:: bash - pip install jupyterhub-simplespawner jupyterhub -f testing/jupyterhub_config.py The default JupyterHub `authenticator @@ -137,15 +135,15 @@ require your system to have user accounts for each user you want to log in to JupyterHub as. DummyAuthenticator allows you to log in with any username & password, -while SimpleSpawner allows you to start servers without having to +while SimpleLocalProcessSpawner allows you to start servers without having to create a unix user for each JupyterHub user. Together, these make it much easier to test JupyterHub. Tip: If you are working on parts of JupyterHub that are common to all authenticators & spawners, we recommend using both DummyAuthenticator & -SimpleSpawner. If you are working on just authenticator related parts, -use only SimpleSpawner. Similarly, if you are working on just spawner -related parts, use only DummyAuthenticator. +SimpleLocalProcessSpawner. If you are working on just authenticator related +parts, use only SimpleLocalProcessSpawner. Similarly, if you are working on +just spawner related parts, use only DummyAuthenticator. Troubleshooting =============== diff --git a/testing/jupyterhub_config.py b/testing/jupyterhub_config.py index 56aea917..bd70ed97 100644 --- a/testing/jupyterhub_config.py +++ b/testing/jupyterhub_config.py @@ -13,6 +13,6 @@ c.JupyterHub.authenticator_class = DummyAuthenticator # Optionally set a global password that all users must use # c.DummyAuthenticator.password = "your_password" -from jupyterhub.spawners import SimpleSpawner +from jupyterhub.spawner import SimpleLocalProcessSpawner -c.JupyterHub.spawner_class = SimpleSpawner +c.JupyterHub.spawner_class = SimpleLocalProcessSpawner From 55323ec2062a69bc92a1eff3b46155b1c0638fb0 Mon Sep 17 00:00:00 2001 From: Jake Bartolone Date: Fri, 5 Jul 2019 12:54:09 -0500 Subject: [PATCH 028/541] corrected docker network create instructions in dockerfiles README --- dockerfiles/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockerfiles/README.md b/dockerfiles/README.md index 5799597a..b17546bd 100644 --- a/dockerfiles/README.md +++ b/dockerfiles/README.md @@ -12,7 +12,7 @@ Dockerfile.alpine contains base image for jupyterhub. It does not work independ * start configurable-http-proxy in another container * specify CONFIGPROXY_AUTH_TOKEN env in both containers -* put both containers on the same network (e.g. docker create network jupyterhub; docker run ... --net jupyterhub) +* put both containers on the same network (e.g. docker network create jupyterhub; docker run ... --net jupyterhub) * tell jupyterhub where CHP is (e.g. c.ConfigurableHTTPProxy.api_url = 'http://chp:8001') * tell jupyterhub not to start the proxy itself (c.ConfigurableHTTPProxy.should_start = False) * Use dummy authenticator for ease of testing. Update following in jupyterhub_config file From 19806899f24a8f1d1ba5b61d67bbdd353f6ce44d Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 10 Jul 2019 11:16:34 +0300 Subject: [PATCH 029/541] Set running_servers at startup --- jupyterhub/app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index be539e62..02735cf3 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -79,6 +79,7 @@ from .utils import ( print_ps_info, make_ssl_context, ) +from .metrics import RUNNING_SERVERS # classes for config from .auth import Authenticator, PAMAuthenticator @@ -1937,6 +1938,9 @@ class JupyterHub(Application): user_summaries = map(_user_summary, self.users.values()) self.log.debug("Loaded users:\n%s", '\n'.join(user_summaries)) + active_counts = self.users.count_active_users() + RUNNING_SERVERS.set(active_counts['active']) + def init_oauth(self): base_url = self.hub.base_url self.oauth_provider = make_provider( From 09cc8569b3b6224c7b3995fc4222d24f6c60789e Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 10 Jul 2019 17:19:43 +0300 Subject: [PATCH 030/541] Set total_users at startup --- jupyterhub/app.py | 2 ++ jupyterhub/metrics.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 02735cf3..5deb1181 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -80,6 +80,7 @@ from .utils import ( make_ssl_context, ) from .metrics import RUNNING_SERVERS +from .metrics import TOTAL_USERS # classes for config from .auth import Authenticator, PAMAuthenticator @@ -1940,6 +1941,7 @@ class JupyterHub(Application): active_counts = self.users.count_active_users() RUNNING_SERVERS.set(active_counts['active']) + TOTAL_USERS.set(len(self.users)) def init_oauth(self): base_url = self.hub.base_url diff --git a/jupyterhub/metrics.py b/jupyterhub/metrics.py index 27de7be6..ef7370ce 100644 --- a/jupyterhub/metrics.py +++ b/jupyterhub/metrics.py @@ -41,8 +41,6 @@ RUNNING_SERVERS = Gauge( TOTAL_USERS = Gauge('total_users', 'total number of users') -TOTAL_USERS.set(0) - CHECK_ROUTES_DURATION_SECONDS = Histogram( 'check_routes_duration_seconds', 'Time taken to validate all routes in proxy' ) From d541c1797423e21c2b4934f50c844c216399cad8 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Fri, 12 Jul 2019 22:07:30 +0200 Subject: [PATCH 031/541] Escape usernames in the frontend To cope with special characters like backslash, to address issue https://github.com/jupyterhub/jupyterhub/issues/2128 --- share/jupyterhub/static/js/home.js | 2 +- share/jupyterhub/static/js/jhapi.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/share/jupyterhub/static/js/home.js b/share/jupyterhub/static/js/home.js index e81b6690..f83b59ac 100644 --- a/share/jupyterhub/static/js/home.js +++ b/share/jupyterhub/static/js/home.js @@ -103,7 +103,7 @@ require(["jquery", "moment", "jhapi", "utils"], function( $(".new-server-btn").click(function() { var row = getRow($(this)); var serverName = row.find(".new-server-name").val(); - window.location.href = "../spawn/" + user + "/" + serverName; + window.location.href = "../spawn/" + escape(user) + "/" + serverName; }); $(".stop-server").click(stopServer); diff --git a/share/jupyterhub/static/js/jhapi.js b/share/jupyterhub/static/js/jhapi.js index c2d597ea..fe8765f4 100644 --- a/share/jupyterhub/static/js/jhapi.js +++ b/share/jupyterhub/static/js/jhapi.js @@ -46,14 +46,14 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.start_server = function(user, options) { options = options || {}; options = update(options, { type: "POST", dataType: null }); - this.api_request(utils.url_path_join("users", user, "server"), options); + this.api_request(utils.url_path_join("users", escape(user), "server"), options); }; JHAPI.prototype.start_named_server = function(user, server_name, options) { options = options || {}; options = update(options, { type: "POST", dataType: null }); this.api_request( - utils.url_path_join("users", user, "servers", server_name), + utils.url_path_join("users", escape(user), "servers", server_name), options ); }; @@ -61,14 +61,14 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.stop_server = function(user, options) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); - this.api_request(utils.url_path_join("users", user, "server"), options); + this.api_request(utils.url_path_join("users", escape(user), "server"), options); }; JHAPI.prototype.stop_named_server = function(user, server_name, options) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); this.api_request( - utils.url_path_join("users", user, "servers", server_name), + utils.url_path_join("users", escape(user), "servers", server_name), options ); }; @@ -84,7 +84,7 @@ define(["jquery", "utils"], function($, utils) { }; JHAPI.prototype.get_user = function(user, options) { - this.api_request(utils.url_path_join("users", user), options); + this.api_request(utils.url_path_join("users", escape(user)), options); }; JHAPI.prototype.add_users = function(usernames, userinfo, options) { @@ -107,7 +107,7 @@ define(["jquery", "utils"], function($, utils) { data: JSON.stringify(userinfo), }); - this.api_request(utils.url_path_join("users", user), options); + this.api_request(utils.url_path_join("users", escape(user), options)); }; JHAPI.prototype.admin_access = function(user, options) { @@ -118,7 +118,7 @@ define(["jquery", "utils"], function($, utils) { }); this.api_request( - utils.url_path_join("users", user, "admin-access"), + utils.url_path_join("users", escape(user), "admin-access"), options ); }; @@ -126,7 +126,7 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.delete_user = function(user, options) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); - this.api_request(utils.url_path_join("users", user), options); + this.api_request(utils.url_path_join("users", escape(user), options)); }; JHAPI.prototype.request_token = function(user, props, options) { @@ -135,14 +135,14 @@ define(["jquery", "utils"], function($, utils) { if (props) { options.data = JSON.stringify(props); } - this.api_request(utils.url_path_join("users", user, "tokens"), options); + this.api_request(utils.url_path_join("users", escape(user), "tokens"), options); }; JHAPI.prototype.revoke_token = function(user, token_id, options) { options = options || {}; options = update(options, { type: "DELETE" }); this.api_request( - utils.url_path_join("users", user, "tokens", token_id), + utils.url_path_join("users", escape(user), "tokens", token_id), options ); }; From dc40cfe80e676e40f6985451eab9d18b7b652088 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Sat, 13 Jul 2019 09:35:41 +0200 Subject: [PATCH 032/541] encodeURIComponent() instead of escape() --- share/jupyterhub/static/js/home.js | 2 +- share/jupyterhub/static/js/jhapi.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/share/jupyterhub/static/js/home.js b/share/jupyterhub/static/js/home.js index f83b59ac..66ebc741 100644 --- a/share/jupyterhub/static/js/home.js +++ b/share/jupyterhub/static/js/home.js @@ -103,7 +103,7 @@ require(["jquery", "moment", "jhapi", "utils"], function( $(".new-server-btn").click(function() { var row = getRow($(this)); var serverName = row.find(".new-server-name").val(); - window.location.href = "../spawn/" + escape(user) + "/" + serverName; + window.location.href = "../spawn/" + encodeURIComponent(user) + "/" + serverName; }); $(".stop-server").click(stopServer); diff --git a/share/jupyterhub/static/js/jhapi.js b/share/jupyterhub/static/js/jhapi.js index fe8765f4..bbe230b6 100644 --- a/share/jupyterhub/static/js/jhapi.js +++ b/share/jupyterhub/static/js/jhapi.js @@ -46,14 +46,14 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.start_server = function(user, options) { options = options || {}; options = update(options, { type: "POST", dataType: null }); - this.api_request(utils.url_path_join("users", escape(user), "server"), options); + this.api_request(utils.url_path_join("users", encodeURIComponent(user), "server"), options); }; JHAPI.prototype.start_named_server = function(user, server_name, options) { options = options || {}; options = update(options, { type: "POST", dataType: null }); this.api_request( - utils.url_path_join("users", escape(user), "servers", server_name), + utils.url_path_join("users", encodeURIComponent(user), "servers", server_name), options ); }; @@ -61,14 +61,14 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.stop_server = function(user, options) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); - this.api_request(utils.url_path_join("users", escape(user), "server"), options); + this.api_request(utils.url_path_join("users", encodeURIComponent(user), "server"), options); }; JHAPI.prototype.stop_named_server = function(user, server_name, options) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); this.api_request( - utils.url_path_join("users", escape(user), "servers", server_name), + utils.url_path_join("users", encodeURIComponent(user), "servers", server_name), options ); }; @@ -84,7 +84,7 @@ define(["jquery", "utils"], function($, utils) { }; JHAPI.prototype.get_user = function(user, options) { - this.api_request(utils.url_path_join("users", escape(user)), options); + this.api_request(utils.url_path_join("users", encodeURIComponent(user)), options); }; JHAPI.prototype.add_users = function(usernames, userinfo, options) { @@ -107,7 +107,7 @@ define(["jquery", "utils"], function($, utils) { data: JSON.stringify(userinfo), }); - this.api_request(utils.url_path_join("users", escape(user), options)); + this.api_request(utils.url_path_join("users", encodeURIComponent(user), options)); }; JHAPI.prototype.admin_access = function(user, options) { @@ -118,7 +118,7 @@ define(["jquery", "utils"], function($, utils) { }); this.api_request( - utils.url_path_join("users", escape(user), "admin-access"), + utils.url_path_join("users", encodeURIComponent(user), "admin-access"), options ); }; @@ -126,7 +126,7 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.delete_user = function(user, options) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); - this.api_request(utils.url_path_join("users", escape(user), options)); + this.api_request(utils.url_path_join("users", encodeURIComponent(user), options)); }; JHAPI.prototype.request_token = function(user, props, options) { @@ -135,14 +135,14 @@ define(["jquery", "utils"], function($, utils) { if (props) { options.data = JSON.stringify(props); } - this.api_request(utils.url_path_join("users", escape(user), "tokens"), options); + this.api_request(utils.url_path_join("users", encodeURIComponent(user), "tokens"), options); }; JHAPI.prototype.revoke_token = function(user, token_id, options) { options = options || {}; options = update(options, { type: "DELETE" }); this.api_request( - utils.url_path_join("users", escape(user), "tokens", token_id), + utils.url_path_join("users", encodeURIComponent(user), "tokens", token_id), options ); }; From 0dc35936613470404758a108aee6863152e4fd79 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Sat, 13 Jul 2019 10:25:32 +0200 Subject: [PATCH 033/541] Escape user variable to frontend --- share/jupyterhub/templates/page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index 70c733e5..d51ac578 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -63,7 +63,7 @@ base_url: "{{base_url}}", prefix: "{{prefix}}", {% if user %} - user: "{{user.name}}", + user: "{{user.name|escapejs}}", {% endif %} {% if admin_access %} admin_access: true, From d686ae1ae76da5b38be0fa9a05869506d117ad8a Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Sat, 13 Jul 2019 10:49:06 +0200 Subject: [PATCH 034/541] json_encode for Tornado framework --- share/jupyterhub/templates/page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index d51ac578..3d9973e7 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -63,7 +63,7 @@ base_url: "{{base_url}}", prefix: "{{prefix}}", {% if user %} - user: "{{user.name|escapejs}}", + user: "{{json_encode(user.name)}}", {% endif %} {% if admin_access %} admin_access: true, From c0464b2e47fab18e1330efa616a1f96fdda35142 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 08:18:20 +0200 Subject: [PATCH 035/541] feat: unicode_escape feature --- jupyterhub/user.py | 5 +++++ share/jupyterhub/templates/page.html | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 333175bb..e7f6b451 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -350,6 +350,11 @@ class User: """My name, escaped for use in URLs, cookies, etc.""" return quote(self.name, safe='@~') + @property + def unicode_escaped_name(self): + """My name, escaped for use in javascript inserts, etc.""" + return self.name.decode('unicode_escape') + @property def proxy_spec(self): """The proxy routespec for my default server""" diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index 3d9973e7..c641c1c4 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -63,7 +63,7 @@ base_url: "{{base_url}}", prefix: "{{prefix}}", {% if user %} - user: "{{json_encode(user.name)}}", + user: "{{user.unicode_escaped_name}}", {% endif %} {% if admin_access %} admin_access: true, From 34e44f2eed6b3ec88071067a094fb22222d0113c Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 10:01:11 +0200 Subject: [PATCH 036/541] feat: user function in page render function --- jupyterhub/handlers/pages.py | 2 +- share/jupyterhub/templates/page.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index d93f2638..2e2de07f 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -67,7 +67,7 @@ class HomeHandler(BaseHandler): html = self.render_template( 'home.html', - user=user, + user=user.unicode_escaped_url, url=url, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index c641c1c4..cebd9d04 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -63,7 +63,7 @@ base_url: "{{base_url}}", prefix: "{{prefix}}", {% if user %} - user: "{{user.unicode_escaped_name}}", + user: "{{user}}", {% endif %} {% if admin_access %} admin_access: true, From 11e32588d791efe337c20b784a1c1b866bd88751 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 10:17:26 +0200 Subject: [PATCH 037/541] chore: use most likely fix for custom username property --- jupyterhub/handlers/pages.py | 2 +- share/jupyterhub/templates/page.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 2e2de07f..d93f2638 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -67,7 +67,7 @@ class HomeHandler(BaseHandler): html = self.render_template( 'home.html', - user=user.unicode_escaped_url, + user=user, url=url, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index cebd9d04..c641c1c4 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -63,7 +63,7 @@ base_url: "{{base_url}}", prefix: "{{prefix}}", {% if user %} - user: "{{user}}", + user: "{{user.unicode_escaped_name}}", {% endif %} {% if admin_access %} admin_access: true, From c24f6b0a6a4ff0b7fc94ea59e9d645d5f7dd3034 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 10:38:51 +0200 Subject: [PATCH 038/541] chore: add logging code --- jupyterhub/user.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jupyterhub/user.py b/jupyterhub/user.py index e7f6b451..16803084 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -353,7 +353,10 @@ class User: @property def unicode_escaped_name(self): """My name, escaped for use in javascript inserts, etc.""" - return self.name.decode('unicode_escape') + unicoded = self.name.decode('unicode_escape') + self.log.info(f'unicode escaping: name={ self.name }, escaped={ unicoded }') + print("in unicode_escaped_name") + return self.name # return normal name for now @property def proxy_spec(self): From 5ae1fdf621fd5ca3eff5c2cdda2ac08ef3dadf9d Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 10:53:14 +0200 Subject: [PATCH 039/541] chore: try custom property --- jupyterhub/handlers/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index d93f2638..2e2de07f 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -67,7 +67,7 @@ class HomeHandler(BaseHandler): html = self.render_template( 'home.html', - user=user, + user=user.unicode_escaped_url, url=url, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, From 69d0a477347337730f49fe2437fd8a7668fea3e7 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 11:04:35 +0200 Subject: [PATCH 040/541] chore: try conversion in template Custom property in users.py didn't work, so try it in the templated file. --- share/jupyterhub/templates/page.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index c641c1c4..80a7dc75 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -63,7 +63,8 @@ base_url: "{{base_url}}", prefix: "{{prefix}}", {% if user %} - user: "{{user.unicode_escaped_name}}", + user: "{{user.name}}", + user_unicode: "{{user.name.decode('unicode_escape')}}", {% endif %} {% if admin_access %} admin_access: true, From 8c8968c2b0ad0b35e65cb4fdc875e6fcccf54e27 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 11:06:13 +0200 Subject: [PATCH 041/541] chore: correct handler Probably this has introduced more errors --- jupyterhub/handlers/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 2e2de07f..d93f2638 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -67,7 +67,7 @@ class HomeHandler(BaseHandler): html = self.render_template( 'home.html', - user=user.unicode_escaped_url, + user=user, url=url, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, From 1555abb2bf636dce5843443e8e912040517ec3dd Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 11:43:49 +0200 Subject: [PATCH 042/541] feat: unicode escaping method --- jupyterhub/handlers/pages.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index d93f2638..6440da5a 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -68,6 +68,7 @@ class HomeHandler(BaseHandler): html = self.render_template( 'home.html', user=user, + user_unicode=codecs.unicode_escape_encode(user)[0].decode(), url=url, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, From fc90be8424d14b02f925330ecbeac38828b8588d Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 12:17:32 +0200 Subject: [PATCH 043/541] fix: user user.name instead of user --- jupyterhub/handlers/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 6440da5a..d2d90857 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -68,7 +68,7 @@ class HomeHandler(BaseHandler): html = self.render_template( 'home.html', user=user, - user_unicode=codecs.unicode_escape_encode(user)[0].decode(), + user_unicode=codecs.unicode_escape_encode(user.name)[0].decode(), url=url, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, From 3d6d60b64e306815e743d474eba130b1d173b320 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 12:34:07 +0200 Subject: [PATCH 044/541] fix: passthrough in template --- share/jupyterhub/templates/page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index 80a7dc75..1496abf3 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -64,7 +64,7 @@ prefix: "{{prefix}}", {% if user %} user: "{{user.name}}", - user_unicode: "{{user.name.decode('unicode_escape')}}", + user_unicode: "{{user_unicode}}", {% endif %} {% if admin_access %} admin_access: true, From d050242d0f74099b4af396e2679a2b89b629fa0b Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 12:52:30 +0200 Subject: [PATCH 045/541] chore: try splitting value coding --- jupyterhub/handlers/pages.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index d2d90857..c6a1c9d6 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -2,6 +2,7 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import asyncio +import codecs import copy import time from collections import defaultdict @@ -53,6 +54,10 @@ class HomeHandler(BaseHandler): @web.authenticated async def get(self): user = self.current_user + user_unicode = codecs.unicode_escape_encode(self.current_user.name)[0].decode() + self.log.info("user: %s", user) + self.log.info("user.name: %s", user.name) + self.log.info("user_unicode: %s", user_unicode) if user.running: # trigger poll_and_notify event in case of a server that died await user.spawner.poll_and_notify() @@ -68,7 +73,7 @@ class HomeHandler(BaseHandler): html = self.render_template( 'home.html', user=user, - user_unicode=codecs.unicode_escape_encode(user.name)[0].decode(), + user_unicode=user_unicode, url=url, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, From 1915ecd0c2064d45dd3720722aa64f83383a3050 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 13:07:18 +0200 Subject: [PATCH 046/541] feat: try unicoding in user model --- jupyterhub/handlers/pages.py | 3 +++ jupyterhub/user.py | 6 ++++-- share/jupyterhub/templates/page.html | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index c6a1c9d6..6ce15120 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -55,9 +55,11 @@ class HomeHandler(BaseHandler): async def get(self): user = self.current_user user_unicode = codecs.unicode_escape_encode(self.current_user.name)[0].decode() + user_unicode_1 = self.current_user.unicode_escaped_name self.log.info("user: %s", user) self.log.info("user.name: %s", user.name) self.log.info("user_unicode: %s", user_unicode) + self.log.info("user_unicode_1: %s", user_unicode_1) if user.running: # trigger poll_and_notify event in case of a server that died await user.spawner.poll_and_notify() @@ -74,6 +76,7 @@ class HomeHandler(BaseHandler): 'home.html', user=user, user_unicode=user_unicode, + user_unicode_1=user_unicode_1, url=url, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 16803084..3f702c04 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -1,5 +1,6 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +import codecs import warnings from collections import defaultdict from datetime import datetime @@ -353,10 +354,11 @@ class User: @property def unicode_escaped_name(self): """My name, escaped for use in javascript inserts, etc.""" - unicoded = self.name.decode('unicode_escape') + #unicoded = self.name.decode('unicode_escape') + unicoded = codecs.unicode_escape_encode(self.name)[0].decode() self.log.info(f'unicode escaping: name={ self.name }, escaped={ unicoded }') print("in unicode_escaped_name") - return self.name # return normal name for now + return unicoded # return normal name for now @property def proxy_spec(self): diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index 1496abf3..51198934 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -65,6 +65,7 @@ {% if user %} user: "{{user.name}}", user_unicode: "{{user_unicode}}", + user_unicode_1: "{{user_unicode_1}}", {% endif %} {% if admin_access %} admin_access: true, From 73309b5741b36ca9c0872285d075cdf8c9f33ecb Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 13:31:53 +0200 Subject: [PATCH 047/541] feat: adopt unicode_escaped_name property --- jupyterhub/handlers/pages.py | 2 -- share/jupyterhub/templates/page.html | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 6ce15120..e016b292 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -75,8 +75,6 @@ class HomeHandler(BaseHandler): html = self.render_template( 'home.html', user=user, - user_unicode=user_unicode, - user_unicode_1=user_unicode_1, url=url, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index 51198934..4ce76e82 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -63,7 +63,8 @@ base_url: "{{base_url}}", prefix: "{{prefix}}", {% if user %} - user: "{{user.name}}", + user: "{{user.unicode_escaped_name}}", + user_original: "{{user.name}}", user_unicode: "{{user_unicode}}", user_unicode_1: "{{user_unicode_1}}", {% endif %} From f1ed6c95f01cf93086e2aadef3e10a47dd9a1311 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 14:03:48 +0200 Subject: [PATCH 048/541] chore: reverse url changes --- share/jupyterhub/static/js/home.js | 2 +- share/jupyterhub/static/js/jhapi.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/share/jupyterhub/static/js/home.js b/share/jupyterhub/static/js/home.js index 66ebc741..f83b59ac 100644 --- a/share/jupyterhub/static/js/home.js +++ b/share/jupyterhub/static/js/home.js @@ -103,7 +103,7 @@ require(["jquery", "moment", "jhapi", "utils"], function( $(".new-server-btn").click(function() { var row = getRow($(this)); var serverName = row.find(".new-server-name").val(); - window.location.href = "../spawn/" + encodeURIComponent(user) + "/" + serverName; + window.location.href = "../spawn/" + escape(user) + "/" + serverName; }); $(".stop-server").click(stopServer); diff --git a/share/jupyterhub/static/js/jhapi.js b/share/jupyterhub/static/js/jhapi.js index bbe230b6..fe8765f4 100644 --- a/share/jupyterhub/static/js/jhapi.js +++ b/share/jupyterhub/static/js/jhapi.js @@ -46,14 +46,14 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.start_server = function(user, options) { options = options || {}; options = update(options, { type: "POST", dataType: null }); - this.api_request(utils.url_path_join("users", encodeURIComponent(user), "server"), options); + this.api_request(utils.url_path_join("users", escape(user), "server"), options); }; JHAPI.prototype.start_named_server = function(user, server_name, options) { options = options || {}; options = update(options, { type: "POST", dataType: null }); this.api_request( - utils.url_path_join("users", encodeURIComponent(user), "servers", server_name), + utils.url_path_join("users", escape(user), "servers", server_name), options ); }; @@ -61,14 +61,14 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.stop_server = function(user, options) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); - this.api_request(utils.url_path_join("users", encodeURIComponent(user), "server"), options); + this.api_request(utils.url_path_join("users", escape(user), "server"), options); }; JHAPI.prototype.stop_named_server = function(user, server_name, options) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); this.api_request( - utils.url_path_join("users", encodeURIComponent(user), "servers", server_name), + utils.url_path_join("users", escape(user), "servers", server_name), options ); }; @@ -84,7 +84,7 @@ define(["jquery", "utils"], function($, utils) { }; JHAPI.prototype.get_user = function(user, options) { - this.api_request(utils.url_path_join("users", encodeURIComponent(user)), options); + this.api_request(utils.url_path_join("users", escape(user)), options); }; JHAPI.prototype.add_users = function(usernames, userinfo, options) { @@ -107,7 +107,7 @@ define(["jquery", "utils"], function($, utils) { data: JSON.stringify(userinfo), }); - this.api_request(utils.url_path_join("users", encodeURIComponent(user), options)); + this.api_request(utils.url_path_join("users", escape(user), options)); }; JHAPI.prototype.admin_access = function(user, options) { @@ -118,7 +118,7 @@ define(["jquery", "utils"], function($, utils) { }); this.api_request( - utils.url_path_join("users", encodeURIComponent(user), "admin-access"), + utils.url_path_join("users", escape(user), "admin-access"), options ); }; @@ -126,7 +126,7 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.delete_user = function(user, options) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); - this.api_request(utils.url_path_join("users", encodeURIComponent(user), options)); + this.api_request(utils.url_path_join("users", escape(user), options)); }; JHAPI.prototype.request_token = function(user, props, options) { @@ -135,14 +135,14 @@ define(["jquery", "utils"], function($, utils) { if (props) { options.data = JSON.stringify(props); } - this.api_request(utils.url_path_join("users", encodeURIComponent(user), "tokens"), options); + this.api_request(utils.url_path_join("users", escape(user), "tokens"), options); }; JHAPI.prototype.revoke_token = function(user, token_id, options) { options = options || {}; options = update(options, { type: "DELETE" }); this.api_request( - utils.url_path_join("users", encodeURIComponent(user), "tokens", token_id), + utils.url_path_join("users", escape(user), "tokens", token_id), options ); }; From 7c78e6c3268b7c4e24aed38b33909d60d3298491 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 14:28:28 +0200 Subject: [PATCH 049/541] chore: try non-escaping user Now the user was double-escaped, resulting in escaped % signs --- share/jupyterhub/static/js/jhapi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/jupyterhub/static/js/jhapi.js b/share/jupyterhub/static/js/jhapi.js index fe8765f4..f905f75e 100644 --- a/share/jupyterhub/static/js/jhapi.js +++ b/share/jupyterhub/static/js/jhapi.js @@ -61,7 +61,7 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.stop_server = function(user, options) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); - this.api_request(utils.url_path_join("users", escape(user), "server"), options); + this.api_request(utils.url_path_join("users", user, "server"), options); }; JHAPI.prototype.stop_named_server = function(user, server_name, options) { From b237ab9e7b4805239b4f6a070da50f38c6995e7b Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 14:38:35 +0200 Subject: [PATCH 050/541] feat: try fixing the spawn url --- jupyterhub/handlers/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index e016b292..cbcc7046 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -226,7 +226,7 @@ class SpawnHandler(BaseHandler): next_url = self.get_next_url( user, default=url_path_join( - self.hub.base_url, "spawn-pending", user.name, server_name + self.hub.base_url, "spawn-pending", user.unicode_escaped_name, server_name ), ) self.redirect(next_url) From 8dce5a87bc001b986acfbec33c8a0484abecc34a Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 16:46:00 +0200 Subject: [PATCH 051/541] revert try ginfing spawn url --- jupyterhub/handlers/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index cbcc7046..e016b292 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -226,7 +226,7 @@ class SpawnHandler(BaseHandler): next_url = self.get_next_url( user, default=url_path_join( - self.hub.base_url, "spawn-pending", user.unicode_escaped_name, server_name + self.hub.base_url, "spawn-pending", user.name, server_name ), ) self.redirect(next_url) From 41db9fe1164ef82fb3de3cff97a6b67b7c358a65 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 16:47:06 +0200 Subject: [PATCH 052/541] chore: cleanup debugging code --- jupyterhub/user.py | 6 +----- share/jupyterhub/templates/page.html | 3 --- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 3f702c04..b929569c 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -354,11 +354,7 @@ class User: @property def unicode_escaped_name(self): """My name, escaped for use in javascript inserts, etc.""" - #unicoded = self.name.decode('unicode_escape') - unicoded = codecs.unicode_escape_encode(self.name)[0].decode() - self.log.info(f'unicode escaping: name={ self.name }, escaped={ unicoded }') - print("in unicode_escaped_name") - return unicoded # return normal name for now + return codecs.unicode_escape_encode(self.name)[0].decode() @property def proxy_spec(self): diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index 4ce76e82..c641c1c4 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -64,9 +64,6 @@ prefix: "{{prefix}}", {% if user %} user: "{{user.unicode_escaped_name}}", - user_original: "{{user.name}}", - user_unicode: "{{user_unicode}}", - user_unicode_1: "{{user_unicode_1}}", {% endif %} {% if admin_access %} admin_access: true, From 8a37d2daec7cf56ce7b98294e9a8b50c954e2803 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 17:13:19 +0200 Subject: [PATCH 053/541] chore: cleanup comments --- jupyterhub/handlers/pages.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index e016b292..0a7b7309 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -54,12 +54,6 @@ class HomeHandler(BaseHandler): @web.authenticated async def get(self): user = self.current_user - user_unicode = codecs.unicode_escape_encode(self.current_user.name)[0].decode() - user_unicode_1 = self.current_user.unicode_escaped_name - self.log.info("user: %s", user) - self.log.info("user.name: %s", user.name) - self.log.info("user_unicode: %s", user_unicode) - self.log.info("user_unicode_1: %s", user_unicode_1) if user.running: # trigger poll_and_notify event in case of a server that died await user.spawner.poll_and_notify() From 859dc34ea6ff07a11b570ec9986191cfe6ab4a32 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 18:45:48 +0200 Subject: [PATCH 054/541] chore: rename to json_escaped_name and unittests --- jupyterhub/tests/test_orm.py | 8 ++++++++ jupyterhub/user.py | 6 +++--- share/jupyterhub/templates/page.html | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/jupyterhub/tests/test_orm.py b/jupyterhub/tests/test_orm.py index d609b27b..e364e32f 100644 --- a/jupyterhub/tests/test_orm.py +++ b/jupyterhub/tests/test_orm.py @@ -73,6 +73,14 @@ def test_user(db): found = orm.User.find(db, 'badger') assert found is None +def test_user_escaping(db): + orm_user = orm.User(name='company\\user@company.com,\"quoted\"') + db.add(orm_user) + db.commit() + user = User(orm_user) + assert user.name == 'company\\user@company.com,\"quoted\"' + assert user.escaped_name == 'company%5Cuser@company.com%2C%22quoted%22' + assert user.json_escaped_name == 'company\\\\user@company.com,\\\"quoted\\\"' def test_tokens(db): user = orm.User(name='inara') diff --git a/jupyterhub/user.py b/jupyterhub/user.py index b929569c..9444d574 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -1,6 +1,6 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -import codecs +import json import warnings from collections import defaultdict from datetime import datetime @@ -352,9 +352,9 @@ class User: return quote(self.name, safe='@~') @property - def unicode_escaped_name(self): + def json_escaped_name(self): """My name, escaped for use in javascript inserts, etc.""" - return codecs.unicode_escape_encode(self.name)[0].decode() + return json.dumps(self.name)[1:-1] @property def proxy_spec(self): diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index c641c1c4..cd50ee11 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -63,7 +63,7 @@ base_url: "{{base_url}}", prefix: "{{prefix}}", {% if user %} - user: "{{user.unicode_escaped_name}}", + user: "{{user.json_escaped_name}}", {% endif %} {% if admin_access %} admin_access: true, From d8d58b2ebdcef17ba5333d5ab675317d9d8e234e Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 18:51:22 +0200 Subject: [PATCH 055/541] chore: undo escape() functions --- share/jupyterhub/static/js/home.js | 2 +- share/jupyterhub/static/js/jhapi.js | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/share/jupyterhub/static/js/home.js b/share/jupyterhub/static/js/home.js index f83b59ac..e81b6690 100644 --- a/share/jupyterhub/static/js/home.js +++ b/share/jupyterhub/static/js/home.js @@ -103,7 +103,7 @@ require(["jquery", "moment", "jhapi", "utils"], function( $(".new-server-btn").click(function() { var row = getRow($(this)); var serverName = row.find(".new-server-name").val(); - window.location.href = "../spawn/" + escape(user) + "/" + serverName; + window.location.href = "../spawn/" + user + "/" + serverName; }); $(".stop-server").click(stopServer); diff --git a/share/jupyterhub/static/js/jhapi.js b/share/jupyterhub/static/js/jhapi.js index f905f75e..c2d597ea 100644 --- a/share/jupyterhub/static/js/jhapi.js +++ b/share/jupyterhub/static/js/jhapi.js @@ -46,14 +46,14 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.start_server = function(user, options) { options = options || {}; options = update(options, { type: "POST", dataType: null }); - this.api_request(utils.url_path_join("users", escape(user), "server"), options); + this.api_request(utils.url_path_join("users", user, "server"), options); }; JHAPI.prototype.start_named_server = function(user, server_name, options) { options = options || {}; options = update(options, { type: "POST", dataType: null }); this.api_request( - utils.url_path_join("users", escape(user), "servers", server_name), + utils.url_path_join("users", user, "servers", server_name), options ); }; @@ -68,7 +68,7 @@ define(["jquery", "utils"], function($, utils) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); this.api_request( - utils.url_path_join("users", escape(user), "servers", server_name), + utils.url_path_join("users", user, "servers", server_name), options ); }; @@ -84,7 +84,7 @@ define(["jquery", "utils"], function($, utils) { }; JHAPI.prototype.get_user = function(user, options) { - this.api_request(utils.url_path_join("users", escape(user)), options); + this.api_request(utils.url_path_join("users", user), options); }; JHAPI.prototype.add_users = function(usernames, userinfo, options) { @@ -107,7 +107,7 @@ define(["jquery", "utils"], function($, utils) { data: JSON.stringify(userinfo), }); - this.api_request(utils.url_path_join("users", escape(user), options)); + this.api_request(utils.url_path_join("users", user), options); }; JHAPI.prototype.admin_access = function(user, options) { @@ -118,7 +118,7 @@ define(["jquery", "utils"], function($, utils) { }); this.api_request( - utils.url_path_join("users", escape(user), "admin-access"), + utils.url_path_join("users", user, "admin-access"), options ); }; @@ -126,7 +126,7 @@ define(["jquery", "utils"], function($, utils) { JHAPI.prototype.delete_user = function(user, options) { options = options || {}; options = update(options, { type: "DELETE", dataType: null }); - this.api_request(utils.url_path_join("users", escape(user), options)); + this.api_request(utils.url_path_join("users", user), options); }; JHAPI.prototype.request_token = function(user, props, options) { @@ -135,14 +135,14 @@ define(["jquery", "utils"], function($, utils) { if (props) { options.data = JSON.stringify(props); } - this.api_request(utils.url_path_join("users", escape(user), "tokens"), options); + this.api_request(utils.url_path_join("users", user, "tokens"), options); }; JHAPI.prototype.revoke_token = function(user, token_id, options) { options = options || {}; options = update(options, { type: "DELETE" }); this.api_request( - utils.url_path_join("users", escape(user), "tokens", token_id), + utils.url_path_join("users", user, "tokens", token_id), options ); }; From fad6900779f524f7bf4233731056ffc5d4d05fb5 Mon Sep 17 00:00:00 2001 From: Jim Crist Date: Tue, 16 Jul 2019 12:03:25 -0500 Subject: [PATCH 056/541] Update a few links [ci skip] These projects recently moved under the JupyterHub organization, updated the links accordingly. --- README.md | 14 +++++++------- docs/source/gallery-jhub-deployments.md | 2 +- docs/source/reference/spawners.md | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6c1fc6d0..2fc37039 100644 --- a/README.md +++ b/README.md @@ -145,12 +145,12 @@ To start the Hub on a specific url and port ``10.0.1.2:443`` with **https**: ### Authenticators -| Authenticator | Description | -| --------------------------------------------------------------------------- | ------------------------------------------------- | -| PAMAuthenticator | Default, built-in authenticator | -| [OAuthenticator](https://github.com/jupyterhub/oauthenticator) | OAuth + JupyterHub Authenticator = OAuthenticator | -| [ldapauthenticator](https://github.com/jupyterhub/ldapauthenticator) | Simple LDAP Authenticator Plugin for JupyterHub | -| [kerberosauthenticator](https://github.com/jcrist/kerberosauthenticator) | Kerberos Authenticator Plugin for JupyterHub | +| Authenticator | Description | +| ---------------------------------------------------------------------------- | ------------------------------------------------- | +| PAMAuthenticator | Default, built-in authenticator | +| [OAuthenticator](https://github.com/jupyterhub/oauthenticator) | OAuth + JupyterHub Authenticator = OAuthenticator | +| [ldapauthenticator](https://github.com/jupyterhub/ldapauthenticator) | Simple LDAP Authenticator Plugin for JupyterHub | +| [kerberosauthenticator](https://github.com/jupyterhub/kerberosauthenticator) | Kerberos Authenticator Plugin for JupyterHub | ### Spawners @@ -162,7 +162,7 @@ To start the Hub on a specific url and port ``10.0.1.2:443`` with **https**: | [sudospawner](https://github.com/jupyterhub/sudospawner) | Spawn single-user servers without being root | | [systemdspawner](https://github.com/jupyterhub/systemdspawner) | Spawn single-user notebook servers using systemd | | [batchspawner](https://github.com/jupyterhub/batchspawner) | Designed for clusters using batch scheduling software | -| [yarnspawner](https://github.com/jcrist/yarnspawner) | Spawn single-user notebook servers distributed on a Hadoop cluster | +| [yarnspawner](https://github.com/jupyterhub/yarnspawner) | Spawn single-user notebook servers distributed on a Hadoop cluster | | [wrapspawner](https://github.com/jupyterhub/wrapspawner) | WrapSpawner and ProfilesSpawner enabling runtime configuration of spawners | ## Docker diff --git a/docs/source/gallery-jhub-deployments.md b/docs/source/gallery-jhub-deployments.md index 84f03e97..55a3dff6 100644 --- a/docs/source/gallery-jhub-deployments.md +++ b/docs/source/gallery-jhub-deployments.md @@ -173,7 +173,7 @@ easy to do with RStudio too. ### Hadoop -- [Deploying JupyterHub on Hadoop](https://jcrist.github.io/jupyterhub-on-hadoop/) +- [Deploying JupyterHub on Hadoop](https://jupyterhub-on-hadoop.readthedocs.io) ## Miscellaneous diff --git a/docs/source/reference/spawners.md b/docs/source/reference/spawners.md index 7ecc28c8..ae2c595f 100644 --- a/docs/source/reference/spawners.md +++ b/docs/source/reference/spawners.md @@ -25,7 +25,7 @@ Some examples include: run without being root, by spawning an intermediate process via `sudo` - [BatchSpawner](https://github.com/jupyterhub/batchspawner) for spawning remote servers using batch systems -- [YarnSpawner](https://github.com/jcrist/yarnspawner) for spawning notebook +- [YarnSpawner](https://github.com/jupyterhub/yarnspawner) for spawning notebook servers in YARN containers on a Hadoop cluster - [RemoteSpawner](https://github.com/zonca/remotespawner) to spawn notebooks and a remote server and tunnel the port via SSH From 6a2876a9fa95bfe433719d00d65bc4f7f9c54670 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 19:23:06 +0200 Subject: [PATCH 057/541] chore: satisfy Black --- jupyterhub/tests/test_orm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jupyterhub/tests/test_orm.py b/jupyterhub/tests/test_orm.py index e364e32f..4790f386 100644 --- a/jupyterhub/tests/test_orm.py +++ b/jupyterhub/tests/test_orm.py @@ -73,6 +73,7 @@ def test_user(db): found = orm.User.find(db, 'badger') assert found is None + def test_user_escaping(db): orm_user = orm.User(name='company\\user@company.com,\"quoted\"') db.add(orm_user) @@ -82,6 +83,7 @@ def test_user_escaping(db): assert user.escaped_name == 'company%5Cuser@company.com%2C%22quoted%22' assert user.json_escaped_name == 'company\\\\user@company.com,\\\"quoted\\\"' + def test_tokens(db): user = orm.User(name='inara') db.add(user) From 88bff9d03d562785b62a63e410a75cbffb91b97e Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Tue, 16 Jul 2019 19:25:36 +0200 Subject: [PATCH 058/541] chore: include proposed docstring fix --- jupyterhub/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 9444d574..97a3543f 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -353,7 +353,7 @@ class User: @property def json_escaped_name(self): - """My name, escaped for use in javascript inserts, etc.""" + """The user name, escaped for use in javascript inserts, etc.""" return json.dumps(self.name)[1:-1] @property From 254687e841a4f933b9531af01552c8627fe5ddef Mon Sep 17 00:00:00 2001 From: Iram Lee Date: Tue, 16 Jul 2019 14:33:54 -0500 Subject: [PATCH 059/541] fix typos on technical reference documentation --- docs/source/reference/proxy.md | 8 ++++---- docs/source/reference/spawners.md | 2 +- docs/source/reference/websecurity.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/reference/proxy.md b/docs/source/reference/proxy.md index cdc083cf..fd58816a 100644 --- a/docs/source/reference/proxy.md +++ b/docs/source/reference/proxy.md @@ -19,7 +19,7 @@ In general, for a proxy to be usable by JupyterHub, it must: 1. support websockets without prior knowledge of the URL where websockets may occur -2. support trie-based routing (i.e. allow different routes on `/foo` and +2. support trie-based routing (i.e. allow different routes on `/foo` and `/foo/bar` and route based on specificity) 3. adding or removing a route should not cause existing connections to drop @@ -62,7 +62,7 @@ Hub should call these methods when the Hub itself starts and stops. ## Encryption When using `internal_ssl` to encrypt traffic behind the proxy, at minimum, -your `Proxy` will need client ssl certificates which the `Hub` must be made +your `Proxy` will need client ssl certificates which the `Hub` must be made aware of. These can be generated with the command `jupyterhub --generate-certs` which will write them to the `internal_certs_location` in folders named `proxy_api` and `proxy_client`. Alternatively, these can be provided to the @@ -102,7 +102,7 @@ route to be proxied, such as `/user/name/`. A routespec will: ### Adding a route When adding a route, JupyterHub may pass a JSON-serializable dict as a `data` -argument that should be attacked to the proxy route. When that route is +argument that should be attached to the proxy route. When that route is retrieved, the `data` argument should be returned as well. If your proxy implementation doesn't support storing data attached to routes, then your Python wrapper may have to handle storing the `data` piece itself, e.g in a @@ -204,7 +204,7 @@ setup( ``` If you have added this metadata to your package, -users can select your authenticator with the configuration: +users can select your proxy with the configuration: ```python c.JupyterHub.proxy_class = 'mything' diff --git a/docs/source/reference/spawners.md b/docs/source/reference/spawners.md index 7ecc28c8..91abf7ce 100644 --- a/docs/source/reference/spawners.md +++ b/docs/source/reference/spawners.md @@ -195,7 +195,7 @@ setup( ``` If you have added this metadata to your package, -users can select your authenticator with the configuration: +users can select your spawner with the configuration: ```python c.JupyterHub.spawner_class = 'myservice' diff --git a/docs/source/reference/websecurity.md b/docs/source/reference/websecurity.md index ccdc616b..b9b1df68 100644 --- a/docs/source/reference/websecurity.md +++ b/docs/source/reference/websecurity.md @@ -76,7 +76,7 @@ resolves the cross-site issues. ### Disable user config -If subdomains are not available or not desirable, JupyterHub provides a a +If subdomains are not available or not desirable, JupyterHub provides a configuration option `Spawner.disable_user_config`, which can be set to prevent the user-owned configuration files from being loaded. After implementing this option, PATHs and package installation and PATHs are the other things that the From 8a2eba1156b10dbec5b16babc97b047b17261d9b Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 16 Jul 2019 13:45:36 -0700 Subject: [PATCH 060/541] Some theme updates; no double NEXT/PREV buttons. - Install pip in the docs conda env (or conda complains). - Do not override page.html, the next/previous buttons are now handled by alabaster_jupyterhub (this actually remove the duplicated next/prev buttons) - use alabaster_jupyterhub when building locally, this make it easy for new contributor to get the _exact_ same appearance than on readthedocs. --- docs/environment.yml | 1 + docs/source/_templates/page.html | 30 ------------------------------ docs/source/conf.py | 4 +--- 3 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 docs/source/_templates/page.html diff --git a/docs/environment.yml b/docs/environment.yml index b23b5601..2dbfd535 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -4,6 +4,7 @@ name: jhub_docs channels: - conda-forge dependencies: +- pip - nodejs - python=3.6 - alembic diff --git a/docs/source/_templates/page.html b/docs/source/_templates/page.html deleted file mode 100644 index 8df1d7ec..00000000 --- a/docs/source/_templates/page.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends '!page.html' %} - -{# Custom template for page.html - - Alabaster theme does not provide blocks for prev/next at bottom of each page. - This is _in addition_ to the prev/next in the sidebar. The "Prev/Next" text - or symbols are handled by CSS classes in _static/custom.css -#} - -{% macro prev_next(prev, next, prev_title='', next_title='') %} - {%- if prev %} - {{ prev_title or prev.title }} - {%- endif %} - {%- if next %} - {{ next_title or next.title }} - {%- endif %} -
-{% endmacro %} - - -{% block body %} -
- {{ prev_next(prev, next, 'Previous', 'Next') }} -
- - {{super()}} -
- {{ prev_next(prev, next) }} -
-{% endblock %} diff --git a/docs/source/conf.py b/docs/source/conf.py index 2e2e2087..6e83f379 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -192,9 +192,7 @@ intersphinx_mapping = {'https://docs.python.org/3/': None} # -- Read The Docs -------------------------------------------------------- on_rtd = os.environ.get('READTHEDOCS', None) == 'True' -if not on_rtd: - html_theme = 'alabaster' -else: +if on_rtd: # readthedocs.org uses their theme by default, so no need to specify it # build rest-api, since RTD doesn't run make from subprocess import check_call as sh From 30c69f94c82b35a9293f4f5928d6af6c1d73ccd5 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Wed, 17 Jul 2019 07:23:39 +0200 Subject: [PATCH 061/541] fix: spawn redirect for users with backslash The 302 redirect served after the spawn POST was not escaping the backslash. --- jupyterhub/handlers/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index d93f2638..13877adb 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -219,7 +219,7 @@ class SpawnHandler(BaseHandler): next_url = self.get_next_url( user, default=url_path_join( - self.hub.base_url, "spawn-pending", user.name, server_name + self.hub.base_url, "spawn-pending", user.escaped_name, server_name ), ) self.redirect(next_url) From ae7974564c5091df8688e05a85af84a25a29944a Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Wed, 17 Jul 2019 08:34:45 +0200 Subject: [PATCH 062/541] fix: use user.escaped_name in page urls --- jupyterhub/handlers/pages.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 13877adb..6b72523c 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -61,9 +61,9 @@ class HomeHandler(BaseHandler): # to establish that this is an explicit spawn request rather # than an implicit one, which can be caused by any link to `/user/:name(/:server_name)` if user.active: - url = url_path_join(self.base_url, 'user', user.name) + url = url_path_join(self.base_url, 'user', user.escaped_name) else: - url = url_path_join(self.hub.base_url, 'spawn', user.name) + url = url_path_join(self.hub.base_url, 'spawn', user.escaped_name) html = self.render_template( 'home.html', @@ -132,7 +132,7 @@ class SpawnHandler(BaseHandler): # which may get handled by the default server if they aren't ready yet pending_url = url_path_join( - self.hub.base_url, "spawn-pending", user.name, server_name + self.hub.base_url, "spawn-pending", user.escaped_name, server_name ) if self.get_argument('next', None): From 9d2823e84b11294f506f3634762727abf2ccaf35 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Wed, 17 Jul 2019 09:05:25 +0200 Subject: [PATCH 063/541] fix: user.escaped_name in base.py urls --- jupyterhub/handlers/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index f9c8485b..f0862cdf 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -1357,7 +1357,7 @@ class UserUrlHandler(BaseHandler): return pending_url = url_concat( - url_path_join(self.hub.base_url, 'spawn-pending', user.name, server_name), + url_path_join(self.hub.base_url, 'spawn-pending', user.escaped_name, server_name), {'next': self.request.uri}, ) if spawner.pending or spawner._failed: @@ -1371,7 +1371,7 @@ class UserUrlHandler(BaseHandler): # without explicit user action self.set_status(503) spawn_url = url_concat( - url_path_join(self.hub.base_url, "spawn", user.name, server_name), + url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name), {"next": self.request.uri}, ) html = self.render_template( @@ -1459,7 +1459,7 @@ class UserRedirectHandler(BaseHandler): user_url = url_concat(user_url, parse_qsl(self.request.query)) url = url_concat( - url_path_join(self.hub.base_url, "spawn", user.name), {"next": user_url} + url_path_join(self.hub.base_url, "spawn", user.escaped_name), {"next": user_url} ) self.redirect(url) From 37642408a481f2a3280275444056360d8de94369 Mon Sep 17 00:00:00 2001 From: Kenan Erdogan Date: Wed, 17 Jul 2019 10:44:44 +0200 Subject: [PATCH 064/541] close `
` tag in home.html --- share/jupyterhub/templates/home.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/share/jupyterhub/templates/home.html b/share/jupyterhub/templates/home.html index a141b2d4..88379e56 100644 --- a/share/jupyterhub/templates/home.html +++ b/share/jupyterhub/templates/home.html @@ -4,7 +4,6 @@ {% endif %} {% block main %} -
@@ -83,7 +82,8 @@ {% endif %} -{% endblock %} +
+{% endblock main %} {% block script %} {{ super() }} From b37b13a939f2c19340247b17d7c4f8ddbafef3f1 Mon Sep 17 00:00:00 2001 From: Nico Rikken Date: Wed, 17 Jul 2019 11:05:35 +0200 Subject: [PATCH 065/541] chore: satisfy black checker --- jupyterhub/handlers/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index f0862cdf..19504c9c 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -1357,7 +1357,9 @@ class UserUrlHandler(BaseHandler): return pending_url = url_concat( - url_path_join(self.hub.base_url, 'spawn-pending', user.escaped_name, server_name), + url_path_join( + self.hub.base_url, 'spawn-pending', user.escaped_name, server_name + ), {'next': self.request.uri}, ) if spawner.pending or spawner._failed: @@ -1459,7 +1461,8 @@ class UserRedirectHandler(BaseHandler): user_url = url_concat(user_url, parse_qsl(self.request.query)) url = url_concat( - url_path_join(self.hub.base_url, "spawn", user.escaped_name), {"next": user_url} + url_path_join(self.hub.base_url, "spawn", user.escaped_name), + {"next": user_url}, ) self.redirect(url) From 2e67a534cf810a4734f26ae599ed2e01924f1215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Wed, 24 Jul 2019 09:30:02 -0400 Subject: [PATCH 066/541] Update flask hub authentication services example in doc The flask example in the documentation was still using the input argument `cookie_cache_max_age` when instantiating `HubAuth` object. `cookie_cache_max_age` is deprecated since JupyterHub 0.8 and should be replaced by `cache_max_age`. --- docs/source/reference/services.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/reference/services.md b/docs/source/reference/services.md index 24de3bdd..7686d4a4 100644 --- a/docs/source/reference/services.md +++ b/docs/source/reference/services.md @@ -249,7 +249,7 @@ prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/') auth = HubAuth( api_token=os.environ['JUPYTERHUB_API_TOKEN'], - cookie_cache_max_age=60, + cache_max_age=60, ) app = Flask(__name__) From 444f0ba00c754886dd8682100ddd06029df57f15 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Fri, 26 Jul 2019 14:29:29 +1200 Subject: [PATCH 067/541] Update spawn-form example --- examples/spawn-form/jupyterhub_config.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/spawn-form/jupyterhub_config.py b/examples/spawn-form/jupyterhub_config.py index ff7c2526..58fe59dc 100644 --- a/examples/spawn-form/jupyterhub_config.py +++ b/examples/spawn-form/jupyterhub_config.py @@ -10,10 +10,15 @@ class DemoFormSpawner(LocalProcessSpawner): def _options_form_default(self): default_env = "YOURNAME=%s\n" % self.user.name return """ - - - - +
+ + +
+
+ + +
""".format( env=default_env ) From 749b9e0997da4934286c3da2663321d980e35b65 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 5 Aug 2019 14:42:48 +0300 Subject: [PATCH 068/541] /hub/admin redirect to login --- jupyterhub/handlers/pages.py | 1 + jupyterhub/tests/test_pages.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 8f4ffb38..c27dd4a6 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -349,6 +349,7 @@ class SpawnPendingHandler(BaseHandler): class AdminHandler(BaseHandler): """Render the admin page.""" + @web.authenticated @admin_only def get(self): available = {'name', 'admin', 'running', 'last_activity'} diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 61a98065..0ab01c9f 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -92,8 +92,9 @@ async def test_home_auth(app): async def test_admin_no_auth(app): - r = await get_page('admin', app) - assert r.status_code == 403 + r = await get_page('admin', app, allow_redirects=False) + assert r.status_code == 302 + assert '/hub/login' in r.headers['Location'] async def test_admin_not_admin(app): From 2cc0eb885a36ce38a001af4e6705d12862ee6c24 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Mon, 5 Aug 2019 13:54:32 +0200 Subject: [PATCH 069/541] Replace header logo: jupyter -> jupyterhub --- jupyterhub/app.py | 2 +- jupyterhub/tests/test_pages.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 5deb1181..d8770780 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -592,7 +592,7 @@ class JupyterHub(Application): @default('logo_file') def _logo_file_default(self): - return os.path.join(self.data_files_path, 'static', 'images', 'jupyter.png') + return os.path.join(self.data_files_path, 'static', 'images', 'jupyterhub-80.png') jinja_environment_options = Dict( help="Supply extra arguments that will be passed to Jinja environment." diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 61a98065..05f5aacc 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -611,7 +611,7 @@ async def test_static_files(app): r = await async_requests.get(ujoin(base_url, 'logo')) r.raise_for_status() assert r.headers['content-type'] == 'image/png' - r = await async_requests.get(ujoin(base_url, 'static', 'images', 'jupyter.png')) + r = await async_requests.get(ujoin(base_url, 'static', 'images', 'jupyterhub-80.png')) r.raise_for_status() assert r.headers['content-type'] == 'image/png' r = await async_requests.get(ujoin(base_url, 'static', 'css', 'style.min.css')) From 5fa268dab119306d915f85fe8e40c28dab24f1ff Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 7 Aug 2019 00:37:30 +0200 Subject: [PATCH 070/541] Apply black autoformatting --- jupyterhub/app.py | 4 +++- jupyterhub/tests/test_pages.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index d8770780..7410089e 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -592,7 +592,9 @@ class JupyterHub(Application): @default('logo_file') def _logo_file_default(self): - return os.path.join(self.data_files_path, 'static', 'images', 'jupyterhub-80.png') + return os.path.join( + self.data_files_path, 'static', 'images', 'jupyterhub-80.png' + ) jinja_environment_options = Dict( help="Supply extra arguments that will be passed to Jinja environment." diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 05f5aacc..82268be2 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -611,7 +611,9 @@ async def test_static_files(app): r = await async_requests.get(ujoin(base_url, 'logo')) r.raise_for_status() assert r.headers['content-type'] == 'image/png' - r = await async_requests.get(ujoin(base_url, 'static', 'images', 'jupyterhub-80.png')) + r = await async_requests.get( + ujoin(base_url, 'static', 'images', 'jupyterhub-80.png') + ) r.raise_for_status() assert r.headers['content-type'] == 'image/png' r = await async_requests.get(ujoin(base_url, 'static', 'css', 'style.min.css')) From 6e71e617ed3eba15a63f1e37be421de3ae6c66da Mon Sep 17 00:00:00 2001 From: Katsarov Date: Sat, 10 Aug 2019 07:48:41 +0200 Subject: [PATCH 071/541] update the pending deprecation message in api_tokens to recommend services, not service_tokens --- jupyterhub/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 7410089e..de788dc2 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -811,14 +811,14 @@ class JupyterHub(Application): api_tokens = Dict( Unicode(), - help="""PENDING DEPRECATION: consider using service_tokens + help="""PENDING DEPRECATION: consider using services Dict of token:username to be loaded into the database. Allows ahead-of-time generation of API tokens for use by externally managed services, which authenticate as JupyterHub users. - Consider using service_tokens for general services that talk to the JupyterHub API. + Consider using services for general services that talk to the JupyterHub API. """, ).tag(config=True) From 1d1e108e09049f06b37bd640e0ad4ebda9c6f381 Mon Sep 17 00:00:00 2001 From: Rollin Thomas Date: Mon, 12 Aug 2019 09:14:25 -0700 Subject: [PATCH 072/541] Add `server_name` to `spawn_url` --- jupyterhub/handlers/pages.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index c27dd4a6..05006cf0 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -283,7 +283,8 @@ class SpawnPendingHandler(BaseHandler): # We should point the user to Home if the most recent spawn failed. exc = spawner._spawn_future.exception() self.log.error("Previous spawn for %s failed: %s", spawner._log_name, exc) - spawn_url = url_path_join(self.hub.base_url, "spawn", user.escaped_name) + spawn_url = url_path_join(self.hub.base_url, "spawn", user.escaped_name, + server_name) self.set_status(500) html = self.render_template( "not_running.html", @@ -328,7 +329,8 @@ class SpawnPendingHandler(BaseHandler): # further, set status to 404 because this is not # serving the expected page if status is not None: - spawn_url = url_path_join(self.hub.base_url, "spawn", user.escaped_name) + spawn_url = url_path_join(self.hub.base_url, "spawn", user.escaped_name, + server_name) html = self.render_template( "not_running.html", user=user, From 037730761cec9aaa347b86504322b381e2e96d33 Mon Sep 17 00:00:00 2001 From: Rollin Thomas Date: Mon, 12 Aug 2019 10:06:22 -0700 Subject: [PATCH 073/541] Reformat --- jupyterhub/handlers/pages.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 05006cf0..2aa2c3e8 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -283,8 +283,9 @@ class SpawnPendingHandler(BaseHandler): # We should point the user to Home if the most recent spawn failed. exc = spawner._spawn_future.exception() self.log.error("Previous spawn for %s failed: %s", spawner._log_name, exc) - spawn_url = url_path_join(self.hub.base_url, "spawn", user.escaped_name, - server_name) + spawn_url = url_path_join( + self.hub.base_url, "spawn", user.escaped_name, server_name + ) self.set_status(500) html = self.render_template( "not_running.html", @@ -329,8 +330,9 @@ class SpawnPendingHandler(BaseHandler): # further, set status to 404 because this is not # serving the expected page if status is not None: - spawn_url = url_path_join(self.hub.base_url, "spawn", user.escaped_name, - server_name) + spawn_url = url_path_join( + self.hub.base_url, "spawn", user.escaped_name, server_name + ) html = self.render_template( "not_running.html", user=user, From cda7f73cfa75138ae11b757fa24b861cb7cf4f61 Mon Sep 17 00:00:00 2001 From: Rick Gerkin Date: Fri, 16 Aug 2019 04:59:51 +0000 Subject: [PATCH 074/541] Added support for consistent UIDs at user creation time --- jupyterhub/auth.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index bc906a70..0834e3d7 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -660,6 +660,15 @@ class LocalAuthenticator(Authenticator): # This appears to be the Linux non-interactive adduser command: return ['adduser', '-q', '--gecos', '""', '--disabled-password'] + uids = Dict( + help=""" + Dictionary of uids to use at user creation time. + This helps ensure that users created from the database + get the same uid each time they are created + in temporary deployments or containers. + """ + ).tag(config=True) + group_whitelist = Set( help=""" Whitelist all users from this UNIX group. @@ -762,7 +771,15 @@ class LocalAuthenticator(Authenticator): Tested to work on FreeBSD and Linux, at least. """ name = user.name - cmd = [arg.replace('USERNAME', name) for arg in self.add_user_cmd] + [name] + cmd = [arg.replace('USERNAME', name) for arg in self.add_user_cmd] + try: + uid = self.uids[name] + cmd += ['--uid', '%d' % uid] + except AttributeError: + pass + except KeyError: + self.log.warning("No UID for user %s" % name) + cmd += [name] self.log.info("Creating user: %s", ' '.join(map(pipes.quote, cmd))) p = Popen(cmd, stdout=PIPE, stderr=STDOUT) p.wait() From 8d7f55ce922ab7fef5a6cfbeaa0abb7a8b0dbf19 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 21 Aug 2019 13:59:25 +0300 Subject: [PATCH 075/541] Fix postgres test --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 788bfa4f..55f32b32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,8 @@ env: - MYSQL_HOST=127.0.0.1 - MYSQL_TCP_PORT=13306 services: - - postgres + - postgresql + - mysql - docker # installing dependencies From 4bfc69dc80810f8083762d5e59ebace49f5a6a34 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 21 Aug 2019 22:01:43 +0300 Subject: [PATCH 076/541] Pin mysql-connector-python to 8.0.11 on travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 55f32b32..14af4ef1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ before_install: DB=mysql bash ci/init-db.sh # FIXME: mysql-connector-python 8.0.16 incorrectly decodes bytes to str # ref: https://bugs.mysql.com/bug.php?id=94944 - pip install 'mysql-connector-python==8.0.15' + pip install 'mysql-connector-python==8.0.11' elif [[ $JUPYTERHUB_TEST_DB_URL == postgresql* ]]; then psql -c "CREATE USER $PGUSER WITH PASSWORD '$PGPASSWORD';" -U postgres DB=postgres bash ci/init-db.sh From 077727595cd6f349c982dfb81a0e886966e67e9a Mon Sep 17 00:00:00 2001 From: Matt Shannon Date: Wed, 21 Aug 2019 13:05:04 -0700 Subject: [PATCH 077/541] Add Jupyter community link --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2fc37039..4d593c2d 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,7 @@ our JupyterHub [Gitter](https://gitter.im/jupyterhub/jupyterhub) channel. - [Documentation for JupyterHub's REST API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/master/docs/rest-api.yml#/default) - [Documentation for Project Jupyter](http://jupyter.readthedocs.io/en/latest/index.html) | [PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf) - [Project Jupyter website](https://jupyter.org) +- [Project Jupyter community](https://jupyter.org/community) JupyterHub follows the Jupyter [Community Guides](https://jupyter.readthedocs.io/en/latest/community/content-community.html). From f7e5904c5b43ff4d48839fdf35f1a8acbdd1f32b Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 22 Aug 2019 10:04:11 +0300 Subject: [PATCH 078/541] No need to start mysql service --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 14af4ef1..d9dbbd7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,6 @@ env: - MYSQL_TCP_PORT=13306 services: - postgresql - - mysql - docker # installing dependencies From cbbead37809a4f995cdac5f78978d74889e3a343 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 14 Aug 2019 13:21:13 +0300 Subject: [PATCH 079/541] Fix uncaught exception in pre_spawn_start --- jupyterhub/handlers/pages.py | 14 +++++++++++++- jupyterhub/user.py | 22 ++++++++++++---------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 2aa2c3e8..68264afc 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -172,7 +172,19 @@ class SpawnHandler(BaseHandler): spawner._spawn_future = None # not running, no form. Trigger spawn and redirect back to /user/:name f = asyncio.ensure_future(self.spawn_single_user(user, server_name)) - await asyncio.wait([f], timeout=1) + done, pending = await asyncio.wait([f], timeout=1) + # If spawn_single_user throws an exception, raise a 500 error + # otherwise it may cause a redirect loop + if f in done: + future, = done + exc = future.exception() + if exc: + raise web.HTTPError( + 500, + "Error in Authenticator.pre_spawn_start: %s %s" + % (type(exc).__name__, str(exc)), + ) + return self.redirect(pending_url) @web.authenticated diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 97a3543f..407310db 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -529,17 +529,19 @@ class User: # trigger pre-spawn hook on authenticator authenticator = self.authenticator - if authenticator: - await maybe_future(authenticator.pre_spawn_start(self, spawner)) - - spawner._start_pending = True - # update spawner start time, and activity for both spawner and user - self.last_activity = ( - spawner.orm_spawner.started - ) = spawner.orm_spawner.last_activity = datetime.utcnow() - db.commit() - # wait for spawner.start to return try: + if authenticator: + # pre_spawn_start can thow errors that can lead to a redirect loop + # if left uncaught (see https://github.com/jupyterhub/jupyterhub/issues/2683) + await maybe_future(authenticator.pre_spawn_start(self, spawner)) + + spawner._start_pending = True + # update spawner start time, and activity for both spawner and user + self.last_activity = ( + spawner.orm_spawner.started + ) = spawner.orm_spawner.last_activity = datetime.utcnow() + db.commit() + # wait for spawner.start to return # run optional preparation work to bootstrap the notebook await maybe_future(spawner.run_pre_spawn_hook()) if self.settings.get('internal_ssl'): From 8a61eb17383c0dd128e5718e9a20d5457ce00936 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 14 Aug 2019 13:21:40 +0300 Subject: [PATCH 080/541] Test pre_spawn_start exception --- jupyterhub/tests/test_pages.py | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 6bc5663d..77b5cf1e 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -790,3 +790,42 @@ async def test_metrics_auth(app): async def test_health_check_request(app): r = await get_page('health', app) assert r.status_code == 200 + + +async def test_pre_spawn_start_exc_no_form(app): + exc = "pre_spawn_start error" + + # throw exception from pre_spawn_start + @gen.coroutine + def mock_pre_spawn_start(user, spawner): + raise Exception(exc) + + with mock.patch.object(app.authenticator, 'pre_spawn_start', mock_pre_spawn_start): + cookies = await app.login_user('summer') + # spawn page should thow a 500 error and show the pre_spawn_start error message + r = await get_page('spawn', app, cookies=cookies) + assert r.status_code == 500 + assert exc in r.text + + +async def test_pre_spawn_start_exc_options_form(app): + exc = "pre_spawn_start error" + + # throw exception from pre_spawn_start + @gen.coroutine + def mock_pre_spawn_start(user, spawner): + raise Exception(exc) + + with mock.patch.dict( + app.users.settings, {'spawner_class': FormSpawner} + ), mock.patch.object(app.authenticator, 'pre_spawn_start', mock_pre_spawn_start): + cookies = await app.login_user('spring') + user = app.users['spring'] + # spawn page shouldn't throw any error until the spawn is started + r = await get_page('spawn', app, cookies=cookies) + assert r.url.endswith('/spawn') + r.raise_for_status() + assert FormSpawner.options_form in r.text + # spawning the user server should throw the pre_spawn_start error + with pytest.raises(Exception, match="%s" % exc): + await user.spawn() From 0058ed803d611d1d585f7d96b80c3b0586f64560 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 22 Aug 2019 15:06:08 +0300 Subject: [PATCH 081/541] Address feedback --- jupyterhub/handlers/pages.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 68264afc..53525dc8 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -175,16 +175,14 @@ class SpawnHandler(BaseHandler): done, pending = await asyncio.wait([f], timeout=1) # If spawn_single_user throws an exception, raise a 500 error # otherwise it may cause a redirect loop - if f in done: - future, = done - exc = future.exception() + if f.done() and f.exception(): + exc = f.exception() if exc: raise web.HTTPError( 500, "Error in Authenticator.pre_spawn_start: %s %s" % (type(exc).__name__, str(exc)), ) - return self.redirect(pending_url) @web.authenticated From 03693c379e19f3823c3bad7f60cb0a4927e183e0 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 22 Aug 2019 15:53:40 +0300 Subject: [PATCH 082/541] Removed unnecesary check --- jupyterhub/handlers/pages.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 53525dc8..aa2f37cc 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -177,12 +177,11 @@ class SpawnHandler(BaseHandler): # otherwise it may cause a redirect loop if f.done() and f.exception(): exc = f.exception() - if exc: - raise web.HTTPError( - 500, - "Error in Authenticator.pre_spawn_start: %s %s" - % (type(exc).__name__, str(exc)), - ) + raise web.HTTPError( + 500, + "Error in Authenticator.pre_spawn_start: %s %s" + % (type(exc).__name__, str(exc)), + ) self.redirect(pending_url) @web.authenticated From ced45d101a47f7a9af7eaae0cfae82d14634afe7 Mon Sep 17 00:00:00 2001 From: Richard C Gerkin Date: Thu, 22 Aug 2019 09:33:15 -0700 Subject: [PATCH 083/541] Update jupyterhub/auth.py Co-Authored-By: Min RK --- jupyterhub/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index 0834e3d7..87ace62d 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -778,7 +778,7 @@ class LocalAuthenticator(Authenticator): except AttributeError: pass except KeyError: - self.log.warning("No UID for user %s" % name) + self.log.debug("No UID for user %s" % name) cmd += [name] self.log.info("Creating user: %s", ' '.join(map(pipes.quote, cmd))) p = Popen(cmd, stdout=PIPE, stderr=STDOUT) From 41b2e6e401f4f00b5bad2ba6281163d84484cb31 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Thu, 2 May 2019 16:10:45 -0700 Subject: [PATCH 084/541] Add eventlogging infrastructure - Introduce the EventLog class from BinderHub for emitting structured event data - Instrument server starts and stops to emit events - Defaults to not saving any events anywhere --- jupyterhub/app.py | 10 ++ jupyterhub/event-schemas/server-actions.json | 24 ++++ jupyterhub/events.py | 119 +++++++++++++++++++ jupyterhub/handlers/base.py | 14 +++ 4 files changed, 167 insertions(+) create mode 100644 jupyterhub/event-schemas/server-actions.json create mode 100644 jupyterhub/events.py diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 7410089e..0ec5355c 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -11,6 +11,8 @@ import re import signal import socket import sys +import json +from glob import glob from concurrent.futures import ThreadPoolExecutor from datetime import datetime from datetime import timezone @@ -87,6 +89,7 @@ from .auth import Authenticator, PAMAuthenticator from .crypto import CryptKeeper from .spawner import Spawner, LocalProcessSpawner from .objects import Hub, Server +from .events import EventLog # For faking stats from .emptyclass import EmptyClass @@ -2069,6 +2072,7 @@ class JupyterHub(Application): internal_ssl_ca=self.internal_ssl_ca, trusted_alt_names=self.trusted_alt_names, shutdown_on_logout=self.shutdown_on_logout, + event_log=self.event_log ) # allow configured settings to have priority settings.update(self.tornado_settings) @@ -2144,6 +2148,12 @@ class JupyterHub(Application): _log_cls("Authenticator", self.authenticator_class) _log_cls("Spawner", self.spawner_class) + self.event_log = EventLog(parent=self) + + for schema_file in glob(os.path.join(here, 'event-schemas','*.json')): + with open(schema_file) as f: + self.event_log.register_schema(json.load(f)) + self.init_pycurl() self.init_secrets() self.init_internal_ssl() diff --git a/jupyterhub/event-schemas/server-actions.json b/jupyterhub/event-schemas/server-actions.json new file mode 100644 index 00000000..0b390aa4 --- /dev/null +++ b/jupyterhub/event-schemas/server-actions.json @@ -0,0 +1,24 @@ +{ + "$id": "hub.jupyter.org/server-action", + "version": 1, + "title": "JupyterHub server events", + "description": "JupyterHub emits this event when a user's server starts or stops", + "type": "object", + "properties": { + "action": { + "enum": [ + "start", + "stop" + ], + "description": "Action taken on this user's server" + }, + "username": { + "type": "string", + "description": "Name of the user whose server this action applies to" + }, + "servername": { + "type": "string", + "description": "Name of the server this action applies to" + } + } +} \ No newline at end of file diff --git a/jupyterhub/events.py b/jupyterhub/events.py new file mode 100644 index 00000000..14b9f1d6 --- /dev/null +++ b/jupyterhub/events.py @@ -0,0 +1,119 @@ +""" +Emit structured, discrete events when various actions happen. +""" +from traitlets.config import Configurable + +import logging +from datetime import datetime +import jsonschema +from pythonjsonlogger import jsonlogger +from traitlets import TraitType +import json +import six + + +class Callable(TraitType): + """ + A trait which is callable. + + Classes are callable, as are instances + with a __call__() method. + """ + info_text = 'a callable' + def validate(self, obj, value): + if six.callable(value): + return value + else: + self.error(obj, value) + +def _skip_message(record, **kwargs): + """ + Remove 'message' from log record. + + It is always emitted with 'null', and we do not want it, + since we are always emitting events only + """ + del record['message'] + return json.dumps(record, **kwargs) + + +class EventLog(Configurable): + """ + Send structured events to a logging sink + """ + handlers_maker = Callable( + None, + config=True, + allow_none=True, + help=""" + Callable that returns a list of logging.Handler instances to send events to. + + When set to None (the default), events are discarded. + """ + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.log = logging.getLogger(__name__) + # We don't want events to show up in the default logs + self.log.propagate = False + self.log.setLevel(logging.INFO) + + if self.handlers_maker: + self.handlers = self.handlers_maker(self) + formatter = jsonlogger.JsonFormatter(json_serializer=_skip_message) + for handler in self.handlers: + handler.setFormatter(formatter) + self.log.addHandler(handler) + + self.schemas = {} + + def register_schema(self, schema): + """ + Register a given JSON Schema with this event emitter + + 'version' and '$id' are required fields. + """ + # Check if our schema itself is valid + # This throws an exception if it isn't valid + jsonschema.validators.validator_for(schema).check_schema(schema) + + # Check that the properties we require are present + required_schema_fields = {'$id', 'version'} + for rsf in required_schema_fields: + if rsf not in schema: + raise ValueError( + f'{rsf} is required in schema specification' + ) + + # Make sure reserved, auto-added fields are not in schema + reserved_fields = {'timestamp', 'schema', 'version'} + for rf in reserved_fields: + if rf in schema['properties']: + raise ValueError( + f'{rf} field is reserved by event emitter & can not be explicitly set in schema' + ) + + self.schemas[(schema['$id'], schema['version'])] = schema + + def emit(self, schema_name, version, event): + """ + Emit event with given schema / version in a capsule. + """ + if not self.handlers_maker: + # If we don't have a handler setup, ignore everything + return + + if (schema_name, version) not in self.schemas: + raise ValueError(f'Schema {schema_name} version {version} not registered') + schema = self.schemas[(schema_name, version)] + jsonschema.validate(event, schema) + + capsule = { + 'timestamp': datetime.utcnow().isoformat() + 'Z', + 'schema': schema_name, + 'version': version + } + capsule.update(event) + self.log.info(capsule) \ No newline at end of file diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 19504c9c..19dfd467 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -156,6 +156,10 @@ class BaseHandler(RequestHandler): def oauth_provider(self): return self.settings['oauth_provider'] + @property + def event_log(self): + return self.settings['event_log'] + def finish(self, *args, **kwargs): """Roll back any uncommitted transactions from the handler.""" if self.db.dirty: @@ -846,6 +850,11 @@ class BaseHandler(RequestHandler): SERVER_SPAWN_DURATION_SECONDS.labels( status=ServerSpawnStatus.success ).observe(time.perf_counter() - spawn_start_time) + self.event_log.emit('hub.jupyter.org/server-action', 1, { + 'action': 'start', + 'username': user.name, + 'servername': server_name + }) proxy_add_start_time = time.perf_counter() spawner._proxy_pending = True try: @@ -1026,6 +1035,11 @@ class BaseHandler(RequestHandler): SERVER_STOP_DURATION_SECONDS.labels( status=ServerStopStatus.success ).observe(toc - tic) + self.event_log.emit('hub.jupyter.org/server-action', 1, { + 'action': 'stop', + 'username': user.name, + 'servername': server_name + }) except: SERVER_STOP_DURATION_SECONDS.labels( status=ServerStopStatus.failure From 1e578a25d37a5bb73d77e40cbec2cb8c8eb65b7d Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Thu, 2 May 2019 16:23:02 -0700 Subject: [PATCH 085/541] Add jsonschema and python-json-logger as dependencies They're pure python, and should be ok --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index ad40788f..3b49dc97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,5 @@ requests SQLAlchemy>=1.1 tornado>=5.0 traitlets>=4.3.2 +jsonschema +python-json-logger From eca4f33afc4b09844643f1de14a6cb4aea99dfcf Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Thu, 2 May 2019 16:42:17 -0700 Subject: [PATCH 086/541] Don't use f strings yet jupyterhub still supports Python 3.5 --- jupyterhub/events.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/jupyterhub/events.py b/jupyterhub/events.py index 14b9f1d6..9176fcad 100644 --- a/jupyterhub/events.py +++ b/jupyterhub/events.py @@ -84,7 +84,7 @@ class EventLog(Configurable): for rsf in required_schema_fields: if rsf not in schema: raise ValueError( - f'{rsf} is required in schema specification' + '{} is required in schema specification'.format(rsf) ) # Make sure reserved, auto-added fields are not in schema @@ -92,7 +92,9 @@ class EventLog(Configurable): for rf in reserved_fields: if rf in schema['properties']: raise ValueError( - f'{rf} field is reserved by event emitter & can not be explicitly set in schema' + '{rf} field is reserved by event emitter & can not be explicitly set in schema'.format( + rf=rf + ) ) self.schemas[(schema['$id'], schema['version'])] = schema @@ -106,7 +108,9 @@ class EventLog(Configurable): return if (schema_name, version) not in self.schemas: - raise ValueError(f'Schema {schema_name} version {version} not registered') + raise ValueError('Schema {schema_name} version {version} not registered'.format( + schema_name=schema_name, version=version + )) schema = self.schemas[(schema_name, version)] jsonschema.validate(event, schema) From 5aaa5263fab0439fe1d87ada85a1746a65c45a87 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Thu, 2 May 2019 16:56:50 -0700 Subject: [PATCH 087/541] Emitted schemas must be whitelisted by admins Privacy by default! --- jupyterhub/events.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/jupyterhub/events.py b/jupyterhub/events.py index 9176fcad..688b4e5b 100644 --- a/jupyterhub/events.py +++ b/jupyterhub/events.py @@ -7,7 +7,7 @@ import logging from datetime import datetime import jsonschema from pythonjsonlogger import jsonlogger -from traitlets import TraitType +from traitlets import TraitType, List import json import six @@ -52,6 +52,17 @@ class EventLog(Configurable): """ ) + allowed_schemas = List( + [], + config=True, + help=""" + Fully qualified names of schemas to record. + + Each schema you want to record must be manually specified. + The default, an empty list, means no events are recorded. + """ + ) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -103,8 +114,9 @@ class EventLog(Configurable): """ Emit event with given schema / version in a capsule. """ - if not self.handlers_maker: - # If we don't have a handler setup, ignore everything + if not (self.handlers_maker and schema_name in self.allowed_schemas): + # if handler isn't set up or schema is not explicitly whitelisted, + # don't do anything return if (schema_name, version) not in self.schemas: From 1225ff47be2f5a7ea4ae1a4806b77320978fc895 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 12 Jun 2019 09:41:29 -0700 Subject: [PATCH 088/541] Use dunder formatting for capsule --- jupyterhub/events.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/jupyterhub/events.py b/jupyterhub/events.py index 688b4e5b..0f4db756 100644 --- a/jupyterhub/events.py +++ b/jupyterhub/events.py @@ -99,14 +99,10 @@ class EventLog(Configurable): ) # Make sure reserved, auto-added fields are not in schema - reserved_fields = {'timestamp', 'schema', 'version'} - for rf in reserved_fields: - if rf in schema['properties']: - raise ValueError( - '{rf} field is reserved by event emitter & can not be explicitly set in schema'.format( - rf=rf - ) - ) + if any([p.startswith('__') for p in schema['properties']]): + raise ValueError( + 'Schema {} has properties beginning with __, which is not allowed' + ) self.schemas[(schema['$id'], schema['version'])] = schema @@ -127,9 +123,9 @@ class EventLog(Configurable): jsonschema.validate(event, schema) capsule = { - 'timestamp': datetime.utcnow().isoformat() + 'Z', - 'schema': schema_name, - 'version': version + '__timestamp__': datetime.utcnow().isoformat() + 'Z', + '__schema__': schema_name, + '__version__': version } capsule.update(event) self.log.info(capsule) \ No newline at end of file From dcde4020c21a4fa137c652e59082d3276719b190 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Tue, 9 Jul 2019 13:44:50 -0700 Subject: [PATCH 089/541] Use EventLog class from jupyter_telemetry Full circle, since the code in jupyter_telemetry came from here: https://github.com/jupyter/telemetry/pull/6 --- jupyterhub/app.py | 12 ++-- jupyterhub/events.py | 131 ------------------------------------ jupyterhub/handlers/base.py | 8 +-- 3 files changed, 10 insertions(+), 141 deletions(-) delete mode 100644 jupyterhub/events.py diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 0ec5355c..1353a3b7 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -59,6 +59,8 @@ from traitlets import ( ) from traitlets.config import Application, Configurable, catch_config_error +from jupyter_telemetry.eventlog import EventLog + here = os.path.dirname(__file__) import jupyterhub @@ -89,7 +91,6 @@ from .auth import Authenticator, PAMAuthenticator from .crypto import CryptKeeper from .spawner import Spawner, LocalProcessSpawner from .objects import Hub, Server -from .events import EventLog # For faking stats from .emptyclass import EmptyClass @@ -2072,7 +2073,7 @@ class JupyterHub(Application): internal_ssl_ca=self.internal_ssl_ca, trusted_alt_names=self.trusted_alt_names, shutdown_on_logout=self.shutdown_on_logout, - event_log=self.event_log + eventlog=self.eventlog ) # allow configured settings to have priority settings.update(self.tornado_settings) @@ -2148,11 +2149,10 @@ class JupyterHub(Application): _log_cls("Authenticator", self.authenticator_class) _log_cls("Spawner", self.spawner_class) - self.event_log = EventLog(parent=self) + self.eventlog = EventLog(parent=self) - for schema_file in glob(os.path.join(here, 'event-schemas','*.json')): - with open(schema_file) as f: - self.event_log.register_schema(json.load(f)) + for schema_file in glob(os.path.join(here, 'event-schemas', '*.json')): + self.eventlog.register_schema_file(schema_file) self.init_pycurl() self.init_secrets() diff --git a/jupyterhub/events.py b/jupyterhub/events.py deleted file mode 100644 index 0f4db756..00000000 --- a/jupyterhub/events.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -Emit structured, discrete events when various actions happen. -""" -from traitlets.config import Configurable - -import logging -from datetime import datetime -import jsonschema -from pythonjsonlogger import jsonlogger -from traitlets import TraitType, List -import json -import six - - -class Callable(TraitType): - """ - A trait which is callable. - - Classes are callable, as are instances - with a __call__() method. - """ - info_text = 'a callable' - def validate(self, obj, value): - if six.callable(value): - return value - else: - self.error(obj, value) - -def _skip_message(record, **kwargs): - """ - Remove 'message' from log record. - - It is always emitted with 'null', and we do not want it, - since we are always emitting events only - """ - del record['message'] - return json.dumps(record, **kwargs) - - -class EventLog(Configurable): - """ - Send structured events to a logging sink - """ - handlers_maker = Callable( - None, - config=True, - allow_none=True, - help=""" - Callable that returns a list of logging.Handler instances to send events to. - - When set to None (the default), events are discarded. - """ - ) - - allowed_schemas = List( - [], - config=True, - help=""" - Fully qualified names of schemas to record. - - Each schema you want to record must be manually specified. - The default, an empty list, means no events are recorded. - """ - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.log = logging.getLogger(__name__) - # We don't want events to show up in the default logs - self.log.propagate = False - self.log.setLevel(logging.INFO) - - if self.handlers_maker: - self.handlers = self.handlers_maker(self) - formatter = jsonlogger.JsonFormatter(json_serializer=_skip_message) - for handler in self.handlers: - handler.setFormatter(formatter) - self.log.addHandler(handler) - - self.schemas = {} - - def register_schema(self, schema): - """ - Register a given JSON Schema with this event emitter - - 'version' and '$id' are required fields. - """ - # Check if our schema itself is valid - # This throws an exception if it isn't valid - jsonschema.validators.validator_for(schema).check_schema(schema) - - # Check that the properties we require are present - required_schema_fields = {'$id', 'version'} - for rsf in required_schema_fields: - if rsf not in schema: - raise ValueError( - '{} is required in schema specification'.format(rsf) - ) - - # Make sure reserved, auto-added fields are not in schema - if any([p.startswith('__') for p in schema['properties']]): - raise ValueError( - 'Schema {} has properties beginning with __, which is not allowed' - ) - - self.schemas[(schema['$id'], schema['version'])] = schema - - def emit(self, schema_name, version, event): - """ - Emit event with given schema / version in a capsule. - """ - if not (self.handlers_maker and schema_name in self.allowed_schemas): - # if handler isn't set up or schema is not explicitly whitelisted, - # don't do anything - return - - if (schema_name, version) not in self.schemas: - raise ValueError('Schema {schema_name} version {version} not registered'.format( - schema_name=schema_name, version=version - )) - schema = self.schemas[(schema_name, version)] - jsonschema.validate(event, schema) - - capsule = { - '__timestamp__': datetime.utcnow().isoformat() + 'Z', - '__schema__': schema_name, - '__version__': version - } - capsule.update(event) - self.log.info(capsule) \ No newline at end of file diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 19dfd467..b5f37cc8 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -157,8 +157,8 @@ class BaseHandler(RequestHandler): return self.settings['oauth_provider'] @property - def event_log(self): - return self.settings['event_log'] + def eventlog(self): + return self.settings['eventlog'] def finish(self, *args, **kwargs): """Roll back any uncommitted transactions from the handler.""" @@ -850,7 +850,7 @@ class BaseHandler(RequestHandler): SERVER_SPAWN_DURATION_SECONDS.labels( status=ServerSpawnStatus.success ).observe(time.perf_counter() - spawn_start_time) - self.event_log.emit('hub.jupyter.org/server-action', 1, { + self.eventlog.record_event('hub.jupyter.org/server-action', 1, { 'action': 'start', 'username': user.name, 'servername': server_name @@ -1035,7 +1035,7 @@ class BaseHandler(RequestHandler): SERVER_STOP_DURATION_SECONDS.labels( status=ServerStopStatus.success ).observe(toc - tic) - self.event_log.emit('hub.jupyter.org/server-action', 1, { + self.eventlog.record_event('hub.jupyter.org/server-action', 1, { 'action': 'stop', 'username': user.name, 'servername': server_name From aea2eefa778386b1837b3c10fab1a442a0510ac5 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Tue, 9 Jul 2019 16:50:20 -0700 Subject: [PATCH 090/541] Add lots of documentation to event schema Move it to YAML, since jupyter_telemetry supports these natively. --- jupyterhub/app.py | 7 ++- jupyterhub/event-schemas/server-actions.json | 24 -------- .../event-schemas/server-actions/v1.yaml | 59 +++++++++++++++++++ 3 files changed, 64 insertions(+), 26 deletions(-) delete mode 100644 jupyterhub/event-schemas/server-actions.json create mode 100644 jupyterhub/event-schemas/server-actions/v1.yaml diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 1353a3b7..d34740f0 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2151,8 +2151,11 @@ class JupyterHub(Application): self.eventlog = EventLog(parent=self) - for schema_file in glob(os.path.join(here, 'event-schemas', '*.json')): - self.eventlog.register_schema_file(schema_file) + for dirname, _, files in os.walk(os.path.join(here, 'event-schemas')): + for file in files: + if not file.endswith('.yaml'): + continue + self.eventlog.register_schema_file(os.path.join(dirname, file)) self.init_pycurl() self.init_secrets() diff --git a/jupyterhub/event-schemas/server-actions.json b/jupyterhub/event-schemas/server-actions.json deleted file mode 100644 index 0b390aa4..00000000 --- a/jupyterhub/event-schemas/server-actions.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$id": "hub.jupyter.org/server-action", - "version": 1, - "title": "JupyterHub server events", - "description": "JupyterHub emits this event when a user's server starts or stops", - "type": "object", - "properties": { - "action": { - "enum": [ - "start", - "stop" - ], - "description": "Action taken on this user's server" - }, - "username": { - "type": "string", - "description": "Name of the user whose server this action applies to" - }, - "servername": { - "type": "string", - "description": "Name of the server this action applies to" - } - } -} \ No newline at end of file diff --git a/jupyterhub/event-schemas/server-actions/v1.yaml b/jupyterhub/event-schemas/server-actions/v1.yaml new file mode 100644 index 00000000..35ffad67 --- /dev/null +++ b/jupyterhub/event-schemas/server-actions/v1.yaml @@ -0,0 +1,59 @@ +"$id": hub.jupyter.org/server-action +version: 1 +title: JupyterHub server events +description: | + Record actions on user servers made via JupyterHub. + + JupyterHub can perform various actions on user servers via + direct interaction from users, or via the API. This event is + recorded whenever either of those happen. + + Limitations: + + 1. This does not record all server starts / stops, only those + explicitly performed by JupyterHub. For example, a user's server + can go down because the node it was running on dies. That will + not cause an event to be recorded, since it was not initiated + by JupyterHub. In practice this happens often, so this is not + a complete record. + 2. Events are only recorded when an action succeeds. +type: object +required: +- action +- username +- servername +properties: + action: + enum: + - start + - stop + description: | + Action performed by JupyterHub. + + This is a required field. + + Possibl Values: + + 1. start + A user's server was successfully started + + 2. stop + A user's server was successfully stopped + username: + type: string + description: | + Name of the user whose server this action was performed on. + + This is the normalized name used by JupyterHub itself, + which is derived from the authentication provider used but + might not be the same as used in the authentication provider. + servername: + type: string + description: | + Name of the server this action was performed on. + + JupyterHub supports each user having multiple servers with + arbitrary names, and this field specifies the name of the + server. + + The 'default' server is denoted by the empty string From 2b1bfa0ba7eb82fbd3d51f17ffa0f0ee963fbd63 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Tue, 9 Jul 2019 16:51:17 -0700 Subject: [PATCH 091/541] Depend on the jupyter_telemetry package --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3b49dc97..3ae33109 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,4 @@ requests SQLAlchemy>=1.1 tornado>=5.0 traitlets>=4.3.2 -jsonschema -python-json-logger +jupyter_telemetry From c34bcabcb970ce0f3328a453e8b98d5b72a44bfa Mon Sep 17 00:00:00 2001 From: Zsailer Date: Wed, 21 Aug 2019 11:00:37 -0700 Subject: [PATCH 092/541] add docs for event-logging --- docs/requirements.txt | 1 + docs/source/conf.py | 1 + docs/source/events/index.rst | 50 +++++++++++++++++++++++++++ docs/source/events/server-actions.rst | 1 + docs/source/index.rst | 8 +++++ 5 files changed, 61 insertions(+) create mode 100644 docs/source/events/index.rst create mode 100644 docs/source/events/server-actions.rst diff --git a/docs/requirements.txt b/docs/requirements.txt index 5f8b447a..0ac4a7ce 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -5,3 +5,4 @@ alabaster_jupyterhub recommonmark==0.5.0 sphinx-copybutton sphinx>=1.7 +sphinx-jsonschema \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 6e83f379..0e84f38c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,6 +19,7 @@ extensions = [ 'sphinx.ext.napoleon', 'autodoc_traits', 'sphinx_copybutton', + 'sphinx-jsonschema' ] templates_path = ['_templates'] diff --git a/docs/source/events/index.rst b/docs/source/events/index.rst new file mode 100644 index 00000000..4ecf1ebd --- /dev/null +++ b/docs/source/events/index.rst @@ -0,0 +1,50 @@ +Eventlogging and Telemetry +========================== + +JupyterHub can be configured to record structured events from a running server using Jupyter's `Telemetry System`_. The types of events that JupyterHub emits are defined by `JSON schemas`_ listed below_ + + emitted as JSON data, defined and validated by the JSON schemas listed below. + + +.. _logging: https://docs.python.org/3/library/logging.html +.. _`Telemetry System`: https://github.com/jupyter/telemetry +.. _`JSON schemas`: https://json-schema.org/ + +How to emit events +------------------ + +Eventlogging is handled by its ``Eventlog`` object. This leverages Python's standing logging_ library to emit, filter, and collect event data. + + +To begin recording events, you'll need to set two configurations: + + 1. ``handlers``: tells the EventLog *where* to route your events. This trait is a list of Python logging handlers that route events to + 2. ``allows_schemas``: tells the EventLog *which* events should be recorded. No events are emitted by default; all recorded events must be listed here. + +Here's a basic example: + +.. code-block:: + + import logging + + c.EventLog.handlers = [ + logging.FileHandler('event.log'), + ] + + c.EventLog.allowed_schemas = [ + 'hub.jupyter.org/server-action' + ] + +The output is a file, ``"event.log"``, with events recorded as JSON data. + + + +.. _below: + +Event schemas +------------- + +.. toctree:: + :maxdepth: 2 + + server-actions.rst \ No newline at end of file diff --git a/docs/source/events/server-actions.rst b/docs/source/events/server-actions.rst new file mode 100644 index 00000000..7db66a27 --- /dev/null +++ b/docs/source/events/server-actions.rst @@ -0,0 +1 @@ +.. jsonschema:: ../../../jupyterhub/event-schemas/server-actions/v1.yaml \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index db23641a..274905f5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -134,6 +134,14 @@ helps keep our community welcoming to as many people as possible. contributing/roadmap contributing/security +Eventlogging and Telemetry +-------------------------- + +.. toctree:: + :maxdepth: 1 + + events/index + Upgrading JupyterHub -------------------- From 439e4381f06f2cda69afd09992d9b04410fc4947 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Thu, 22 Aug 2019 10:51:47 -0700 Subject: [PATCH 093/541] add tests for eventlog --- jupyterhub/app.py | 20 ++++---- jupyterhub/tests/test_eventlog.py | 79 +++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 jupyterhub/tests/test_eventlog.py diff --git a/jupyterhub/app.py b/jupyterhub/app.py index d34740f0..1451cc64 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2099,6 +2099,17 @@ class JupyterHub(Application): e, ) + def init_eventlog(self): + """Set up the event logging system.""" + self.eventlog = EventLog(parent=self) + + for dirname, _, files in os.walk(os.path.join(here, 'event-schemas')): + for file in files: + if not file.endswith('.yaml'): + continue + self.eventlog.register_schema_file(os.path.join(dirname, file)) + + def write_pid_file(self): pid = os.getpid() if self.pid_file: @@ -2149,14 +2160,7 @@ class JupyterHub(Application): _log_cls("Authenticator", self.authenticator_class) _log_cls("Spawner", self.spawner_class) - self.eventlog = EventLog(parent=self) - - for dirname, _, files in os.walk(os.path.join(here, 'event-schemas')): - for file in files: - if not file.endswith('.yaml'): - continue - self.eventlog.register_schema_file(os.path.join(dirname, file)) - + self.init_eventlog() self.init_pycurl() self.init_secrets() self.init_internal_ssl() diff --git a/jupyterhub/tests/test_eventlog.py b/jupyterhub/tests/test_eventlog.py new file mode 100644 index 00000000..1bb7cf68 --- /dev/null +++ b/jupyterhub/tests/test_eventlog.py @@ -0,0 +1,79 @@ +"""Tests for Eventlogging in JupyterHub. + +To test a new schema or event, simply add it to the +`valid_events` and `invalid_events` variables below. + +You *shouldn't* need to write new tests. +""" +import io +import json +import logging +import jsonschema +import pytest +from .mocking import MockHub +from traitlets.config import Config + + +# To test new schemas, add them to the `valid_events` +# and `invalid_events` dictionary below. + +# To test valid events, add event item with the form: +# { ( '', ) : { } } +valid_events = [ + ('hub.jupyter.org/server-action', 1, dict(action='start', username='test-username', servername='test-servername')), +] + +# To test invalid events, add event item with the form: +# { ( '', ) : { } } +invalid_events = [ + # Missing required keys + ('hub.jupyter.org/server-action', 1, dict(action='start')), +] + + +@pytest.fixture +def get_hub_and_sink(): + """Get a hub instance with all registered schemas and record an event with it.""" + # Get a mockhub. + hub = MockHub.instance() + sink = io.StringIO() + handler = logging.StreamHandler(sink) + + def _record_from_hub(schema): + # Update the hub config with handler info. + cfg = Config() + cfg.EventLog.handlers = [handler] + cfg.EventLog.allowed_schemas = [schema] + + # Get hub app. + hub.update_config(cfg) + hub.init_eventlog() + + # Record an event from the hub. + return hub, sink + + yield _record_from_hub + + # teardown + hub.clear_instance() + handler.flush() + + +@pytest.mark.parametrize('schema, version, event', valid_events) +def test_valid_events(get_hub_and_sink, schema, version, event): + hub, sink = get_hub_and_sink(schema) + # Record event. + hub.eventlog.record_event(schema, version, event) + # Inspect consumed event. + output = sink.getvalue() + x = json.loads(output) + + assert x is not None + +@pytest.mark.parametrize('schema, version, event', invalid_events) +def test_invalid_events(get_hub_and_sink, schema, version, event): + hub, sink = get_hub_and_sink(schema) + + # Make sure an error is thrown when bad events are recorded. + with pytest.raises(jsonschema.ValidationError): + recorded_event = hub.eventlog.record_event(schema, version, event) From 263c5e838e25b25bd4c10c4eb55fa5ab1c8d8d3f Mon Sep 17 00:00:00 2001 From: Zsailer Date: Thu, 22 Aug 2019 11:11:27 -0700 Subject: [PATCH 094/541] rename test fixture --- jupyterhub/tests/test_eventlog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterhub/tests/test_eventlog.py b/jupyterhub/tests/test_eventlog.py index 1bb7cf68..99591b7b 100644 --- a/jupyterhub/tests/test_eventlog.py +++ b/jupyterhub/tests/test_eventlog.py @@ -39,7 +39,7 @@ def get_hub_and_sink(): sink = io.StringIO() handler = logging.StreamHandler(sink) - def _record_from_hub(schema): + def _get_hub_and_sink(schema): # Update the hub config with handler info. cfg = Config() cfg.EventLog.handlers = [handler] @@ -52,7 +52,7 @@ def get_hub_and_sink(): # Record an event from the hub. return hub, sink - yield _record_from_hub + yield _get_hub_and_sink # teardown hub.clear_instance() From c9d52bea43ea9b6d90336ae7961402c512615f62 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Thu, 22 Aug 2019 12:19:46 -0700 Subject: [PATCH 095/541] verify test data was emitted --- jupyterhub/tests/test_eventlog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jupyterhub/tests/test_eventlog.py b/jupyterhub/tests/test_eventlog.py index 99591b7b..4b3d3fe6 100644 --- a/jupyterhub/tests/test_eventlog.py +++ b/jupyterhub/tests/test_eventlog.py @@ -66,9 +66,9 @@ def test_valid_events(get_hub_and_sink, schema, version, event): hub.eventlog.record_event(schema, version, event) # Inspect consumed event. output = sink.getvalue() - x = json.loads(output) - - assert x is not None + data = json.loads(output) + # Verify event data was recorded + assert data is not None @pytest.mark.parametrize('schema, version, event', invalid_events) def test_invalid_events(get_hub_and_sink, schema, version, event): From 572e008f1dfda93911d076b9a0adad4655738d9d Mon Sep 17 00:00:00 2001 From: Roman Lukin Date: Fri, 23 Aug 2019 16:09:38 +0300 Subject: [PATCH 096/541] Fix mistypos --- docs/source/changelog.md | 2 +- onbuild/Dockerfile | 2 +- onbuild/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/changelog.md b/docs/source/changelog.md index 4cd14dbd..5a5b6112 100644 --- a/docs/source/changelog.md +++ b/docs/source/changelog.md @@ -283,7 +283,7 @@ and tornado < 5.0. coroutines, and CPU/memory/FD consumption. - Add async `Spawner.get_options_form` alternative to `.options_form`, so it can be a coroutine. - Add `JupyterHub.redirect_to_server` config to govern whether - users should be sent to their server on login or the JuptyerHub home page. + users should be sent to their server on login or the JupyterHub home page. - html page templates can be more easily customized and extended. - Allow registering external OAuth clients for using the Hub as an OAuth provider. - Add basic prometheus metrics at `/hub/metrics` endpoint. diff --git a/onbuild/Dockerfile b/onbuild/Dockerfile index ad941aac..1e1bf8f4 100644 --- a/onbuild/Dockerfile +++ b/onbuild/Dockerfile @@ -1,6 +1,6 @@ # JupyterHub Dockerfile that loads your jupyterhub_config.py # -# Adds ONBUILD step to jupyter/jupyterhub to load your juptyerhub_config.py into the image +# Adds ONBUILD step to jupyter/jupyterhub to load your jupyterhub_config.py into the image # # Derivative images must have jupyterhub_config.py next to the Dockerfile. diff --git a/onbuild/README.md b/onbuild/README.md index 8964110d..fae5e4fb 100644 --- a/onbuild/README.md +++ b/onbuild/README.md @@ -2,7 +2,7 @@ If you base a Dockerfile on this image: - FROM juptyerhub/jupyterhub-onbuild:0.6 + FROM jupyterhub/jupyterhub-onbuild:0.6 ... then your `jupyterhub_config.py` adjacent to your Dockerfile will be loaded into the image and used by JupyterHub. From 7b1c4aedcf3be4d8445757ee54ab78a5e25ec14a Mon Sep 17 00:00:00 2001 From: Richard C Gerkin Date: Fri, 23 Aug 2019 08:19:32 -0700 Subject: [PATCH 097/541] Don't catch AttributeError --- jupyterhub/auth.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index 87ace62d..342abfac 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -1,4 +1,4 @@ -"""Base Authenticator class and the default PAM Authenticator""" + """Base Authenticator class and the default PAM Authenticator""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. import inspect @@ -775,8 +775,6 @@ class LocalAuthenticator(Authenticator): try: uid = self.uids[name] cmd += ['--uid', '%d' % uid] - except AttributeError: - pass except KeyError: self.log.debug("No UID for user %s" % name) cmd += [name] From a800496f6c1afe8d2a088d75892f52836a61593d Mon Sep 17 00:00:00 2001 From: Katsarov Date: Sat, 24 Aug 2019 11:58:37 +0200 Subject: [PATCH 098/541] create a warning when creating a service implicitly from service_tokens --- jupyterhub/app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index de788dc2..2f2c1d05 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -1673,6 +1673,12 @@ class JupyterHub(Application): raise ValueError("Token name %r is not in whitelist" % name) if not self.authenticator.validate_username(name): raise ValueError("Token name %r is not valid" % name) + if kind == 'service': + if not any(service["name"] == name for service in self.services): + self.log.warn( + "Warning: service '%s' not in services, creating implicitly. It is recommended to register services using services list." + % name + ) orm_token = orm.APIToken.find(db, token) if orm_token is None: obj = Class.find(db, name) From 2b6ad596d27a32e9e4d6885feed5630509367152 Mon Sep 17 00:00:00 2001 From: Aaron Huang Date: Wed, 22 May 2019 14:01:01 +0800 Subject: [PATCH 099/541] Remove user after login cookie cleared Signed-off-by: Aaron Huang --- jupyterhub/handlers/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 19504c9c..8eebae94 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -484,6 +484,8 @@ class BaseHandler(RequestHandler): path=url_path_join(self.base_url, 'services'), **kwargs ) + # Reset _jupyterhub_user + self._jupyterhub_user = None def _set_cookie(self, key, value, encrypted=True, **overrides): """Setting any cookie should go through here From 9db18439af8c0f3caf7567ba33cf3b000939dfab Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 28 Aug 2019 19:01:42 +0200 Subject: [PATCH 100/541] Add JupyterHub.init_spawners_timeout If init_spawners takes too long (default: 10 seconds) to complete, app start will be allowed to continue while finishing in the background. Adds new `check` pending state for the initial check. Checking lots of spawners can take a long time, so allowing this to be async limits the impact on startup time at the expense of starting the Hub in a not-quite-fully-ready state. --- jupyterhub/app.py | 98 +++++++++++++++++++++++++++++++++++++++++-- jupyterhub/spawner.py | 3 ++ jupyterhub/user.py | 1 + 3 files changed, 98 insertions(+), 4 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index de788dc2..6d47d1f8 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -11,8 +11,10 @@ import re import signal import socket import sys +import time from concurrent.futures import ThreadPoolExecutor from datetime import datetime +from datetime import timedelta from datetime import timezone from functools import partial from getpass import getuser @@ -984,6 +986,28 @@ class JupyterHub(Application): """, ).tag(config=True) + init_spawners_timeout = Integer( + 10, + help=""" + Timeout (in seconds) to wait for spawners to initialize + + Checking if spawners are healthy can take a long time + if many spawners are active at hub start time. + + If it takes longer than this timeout to check, + init_spawner will be left to complete in the background + and the http server is allowed to start. + + A timeout of -1 means wait forever, + which can mean a slow startup of the Hub + but ensures that the Hub is fully consistent by the time it starts responding to requests. + This matches the behavior of jupyterhub 1.0. + + .. versionadded: 1.1.0 + + """, + ).tag(config=True) + db_url = Unicode( 'sqlite:///jupyterhub.sqlite', help="url for the database. e.g. `sqlite:///jupyterhub.sqlite`", @@ -1829,6 +1853,7 @@ class JupyterHub(Application): ) async def init_spawners(self): + self.log.debug("Initializing spawners") db = self.db def _user_summary(user): @@ -1919,6 +1944,8 @@ class JupyterHub(Application): else: self.log.debug("%s not running", spawner._log_name) + spawner._check_pending = False + # parallelize checks for running Spawners check_futures = [] for orm_user in db.query(orm.User): @@ -1929,11 +1956,22 @@ class JupyterHub(Application): # spawner should be running # instantiate Spawner wrapper and check if it's still alive spawner = user.spawners[name] + # signal that check is pending to avoid race conditions + spawner._check_pending = True f = asyncio.ensure_future(check_spawner(user, name, spawner)) check_futures.append(f) + TOTAL_USERS.set(len(self.users)) + + # it's important that we get here before the first await + # so that we know all spawners are instantiated and in the check-pending state + # await checks after submitting them all - await gen.multi(check_futures) + if check_futures: + self.log.debug( + "Awaiting checks for %i possibly-running spawners", len(check_futures) + ) + await gen.multi(check_futures) db.commit() # only perform this query if we are going to log it @@ -1943,7 +1981,7 @@ class JupyterHub(Application): active_counts = self.users.count_active_users() RUNNING_SERVERS.set(active_counts['active']) - TOTAL_USERS.set(len(self.users)) + return len(check_futures) def init_oauth(self): base_url = self.hub.base_url @@ -2106,6 +2144,7 @@ class JupyterHub(Application): super().initialize(*args, **kwargs) if self.generate_config or self.generate_certs or self.subapp: return + self._start_future = asyncio.Future() self.load_config_file(self.config_file) self.init_logging() if 'JupyterHubApp' in self.config: @@ -2156,11 +2195,61 @@ class JupyterHub(Application): self.init_services() await self.init_api_tokens() self.init_tornado_settings() - await self.init_spawners() - self.cleanup_oauth_clients() self.init_handlers() self.init_tornado_application() + # init_spawners can take a while + init_spawners_timeout = self.init_spawners_timeout + if init_spawners_timeout < 0: + # negative timeout means forever (previous, most stable behavior) + init_spawners_timeout = 86400 + print(init_spawners_timeout) + + init_start_time = time.perf_counter() + init_spawners_future = asyncio.ensure_future(self.init_spawners()) + + def log_init_time(f): + n_spawners = f.result() + self.log.info( + "Initialized %i spawners in %.3f seconds", + n_spawners, + time.perf_counter() - init_start_time, + ) + + init_spawners_future.add_done_callback(log_init_time) + + try: + + # don't allow a zero timeout because we still need to be sure + # that the Spawner objects are defined and pending + await gen.with_timeout( + timedelta(seconds=max(init_spawners_timeout, 1)), init_spawners_future + ) + except gen.TimeoutError: + self.log.warning( + "init_spawners did not complete within %i seconds. " + "Allowing to complete in the background.", + self.init_spawners_timeout, + ) + + if init_spawners_future.done(): + self.cleanup_oauth_clients() + else: + # schedule async operations after init_spawners finishes + async def finish_init_spawners(): + await init_spawners_future + # schedule cleanup after spawners are all set up + # because it relies on the state resolved by init_spawners + self.cleanup_oauth_clients() + # trigger a proxy check as soon as all spawners are ready + # because this may be *after* the check made as part of normal startup. + # To avoid races with partially-complete start, + # ensure that start is complete before running this check. + await self._start_future + await self.proxy.check_routes(self.users, self._service_map) + + asyncio.ensure_future(finish_init_spawners()) + async def cleanup(self): """Shutdown managed services and various subprocesses. Cleanup runtime files.""" @@ -2446,6 +2535,7 @@ class JupyterHub(Application): atexit.register(self.atexit) # register cleanup on both TERM and INT self.init_signal() + self._start_future.set_result(None) def init_signal(self): loop = asyncio.get_event_loop() diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index af84c122..5bcbfa54 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -86,6 +86,7 @@ class Spawner(LoggingConfigurable): _start_pending = False _stop_pending = False _proxy_pending = False + _check_pending = False _waiting_for_response = False _jupyterhub_version = None _spawn_future = None @@ -121,6 +122,8 @@ class Spawner(LoggingConfigurable): return 'spawn' elif self._stop_pending: return 'stop' + elif self._check_pending: + return 'check' return None @property diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 97a3543f..4b7ca622 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -725,6 +725,7 @@ class User: spawner = self.spawners[server_name] spawner._spawn_pending = False spawner._start_pending = False + spawner._check_pending = False spawner.stop_polling() spawner._stop_pending = True From 74958d93976eb18bf333d1b7c6d20d8670c7acac Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 28 Aug 2019 19:02:58 +0200 Subject: [PATCH 101/541] catch some CancelledErrors which can occur during app shutdown --- jupyterhub/apihandlers/users.py | 13 ++++++++----- jupyterhub/handlers/base.py | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 1c633723..7231ea2d 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -589,11 +589,14 @@ class SpawnProgressAPIHandler(APIHandler): async with aclosing( iterate_until(spawn_future, spawner._generate_progress()) ) as events: - async for event in events: - # don't allow events to sneakily set the 'ready' flag - if 'ready' in event: - event.pop('ready', None) - await self.send_event(event) + try: + async for event in events: + # don't allow events to sneakily set the 'ready' flag + if 'ready' in event: + event.pop('ready', None) + await self.send_event(event) + except asyncio.CancelledError: + pass # progress finished, wait for spawn to actually resolve, # in case progress finished early diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 19504c9c..2b2d18e3 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -877,7 +877,7 @@ class BaseHandler(RequestHandler): # clear spawner._spawn_future when it's done # keep an exception around, though, to prevent repeated implicit spawns # if spawn is failing - if f.exception() is None: + if f.cancelled() or f.exception() is None: spawner._spawn_future = None # Now we're all done. clear _spawn_pending flag spawner._spawn_pending = False @@ -888,7 +888,7 @@ class BaseHandler(RequestHandler): # update failure count and abort if consecutive failure limit # is reached def _track_failure_count(f): - if f.exception() is None: + if f.cancelled() or f.exception() is None: # spawn succeeded, reset failure count self.settings['failure_count'] = 0 return From 096b159c23a81dac37c73de9e373dadf9e59d78d Mon Sep 17 00:00:00 2001 From: Chico Venancio Date: Fri, 30 Aug 2019 13:00:56 -0300 Subject: [PATCH 102/541] ORM: allow MySQL variables to not exist In current versions of MySQL and MariaDB `innodb_file_format` and `innodb_large_prefix` have been removed. This allows them to not exist and makes sure the format for the rows are `Dynamic` (default for current versions). --- jupyterhub/orm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index 471dd4e2..3198a046 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -770,8 +770,8 @@ def mysql_large_prefix_check(engine): ).fetchall() ) if ( - variables['innodb_file_format'] == 'Barracuda' - and variables['innodb_large_prefix'] == 'ON' + variables.get('innodb_file_format', 'Barracuda') == 'Barracuda' + and variables.get('innodb_large_prefix', 'ON') == 'ON' ): return True else: From 2545cd9bb39fbee5fad55548ea2c7654facf69a9 Mon Sep 17 00:00:00 2001 From: Kenan Erdogan Date: Mon, 2 Sep 2019 19:04:59 +0200 Subject: [PATCH 103/541] change redirecting to relative to home page in js --- share/jupyterhub/static/js/home.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/jupyterhub/static/js/home.js b/share/jupyterhub/static/js/home.js index e81b6690..e893e23c 100644 --- a/share/jupyterhub/static/js/home.js +++ b/share/jupyterhub/static/js/home.js @@ -103,7 +103,7 @@ require(["jquery", "moment", "jhapi", "utils"], function( $(".new-server-btn").click(function() { var row = getRow($(this)); var serverName = row.find(".new-server-name").val(); - window.location.href = "../spawn/" + user + "/" + serverName; + window.location.href = "./spawn/" + user + "/" + serverName; }); $(".stop-server").click(stopServer); From 6d696758e43ed8d236d8c705b34b7243d2f620a2 Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Fri, 6 Sep 2019 17:41:34 +0200 Subject: [PATCH 104/541] use autodoc-traits extension for docbuild --- docs/environment.yml | 1 + docs/requirements.txt | 1 + docs/source/conf.py | 3 +-- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/environment.yml b/docs/environment.yml index 2dbfd535..faa2f1ca 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -24,3 +24,4 @@ dependencies: - attrs>=17.4.0 - sphinx-copybutton - alabaster_jupyterhub + - autodoc-traits diff --git a/docs/requirements.txt b/docs/requirements.txt index 5f8b447a..44c782f0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,6 +2,7 @@ # if you change this file -r ../requirements.txt alabaster_jupyterhub +autodoc-traits recommonmark==0.5.0 sphinx-copybutton sphinx>=1.7 diff --git a/docs/source/conf.py b/docs/source/conf.py index 6e83f379..45411969 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,7 +17,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon', - 'autodoc_traits', + 'autodoc-traits', 'sphinx_copybutton', ] @@ -37,7 +37,6 @@ from os.path import dirname docs = dirname(dirname(__file__)) root = dirname(docs) sys.path.insert(0, root) -sys.path.insert(0, os.path.join(docs, 'sphinxext')) import jupyterhub From 36a1ad0078a9f03d1c9655f85189fb2d25c92409 Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Fri, 6 Sep 2019 17:42:32 +0200 Subject: [PATCH 105/541] remove sphinxext directory --- docs/sphinxext/autodoc_traits.py | 57 -------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 docs/sphinxext/autodoc_traits.py diff --git a/docs/sphinxext/autodoc_traits.py b/docs/sphinxext/autodoc_traits.py deleted file mode 100644 index 3d54f8bb..00000000 --- a/docs/sphinxext/autodoc_traits.py +++ /dev/null @@ -1,57 +0,0 @@ -"""autodoc extension for configurable traits""" -from sphinx.domains.python import PyClassmember -from sphinx.ext.autodoc import AttributeDocumenter -from sphinx.ext.autodoc import ClassDocumenter -from traitlets import TraitType -from traitlets import Undefined - - -class ConfigurableDocumenter(ClassDocumenter): - """Specialized Documenter subclass for traits with config=True""" - - objtype = 'configurable' - directivetype = 'class' - - def get_object_members(self, want_all): - """Add traits with .tag(config=True) to members list""" - check, members = super().get_object_members(want_all) - get_traits = ( - self.object.class_own_traits - if self.options.inherited_members - else self.object.class_traits - ) - trait_members = [] - for name, trait in sorted(get_traits(config=True).items()): - # put help in __doc__ where autodoc will look for it - trait.__doc__ = trait.help - trait_members.append((name, trait)) - return check, trait_members + members - - -class TraitDocumenter(AttributeDocumenter): - objtype = 'trait' - directivetype = 'attribute' - member_order = 1 - priority = 100 - - @classmethod - def can_document_member(cls, member, membername, isattr, parent): - return isinstance(member, TraitType) - - def add_directive_header(self, sig): - default = self.object.get_default_value() - if default is Undefined: - default_s = '' - else: - default_s = repr(default) - self.options.annotation = 'c.{name} = {trait}({default})'.format( - name=self.format_name(), - trait=self.object.__class__.__name__, - default=default_s, - ) - super().add_directive_header(sig) - - -def setup(app): - app.add_autodocumenter(ConfigurableDocumenter) - app.add_autodocumenter(TraitDocumenter) From a239a25ae0499c49021e3e4cc368b8fd6f5c20c3 Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Sat, 7 Sep 2019 02:19:19 +0200 Subject: [PATCH 106/541] fix case --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 45411969..5f8d3632 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,7 +17,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon', - 'autodoc-traits', + 'autodoc_traits', 'sphinx_copybutton', ] From b9bdc99c1d0632ccdbf434aae019d87659408443 Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Sun, 8 Sep 2019 12:14:59 +0200 Subject: [PATCH 107/541] move pull request template --- .github/PULL_REQUEST_TEMPLATE/.keep | 0 .../PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE/.keep rename PULL_REQUEST_TEMPLATE.md => .github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE/.keep b/.github/PULL_REQUEST_TEMPLATE/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md From 444029699a5c785edf4edc0577e867cfc2615f25 Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Sun, 8 Sep 2019 12:30:44 +0200 Subject: [PATCH 108/541] update the issue template --- .../installation-and-configuration-issues.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md b/.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md index 9a6c731b..57db1984 100644 --- a/.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md +++ b/.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md @@ -4,4 +4,19 @@ about: Installation and configuration assistance --- -If you are having issues with installation or configuration, you may ask for help on the JupyterHub gitter channel or file an issue here. + From 0bcd6adde6a65eccc567a278f6ef9feb79030e69 Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Sun, 8 Sep 2019 12:39:11 +0200 Subject: [PATCH 109/541] Edit bug report --- .github/ISSUE_TEMPLATE/bug_report.md | 36 ++++++++++--------- .../installation-and-configuration-issues.md | 2 +- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 476d8bb6..f5540a34 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,37 +1,39 @@ --- -name: Bug report +name: Issue report about: Create a report to help us improve --- + **Describe the bug** A clear and concise description of what the bug is. + + **To Reproduce** -Steps to reproduce the behavior: + **Expected behavior** + -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. - -- Running `jupyter troubleshoot` from the command line, if possible, and posting -its output would also be helpful. -- Running in `--debug` mode can also be helpful for troubleshooting. +**Compute Information** + - Operating System + - JupyterHub Version [e.g. 22] diff --git a/.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md b/.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md index 57db1984..2505d1d8 100644 --- a/.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md +++ b/.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md @@ -1,5 +1,5 @@ --- -name: Installation and configuration issues +name: Installation and configuration questions about: Installation and configuration assistance --- From e07aaa603ae098132eb9996b55c0aa659277547f Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Sun, 8 Sep 2019 15:59:58 +0200 Subject: [PATCH 110/541] fix typo found by @blink1073 review --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f5540a34..dde161d9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -12,7 +12,7 @@ If you are reporting an issue with JupyterHub, please use the GitHub search feat Some tips: - Running `jupyter troubleshoot` from the command line, if possible, and posting its output would also be helpful. -- Running JupyterHub in `--debug` mode (`jupyterhub --debug` can also be helpful for troubleshooting. +- Running JupyterHub in `--debug` mode (`jupyterhub --debug`) can also be helpful for troubleshooting. ---> **Describe the bug** From dcde9f6222b5e3f61cbb2254296bab77668f2d7a Mon Sep 17 00:00:00 2001 From: Ed Slavich Date: Wed, 18 Sep 2019 18:34:05 -0400 Subject: [PATCH 111/541] Remove duplicate hub and authenticator traitlets from Spawner --- jupyterhub/spawner.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index af84c122..6f3e4009 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -207,8 +207,6 @@ class Spawner(LoggingConfigurable): return self.orm_spawner.name return '' - hub = Any() - authenticator = Any() internal_ssl = Bool(False) internal_trust_bundles = Dict() internal_certs_location = Unicode('') From 7fcd6ad45038720a06977b9b76891fe9df70bf89 Mon Sep 17 00:00:00 2001 From: William Krinsman Date: Wed, 18 Sep 2019 12:07:48 -0700 Subject: [PATCH 112/541] Added configurable default server name attribute to better match behavior described for user-redirect in urls.md in the docs --- jupyterhub/app.py | 17 +++++++++++++++++ jupyterhub/user.py | 9 +++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 2f2c1d05..78898dcd 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -911,6 +911,22 @@ class JupyterHub(Application): """, ).tag(config=True) + default_server_name = Unicode( + "", + help="If named servers are enabled, default name of server to spawn or open, e.g. by user-redirect.", + ).tag(config=True) + # Ensure that default_server_name doesn't do anything if named servers aren't allowed + _default_server_name = Unicode( + help="Non-configurable version exposed to JupyterHub." + ) + + @default('_default_server_name') + def _set_default_server_name(self): + if self.allow_named_servers: + return self.default_server_name + else: + return "" + # class for spawning single-user servers spawner_class = EntryPointType( default_value=LocalProcessSpawner, @@ -2060,6 +2076,7 @@ class JupyterHub(Application): domain=self.domain, statsd=self.statsd, allow_named_servers=self.allow_named_servers, + default_server_name=self._default_server_name, named_server_limit_per_user=self.named_server_limit_per_user, oauth_provider=self.oauth_provider, concurrent_spawn_limit=self.concurrent_spawn_limit, diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 97a3543f..6a5ad1a9 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -389,9 +389,14 @@ class User: Full name.domain/path if using subdomains, otherwise just my /base/url """ if self.settings.get('subdomain_host'): - return '{host}{path}'.format(host=self.host, path=self.base_url) + url = '{host}{path}'.format(host=self.host, path=self.base_url) else: - return self.base_url + url = self.base_url + + if self.settings.get('default_server_name'): + return url_path_join(url, self.settings.get('default_server_name')) + else: + return url def server_url(self, server_name=''): """Get the url for a server with a given name""" From 7fd3271c9bd1f793a550989e5f4f4dfa0a2a6c01 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 19 Sep 2019 15:16:37 +0200 Subject: [PATCH 113/541] rely on app fixture to get configured app re-run init_eventlog to ensure event logging is hooked up --- jupyterhub/tests/test_eventlog.py | 78 +++++++++++++++---------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/jupyterhub/tests/test_eventlog.py b/jupyterhub/tests/test_eventlog.py index 4b3d3fe6..dc9cfda2 100644 --- a/jupyterhub/tests/test_eventlog.py +++ b/jupyterhub/tests/test_eventlog.py @@ -1,18 +1,21 @@ -"""Tests for Eventlogging in JupyterHub. +"""Tests for Eventlogging in JupyterHub. -To test a new schema or event, simply add it to the -`valid_events` and `invalid_events` variables below. +To test a new schema or event, simply add it to the +`valid_events` and `invalid_events` variables below. You *shouldn't* need to write new tests. """ import io import json import logging +from unittest import mock + import jsonschema import pytest -from .mocking import MockHub from traitlets.config import Config +from .mocking import MockHub + # To test new schemas, add them to the `valid_events` # and `invalid_events` dictionary below. @@ -20,60 +23,57 @@ from traitlets.config import Config # To test valid events, add event item with the form: # { ( '', ) : { } } valid_events = [ - ('hub.jupyter.org/server-action', 1, dict(action='start', username='test-username', servername='test-servername')), + ( + 'hub.jupyter.org/server-action', + 1, + dict(action='start', username='test-username', servername='test-servername'), + ) ] # To test invalid events, add event item with the form: # { ( '', ) : { } } invalid_events = [ # Missing required keys - ('hub.jupyter.org/server-action', 1, dict(action='start')), + ('hub.jupyter.org/server-action', 1, dict(action='start')) ] @pytest.fixture -def get_hub_and_sink(): - """Get a hub instance with all registered schemas and record an event with it.""" - # Get a mockhub. - hub = MockHub.instance() - sink = io.StringIO() +def eventlog_sink(app): + """Return eventlog and sink objects""" + sink = io.StringIO() handler = logging.StreamHandler(sink) + # Update the EventLog config with handler + cfg = Config() + cfg.EventLog.handlers = [handler] - def _get_hub_and_sink(schema): - # Update the hub config with handler info. - cfg = Config() - cfg.EventLog.handlers = [handler] - cfg.EventLog.allowed_schemas = [schema] - - # Get hub app. - hub.update_config(cfg) - hub.init_eventlog() - - # Record an event from the hub. - return hub, sink - - yield _get_hub_and_sink - - # teardown - hub.clear_instance() - handler.flush() + with mock.patch.object(app.config, 'EventLog', cfg.EventLog): + # recreate the eventlog object with our config + app.init_eventlog() + # return the sink from the fixture + yield app.eventlog, sink + # reset eventlog with original config + app.init_eventlog() @pytest.mark.parametrize('schema, version, event', valid_events) -def test_valid_events(get_hub_and_sink, schema, version, event): - hub, sink = get_hub_and_sink(schema) - # Record event. - hub.eventlog.record_event(schema, version, event) - # Inspect consumed event. +def test_valid_events(eventlog_sink, schema, version, event): + eventlog, sink = eventlog_sink + eventlog.allowed_schemas = [schema] + # Record event + eventlog.record_event(schema, version, event) + # Inspect consumed event output = sink.getvalue() data = json.loads(output) # Verify event data was recorded assert data is not None + @pytest.mark.parametrize('schema, version, event', invalid_events) -def test_invalid_events(get_hub_and_sink, schema, version, event): - hub, sink = get_hub_and_sink(schema) - - # Make sure an error is thrown when bad events are recorded. +def test_invalid_events(eventlog_sink, schema, version, event): + eventlog, sink = eventlog_sink + eventlog.allowed_schemas = [schema] + + # Make sure an error is thrown when bad events are recorded with pytest.raises(jsonschema.ValidationError): - recorded_event = hub.eventlog.record_event(schema, version, event) + recorded_event = eventlog.record_event(schema, version, event) From 949d8d0bfa7e565f2de808593c1fb32a5d86808b Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 19 Sep 2019 15:46:09 +0200 Subject: [PATCH 114/541] avoid disabling existing loggers when invoking alembic causes some weird behavior, such as event log not working --- jupyterhub/alembic/env.py | 2 +- jupyterhub/tests/test_eventlog.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/jupyterhub/alembic/env.py b/jupyterhub/alembic/env.py index 4846f4c1..584f82c1 100644 --- a/jupyterhub/alembic/env.py +++ b/jupyterhub/alembic/env.py @@ -28,7 +28,7 @@ if 'jupyterhub' in sys.modules: alembic_logger.propagate = True alembic_logger.parent = app.log else: - fileConfig(config.config_file_name) + fileConfig(config.config_file_name, disable_existing_loggers=False) else: fileConfig(config.config_file_name) diff --git a/jupyterhub/tests/test_eventlog.py b/jupyterhub/tests/test_eventlog.py index dc9cfda2..3cfbaec1 100644 --- a/jupyterhub/tests/test_eventlog.py +++ b/jupyterhub/tests/test_eventlog.py @@ -64,6 +64,7 @@ def test_valid_events(eventlog_sink, schema, version, event): eventlog.record_event(schema, version, event) # Inspect consumed event output = sink.getvalue() + assert output data = json.loads(output) # Verify event data was recorded assert data is not None From ac32ae496e6dfb645e35f3cf1ae2d0c77ec21edf Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 19 Sep 2019 15:51:02 +0200 Subject: [PATCH 115/541] run pre-commit hook --- docs/requirements.txt | 2 +- docs/source/conf.py | 2 +- docs/source/events/index.rst | 2 +- docs/source/events/server-actions.rst | 2 +- jupyterhub/app.py | 7 +++---- jupyterhub/handlers/base.py | 20 ++++++++++++-------- requirements.txt | 2 +- 7 files changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 0ac4a7ce..980376d5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,5 +4,5 @@ alabaster_jupyterhub recommonmark==0.5.0 sphinx-copybutton +sphinx-jsonschema sphinx>=1.7 -sphinx-jsonschema \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 0e84f38c..036cac22 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,7 @@ extensions = [ 'sphinx.ext.napoleon', 'autodoc_traits', 'sphinx_copybutton', - 'sphinx-jsonschema' + 'sphinx-jsonschema', ] templates_path = ['_templates'] diff --git a/docs/source/events/index.rst b/docs/source/events/index.rst index 4ecf1ebd..d662b8bf 100644 --- a/docs/source/events/index.rst +++ b/docs/source/events/index.rst @@ -47,4 +47,4 @@ Event schemas .. toctree:: :maxdepth: 2 - server-actions.rst \ No newline at end of file + server-actions.rst diff --git a/docs/source/events/server-actions.rst b/docs/source/events/server-actions.rst index 7db66a27..12018713 100644 --- a/docs/source/events/server-actions.rst +++ b/docs/source/events/server-actions.rst @@ -1 +1 @@ -.. jsonschema:: ../../../jupyterhub/event-schemas/server-actions/v1.yaml \ No newline at end of file +.. jsonschema:: ../../../jupyterhub/event-schemas/server-actions/v1.yaml diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 1451cc64..b7da6a4f 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -5,19 +5,19 @@ import asyncio import atexit import binascii +import json import logging import os import re import signal import socket import sys -import json -from glob import glob from concurrent.futures import ThreadPoolExecutor from datetime import datetime from datetime import timezone from functools import partial from getpass import getuser +from glob import glob from operator import itemgetter from textwrap import dedent from urllib.parse import unquote @@ -2073,7 +2073,7 @@ class JupyterHub(Application): internal_ssl_ca=self.internal_ssl_ca, trusted_alt_names=self.trusted_alt_names, shutdown_on_logout=self.shutdown_on_logout, - eventlog=self.eventlog + eventlog=self.eventlog, ) # allow configured settings to have priority settings.update(self.tornado_settings) @@ -2109,7 +2109,6 @@ class JupyterHub(Application): continue self.eventlog.register_schema_file(os.path.join(dirname, file)) - def write_pid_file(self): pid = os.getpid() if self.pid_file: diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index b5f37cc8..e5249cb5 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -850,11 +850,11 @@ class BaseHandler(RequestHandler): SERVER_SPAWN_DURATION_SECONDS.labels( status=ServerSpawnStatus.success ).observe(time.perf_counter() - spawn_start_time) - self.eventlog.record_event('hub.jupyter.org/server-action', 1, { - 'action': 'start', - 'username': user.name, - 'servername': server_name - }) + self.eventlog.record_event( + 'hub.jupyter.org/server-action', + 1, + {'action': 'start', 'username': user.name, 'servername': server_name}, + ) proxy_add_start_time = time.perf_counter() spawner._proxy_pending = True try: @@ -1035,11 +1035,15 @@ class BaseHandler(RequestHandler): SERVER_STOP_DURATION_SECONDS.labels( status=ServerStopStatus.success ).observe(toc - tic) - self.eventlog.record_event('hub.jupyter.org/server-action', 1, { + self.eventlog.record_event( + 'hub.jupyter.org/server-action', + 1, + { 'action': 'stop', 'username': user.name, - 'servername': server_name - }) + 'servername': server_name, + }, + ) except: SERVER_STOP_DURATION_SECONDS.labels( status=ServerStopStatus.failure diff --git a/requirements.txt b/requirements.txt index 3ae33109..7821b905 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ async_generator>=1.8 certipy>=0.1.2 entrypoints jinja2 +jupyter_telemetry oauthlib>=3.0 pamela prometheus_client>=0.0.21 @@ -11,4 +12,3 @@ requests SQLAlchemy>=1.1 tornado>=5.0 traitlets>=4.3.2 -jupyter_telemetry From f79495e6bfda54dbae554121458a520dda6033a6 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 19 Sep 2019 16:12:29 +0200 Subject: [PATCH 116/541] fix relative links for spawn --- share/jupyterhub/static/js/home.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/share/jupyterhub/static/js/home.js b/share/jupyterhub/static/js/home.js index be4e39ea..e10ed7c3 100644 --- a/share/jupyterhub/static/js/home.js +++ b/share/jupyterhub/static/js/home.js @@ -105,9 +105,9 @@ require(["jquery", "moment", "jhapi", "utils"], function( var serverName = row.find(".new-server-name").val(); if (serverName === "") { // ../spawn/user/ causes a 404, ../spawn/user redirects correctly to the default server - window.location.href = "../spawn/" + user; + window.location.href = "./spawn/" + user; } else { - window.location.href = "../spawn/" + user + "/" + serverName; + window.location.href = "./spawn/" + user + "/" + serverName; } }); From 898fea9fdc4c28b0c2d92c8d64e889ff89ba0880 Mon Sep 17 00:00:00 2001 From: Zachary Sailer Date: Thu, 19 Sep 2019 11:23:41 -0700 Subject: [PATCH 117/541] Minor typos found by @minrk --- docs/source/events/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/events/index.rst b/docs/source/events/index.rst index d662b8bf..90e30acb 100644 --- a/docs/source/events/index.rst +++ b/docs/source/events/index.rst @@ -13,7 +13,7 @@ JupyterHub can be configured to record structured events from a running server u How to emit events ------------------ -Eventlogging is handled by its ``Eventlog`` object. This leverages Python's standing logging_ library to emit, filter, and collect event data. +Event logging is handled by its ``Eventlog`` object. This leverages Python's standing logging_ library to emit, filter, and collect event data. To begin recording events, you'll need to set two configurations: From 9cf22e4106ba5f1348e03fef4b5cd210ed0d3538 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Sun, 22 Sep 2019 23:07:53 +1200 Subject: [PATCH 118/541] Replace deprecated calls --- jupyterhub/objects.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/jupyterhub/objects.py b/jupyterhub/objects.py index d2e32639..de2ac7a6 100644 --- a/jupyterhub/objects.py +++ b/jupyterhub/objects.py @@ -213,8 +213,4 @@ class Hub(Server): return url_path_join(self.url, 'api') def __repr__(self): - return "<%s %s:%s>" % ( - self.__class__.__name__, - self.server.ip, - self.server.port, - ) + return "<%s %s:%s>" % (self.__class__.__name__, self.ip, self.port) From 5d3dc509bd913530752439002178676991548b56 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Mon, 23 Sep 2019 13:13:21 +1200 Subject: [PATCH 119/541] Remove tornado deprecated/unnecessary call (>5) --- jupyterhub/app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 2f2c1d05..b71ee7cd 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -36,7 +36,6 @@ from tornado.ioloop import IOLoop, PeriodicCallback from tornado.log import app_log, access_log, gen_log import tornado.options from tornado import gen, web -from tornado.platform.asyncio import AsyncIOMainLoop from traitlets import ( Unicode, @@ -2538,7 +2537,6 @@ class JupyterHub(Application): @classmethod def launch_instance(cls, argv=None): self = cls.instance() - AsyncIOMainLoop().install() loop = IOLoop.current() task = asyncio.ensure_future(self.launch_instance_async(argv)) try: From a8c0609eb9a50962c5f6c26e05e55f43f629e57f Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 24 Sep 2019 11:32:23 +0200 Subject: [PATCH 120/541] blacklist urllib3 versions with encoding bug I *think* this should only affect testing, not production --- dev-requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 54a64481..7b14b306 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -14,4 +14,7 @@ pytest-asyncio pytest-cov pytest>=3.3 requests-mock +# blacklist urllib3 releases affected by https://github.com/urllib3/urllib3/issues/1683 +# I *think* this should only affect testing, not production +urllib3!=1.25.4,!=1.25.5 virtualenv From d51d39728a0984aabb6f5e4bfa420e6431ac22f7 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 24 Sep 2019 14:40:34 +0200 Subject: [PATCH 121/541] Errant indentation --- jupyterhub/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index 342abfac..faeb584a 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -1,4 +1,4 @@ - """Base Authenticator class and the default PAM Authenticator""" +"""Base Authenticator class and the default PAM Authenticator""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. import inspect From 1701149fd76033fa5fa24a286ca746142436c9aa Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Tue, 24 Sep 2019 12:04:11 -0700 Subject: [PATCH 122/541] add missing package for json schema --- docs/environment.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/environment.yml b/docs/environment.yml index faa2f1ca..f1e2df83 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -25,3 +25,5 @@ dependencies: - sphinx-copybutton - alabaster_jupyterhub - autodoc-traits + - sphinx-jsonschema + From b41a383eae52dd5ceee565df7da358d2c504cf96 Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Tue, 24 Sep 2019 12:20:42 -0700 Subject: [PATCH 123/541] fix trailing space in file --- docs/environment.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/environment.yml b/docs/environment.yml index f1e2df83..e8f9d380 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -26,4 +26,3 @@ dependencies: - alabaster_jupyterhub - autodoc-traits - sphinx-jsonschema - From 0f93571ca5060d6863cb564e916d69beb7370040 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 26 Sep 2019 14:58:26 +0200 Subject: [PATCH 124/541] verify proxy is accessible before listening on the hub lighter weight than check_routes --- jupyterhub/app.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 9634773b..54422dd0 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2452,6 +2452,20 @@ class JupyterHub(Application): loop.stop() return + # start the proxy + if self.proxy.should_start: + try: + await self.proxy.start() + except Exception as e: + self.log.critical("Failed to start proxy", exc_info=True) + self.exit(1) + else: + self.log.info("Not starting proxy") + + # verify that we can talk to the proxy before listening. + # avoids delayed failure if we can't talk to the proxy + await self.proxy.get_all_routes() + ssl_context = make_ssl_context( self.internal_ssl_key, self.internal_ssl_cert, @@ -2489,16 +2503,6 @@ class JupyterHub(Application): self.log.error("Failed to bind hub to %s", self.hub.bind_url) raise - # start the proxy - if self.proxy.should_start: - try: - await self.proxy.start() - except Exception as e: - self.log.critical("Failed to start proxy", exc_info=True) - self.exit(1) - else: - self.log.info("Not starting proxy") - # start the service(s) for service_name, service in self._service_map.items(): msg = ( From 231d14e95d9c6e1a93820f6c8ac207a40f8df93b Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Thu, 26 Sep 2019 17:33:38 +0300 Subject: [PATCH 125/541] Reduce verbosity for "Failing suspected API request to not-running server" - API requests to non-running servers are not uncommon when you cull servers and people leave tabs open and active. It returns with 503 and logs all headers, which can take up half of our total log lines - This avoids logging headers for all 502 and 503 return statuses. #2747 presented an alternative (more complex) implementation, but this turned out to be appropriate. - Closes: #2747 --- jupyterhub/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/log.py b/jupyterhub/log.py index a9992acf..aca5383b 100644 --- a/jupyterhub/log.py +++ b/jupyterhub/log.py @@ -162,7 +162,7 @@ def log_request(handler): location='', ) msg = "{status} {method} {uri}{location} ({user}@{ip}) {request_time:.2f}ms" - if status >= 500 and status != 502: + if status >= 500 and status not in {502, 503}: log_method(json.dumps(headers, indent=2)) elif status in {301, 302}: # log redirect targets From 8a03b7308662688d4c5b6aa14506f68a08d8b78d Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 27 Sep 2019 09:32:51 +0200 Subject: [PATCH 126/541] Log JupyterHub version on startup --- jupyterhub/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 54422dd0..324656f4 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2184,6 +2184,7 @@ class JupyterHub(Application): self._start_future = asyncio.Future() self.load_config_file(self.config_file) self.init_logging() + self.log.info("Running JupyterHub version %s", jupyterhub.__version__) if 'JupyterHubApp' in self.config: self.log.warning( "Use JupyterHub in config, not JupyterHubApp. Outdated config:\n%s", From f13bd59f6f6b32df3fa112fbff60ca295024af3e Mon Sep 17 00:00:00 2001 From: Dan Allan Date: Tue, 1 Oct 2019 16:13:29 -0400 Subject: [PATCH 127/541] Expose spawner.user_options in REST API. --- jupyterhub/apihandlers/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index 600876cd..d59cfb12 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -141,6 +141,7 @@ class APIHandler(BaseHandler): 'ready': spawner.ready, 'state': spawner.get_state() if include_state else None, 'url': url_path_join(spawner.user.url, spawner.name, '/'), + 'user_options': spawner.user_options, 'progress_url': spawner._progress_url, } From cdba57e96ad8a3c61cd293bdd12025589a27736f Mon Sep 17 00:00:00 2001 From: Dan Allan Date: Tue, 1 Oct 2019 16:44:27 -0400 Subject: [PATCH 128/541] Update expected test result to include user_options. --- jupyterhub/tests/test_named_servers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jupyterhub/tests/test_named_servers.py b/jupyterhub/tests/test_named_servers.py index 0f6809c1..49924db7 100644 --- a/jupyterhub/tests/test_named_servers.py +++ b/jupyterhub/tests/test_named_servers.py @@ -57,6 +57,7 @@ async def test_default_server(app, named_servers): username ), 'state': {'pid': 0}, + 'user_options': {}, } }, } @@ -116,6 +117,7 @@ async def test_create_named_server(app, named_servers): username, servername ), 'state': {'pid': 0}, + 'user_options': {}, } for name in [servername] }, From cd0b3e05e2a881c994e35206f3f060183f4a5b3b Mon Sep 17 00:00:00 2001 From: Rollin Thomas Date: Sat, 5 Oct 2019 10:43:51 -0700 Subject: [PATCH 129/541] Add service links --- jupyterhub/handlers/base.py | 11 +++++++++++ share/jupyterhub/templates/page.html | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 03d9cf3b..054e4a82 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -1107,11 +1107,22 @@ class BaseHandler(RequestHandler): logout_url=self.settings['logout_url'], static_url=self.static_url, version_hash=self.version_hash, + services=self.get_accessible_services(user), ) if self.settings['template_vars']: ns.update(self.settings['template_vars']) return ns + def get_accessible_services(self, user): + accessible_services = list() + for service in self.services.values(): + if not service.url: + continue + if service.admin and not user.admin: + continue + accessible_services.append(service) + return accessible_services + def write_error(self, status_code, **kwargs): """render custom error pages""" exc_info = kwargs.get('exc_info') diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index cd50ee11..2ffac9ec 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -118,6 +118,16 @@ {% if user.admin %}
  • Admin
  • {% endif %} + {% if services %} + + {% endif %} {% endblock %} {% endif %} From 561f4d08895b2092fc541bc1530f3eab9f7e48d4 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 8 Oct 2019 15:28:47 +0200 Subject: [PATCH 130/541] add `service.oauth_no_confirm` configuration allows services to be explicitly blessed to skip the extra oauth confirmation page added in 1.0 This confirmation page is unhelpful for many admin-managed services, and is mainly intended for cross-user access. The default behavior is unchanged, but services can now opt-out of confirmation (as is done already for the user's own servers). Use with caution, as this eliminates users' ability to confirm that a service should be able to authenticate them. --- jupyterhub/apihandlers/auth.py | 30 +++++++++++++++++++++++++++--- jupyterhub/app.py | 10 ++++++++++ jupyterhub/services/service.py | 22 +++++++++++++++++++++- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py index 07e4d18b..cf1eba14 100644 --- a/jupyterhub/apihandlers/auth.py +++ b/jupyterhub/apihandlers/auth.py @@ -198,6 +198,30 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler): raise self.send_oauth_response(headers, body, status) + def needs_oauth_confirm(self, user, oauth_client): + """Return whether the given oauth client needs to prompt for access for the given user + + Checks whitelist for oauth clients + + (i.e. the user's own server) + + .. versionadded: 1.1 + """ + # get the oauth client ids for the user's own server(s) + own_oauth_client_ids = set( + spawner.oauth_client_id for spawner in user.spawners.values() + ) + if ( + # it's the user's own server + oauth_client.identifier in own_oauth_client_ids + # or it's in the global whitelist + or oauth_client.identifier + in self.settings.get('oauth_no_confirm_whitelist', set()) + ): + return False + # default: require confirmation + return True + @web.authenticated def get(self): """GET /oauth/authorization @@ -205,7 +229,8 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler): Render oauth confirmation page: "Server at ... would like permission to ...". - Users accessing their own server will skip confirmation. + Users accessing their own server or a service whitelist + will skip confirmation. """ uri, http_method, body, headers = self.extract_oauth_params() @@ -215,13 +240,12 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler): ) credentials = self.add_credentials(credentials) client = self.oauth_provider.fetch_by_client_id(credentials['client_id']) - if client.redirect_uri.startswith(self.current_user.url): + if not self.needs_oauth_confirm(self.current_user, client): self.log.debug( "Skipping oauth confirmation for %s accessing %s", self.current_user, client.description, ) - # access to my own server doesn't require oauth confirmation # this is the pre-1.0 behavior for all oauth self._complete_login(uri, headers, scopes, credentials) return diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 2f2c1d05..720ab426 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2030,6 +2030,15 @@ class JupyterHub(Application): else: version_hash = datetime.now().strftime("%Y%m%d%H%M%S") + oauth_no_confirm_whitelist = set() + for service in self._service_map.values(): + if service.oauth_no_confirm: + self.log.warning( + "Allowing service %s to complete OAuth without confirmation", + service.name, + ) + oauth_no_confirm_whitelist.add(service.oauth_client_id) + settings = dict( log_function=log_request, config=self.config, @@ -2062,6 +2071,7 @@ class JupyterHub(Application): allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, oauth_provider=self.oauth_provider, + oauth_no_confirm_whitelist=oauth_no_confirm_whitelist, concurrent_spawn_limit=self.concurrent_spawn_limit, spawn_throttle_retry_range=self.spawn_throttle_retry_range, active_server_limit=self.active_server_limit, diff --git a/jupyterhub/services/service.py b/jupyterhub/services/service.py index 9da030b7..e5fa0d9d 100644 --- a/jupyterhub/services/service.py +++ b/jupyterhub/services/service.py @@ -147,11 +147,15 @@ class Service(LoggingConfigurable): - name: str the name of the service - - admin: bool(false) + - admin: bool(False) whether the service should have administrative privileges - url: str (None) The URL where the service is/should be. If specified, the service will be added to the proxy at /services/:name + - oauth_no_confirm: bool(False) + .. versionadded:: 1.1 + Whether this service should be allowed to complete oauth + with logged-in users without prompting for confirmation. If a service is to be managed by the Hub, it has a few extra options: @@ -184,6 +188,7 @@ class Service(LoggingConfigurable): If managed, will be passed as JUPYTERHUB_SERVICE_URL env. """ ).tag(input=True) + api_token = Unicode( help="""The API token to use for the service. @@ -197,6 +202,21 @@ class Service(LoggingConfigurable): """ ).tag(input=True) + oauth_no_confirm = Bool( + False, + help="""Skip OAuth confirmation when users access this service. + + By default, when users authenticate with a service using JupyterHub, + they are prompted to confirm that they want to grant that service + access to their credentials. + Setting oauth_no_confirm=True skips the confirmation for this service. + Useful for admin-managed services that are considered part of the Hub, + which shouldn't need extra prompts for login. + + .. versionadded: 1.1 + """, + ).tag(input=True) + # Managed service API: spawner = Any() From 2ad1159f69f57106b1c1272812f1f93a8f1d7062 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 10 Oct 2019 10:49:55 +0200 Subject: [PATCH 131/541] Apply suggestions from code review Co-Authored-By: Carol Willing --- jupyterhub/app.py | 2 +- jupyterhub/services/service.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 720ab426..efb5ebd0 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2034,7 +2034,7 @@ class JupyterHub(Application): for service in self._service_map.values(): if service.oauth_no_confirm: self.log.warning( - "Allowing service %s to complete OAuth without confirmation", + "Allowing service %s to complete OAuth without confirmation on an authorization web page", service.name, ) oauth_no_confirm_whitelist.add(service.oauth_client_id) diff --git a/jupyterhub/services/service.py b/jupyterhub/services/service.py index e5fa0d9d..3cd3c72a 100644 --- a/jupyterhub/services/service.py +++ b/jupyterhub/services/service.py @@ -209,9 +209,9 @@ class Service(LoggingConfigurable): By default, when users authenticate with a service using JupyterHub, they are prompted to confirm that they want to grant that service access to their credentials. - Setting oauth_no_confirm=True skips the confirmation for this service. - Useful for admin-managed services that are considered part of the Hub, - which shouldn't need extra prompts for login. + Setting oauth_no_confirm=True skips the confirmation web page for this service. + Skipping the confirmation page is useful for admin-managed services that are considered part of the Hub + and shouldn't need extra prompts for login. .. versionadded: 1.1 """, From 25ef67e8e097f7522021c49d54ee54871568c9ab Mon Sep 17 00:00:00 2001 From: Kenan Erdogan Date: Fri, 11 Oct 2019 16:02:36 +0200 Subject: [PATCH 132/541] fix: in SpawnHandler check if named servers are allowed before launching a named server, check also limit of named servers --- jupyterhub/handlers/pages.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index aa2f37cc..eee10dd5 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -118,6 +118,23 @@ class SpawnHandler(BaseHandler): if user is None: raise web.HTTPError(404, "No such user: %s" % for_user) + if server_name: + if not self.allow_named_servers: + raise web.HTTPError(400, "Named servers are not enabled.") + if ( + self.named_server_limit_per_user > 0 + and server_name not in user.orm_spawners + ): + named_spawners = list(user.all_spawners(include_default=False)) + if self.named_server_limit_per_user <= len(named_spawners): + raise web.HTTPError( + 400, + "User {} already has the maximum of {} named servers." + " One must be deleted before a new server can be created".format( + user.name, self.named_server_limit_per_user + ), + ) + if not self.allow_named_servers and user.running: url = self.get_next_url(user, default=user.server_url(server_name)) self.log.info("User is running: %s", user.name) From cc95d30dc1809f2076fcaa0007a572bbeeee7d8c Mon Sep 17 00:00:00 2001 From: Kenan Erdogan Date: Fri, 11 Oct 2019 16:10:09 +0200 Subject: [PATCH 133/541] fix test_named_server_spawn_form: add named_servers fixture --- jupyterhub/tests/test_named_servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/tests/test_named_servers.py b/jupyterhub/tests/test_named_servers.py index 0f6809c1..e88dd2d5 100644 --- a/jupyterhub/tests/test_named_servers.py +++ b/jupyterhub/tests/test_named_servers.py @@ -232,7 +232,7 @@ async def test_named_server_limit(app, named_servers): assert r.text == '' -async def test_named_server_spawn_form(app, username): +async def test_named_server_spawn_form(app, username, named_servers): server_name = "myserver" base_url = public_url(app) cookies = await app.login_user(username) From bc324500058a3d6f69710f81d261d7e487230573 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Sat, 12 Oct 2019 13:54:01 +1300 Subject: [PATCH 134/541] Fix header project name typo --- setup.py | 2 +- share/jupyterhub/static/js/utils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 21b5ab4a..5a5f4176 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # coding: utf-8 -# Copyright (c) Juptyer Development Team. +# Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. # ----------------------------------------------------------------------------- # Minimal Python version sanity check (from IPython) diff --git a/share/jupyterhub/static/js/utils.js b/share/jupyterhub/static/js/utils.js index 63ed0bd0..c09f48cf 100644 --- a/share/jupyterhub/static/js/utils.js +++ b/share/jupyterhub/static/js/utils.js @@ -2,7 +2,7 @@ // Original Copyright (c) IPython Development Team. // Distributed under the terms of the Modified BSD License. -// Modifications Copyright (c) Juptyer Development Team. +// Modifications Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. define(["jquery"], function($) { From 8c1620e6c5086e15e9edaec4a2b822c2b7115e9c Mon Sep 17 00:00:00 2001 From: Will Starms Date: Sat, 12 Oct 2019 18:40:58 -0500 Subject: [PATCH 135/541] server version display also tests --- jupyterhub/app.py | 5 +++++ jupyterhub/handlers/pages.py | 3 +++ jupyterhub/tests/test_pages.py | 15 +++++++++++++++ share/jupyterhub/static/less/admin.less | 6 ++++++ share/jupyterhub/templates/admin.html | 7 +++++++ 5 files changed, 36 insertions(+) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 324656f4..bc04b31f 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -1185,6 +1185,10 @@ class JupyterHub(Application): False, help="""Shuts down all user servers on logout""" ).tag(config=True) + server_tokens = Bool( + True, help="""Display JupyterHub version information on admin page""" + ).tag(config=True) + @default('statsd') def _statsd(self): if self.statsd_host: @@ -2133,6 +2137,7 @@ class JupyterHub(Application): internal_ssl_ca=self.internal_ssl_ca, trusted_alt_names=self.trusted_alt_names, shutdown_on_logout=self.shutdown_on_logout, + server_tokens=self.server_tokens, eventlog=self.eventlog, ) # allow configured settings to have priority diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index aa2f37cc..8b0afa3d 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -14,6 +14,7 @@ from tornado import gen from tornado import web from tornado.httputil import url_concat +from .. import __version__ from .. import orm from ..metrics import SERVER_POLL_DURATION_SECONDS from ..metrics import ServerPollStatus @@ -422,6 +423,8 @@ class AdminHandler(BaseHandler): sort={s: o for s, o in zip(sorts, orders)}, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, + server_tokens=self.settings.get('server_tokens', True), + server_version='{} {}'.format(__version__, self.version_hash), ) self.finish(html) diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 77b5cf1e..3b8f242a 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -110,6 +110,21 @@ async def test_admin(app): assert r.url.endswith('/admin') +async def test_admin_version(app): + cookies = await app.login_user('admin') + r = await get_page('admin', app, cookies=cookies, allow_redirects=False) + r.raise_for_status() + assert "version_footer" in r.text + + +async def test_admin_version_disabled(app): + cookies = await app.login_user('admin') + with mock.patch.dict(app.tornado_settings, {'server_tokens': False}): + r = await get_page('admin', app, cookies=cookies, allow_redirects=False) + r.raise_for_status() + assert "version_footer" not in r.text + + @pytest.mark.parametrize('sort', ['running', 'last_activity', 'admin', 'name']) async def test_admin_sort(app, sort): cookies = await app.login_user('admin') diff --git a/share/jupyterhub/static/less/admin.less b/share/jupyterhub/static/less/admin.less index 70a262d9..6189e4e9 100644 --- a/share/jupyterhub/static/less/admin.less +++ b/share/jupyterhub/static/less/admin.less @@ -1,3 +1,9 @@ i.sort-icon { margin-left: 4px; } + +.version_footer { + position: fixed; + bottom: 0; + width: 100%; +} diff --git a/share/jupyterhub/templates/admin.html b/share/jupyterhub/templates/admin.html index 12edbdcd..f5ddec4a 100644 --- a/share/jupyterhub/templates/admin.html +++ b/share/jupyterhub/templates/admin.html @@ -103,6 +103,13 @@
    +{%- if server_tokens -%} + +{%- endif -%} {% call modal('Delete User', btn_class='btn-danger delete-button') %} Are you sure you want to delete user USER? From d942f52eebc74d5f07a27753d4dbee28a704a05d Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Wed, 16 Oct 2019 12:00:43 +1300 Subject: [PATCH 136/541] Add docs for fixtures in CONTRIBUTING.md --- CONTRIBUTING.md | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cfced3c8..fda9d36f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,6 +97,35 @@ and other collections of tests for different components. When writing a new test, there should usually be a test of similar functionality already written and related tests should be added nearby. -When in doubt, feel free to ask. -TODO: describe some details about fixtures, etc. +The fixtures live in `jupyterhub/tests/conftest.py`. There are +fixtures that can be used for JupyterHub components, such as: + +- `app`: an instance of JupyterHub with mocked parts +- `auth_state_enabled`: enables persisting auth_state (like authentication tokens) +- `db`: a sqlite in-memory DB session +- `io_loop`: a Tornado event loop +- `event_loop`: a new asyncio event loop +- `user`: creates a new temporary user +- `admin_user`: creates a new temporary admin user +- single user servers + - `cleanup_after`: allows cleanup of single user servers between tests +- mocked service + - `MockServiceSpawner`: a spawner that mocks services for testing with a short poll interval + - `mockservice`: mocked service with no external service url + - `mockservice_url`: mocked service with a url to test external services + +And fixtures to add functionality or spawning behavior: + +- `admin_access`: grants admin access +- `no_patience`: sets slow-spawning timeouts to zero +- `slow_spawn`: enables the SlowSpawner (a spawner that takes a few seconds to start) +- `never_spawn`: enables the NeverSpawner (a spawner that will never start) +- `bad_spawn`: enables the BadSpawner (a spawner that fails immediately) +- `slow_bad_spawn`: enables the SlowBadSpawner (a spawner that fails after a short delay) + +To read more about fixtures check out the +[pytest docs](https://docs.pytest.org/en/latest/fixture.html) +for how to use the existing fixtures, and how to create new ones. + +When in doubt, feel free to ask. From 3a0a58178281e66402abbb45027841601e2548f4 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 16 Oct 2019 15:32:03 +0300 Subject: [PATCH 137/541] Log proxy class --- jupyterhub/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 324656f4..6e639a88 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2220,6 +2220,7 @@ class JupyterHub(Application): _log_cls("Authenticator", self.authenticator_class) _log_cls("Spawner", self.spawner_class) + _log_cls("Proxy", self.proxy_class) self.init_eventlog() self.init_pycurl() From 60a1c9380148aa7d2d23809f2e579aaa42a24cb1 Mon Sep 17 00:00:00 2001 From: Rick Wagner Date: Wed, 16 Oct 2019 16:45:25 -0700 Subject: [PATCH 138/541] chown jupyterhub dir in user home --- jupyterhub/spawner.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index 74bd7f22..356319de 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -1352,7 +1352,8 @@ class LocalProcessSpawner(Spawner): home = user.pw_dir # Create dir for user's certs wherever we're starting - out_dir = "{home}/.jupyterhub/jupyterhub-certs".format(home=home) + hub_dir = "{home}/.jupyterhub".format(home=home) + out_dir = "{hub_dir}/jupyterhub-certs".format(hub_dir=hub_dir) shutil.rmtree(out_dir, ignore_errors=True) os.makedirs(out_dir, 0o700, exist_ok=True) @@ -1366,7 +1367,7 @@ class LocalProcessSpawner(Spawner): ca = os.path.join(out_dir, os.path.basename(paths['cafile'])) # Set cert ownership to user - for f in [out_dir, key, cert, ca]: + for f in [hub_dir, out_dir, key, cert, ca]: shutil.chown(f, user=uid, group=gid) return {"keyfile": key, "certfile": cert, "cafile": ca} From 3cafc7e49f530f33cf00b42fd4b9af062c506152 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 17 Oct 2019 10:01:10 +0200 Subject: [PATCH 139/541] remove versionadded from Service docstring sphinx doesn't seem to like this here --- jupyterhub/services/service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jupyterhub/services/service.py b/jupyterhub/services/service.py index 3cd3c72a..fbf1f331 100644 --- a/jupyterhub/services/service.py +++ b/jupyterhub/services/service.py @@ -153,7 +153,6 @@ class Service(LoggingConfigurable): The URL where the service is/should be. If specified, the service will be added to the proxy at /services/:name - oauth_no_confirm: bool(False) - .. versionadded:: 1.1 Whether this service should be allowed to complete oauth with logged-in users without prompting for confirmation. From a615f783a3024e059a4111c7873fcf1fd2a0d45c Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Thu, 17 Oct 2019 22:07:43 +1300 Subject: [PATCH 140/541] Remove unused setupegg.py --- setupegg.py | 7 ------- 1 file changed, 7 deletions(-) delete mode 100755 setupegg.py diff --git a/setupegg.py b/setupegg.py deleted file mode 100755 index fa537b36..00000000 --- a/setupegg.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python -"""Wrapper to run setup.py using setuptools.""" -# Import setuptools and call the actual setup -import setuptools - -with open('setup.py', 'rb') as f: - exec(compile(f.read(), 'setup.py', 'exec')) From 2cac46fdb27ced4c85413ff36486d717c8574186 Mon Sep 17 00:00:00 2001 From: Will Starms Date: Thu, 17 Oct 2019 13:43:28 -0500 Subject: [PATCH 141/541] Remove server_tokens setting Revert this if we decide this is a security issue, but we report the version through the API as well --- jupyterhub/app.py | 5 ----- jupyterhub/handlers/pages.py | 1 - jupyterhub/tests/test_pages.py | 8 -------- share/jupyterhub/templates/admin.html | 10 ++++------ 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index bc04b31f..324656f4 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -1185,10 +1185,6 @@ class JupyterHub(Application): False, help="""Shuts down all user servers on logout""" ).tag(config=True) - server_tokens = Bool( - True, help="""Display JupyterHub version information on admin page""" - ).tag(config=True) - @default('statsd') def _statsd(self): if self.statsd_host: @@ -2137,7 +2133,6 @@ class JupyterHub(Application): internal_ssl_ca=self.internal_ssl_ca, trusted_alt_names=self.trusted_alt_names, shutdown_on_logout=self.shutdown_on_logout, - server_tokens=self.server_tokens, eventlog=self.eventlog, ) # allow configured settings to have priority diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 8b0afa3d..509efd08 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -423,7 +423,6 @@ class AdminHandler(BaseHandler): sort={s: o for s, o in zip(sorts, orders)}, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, - server_tokens=self.settings.get('server_tokens', True), server_version='{} {}'.format(__version__, self.version_hash), ) self.finish(html) diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 3b8f242a..d5d31978 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -117,14 +117,6 @@ async def test_admin_version(app): assert "version_footer" in r.text -async def test_admin_version_disabled(app): - cookies = await app.login_user('admin') - with mock.patch.dict(app.tornado_settings, {'server_tokens': False}): - r = await get_page('admin', app, cookies=cookies, allow_redirects=False) - r.raise_for_status() - assert "version_footer" not in r.text - - @pytest.mark.parametrize('sort', ['running', 'last_activity', 'admin', 'name']) async def test_admin_sort(app, sort): cookies = await app.login_user('admin') diff --git a/share/jupyterhub/templates/admin.html b/share/jupyterhub/templates/admin.html index f5ddec4a..a0c6fb87 100644 --- a/share/jupyterhub/templates/admin.html +++ b/share/jupyterhub/templates/admin.html @@ -103,13 +103,11 @@
    -{%- if server_tokens -%} - {% endblock %} +{% block script %} +{{ super () }} +{% if implicit_spawn_seconds %} + +{% endif %} +{% endblock script %} From 0787489e1bbac11d14482a96c1179755a210d2ba Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 20 Feb 2020 12:53:15 +0100 Subject: [PATCH 283/541] maybe_future needs a future! --- jupyterhub/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index b0cc694a..93f56de7 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -1675,7 +1675,7 @@ class JupyterHub(Application): try: f = self.authenticator.add_user(user) if f: - await maybe_future() + await maybe_future(f) except Exception: self.log.exception("Error adding user %s already in db", user.name) if self.authenticator.delete_invalid_users: From d2a1b8e349786de66dcf26011f4cfa138113d07b Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 20 Feb 2020 15:31:38 +0100 Subject: [PATCH 284/541] update docs environments - python 3.7 - node 12 - sync recommonmark 0.6 --- docs/environment.yml | 6 +++--- docs/requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/environment.yml b/docs/environment.yml index 35b5c405..4ba57441 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -5,12 +5,12 @@ channels: - conda-forge dependencies: - pip -- nodejs -- python=3.6 +- nodejs=12 +- python=3.7 - alembic - jinja2 - pamela -- recommonmark==0.6.0 +- recommonmark>=0.6 - requests - sqlalchemy>=1 - tornado>=5.0 diff --git a/docs/requirements.txt b/docs/requirements.txt index 01d85db1..4934ec4d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,7 +4,7 @@ alabaster_jupyterhub autodoc-traits git+https://github.com/pandas-dev/pandas-sphinx-theme.git@master -recommonmark==0.5.0 +recommonmark>=0.6 sphinx-copybutton sphinx-jsonschema sphinx>=1.7 From 0c4db2d99f5a2ba513cbc6c9efc175d5420272b5 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 20 Feb 2020 15:54:43 +0100 Subject: [PATCH 285/541] docs: use metachannel for faster environment solve rtd is having memory issues with conda-forge, which should hopefully be fixed by metachannel this should also make things quicker for anyone --- docs/environment.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/environment.yml b/docs/environment.yml index 4ba57441..0b613bad 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -2,7 +2,10 @@ # if you change the dependencies of JupyterHub in the various `requirements.txt` name: jhub_docs channels: - - conda-forge + # use metachannel for faster solves / less memory on RTD + # which has memory issues with full conda-forge + # only need to list 'leaf' dependencies here + - https://metachannel.conda-forge.org/conda-forge/jupyterhub,sphinx,recommonmark dependencies: - pip - nodejs=12 From 606775f72da2ce648a5a5fbe8e452c72f082e263 Mon Sep 17 00:00:00 2001 From: Tim Head Date: Thu, 20 Feb 2020 16:56:03 +0100 Subject: [PATCH 286/541] Remove unused variable --- jupyterhub/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 93f56de7..41abcf4c 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2019,7 +2019,6 @@ class JupyterHub(Application): if not orm_spawners: continue orm_spawner = orm_spawners[0] - orm_user = orm_spawner.user # instantiate Spawner wrapper and check if it's still alive # spawner should be running user = self.users[orm_spawner.user] From 287b0302d9e6677a826727c973f39e66ea5bc6d5 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Wed, 19 Feb 2020 13:46:45 +1300 Subject: [PATCH 287/541] Add more docs about authentication and cookies, using text posted by MinRK on Discourse --- .../getting-started/security-basics.rst | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/docs/source/getting-started/security-basics.rst b/docs/source/getting-started/security-basics.rst index 7661cfd1..6704e7ce 100644 --- a/docs/source/getting-started/security-basics.rst +++ b/docs/source/getting-started/security-basics.rst @@ -86,7 +86,7 @@ Proxy authentication token -------------------------- The Hub authenticates its requests to the Proxy using a secret token that -the Hub and Proxy agree upon. The value of this string should be a random +the Hub and Proxy agree upon. The value of this token should be a random string (for example, generated by ``openssl rand -hex 32``). Generating and storing token in the configuration file @@ -96,7 +96,7 @@ Or you can set the value in the configuration file, ``jupyterhub_config.py``: .. code-block:: python - c.JupyterHub.proxy_auth_token = '0bc02bede919e99a26de1e2a7a5aadfaf6228de836ec39a05a6c6942831d8fe5' + c.ConfigurableHTTPProxy.api_token = 'abc123...' # any random string Generating and storing as an environment variable ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -183,3 +183,70 @@ itself, ``jupyterhub_config.py``, as a binary string: If the cookie secret value changes for the Hub, all single-user notebook servers must also be restarted. + +.. _cookies: + +Cookies used by JupyterHub authentication +----------------------------------------- + +The following cookies are used by the Hub for handling user authentication. + +jupyterhub-hub-login +~~~~~~~~~~~~~~~~~~~~ + +This is the login token used when visiting Hub-served pages encrypted such +as the main home, the spawn form, etc. If this cookie is set, then the +user is logged in. + +Resetting the Hub cookie secret effectively revokes this cookie. + +This cookie is restricted to the path ``/hub/``. + +jupyterhub-user- +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is the cookie used for authenticating with a single-user server +encrypted. It is set by the single-user server after OAuth with the Hub. + +Effectively the same as ``jupyterhub-hub-login``, but for the +single-user server instead of the Hub. It contains an OAuth access token, +which is checked with the Hub to authenticate the browser. + +Each OAuth access token is associated with a session id (see ``jupyterhub-session-id`` section +below). + +To avoid hitting the Hub on every request, the authentication response +is cached. And to avoid a stale cache the cache key is comprised of both +the token and session id. + +Resetting the Hub cookie secret effectively revokes this cookie. + +This cookie is restricted to the path ``/user/``, so that +only the user’s server receives it. + +jupyterhub-session-id +~~~~~~~~~~~~~~~~~~~~~ + +This is a random string, meaningless in itself, and the only cookie +shared by the Hub and single-user servers. + +Its sole purpose is to to coordinate logout of the multiple OAuth cookies. + +This cookie is set to ``/`` so all endpoints can receive it, or clear it, etc. + +jupyterhub-user--oauth-state +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A short-lived cookie, used solely to store and validate OAuth state. +It is only set while OAuth between the single-user server and the Hub +is processing. + +If you use your browser development tools, you should see this cookie +for a very brief moment before your are logged in, +with an expiration date shorter than ``jupyterhub-hub-login`` or +``jupyterhub-user-``. + +This cookie should not exist after you have successfully logged in. + +This cookie is restricted to the path ``/user/``, so that only +the user’s server receives it. From f5bd5b7751f87dd0ac229be8db909f199cd39a09 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Fri, 21 Feb 2020 10:32:11 +1300 Subject: [PATCH 288/541] Incorporate review feedback --- .../getting-started/security-basics.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/source/getting-started/security-basics.rst b/docs/source/getting-started/security-basics.rst index 6704e7ce..1a79f2e6 100644 --- a/docs/source/getting-started/security-basics.rst +++ b/docs/source/getting-started/security-basics.rst @@ -87,12 +87,13 @@ Proxy authentication token The Hub authenticates its requests to the Proxy using a secret token that the Hub and Proxy agree upon. The value of this token should be a random -string (for example, generated by ``openssl rand -hex 32``). +string (for example, generated by ``openssl rand -hex 32``). You can store +it in the configuration file or an environment variable Generating and storing token in the configuration file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Or you can set the value in the configuration file, ``jupyterhub_config.py``: +You can set the value in the configuration file, ``jupyterhub_config.py``: .. code-block:: python @@ -191,12 +192,16 @@ Cookies used by JupyterHub authentication The following cookies are used by the Hub for handling user authentication. +This section was created based on this post_ from Discourse. + +.. _post: https://discourse.jupyter.org/t/how-to-force-re-login-for-users/1998/6 + jupyterhub-hub-login ~~~~~~~~~~~~~~~~~~~~ -This is the login token used when visiting Hub-served pages encrypted such -as the main home, the spawn form, etc. If this cookie is set, then the -user is logged in. +This is the login token used when visiting Hub-served pages that are +protected by authentication such as the main home, the spawn form, etc. +If this cookie is set, then the user is logged in. Resetting the Hub cookie secret effectively revokes this cookie. @@ -205,8 +210,8 @@ This cookie is restricted to the path ``/hub/``. jupyterhub-user- ~~~~~~~~~~~~~~~~~~~~~~~~~~ -This is the cookie used for authenticating with a single-user server -encrypted. It is set by the single-user server after OAuth with the Hub. +This is the cookie used for authenticating with a single-user server. +It is set by the single-user server after OAuth with the Hub. Effectively the same as ``jupyterhub-hub-login``, but for the single-user server instead of the Hub. It contains an OAuth access token, From 239a4c63a2e644f44297c0f330f8985cd25716b0 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Fri, 21 Feb 2020 10:35:30 +1300 Subject: [PATCH 289/541] Add note that not all proxy implementations use an auth token --- docs/source/getting-started/security-basics.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/source/getting-started/security-basics.rst b/docs/source/getting-started/security-basics.rst index 1a79f2e6..5da0a668 100644 --- a/docs/source/getting-started/security-basics.rst +++ b/docs/source/getting-started/security-basics.rst @@ -86,9 +86,13 @@ Proxy authentication token -------------------------- The Hub authenticates its requests to the Proxy using a secret token that -the Hub and Proxy agree upon. The value of this token should be a random -string (for example, generated by ``openssl rand -hex 32``). You can store -it in the configuration file or an environment variable +the Hub and Proxy agree upon. Note that this applies to the default +``ConfigurableHTTPProxy`` implementation. Not all proxy implementations +use an auth token. + +The value of this token should be a random string (for example, generated by +``openssl rand -hex 32``). You can store it in the configuration file or an +environment variable Generating and storing token in the configuration file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 7735c7ddd42fd1b8c3dcb429581dafc80e463bcd Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 21 Feb 2020 10:05:40 +0100 Subject: [PATCH 290/541] make spawner:server backref explicitly one-to-one using backref(uselist=False), single_parent=True --- jupyterhub/app.py | 13 ++++++++++--- jupyterhub/orm.py | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 41abcf4c..22e7bb53 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -2013,12 +2013,19 @@ class JupyterHub(Application): # parallelize checks for running Spawners # run query on extant Server objects # so this is O(running servers) not O(total users) + # Server objects can be associated with either a Spawner or a Service, + # we are only interested in the ones associated with a Spawner check_futures = [] for orm_server in db.query(orm.Server): - orm_spawners = orm_server.spawner - if not orm_spawners: + orm_spawner = orm_server.spawner + if not orm_spawner: + # sanity check for orphaned Server rows + # this shouldn't happen if we've got our sqlachemy right + if not orm_server.service: + self.log.warning("deleting orphaned server %s", orm_server) + self.db.delete(orm_server) + self.db.commit() continue - orm_spawner = orm_spawners[0] # instantiate Spawner wrapper and check if it's still alive # spawner should be running user = self.users[orm_spawner.user] diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index 4e151092..81c01dd3 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -26,6 +26,7 @@ from sqlalchemy import select from sqlalchemy import Table from sqlalchemy import Unicode from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import backref from sqlalchemy.orm import interfaces from sqlalchemy.orm import object_session from sqlalchemy.orm import relationship @@ -230,7 +231,12 @@ class Spawner(Base): user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE')) server_id = Column(Integer, ForeignKey('servers.id', ondelete='SET NULL')) - server = relationship(Server, backref='spawner', cascade="all") + server = relationship( + Server, + backref=backref('spawner', uselist=False), + single_parent=True, + cascade="all, delete-orphan", + ) state = Column(JSONDict) name = Column(Unicode(255)) @@ -282,7 +288,12 @@ class Service(Base): # service-specific interface _server_id = Column(Integer, ForeignKey('servers.id', ondelete='SET NULL')) - server = relationship(Server, backref='service', cascade='all') + server = relationship( + Server, + backref=backref('service', uselist=False), + single_parent=True, + cascade="all, delete-orphan", + ) pid = Column(Integer) def new_api_token(self, token=None, **kwargs): From 3e6abb7a5e399b1aebfd85ce388c1d9d1962bfba Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 21 Feb 2020 13:52:03 +0100 Subject: [PATCH 291/541] add general faq and put a first q about user-redirect --- docs/source/getting-started/faq.md | 36 +++++++++++++++++++++++++++ docs/source/getting-started/index.rst | 1 + 2 files changed, 37 insertions(+) create mode 100644 docs/source/getting-started/faq.md diff --git a/docs/source/getting-started/faq.md b/docs/source/getting-started/faq.md new file mode 100644 index 00000000..ae912847 --- /dev/null +++ b/docs/source/getting-started/faq.md @@ -0,0 +1,36 @@ +# Frequently asked questions + + +### How do I share links to notebooks? + +In short, where you see `/user/name/notebooks/foo.ipynb` use `/hub/user-redirect/notebooks/foo.ipynb` (replace `/user/name` with `/hub/user-redirect`). + +Sharing links to notebooks is a common activity, +and can look different based on what you mean. +Your first instinct might be to copy the URL you see in the browser, +e.g. `hub.jupyter.org/user/yourname/notebooks/coolthing.ipynb`. +However, let's break down what this URL means: + +`hub.jupyter.org/user/yourname/` is the URL prefix handled by *your server*, +which means that sharing this URL is asking the person you share the link with +to come to *your server* and look at the exact same file. +In most circumstances, this is forbidden by permissions because the person you share with does not have access to your server. +What actually happens when someone visits this URL will depend on whether your server is running and other factors. + +But what is our actual goal? +A typical situation is that you have some shared or common filesystem, +such that the same path corresponds to the same document +(either the exact same document or another copy of it). +Typically, what folks want when they do sharing like this +is for each visitor to open the same file *on their own server*, +so Breq would open `/user/breq/notebooks/foo.ipynb` and +Seivarden would open `/user/seivarden/notebooks/foo.ipynb`, etc. + +JupyterHub has a special URL that does exactly this! +It's called `/hub/user-redirect/...` and after the visitor logs in, +So if you replace `/user/yourname` in your URL bar +with `/hub/user-redirect` any visitor should get the same +URL on their own server, rather than visiting yours. + +In JupyterLab 2.0, this should also be the result of the "Copy Shareable Link" +action in the file browser. diff --git a/docs/source/getting-started/index.rst b/docs/source/getting-started/index.rst index 00bcdfbc..bae95f8f 100644 --- a/docs/source/getting-started/index.rst +++ b/docs/source/getting-started/index.rst @@ -15,4 +15,5 @@ own JupyterHub. authenticators-users-basics spawners-basics services-basics + faq institutional-faq From 84acdd5a7ffb3d79ca04aa86ffcea7e067cce6ee Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 21 Feb 2020 14:10:36 +0100 Subject: [PATCH 292/541] handle uselist=False in our relationship expiry --- jupyterhub/orm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index 81c01dd3..130d16f0 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -634,7 +634,10 @@ def _expire_relationship(target, relationship_prop): return # many-to-many and one-to-many have a list of peers # many-to-one has only one - if relationship_prop.direction is interfaces.MANYTOONE: + if ( + relationship_prop.direction is interfaces.MANYTOONE + or not relationship_prop.uselist + ): peers = [peers] for obj in peers: if inspect(obj).persistent: From 7e3caf7f4870567775b1b34f95afc5112ecbc541 Mon Sep 17 00:00:00 2001 From: Alex Driedger Date: Sat, 22 Feb 2020 16:37:34 -0800 Subject: [PATCH 293/541] Fixed grammar on landing page --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 1f819278..c9722089 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,7 +3,7 @@ JupyterHub ========== `JupyterHub`_ is the best way to serve `Jupyter notebook`_ for multiple users. -It can be used in a classes of students, a corporate data science group or scientific +It can be used in a class of students, a corporate data science group or scientific research group. It is a multi-user **Hub** that spawns, manages, and proxies multiple instances of the single-user `Jupyter notebook`_ server. From 76afec8adb8e30b6102854ebf79bbe1c0726c27c Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Sat, 12 Oct 2019 12:44:02 +1300 Subject: [PATCH 294/541] Update app.bind_url and proxy.public_url when (external) SSL is enabled --- jupyterhub/app.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 0cf649bf..dda4c127 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -576,6 +576,22 @@ class JupyterHub(Application): """, ).tag(config=True) + @validate('bind_url') + def _validate_bind_url(self, proposal): + """ensure protocol field of bind_url matches ssl""" + v = proposal['value'] + proto, sep, rest = v.partition('://') + if self.ssl_cert and proto != 'https': + return 'https' + sep + rest + elif proto != 'http' and not self.ssl_cert: + return 'http' + sep + rest + return v + + @default('bind_url') + def _bind_url_default(self): + proto = 'https' if self.ssl_cert else 'http' + return proto + '://:8000' + subdomain_host = Unicode( '', help="""Run single-user servers on subdomains of this host. From 3b05135f11dc3926ff30adf78eae21e767b7a4f3 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Mon, 24 Feb 2020 20:48:42 +1300 Subject: [PATCH 295/541] Fix couple typos --- docs/source/contributing/tests.rst | 2 +- docs/source/getting-started/security-basics.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/contributing/tests.rst b/docs/source/contributing/tests.rst index 46360eb7..c607df92 100644 --- a/docs/source/contributing/tests.rst +++ b/docs/source/contributing/tests.rst @@ -64,5 +64,5 @@ Troubleshooting Test Failures All the tests are failing ------------------------- -Make sure you have completed all the steps in :ref:`contributing/setup` sucessfully, and +Make sure you have completed all the steps in :ref:`contributing/setup` successfully, and can launch ``jupyterhub`` from the terminal. diff --git a/docs/source/getting-started/security-basics.rst b/docs/source/getting-started/security-basics.rst index 5da0a668..87007311 100644 --- a/docs/source/getting-started/security-basics.rst +++ b/docs/source/getting-started/security-basics.rst @@ -239,7 +239,7 @@ jupyterhub-session-id This is a random string, meaningless in itself, and the only cookie shared by the Hub and single-user servers. -Its sole purpose is to to coordinate logout of the multiple OAuth cookies. +Its sole purpose is to coordinate logout of the multiple OAuth cookies. This cookie is set to ``/`` so all endpoints can receive it, or clear it, etc. From facf52f117c709aa5f21b39139b65cd1a2cd23ff Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Tue, 25 Feb 2020 17:03:01 +0100 Subject: [PATCH 296/541] Removing unneeded pass of request to the template --- jupyterhub/handlers/pages.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 2adbef45..0bcca7a6 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -481,7 +481,6 @@ class AdminHandler(BaseHandler): auth_state = await self.current_user.get_auth_state() html = self.render_template( 'admin.html', - request=self.request, current_user=self.current_user, auth_state=auth_state, admin_access=self.settings.get('admin_access', False), From 7b6ac158ccf4cd3bc4ffafd4ba3681b0a8ddb3df Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Tue, 25 Feb 2020 19:11:09 +0100 Subject: [PATCH 297/541] Removing python-paginate package and adding minimal Pagination class to enable a pagination API for AdminHandler --- jupyterhub/handlers/pages.py | 68 ++++++++++++++++++++++++++++++------ requirements.txt | 1 - 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 0bcca7a6..20052b35 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -10,7 +10,6 @@ from datetime import datetime from http.client import responses from jinja2 import TemplateNotFound -from python_paginate.web.tornado_paginate import Pagination from tornado import gen from tornado import web from tornado.httputil import url_concat @@ -404,15 +403,7 @@ class AdminHandler(BaseHandler): @web.authenticated @admin_only async def get(self): - DEFAULT_PER_PAGE = 100 - page, per_page, offset = Pagination.get_page_args(self) - _per_page = self.get_arguments("per_page") - # No arg called per_page in the URL, - # avoiding default value from the python-paginate lib - # https://github.com/lixxu/python-paginate/blob/master/python_paginate/web/tornado_paginate.py#L23 - if per_page == 10 and len(_per_page) == 0: - per_page = DEFAULT_PER_PAGE available = {'name', 'admin', 'running', 'last_activity'} default_sort = ['admin', 'name'] @@ -472,8 +463,6 @@ class AdminHandler(BaseHandler): pagination = Pagination( url=self.request.uri, total=total, - record_name='users', - display_msg='Displaying {record_name} {start} - {end}. Total {record_name}: {total}', page=page, per_page=per_page, ) @@ -619,6 +608,63 @@ class HealthCheckHandler(BaseHandler): def get(self, *args): self.finish() +class Pagination(BaseHandler): + + _page_name = 'page' + _per_page_name = 'per_page' + _default_page = 1 + _default_per_page = 100 + _max_per_page = 250 + _record_name='users' + _display_msg='Displaying {record_name} {start} - {end}. Total {record_name}: {total}' + + def __init__(self, *args, **kwargs): + """Detail parameters remark. + **url**: current request url + **page**: current page + **per_page**: how many records displayed on one page. By default 100 + **total**: total records for pagination + **display_msg**: text for pagation information + **record_name**: record name showed in pagination information + """ + self.page = kwargs.get(self._page_name, 1) + + if self.per_page > self._max_per_page: + self.per_page = self._max_per_page + + self.total = int(kwargs.get('total', 0)) + self.display_msg = kwargs.get('display_msg', self._display_msg) + + self.record_name = kwargs.get('record_name', self._record_name) + self.url = kwargs.get('url') or self.get_url() + self.init_values() + + def init_values(self): + self._cached = {} + self.skip = (self.page - 1) * self.per_page + pages = divmod(self.total, self.per_page) + self.total_pages = pages[0] + 1 if pages[1] else pages[0] + + self.has_prev = self.page > 1 + self.has_next = self.page < self.total_pages + + @classmethod + def get_page_args(self, handler): + self.page = handler.get_argument(self._page_name, self._default_page) + self.per_page = handler.get_argument(self._per_page_name, self._default_per_page) + try: + self.per_page = int(self.per_page) + if self.per_page > self._max_per_page: + self.per_page = self._max_per_page + except: + self.per_page = self._default_per_page + + try: + self.page = int(self.page) + except: + self.page = self._default_page + + return self.page, self.per_page, self.per_page * (self.page - 1) default_handlers = [ (r'/', RootHandler), diff --git a/requirements.txt b/requirements.txt index 74215f5a..825fd409 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ pamela prometheus_client>=0.0.21 psutil>=5.6.5; sys_platform == 'win32' python-dateutil -python-paginate requests SQLAlchemy>=1.1 tornado>=5.0 From 2f6ea7110626754ef5c52d04235521d3c07e3cbe Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Fri, 4 Oct 2019 19:41:59 +1300 Subject: [PATCH 298/541] Add not_running.js to modify button spawn_url --- share/jupyterhub/static/js/not_running.js | 13 +++++++++++++ share/jupyterhub/templates/not_running.html | 3 +++ 2 files changed, 16 insertions(+) create mode 100644 share/jupyterhub/static/js/not_running.js diff --git a/share/jupyterhub/static/js/not_running.js b/share/jupyterhub/static/js/not_running.js new file mode 100644 index 00000000..12ad601a --- /dev/null +++ b/share/jupyterhub/static/js/not_running.js @@ -0,0 +1,13 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +require(["jquery", "utils"], function($, utils) { + "use strict"; + + var hash = utils.parse_url(window.location.href).hash; + if (hash !== undefined && hash !== '') { + var el = $("#start"); + var current_spawn_url = el.attr("href"); + el.attr("href", current_spawn_url + hash); + } +}); diff --git a/share/jupyterhub/templates/not_running.html b/share/jupyterhub/templates/not_running.html index fa493d97..0ef3ae7d 100644 --- a/share/jupyterhub/templates/not_running.html +++ b/share/jupyterhub/templates/not_running.html @@ -63,4 +63,7 @@ ); {% endif %} + {% endblock script %} From 18205fbf4a14cdfac6e4e148b1feff09d18ac751 Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Wed, 26 Feb 2020 10:36:36 +0100 Subject: [PATCH 299/541] Fixing black formatting issues --- jupyterhub/handlers/pages.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 20052b35..d2ebf235 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -461,10 +461,7 @@ class AdminHandler(BaseHandler): total = self.db.query(orm.User.id).count() pagination = Pagination( - url=self.request.uri, - total=total, - page=page, - per_page=per_page, + url=self.request.uri, total=total, page=page, per_page=per_page, ) auth_state = await self.current_user.get_auth_state() @@ -608,6 +605,7 @@ class HealthCheckHandler(BaseHandler): def get(self, *args): self.finish() + class Pagination(BaseHandler): _page_name = 'page' @@ -615,8 +613,10 @@ class Pagination(BaseHandler): _default_page = 1 _default_per_page = 100 _max_per_page = 250 - _record_name='users' - _display_msg='Displaying {record_name} {start} - {end}. Total {record_name}: {total}' + _record_name = 'users' + _display_msg = ( + 'Displaying {record_name} {start} - {end}. Total {record_name}: {total}' + ) def __init__(self, *args, **kwargs): """Detail parameters remark. @@ -634,7 +634,7 @@ class Pagination(BaseHandler): self.total = int(kwargs.get('total', 0)) self.display_msg = kwargs.get('display_msg', self._display_msg) - + self.record_name = kwargs.get('record_name', self._record_name) self.url = kwargs.get('url') or self.get_url() self.init_values() @@ -651,7 +651,9 @@ class Pagination(BaseHandler): @classmethod def get_page_args(self, handler): self.page = handler.get_argument(self._page_name, self._default_page) - self.per_page = handler.get_argument(self._per_page_name, self._default_per_page) + self.per_page = handler.get_argument( + self._per_page_name, self._default_per_page + ) try: self.per_page = int(self.per_page) if self.per_page > self._max_per_page: @@ -666,6 +668,7 @@ class Pagination(BaseHandler): return self.page, self.per_page, self.per_page * (self.page - 1) + default_handlers = [ (r'/', RootHandler), (r'/home', HomeHandler), From f49cc1fcf0b04f0b7417f8bc32dd100e0fc9fe6b Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Wed, 26 Feb 2020 10:40:44 +0100 Subject: [PATCH 300/541] Improving description of potential parameters --- jupyterhub/handlers/pages.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index d2ebf235..3bb7b63a 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -619,13 +619,13 @@ class Pagination(BaseHandler): ) def __init__(self, *args, **kwargs): - """Detail parameters remark. - **url**: current request url - **page**: current page - **per_page**: how many records displayed on one page. By default 100 - **total**: total records for pagination - **display_msg**: text for pagation information - **record_name**: record name showed in pagination information + """Potential parameters. + **url**: URL in request + **page**: current page in use + **per_page**: number of records to display in the page. By default 100 + **total**: total records considered while paginating + **display_msg**: informative text for pagination + **record_name**: name of the record, showed in pagination info """ self.page = kwargs.get(self._page_name, 1) From f4b7b85b021eebdc90babcecae4c7a16eab55166 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 27 Feb 2020 13:49:47 +0100 Subject: [PATCH 301/541] preserve auth type when logging obfuscated auth header Authorization header has the form " " rather than checking for "token" only, preserve type value, which could be Bearer, Basic, etc. --- jupyterhub/log.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/jupyterhub/log.py b/jupyterhub/log.py index aca5383b..d60b358a 100644 --- a/jupyterhub/log.py +++ b/jupyterhub/log.py @@ -98,8 +98,12 @@ def _scrub_headers(headers): headers = dict(headers) if 'Authorization' in headers: auth = headers['Authorization'] - if auth.startswith('token '): - headers['Authorization'] = 'token [secret]' + if ' ' in auth: + auth_type = auth.split(' ', 1)[0] + else: + # no space, hide the whole thing in case there was a mistake + auth_type = '' + headers['Authorization'] = '{} [secret]'.format(auth_type) if 'Cookie' in headers: c = SimpleCookie(headers['Cookie']) redacted = [] From 996483de94fe13b1f2783fb83314fa7b66209473 Mon Sep 17 00:00:00 2001 From: Simon Li Date: Thu, 27 Feb 2020 17:35:52 +0000 Subject: [PATCH 302/541] Pin sphinx theme (https://github.com/jupyterhub/binderhub/pull/1070) Closes https://github.com/jupyterhub/jupyterhub/issues/2955 --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 4934ec4d..4d3a03bd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,7 +3,7 @@ -r ../requirements.txt alabaster_jupyterhub autodoc-traits -git+https://github.com/pandas-dev/pandas-sphinx-theme.git@master +https://github.com/pandas-dev/pandas-sphinx-theme/archive/ade576c92cfe46a372b6783fb83f45c44fc67976.zip recommonmark>=0.6 sphinx-copybutton sphinx-jsonschema From a2e2b1d5122a5ca613c30a05d5b612574535bf6c Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Fri, 28 Feb 2020 12:01:56 +0100 Subject: [PATCH 303/541] As pointed out in the PR, Pagination isn't a Handler --- jupyterhub/handlers/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 3bb7b63a..8a7e2033 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -606,7 +606,7 @@ class HealthCheckHandler(BaseHandler): self.finish() -class Pagination(BaseHandler): +class Pagination(): _page_name = 'page' _per_page_name = 'per_page' From ede71db11af9dd2680fbce9d84ab670c869bde8a Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Fri, 28 Feb 2020 12:04:53 +0100 Subject: [PATCH 304/541] Moving Pagination class to its own file --- jupyterhub/handlers/pages.py | 65 +----------------------------------- jupyterhub/pagination.py | 65 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 64 deletions(-) create mode 100644 jupyterhub/pagination.py diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 8a7e2033..60b6ae50 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -23,6 +23,7 @@ from ..utils import admin_only from ..utils import maybe_future from ..utils import url_path_join from .base import BaseHandler +from ..pagination import Pagination class RootHandler(BaseHandler): @@ -605,70 +606,6 @@ class HealthCheckHandler(BaseHandler): def get(self, *args): self.finish() - -class Pagination(): - - _page_name = 'page' - _per_page_name = 'per_page' - _default_page = 1 - _default_per_page = 100 - _max_per_page = 250 - _record_name = 'users' - _display_msg = ( - 'Displaying {record_name} {start} - {end}. Total {record_name}: {total}' - ) - - def __init__(self, *args, **kwargs): - """Potential parameters. - **url**: URL in request - **page**: current page in use - **per_page**: number of records to display in the page. By default 100 - **total**: total records considered while paginating - **display_msg**: informative text for pagination - **record_name**: name of the record, showed in pagination info - """ - self.page = kwargs.get(self._page_name, 1) - - if self.per_page > self._max_per_page: - self.per_page = self._max_per_page - - self.total = int(kwargs.get('total', 0)) - self.display_msg = kwargs.get('display_msg', self._display_msg) - - self.record_name = kwargs.get('record_name', self._record_name) - self.url = kwargs.get('url') or self.get_url() - self.init_values() - - def init_values(self): - self._cached = {} - self.skip = (self.page - 1) * self.per_page - pages = divmod(self.total, self.per_page) - self.total_pages = pages[0] + 1 if pages[1] else pages[0] - - self.has_prev = self.page > 1 - self.has_next = self.page < self.total_pages - - @classmethod - def get_page_args(self, handler): - self.page = handler.get_argument(self._page_name, self._default_page) - self.per_page = handler.get_argument( - self._per_page_name, self._default_per_page - ) - try: - self.per_page = int(self.per_page) - if self.per_page > self._max_per_page: - self.per_page = self._max_per_page - except: - self.per_page = self._default_per_page - - try: - self.page = int(self.page) - except: - self.page = self._default_page - - return self.page, self.per_page, self.per_page * (self.page - 1) - - default_handlers = [ (r'/', RootHandler), (r'/home', HomeHandler), diff --git a/jupyterhub/pagination.py b/jupyterhub/pagination.py new file mode 100644 index 00000000..b1396cd8 --- /dev/null +++ b/jupyterhub/pagination.py @@ -0,0 +1,65 @@ +"""Basic class to manage pagination utils.""" +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +class Pagination(): + + _page_name = 'page' + _per_page_name = 'per_page' + _default_page = 1 + _default_per_page = 100 + _max_per_page = 250 + _record_name = 'users' + _display_msg = ( + 'Displaying {record_name} {start} - {end}. Total {record_name}: {total}' + ) + + def __init__(self, *args, **kwargs): + """Potential parameters. + **url**: URL in request + **page**: current page in use + **per_page**: number of records to display in the page. By default 100 + **total**: total records considered while paginating + **display_msg**: informative text for pagination + **record_name**: name of the record, showed in pagination info + """ + self.page = kwargs.get(self._page_name, 1) + + if self.per_page > self._max_per_page: + self.per_page = self._max_per_page + + self.total = int(kwargs.get('total', 0)) + self.display_msg = kwargs.get('display_msg', self._display_msg) + + self.record_name = kwargs.get('record_name', self._record_name) + self.url = kwargs.get('url') or self.get_url() + self.init_values() + + def init_values(self): + self._cached = {} + self.skip = (self.page - 1) * self.per_page + pages = divmod(self.total, self.per_page) + self.total_pages = pages[0] + 1 if pages[1] else pages[0] + + self.has_prev = self.page > 1 + self.has_next = self.page < self.total_pages + + @classmethod + def get_page_args(self, handler): + self.page = handler.get_argument(self._page_name, self._default_page) + self.per_page = handler.get_argument( + self._per_page_name, self._default_per_page + ) + try: + self.per_page = int(self.per_page) + if self.per_page > self._max_per_page: + self.per_page = self._max_per_page + except: + self.per_page = self._default_per_page + + try: + self.page = int(self.page) + except: + self.page = self._default_page + + return self.page, self.per_page, self.per_page * (self.page - 1) From 53927f04901f8d076ce3b49730cdbc0a44b109a8 Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Fri, 28 Feb 2020 12:05:50 +0100 Subject: [PATCH 305/541] Pre-commit fixes --- jupyterhub/handlers/pages.py | 3 ++- jupyterhub/pagination.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 60b6ae50..676bdf54 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -19,11 +19,11 @@ from .. import __version__ from .. import orm from ..metrics import SERVER_POLL_DURATION_SECONDS from ..metrics import ServerPollStatus +from ..pagination import Pagination from ..utils import admin_only from ..utils import maybe_future from ..utils import url_path_join from .base import BaseHandler -from ..pagination import Pagination class RootHandler(BaseHandler): @@ -606,6 +606,7 @@ class HealthCheckHandler(BaseHandler): def get(self, *args): self.finish() + default_handlers = [ (r'/', RootHandler), (r'/home', HomeHandler), diff --git a/jupyterhub/pagination.py b/jupyterhub/pagination.py index b1396cd8..8f16bbe0 100644 --- a/jupyterhub/pagination.py +++ b/jupyterhub/pagination.py @@ -2,7 +2,8 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -class Pagination(): + +class Pagination: _page_name = 'page' _per_page_name = 'per_page' From 392525571f2b0f5858e381b14c3f7c12aae509e0 Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Fri, 28 Feb 2020 12:14:59 +0100 Subject: [PATCH 306/541] Documenting get_page_args method --- jupyterhub/pagination.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/jupyterhub/pagination.py b/jupyterhub/pagination.py index 8f16bbe0..d961f478 100644 --- a/jupyterhub/pagination.py +++ b/jupyterhub/pagination.py @@ -47,6 +47,15 @@ class Pagination: @classmethod def get_page_args(self, handler): + """ + This method gets the arguments used in the webpage to configurate the pagination + In case of no arguments, it uses the default values from this class + + It returns: + - self.page: The page requested for paginating or the default value (1) + - self.per_page: The number of items to return in this page. By default 100 and no more than 250 + - self.per_page * (self.page - 1): The offset to consider when managing pagination via the ORM + """ self.page = handler.get_argument(self._page_name, self._default_page) self.per_page = handler.get_argument( self._per_page_name, self._default_per_page From e82c06cf93c5de13eedac7a5715d4b8f5ecf6b52 Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Fri, 28 Feb 2020 12:31:53 +0100 Subject: [PATCH 307/541] Removing display_msg and record name since it can be coded directly as they're needed in the templates --- jupyterhub/pagination.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/jupyterhub/pagination.py b/jupyterhub/pagination.py index d961f478..76d10342 100644 --- a/jupyterhub/pagination.py +++ b/jupyterhub/pagination.py @@ -10,10 +10,6 @@ class Pagination: _default_page = 1 _default_per_page = 100 _max_per_page = 250 - _record_name = 'users' - _display_msg = ( - 'Displaying {record_name} {start} - {end}. Total {record_name}: {total}' - ) def __init__(self, *args, **kwargs): """Potential parameters. @@ -21,8 +17,6 @@ class Pagination: **page**: current page in use **per_page**: number of records to display in the page. By default 100 **total**: total records considered while paginating - **display_msg**: informative text for pagination - **record_name**: name of the record, showed in pagination info """ self.page = kwargs.get(self._page_name, 1) @@ -30,9 +24,6 @@ class Pagination: self.per_page = self._max_per_page self.total = int(kwargs.get('total', 0)) - self.display_msg = kwargs.get('display_msg', self._display_msg) - - self.record_name = kwargs.get('record_name', self._record_name) self.url = kwargs.get('url') or self.get_url() self.init_values() From 8f46d89ac05c457a05f50696937e4416e06117f9 Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Fri, 28 Feb 2020 13:19:53 +0100 Subject: [PATCH 308/541] Adding info method to pagination and related items in admin template --- jupyterhub/pagination.py | 13 +++++++++++++ share/jupyterhub/templates/admin.html | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/jupyterhub/pagination.py b/jupyterhub/pagination.py index 76d10342..440e4e26 100644 --- a/jupyterhub/pagination.py +++ b/jupyterhub/pagination.py @@ -64,3 +64,16 @@ class Pagination: self.page = self._default_page return self.page, self.per_page, self.per_page * (self.page - 1) + + @property + def info(self): + """Get the pagination information.""" + start = 1 + (self.page - 1) * self.per_page + end = start + self.per_page - 1 + if end > self.total: + end = self.total + + if start > self.total: + start = self.total + + return {'total': self.total, 'start': start, 'end': end} diff --git a/share/jupyterhub/templates/admin.html b/share/jupyterhub/templates/admin.html index 0835d758..b98108de 100644 --- a/share/jupyterhub/templates/admin.html +++ b/share/jupyterhub/templates/admin.html @@ -18,7 +18,9 @@ {% block main %}
    - {{ pagination.info|safe }} +
    + Displaying users {{ pagination.info.start|safe }} - {{ pagination.info.end|safe }}. Total users {{ pagination.info.total|safe }} +
    From 1a2d5913eb3843e8d2a5fcb49fa57d356b434be9 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Fri, 28 Feb 2020 14:55:41 +0100 Subject: [PATCH 309/541] Add .vscode to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 90608475..2eb6e784 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,6 @@ MANIFEST .coverage.* htmlcov .idea/ +.vscode/ .pytest_cache pip-wheel-metadata From cfcd85a188ba40967a938a45cf85ae7fdad7d994 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Fri, 28 Feb 2020 15:17:16 +0100 Subject: [PATCH 310/541] Start named servers by pressing the Enter key --- share/jupyterhub/static/js/home.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/share/jupyterhub/static/js/home.js b/share/jupyterhub/static/js/home.js index e10ed7c3..0422b24f 100644 --- a/share/jupyterhub/static/js/home.js +++ b/share/jupyterhub/static/js/home.js @@ -50,6 +50,17 @@ require(["jquery", "moment", "jhapi", "utils"], function( } } + function startServer() { + var row = getRow($(this)); + var serverName = row.find(".new-server-name").val(); + if (serverName === "") { + // ../spawn/user/ causes a 404, ../spawn/user redirects correctly to the default server + window.location.href = "./spawn/" + user; + } else { + window.location.href = "./spawn/" + user + "/" + serverName; + } + } + function stopServer() { var row = getRow($(this)); var serverName = row.data("server-name"); @@ -100,14 +111,10 @@ require(["jquery", "moment", "jhapi", "utils"], function( }); }); - $(".new-server-btn").click(function() { - var row = getRow($(this)); - var serverName = row.find(".new-server-name").val(); - if (serverName === "") { - // ../spawn/user/ causes a 404, ../spawn/user redirects correctly to the default server - window.location.href = "./spawn/" + user; - } else { - window.location.href = "./spawn/" + user + "/" + serverName; + $(".new-server-btn").click(startServer); + $(".new-server-name").on('keypress', function(e) { + if (e.which === 13) { + startServer.call(this); } }); From 534e251f97ffc39ea21d63ecf75cac4f35800d74 Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Fri, 28 Feb 2020 17:15:19 +0100 Subject: [PATCH 311/541] Adding links generation inside the Pagination class --- jupyterhub/pagination.py | 86 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/jupyterhub/pagination.py b/jupyterhub/pagination.py index 440e4e26..95e5293b 100644 --- a/jupyterhub/pagination.py +++ b/jupyterhub/pagination.py @@ -77,3 +77,89 @@ class Pagination: start = self.total return {'total': self.total, 'start': start, 'end': end} + + def calculate_pages_window(self): + """Calculates the set of pages to render later in links() method. + It returns the list of pages to render via links for the pagination + By default, as we've observed in other applications, we're going to render + only a finite and predefined number of pages, avoiding visual fatigue related + to a long list of pages. By default, we render 7 pages plus some inactive links with the characters '...' + to point out that there are other pages that aren't explicitly rendered. + The primary way of work is to provide current webpage and 5 next pages, the last 2 ones + (in case the current page + 5 does not overflow the total lenght of pages) and the first one for reference. + """ + + self.separator_character = '...' + default_pages_to_render = 7 + after_page = 5 + before_end = 2 + + pages = [] + + if self.total_pages > default_pages_to_render: + if self.page > 1: + pages.extend([1, '...']) + + if self.total_pages < self.page + after_page: + pages.extend(list(range(self.page, self.total_pages))) + else: + if self.total_pages > self.page + after_page + before_end: + pages.extend(list(range(self.page, self.page + after_page))) + pages.append('...') + pages.extend( + list(range(self.total_pages - before_end, self.total_pages)) + ) + else: + pages.extend(list(range(self.page, self.page + after_page))) + + return pages + + else: + return list(range(1, self.total_pages)) + + @property + def links(self): + """Sets the links for the pagination. + Getting the input from calculate_pages_window(), generates the HTML code + for the pages to render, plus the arrows to go onwards and backwards (if needed). + """ + + pages_to_render = self.calculate_pages_window() + print(f"pages_to_render {pages_to_render} ") + + links = ['') + + return ''.join(links) From 492c5072b714c50a9e34460cd2ca8fc1a91d6e8c Mon Sep 17 00:00:00 2001 From: Juan Cruz-Benito Date: Fri, 28 Feb 2020 17:31:19 +0100 Subject: [PATCH 312/541] =?UTF-8?q?Removing=20print=20statements=20?= =?UTF-8?q?=F0=9F=A4=A6=E2=80=8D=E2=99=82=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jupyterhub/pagination.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jupyterhub/pagination.py b/jupyterhub/pagination.py index 95e5293b..3f82eb35 100644 --- a/jupyterhub/pagination.py +++ b/jupyterhub/pagination.py @@ -125,7 +125,6 @@ class Pagination: """ pages_to_render = self.calculate_pages_window() - print(f"pages_to_render {pages_to_render} ") links = [' {% endblock user_row %} + {% endfor %} {% endfor %} From e93cc83d58e39ab95ed5189f2733d3ea953e5830 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Wed, 15 Apr 2020 23:20:42 +0200 Subject: [PATCH 342/541] remove unused imports --- docs/source/conf.py | 1 - examples/external-oauth/whoami-oauth-basic.py | 2 -- examples/service-announcement/announcement.py | 1 - examples/service-whoami-flask/jupyterhub_config.py | 3 --- examples/service-whoami/jupyterhub_config.py | 1 - examples/service-whoami/whoami-oauth.py | 2 +- examples/service-whoami/whoami.py | 2 +- jupyterhub/apihandlers/groups.py | 1 - jupyterhub/apihandlers/proxy.py | 3 --- jupyterhub/crypto.py | 1 - jupyterhub/handlers/base.py | 3 --- jupyterhub/handlers/metrics.py | 1 - jupyterhub/handlers/pages.py | 4 ---- jupyterhub/oauth/provider.py | 3 --- jupyterhub/proxy.py | 1 - jupyterhub/spawner.py | 2 -- jupyterhub/tests/test_api.py | 1 - jupyterhub/tests/test_app.py | 2 -- jupyterhub/tests/test_auth.py | 1 - jupyterhub/tests/test_auth_expiry.py | 1 - jupyterhub/tests/test_dummyauth.py | 2 -- jupyterhub/tests/test_internal_ssl_app.py | 2 -- jupyterhub/tests/test_proxy.py | 1 - jupyterhub/tests/test_services.py | 5 ----- jupyterhub/tests/test_services_auth.py | 1 - jupyterhub/tests/test_singleuser.py | 2 -- jupyterhub/utils.py | 1 - setup.py | 1 - 28 files changed, 2 insertions(+), 49 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index fd0d4b92..bf6d7ed1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # import os -import shlex import sys # Set paths diff --git a/examples/external-oauth/whoami-oauth-basic.py b/examples/external-oauth/whoami-oauth-basic.py index ad98115c..2aca9f55 100644 --- a/examples/external-oauth/whoami-oauth-basic.py +++ b/examples/external-oauth/whoami-oauth-basic.py @@ -5,13 +5,11 @@ so all URLs and requests necessary for OAuth with JupyterHub should be in one pl """ import json import os -import sys from urllib.parse import urlencode from urllib.parse import urlparse from tornado import log from tornado import web -from tornado.auth import OAuth2Mixin from tornado.httpclient import AsyncHTTPClient from tornado.httpclient import HTTPRequest from tornado.httputil import url_concat diff --git a/examples/service-announcement/announcement.py b/examples/service-announcement/announcement.py index 2b140fdb..a0cf5964 100644 --- a/examples/service-announcement/announcement.py +++ b/examples/service-announcement/announcement.py @@ -4,7 +4,6 @@ import json import os from tornado import escape -from tornado import gen from tornado import ioloop from tornado import web diff --git a/examples/service-whoami-flask/jupyterhub_config.py b/examples/service-whoami-flask/jupyterhub_config.py index 54d3c736..63ddaa38 100644 --- a/examples/service-whoami-flask/jupyterhub_config.py +++ b/examples/service-whoami-flask/jupyterhub_config.py @@ -1,6 +1,3 @@ -import os -import sys - c.JupyterHub.services = [ { 'name': 'whoami', diff --git a/examples/service-whoami/jupyterhub_config.py b/examples/service-whoami/jupyterhub_config.py index d9ccd889..1d1f9435 100644 --- a/examples/service-whoami/jupyterhub_config.py +++ b/examples/service-whoami/jupyterhub_config.py @@ -1,4 +1,3 @@ -import os import sys c.JupyterHub.services = [ diff --git a/examples/service-whoami/whoami-oauth.py b/examples/service-whoami/whoami-oauth.py index c1a576c9..72c97dda 100644 --- a/examples/service-whoami/whoami-oauth.py +++ b/examples/service-whoami/whoami-oauth.py @@ -6,7 +6,6 @@ showing the user their own info. """ import json import os -from getpass import getuser from urllib.parse import urlparse from tornado.httpserver import HTTPServer @@ -25,6 +24,7 @@ class WhoAmIHandler(HubOAuthenticated, RequestHandler): # `getuser()` here would mean only the user who started the service # can access the service: + # from getpass import getuser # hub_users = {getuser()} @authenticated diff --git a/examples/service-whoami/whoami.py b/examples/service-whoami/whoami.py index 6dc56c9e..2a5a3373 100644 --- a/examples/service-whoami/whoami.py +++ b/examples/service-whoami/whoami.py @@ -4,7 +4,6 @@ This serves `/services/whoami/`, authenticated with the Hub, showing the user th """ import json import os -from getpass import getuser from urllib.parse import urlparse from tornado.httpserver import HTTPServer @@ -21,6 +20,7 @@ class WhoAmIHandler(HubAuthenticated, RequestHandler): # `getuser()` here would mean only the user who started the service # can access the service: + # from getpass import getuser # hub_users = {getuser()} @authenticated diff --git a/jupyterhub/apihandlers/groups.py b/jupyterhub/apihandlers/groups.py index 78e833f7..e6b439ba 100644 --- a/jupyterhub/apihandlers/groups.py +++ b/jupyterhub/apihandlers/groups.py @@ -3,7 +3,6 @@ # Distributed under the terms of the Modified BSD License. import json -from tornado import gen from tornado import web from .. import orm diff --git a/jupyterhub/apihandlers/proxy.py b/jupyterhub/apihandlers/proxy.py index 83901832..0a43583b 100644 --- a/jupyterhub/apihandlers/proxy.py +++ b/jupyterhub/apihandlers/proxy.py @@ -2,12 +2,9 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import json -from urllib.parse import urlparse -from tornado import gen from tornado import web -from .. import orm from ..utils import admin_only from .base import APIHandler diff --git a/jupyterhub/crypto.py b/jupyterhub/crypto.py index 57bd00d3..039201b1 100644 --- a/jupyterhub/crypto.py +++ b/jupyterhub/crypto.py @@ -6,7 +6,6 @@ from concurrent.futures import ThreadPoolExecutor from traitlets import Any from traitlets import default -from traitlets import Dict from traitlets import Integer from traitlets import List from traitlets import observe diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 9606fa61..f8ecf348 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -2,7 +2,6 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import asyncio -import copy import json import math import random @@ -27,14 +26,12 @@ from tornado.httputil import url_concat from tornado.ioloop import IOLoop from tornado.log import app_log from tornado.web import addslash -from tornado.web import MissingArgumentError from tornado.web import RequestHandler from .. import __version__ from .. import orm from ..metrics import PROXY_ADD_DURATION_SECONDS from ..metrics import PROXY_DELETE_DURATION_SECONDS -from ..metrics import ProxyAddStatus from ..metrics import ProxyDeleteStatus from ..metrics import RUNNING_SERVERS from ..metrics import SERVER_POLL_DURATION_SECONDS diff --git a/jupyterhub/handlers/metrics.py b/jupyterhub/handlers/metrics.py index f7a95b62..0f63d9c3 100644 --- a/jupyterhub/handlers/metrics.py +++ b/jupyterhub/handlers/metrics.py @@ -1,7 +1,6 @@ from prometheus_client import CONTENT_TYPE_LATEST from prometheus_client import generate_latest from prometheus_client import REGISTRY -from tornado import gen from ..utils import metrics_authentication from .base import BaseHandler diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 5cd1f4a7..ec378b9a 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -2,15 +2,12 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import asyncio -import codecs -import copy import time from collections import defaultdict from datetime import datetime from http.client import responses from jinja2 import TemplateNotFound -from tornado import gen from tornado import web from tornado.httputil import url_concat @@ -464,7 +461,6 @@ class AdminHandler(BaseHandler): users = self.db.query(orm.User).outerjoin(orm.Spawner).order_by(*ordered) users = [self._user_from_orm(u) for u in users] - from itertools import chain running = [] for u in users: diff --git a/jupyterhub/oauth/provider.py b/jupyterhub/oauth/provider.py index 6157223f..fd529cc9 100644 --- a/jupyterhub/oauth/provider.py +++ b/jupyterhub/oauth/provider.py @@ -3,15 +3,12 @@ implements https://oauthlib.readthedocs.io/en/latest/oauth2/server.html """ from datetime import datetime -from urllib.parse import urlparse from oauthlib import uri_validate from oauthlib.oauth2 import RequestValidator from oauthlib.oauth2 import WebApplicationServer from oauthlib.oauth2.rfc6749.grant_types import authorization_code from oauthlib.oauth2.rfc6749.grant_types import base -from sqlalchemy.orm import scoped_session -from tornado import web from tornado.escape import url_escape from tornado.log import app_log diff --git a/jupyterhub/proxy.py b/jupyterhub/proxy.py index cbe1cba3..0666bcda 100644 --- a/jupyterhub/proxy.py +++ b/jupyterhub/proxy.py @@ -24,7 +24,6 @@ import time from functools import wraps from subprocess import Popen from urllib.parse import quote -from urllib.parse import urlparse from tornado import gen from tornado.httpclient import AsyncHTTPClient diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index 4abc355b..56761516 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -4,8 +4,6 @@ Contains base Spawner class & default implementation # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import ast -import asyncio -import errno import json import os import pipes diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index aedb4426..47751621 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -1,5 +1,4 @@ """Tests for the REST API.""" -import asyncio import json import re import sys diff --git a/jupyterhub/tests/test_app.py b/jupyterhub/tests/test_app.py index bc59176b..bce50ae6 100644 --- a/jupyterhub/tests/test_app.py +++ b/jupyterhub/tests/test_app.py @@ -7,13 +7,11 @@ import time from subprocess import check_output from subprocess import PIPE from subprocess import Popen -from subprocess import run from tempfile import NamedTemporaryFile from tempfile import TemporaryDirectory from unittest.mock import patch import pytest -from tornado import gen from traitlets.config import Config from .. import orm diff --git a/jupyterhub/tests/test_auth.py b/jupyterhub/tests/test_auth.py index 10ae0b1a..a2fb98c5 100644 --- a/jupyterhub/tests/test_auth.py +++ b/jupyterhub/tests/test_auth.py @@ -1,7 +1,6 @@ """Tests for PAM authentication""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -import os from unittest import mock import pytest diff --git a/jupyterhub/tests/test_auth_expiry.py b/jupyterhub/tests/test_auth_expiry.py index 781c751b..48f85eb4 100644 --- a/jupyterhub/tests/test_auth_expiry.py +++ b/jupyterhub/tests/test_auth_expiry.py @@ -7,7 +7,6 @@ authentication can expire in a number of ways: - doesn't need refresh - needs refresh and cannot be refreshed without new login """ -import asyncio from contextlib import contextmanager from unittest import mock from urllib.parse import parse_qs diff --git a/jupyterhub/tests/test_dummyauth.py b/jupyterhub/tests/test_dummyauth.py index 3f34c343..dbeaf583 100644 --- a/jupyterhub/tests/test_dummyauth.py +++ b/jupyterhub/tests/test_dummyauth.py @@ -1,8 +1,6 @@ """Tests for dummy authentication""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -import pytest - from jupyterhub.auth import DummyAuthenticator diff --git a/jupyterhub/tests/test_internal_ssl_app.py b/jupyterhub/tests/test_internal_ssl_app.py index 95b382c4..cae0519b 100644 --- a/jupyterhub/tests/test_internal_ssl_app.py +++ b/jupyterhub/tests/test_internal_ssl_app.py @@ -1,8 +1,6 @@ """Test the JupyterHub entry point with internal ssl""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -import sys - import jupyterhub.tests.mocking from jupyterhub.tests.test_app import * diff --git a/jupyterhub/tests/test_proxy.py b/jupyterhub/tests/test_proxy.py index 0de5748b..e912bc62 100644 --- a/jupyterhub/tests/test_proxy.py +++ b/jupyterhub/tests/test_proxy.py @@ -2,7 +2,6 @@ import json import os from contextlib import contextmanager -from queue import Queue from subprocess import Popen from urllib.parse import quote from urllib.parse import urlparse diff --git a/jupyterhub/tests/test_services.py b/jupyterhub/tests/test_services.py index 248de1b1..127a9f45 100644 --- a/jupyterhub/tests/test_services.py +++ b/jupyterhub/tests/test_services.py @@ -2,18 +2,13 @@ import asyncio import os import sys -import time from binascii import hexlify from contextlib import contextmanager from subprocess import Popen -from threading import Event -import pytest -import requests from async_generator import async_generator from async_generator import asynccontextmanager from async_generator import yield_ -from tornado import gen from tornado.ioloop import IOLoop from ..utils import maybe_future diff --git a/jupyterhub/tests/test_services_auth.py b/jupyterhub/tests/test_services_auth.py index de4d73e9..a57ba871 100644 --- a/jupyterhub/tests/test_services_auth.py +++ b/jupyterhub/tests/test_services_auth.py @@ -11,7 +11,6 @@ from threading import Thread from unittest import mock from urllib.parse import urlparse -import pytest import requests import requests_mock from pytest import raises diff --git a/jupyterhub/tests/test_singleuser.py b/jupyterhub/tests/test_singleuser.py index 1bd08696..0e4d3bd7 100644 --- a/jupyterhub/tests/test_singleuser.py +++ b/jupyterhub/tests/test_singleuser.py @@ -3,8 +3,6 @@ import sys from subprocess import check_output from urllib.parse import urlparse -import pytest - import jupyterhub from ..utils import url_path_join from .mocking import public_url diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index 851b4d53..524fd093 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -444,7 +444,6 @@ def print_stacks(file=sys.stderr): # local imports because these will not be used often, # no need to add them to startup import asyncio - import resource import traceback from .log import coroutine_frames diff --git a/setup.py b/setup.py index 5a5f4176..dbd1f3d3 100755 --- a/setup.py +++ b/setup.py @@ -10,7 +10,6 @@ from __future__ import print_function import os import shutil import sys -from glob import glob from subprocess import check_call from setuptools import setup From 485c7b72c29ba8ff37ca43e55a9c38de20dea1f4 Mon Sep 17 00:00:00 2001 From: Josh Meek Date: Thu, 16 Apr 2020 09:36:52 -0400 Subject: [PATCH 343/541] Fix use of auxiliary verb on index.rst --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index c9722089..8d51a6bc 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,7 +7,7 @@ It can be used in a class of students, a corporate data science group or scienti research group. It is a multi-user **Hub** that spawns, manages, and proxies multiple instances of the single-user `Jupyter notebook`_ server. -To make life easier, JupyterHub have distributions. Be sure to +To make life easier, JupyterHub has distributions. Be sure to take a look at them before continuing with the configuration of the broad original system of `JupyterHub`_. Today, you can find two main cases: From 8648285375ef47f9504e6c5f06bde8918531ee04 Mon Sep 17 00:00:00 2001 From: Thijs Walcarius Date: Fri, 17 Apr 2020 10:00:25 +0200 Subject: [PATCH 344/541] Fix broken test due to BeautifulSoup 4.9.0 behavior change cfr. https://bugs.launchpad.net/beautifulsoup/+bug/1871335 --- jupyterhub/tests/test_pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index b9d6bba5..20f3ea45 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -354,7 +354,7 @@ async def test_spawn_pending(app, username, slow_spawn): assert page.find('div', {'class': 'progress'}) # validate event source url by consuming it - script = page.body.find('script').text + script = page.body.find('script').string assert 'EventSource' in script # find EventSource url in javascript # maybe not the most robust way to check this? From c234463a6765491c6a8baee10541ad67be28b468 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Thu, 16 Apr 2020 11:47:07 +0300 Subject: [PATCH 345/541] sphinx conf.py: update add_stylesheet -> add_css_file - Seems to be added in 1.0: https://www.sphinx-doc.org/en/latest/changes.html#release-1-0-jul-23-2010 --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index fd0d4b92..14b548ee 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -62,7 +62,7 @@ from recommonmark.transform import AutoStructify def setup(app): app.add_config_value('recommonmark_config', {'enable_eval_rst': True}, True) - app.add_stylesheet('custom.css') + app.add_css_file('custom.css') app.add_transform(AutoStructify) From e882e7954c0473d2941c0fbc7d5f8c8e04a8658a Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Thu, 16 Apr 2020 16:59:05 +0300 Subject: [PATCH 346/541] docs: use recommonmark as an extension - source_parsers deprecated in sphinx 3.0 - Since sphinx 1.4, it can (should) be used as a direct extension: https://github.com/readthedocs/recommonmark/pull/43 --- docs/source/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 14b548ee..64e4939a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,6 +20,7 @@ extensions = [ 'autodoc_traits', 'sphinx_copybutton', 'sphinx-jsonschema', + 'recommonmark', ] templates_path = ['_templates'] @@ -66,8 +67,6 @@ def setup(app): app.add_transform(AutoStructify) -source_parsers = {'.md': 'recommonmark.parser.CommonMarkParser'} - source_suffix = ['.rst', '.md'] # source_encoding = 'utf-8-sig' From 9d6e8e6b6fb02ca7952b158bf665b3ad8a8a0223 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Thu, 16 Apr 2020 11:45:35 +0300 Subject: [PATCH 347/541] Temporary patch autodoc-traits to fix build error [temporary] - This commit should be removed later after autodoc-traits is fixed upstream --- docs/requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index e0ea5aa4..472fcc28 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,9 @@ -r ../requirements.txt alabaster_jupyterhub -autodoc-traits +# Temporary fix of #3021. Revert back to released autodoc-traits when +# 0.1.0 released. +https://github.com/jupyterhub/autodoc-traits/archive/75885ee24636efbfebfceed1043459715049cd84.zip pydata-sphinx-theme recommonmark>=0.6 sphinx-copybutton From 6f2e409fb9bb4959310f26f2316bc4d4d228acc0 Mon Sep 17 00:00:00 2001 From: Thijs Walcarius Date: Mon, 6 Apr 2020 16:26:16 +0200 Subject: [PATCH 348/541] Allow bypassing of spawn form by calling options in query arguments of /spawn --- jupyterhub/handlers/pages.py | 26 ++++++++++++++++++++++++++ jupyterhub/tests/test_pages.py | 22 ++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 5cd1f4a7..6f1a43f2 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -173,6 +173,32 @@ class SpawnHandler(BaseHandler): auth_state = await user.get_auth_state() await spawner.run_auth_state_hook(auth_state) + # Try to start server directly when query arguments are passed. + form_options = {} + for key, byte_list in self.request.query_arguments.items(): + form_options[key] = [bs.decode('utf8') for bs in byte_list] + + # 'next' is reserved argument for redirect after spawn + form_options.pop('next', None) + + if len(form_options) > 0: + try: + self.log.debug( + "Triggering spawn with supplied query arguments for %s", + spawner._log_name, + ) + options = await maybe_future(spawner.options_from_form(form_options)) + pending_url = self._get_pending_url(user, server_name) + return await self._wrap_spawn_single_user( + user, server_name, spawner, pending_url, options + ) + except Exception as e: + self.log.error( + "Failed to spawn single-user server with query arguments", + exc_info=True, + ) + # fallback to behavior without failing query arguments + spawner_options_form = await spawner.get_options_form() if spawner_options_form: self.log.debug("Serving options form for %s", spawner._log_name) diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 20f3ea45..7ca4cb22 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -255,6 +255,28 @@ async def test_spawn_page_admin(app, admin_access): assert "Spawning server for {}".format(u.name) in r.text +async def test_spawn_with_query_arguments(app): + with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}): + base_url = ujoin(public_host(app), app.hub.base_url) + cookies = await app.login_user('jones') + orm_u = orm.User.find(app.db, 'jones') + u = app.users[orm_u] + await u.stop() + next_url = ujoin(app.base_url, 'user/jones/tree') + r = await async_requests.get( + url_concat( + ujoin(base_url, 'spawn'), {'next': next_url, 'energy': '510keV'}, + ), + cookies=cookies, + ) + r.raise_for_status() + assert r.history + assert u.spawner.user_options == { + 'energy': '510keV', + 'notspecified': 5, + } + + async def test_spawn_form(app): with mock.patch.dict(app.users.settings, {'spawner_class': FormSpawner}): base_url = ujoin(public_host(app), app.hub.base_url) From 24387664184846db36cc0a13f984b5d3a07212a8 Mon Sep 17 00:00:00 2001 From: Thijs Walcarius Date: Tue, 7 Apr 2020 13:56:32 +0200 Subject: [PATCH 349/541] Show error message when spawning via query-arguments failed. Add options_from_query function --- jupyterhub/handlers/pages.py | 16 ++++++++++------ jupyterhub/spawner.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 6f1a43f2..93c59fdb 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -174,20 +174,21 @@ class SpawnHandler(BaseHandler): await spawner.run_auth_state_hook(auth_state) # Try to start server directly when query arguments are passed. - form_options = {} + error_message = '' + query_options = {} for key, byte_list in self.request.query_arguments.items(): - form_options[key] = [bs.decode('utf8') for bs in byte_list] + query_options[key] = [bs.decode('utf8') for bs in byte_list] # 'next' is reserved argument for redirect after spawn - form_options.pop('next', None) + query_options.pop('next', None) - if len(form_options) > 0: + if len(query_options) > 0: try: self.log.debug( "Triggering spawn with supplied query arguments for %s", spawner._log_name, ) - options = await maybe_future(spawner.options_from_form(form_options)) + options = await maybe_future(spawner.options_from_query(query_options)) pending_url = self._get_pending_url(user, server_name) return await self._wrap_spawn_single_user( user, server_name, spawner, pending_url, options @@ -197,13 +198,16 @@ class SpawnHandler(BaseHandler): "Failed to spawn single-user server with query arguments", exc_info=True, ) + error_message = str(e) # fallback to behavior without failing query arguments spawner_options_form = await spawner.get_options_form() if spawner_options_form: self.log.debug("Serving options form for %s", spawner._log_name) form = await self._render_form( - for_user=user, spawner_options_form=spawner_options_form + for_user=user, + spawner_options_form=spawner_options_form, + message=error_message, ) self.finish(form) else: diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index 4abc355b..54875837 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -384,6 +384,37 @@ class Spawner(LoggingConfigurable): """ return form_data + def options_from_query(self, query_data): + """Interpret query arguments passed to /spawn + + Query arguments will always arrive as a dict of unicode strings. + Override this function to understand single-values, numbers, etc. + + By default, options_from_form is called from this function. You can however override + this function if you need to process the query arguments differently. + + This should coerce form data into the structure expected by self.user_options, + which must be a dict, and should be JSON-serializeable, + though it can contain bytes in addition to standard JSON data types. + + This method should not have any side effects. + Any handling of `user_options` should be done in `.start()` + to ensure consistent behavior across servers + spawned via the API and form submission page. + + Instances will receive this data on self.user_options, after passing through this function, + prior to `Spawner.start`. + + .. versionadded:: 1.2 + user_options are persisted in the JupyterHub database to be reused + on subsequent spawns if no options are given. + user_options is serialized to JSON as part of this persistence + (with additional support for bytes in case of uploaded file data), + and any non-bytes non-jsonable values will be replaced with None + if the user_options are re-used. + """ + return self.options_from_form(query_data) + user_options = Dict( help=""" Dict of user specified options for the user's spawned instance of a single-user server. From 6283e7ec837e25a80de3add190d11d4a9b46e9c3 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Wed, 15 Apr 2020 23:21:53 +0200 Subject: [PATCH 350/541] support kubespawner running on a IPv6 only cluster --- jupyterhub/app.py | 10 +++++++--- jupyterhub/objects.py | 13 +++++++++---- jupyterhub/services/service.py | 2 +- jupyterhub/user.py | 7 ++++++- jupyterhub/utils.py | 4 ++-- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 727b964f..50809e19 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -561,7 +561,11 @@ class JupyterHub(Application): def _url_part_changed(self, change): """propagate deprecated ip/port/base_url config to the bind_url""" urlinfo = urlparse(self.bind_url) - urlinfo = urlinfo._replace(netloc='%s:%i' % (self.ip, self.port)) + if ':' in self.ip: + fmt = '[%s]:%i' + else: + fmt = '%s:%i' + urlinfo = urlinfo._replace(netloc=fmt % (self.ip, self.port)) urlinfo = urlinfo._replace(path=self.base_url) bind_url = urlunparse(urlinfo) if bind_url != self.bind_url: @@ -727,10 +731,10 @@ class JupyterHub(Application): help="""The ip or hostname for proxies and spawners to use for connecting to the Hub. - Use when the bind address (`hub_ip`) is 0.0.0.0 or otherwise different + Use when the bind address (`hub_ip`) is 0.0.0.0, :: or otherwise different from the connect address. - Default: when `hub_ip` is 0.0.0.0, use `socket.gethostname()`, otherwise use `hub_ip`. + Default: when `hub_ip` is 0.0.0.0 or ::, use `socket.gethostname()`, otherwise use `hub_ip`. Note: Some spawners or proxy implementations might not support hostnames. Check your spawner or proxy documentation to see if they have extra requirements. diff --git a/jupyterhub/objects.py b/jupyterhub/objects.py index de2ac7a6..e5aa9c1c 100644 --- a/jupyterhub/objects.py +++ b/jupyterhub/objects.py @@ -53,7 +53,7 @@ class Server(HasTraits): Never used in APIs, only logging, since it can be non-connectable value, such as '', meaning all interfaces. """ - if self.ip in {'', '0.0.0.0'}: + if self.ip in {'', '0.0.0.0', '::'}: return self.url.replace(self._connect_ip, self.ip or '*', 1) return self.url @@ -87,13 +87,13 @@ class Server(HasTraits): """The address to use when connecting to this server When `ip` is set to a real ip address, the same value is used. - When `ip` refers to 'all interfaces' (e.g. '0.0.0.0'), + When `ip` refers to 'all interfaces' (e.g. '0.0.0.0' or '::'), clients connect via hostname by default. Setting `connect_ip` explicitly overrides any default behavior. """ if self.connect_ip: return self.connect_ip - elif self.ip in {'', '0.0.0.0'}: + elif self.ip in {'', '0.0.0.0', '::'}: # if listening on all interfaces, default to hostname for connect return socket.gethostname() else: @@ -149,7 +149,12 @@ class Server(HasTraits): if self.connect_url: parsed = urlparse(self.connect_url) return "{proto}://{host}".format(proto=parsed.scheme, host=parsed.netloc) - return "{proto}://{ip}:{port}".format( + + if ':' in self._connect_ip: + fmt = "{proto}://[{ip}]:{port}" + else: + fmt = "{proto}://{ip}:{port}" + return fmt.format( proto=self.proto, ip=self._connect_ip, port=self._connect_port ) diff --git a/jupyterhub/services/service.py b/jupyterhub/services/service.py index fbf1f331..dcd28946 100644 --- a/jupyterhub/services/service.py +++ b/jupyterhub/services/service.py @@ -342,7 +342,7 @@ class Service(LoggingConfigurable): env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url hub = self.hub - if self.hub.ip in ('0.0.0.0', ''): + if self.hub.ip in ('', '0.0.0.0', '::'): # if the Hub is listening on all interfaces, # tell services to connect via localhost # since they are always local subprocesses diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 71ceac4e..4f049817 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -566,7 +566,12 @@ class User: else: # >= 0.7 returns (ip, port) proto = 'https' if self.settings['internal_ssl'] else 'http' - url = '%s://%s:%i' % ((proto,) + url) + + # check if spawner returned an IPv6 address + if ':' in url[0]: + url = '%s://[%s]:%i' % ((proto,) + url) + else: + url = '%s://%s:%i' % ((proto,) + url) urlinfo = urlparse(url) server.proto = urlinfo.scheme server.ip = urlinfo.hostname diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index 851b4d53..82bcc0c8 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -66,7 +66,7 @@ def can_connect(ip, port): Return True if we can connect, False otherwise. """ - if ip in {'', '0.0.0.0'}: + if ip in {'', '0.0.0.0', '::'}: ip = '127.0.0.1' try: socket.create_connection((ip, port)).close() @@ -179,7 +179,7 @@ async def exponential_backoff( async def wait_for_server(ip, port, timeout=10): """Wait for any server to show up at ip:port.""" - if ip in {'', '0.0.0.0'}: + if ip in {'', '0.0.0.0', '::'}: ip = '127.0.0.1' await exponential_backoff( lambda: can_connect(ip, port), From 17dde3a2a9d14b836ad7774b5b755a3391279ff6 Mon Sep 17 00:00:00 2001 From: Michael Blackmon Date: Mon, 20 Apr 2020 10:38:19 -0400 Subject: [PATCH 351/541] remove margin styling from submit button --- share/jupyterhub/static/less/login.less | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/share/jupyterhub/static/less/login.less b/share/jupyterhub/static/less/login.less index d0040908..f5ca78e5 100644 --- a/share/jupyterhub/static/less/login.less +++ b/share/jupyterhub/static/less/login.less @@ -6,7 +6,7 @@ .bg-warning(); padding:10px; } - + .service-login { text-align: center; display: table-cell; @@ -27,9 +27,9 @@ } input[type=submit] { - margin-top: 16px; + margin-top: 0px; } - + .form-control:focus, input[type=submit]:focus { box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @jupyter-orange; border-color: @jupyter-orange; From 5fbd2838c9edbcc7bc40003597174bab30f50939 Mon Sep 17 00:00:00 2001 From: Michael Blackmon Date: Mon, 20 Apr 2020 10:39:57 -0400 Subject: [PATCH 352/541] add style class for feedback, widget and container --- share/jupyterhub/static/less/page.less | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/share/jupyterhub/static/less/page.less b/share/jupyterhub/static/less/page.less index 6193b357..328ffe7e 100644 --- a/share/jupyterhub/static/less/page.less +++ b/share/jupyterhub/static/less/page.less @@ -26,3 +26,19 @@ // .progress-log-event:hover { // background: rgba(66, 165, 245, 0.2); // } + + +.feedback { + &-container { + margin-top: 16px; + } + + &-widget { + padding: 5px 0px 0px 6px; + i { + font-size: 2em; + color: lightgrey; + } + } + +} From 42e7d1a3fbc2d997258a88f98716d80dc2aa6798 Mon Sep 17 00:00:00 2001 From: Michael Blackmon Date: Mon, 20 Apr 2020 10:59:34 -0400 Subject: [PATCH 353/541] put submit button & widget in feedback-container; extend template to include script block with form onsubmit handler --- share/jupyterhub/templates/spawn.html | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/share/jupyterhub/templates/spawn.html b/share/jupyterhub/templates/spawn.html index f1330b1f..d44bdd0d 100644 --- a/share/jupyterhub/templates/spawn.html +++ b/share/jupyterhub/templates/spawn.html @@ -23,9 +23,26 @@
    {{spawner_options_form | safe}}
    - + {% endblock %} + +{% block script %} +{{ super() }} + +{% endblock %} From 10bb5ef3c0ce25cb131aa134d89433bb4519f556 Mon Sep 17 00:00:00 2001 From: Michael Blackmon Date: Mon, 20 Apr 2020 11:00:40 -0400 Subject: [PATCH 354/541] wrap button & widget in feedback-container; add js block with onsubmit handler --- share/jupyterhub/templates/login.html | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/share/jupyterhub/templates/login.html b/share/jupyterhub/templates/login.html index 130339c9..cd585ded 100644 --- a/share/jupyterhub/templates/login.html +++ b/share/jupyterhub/templates/login.html @@ -56,13 +56,18 @@ tabindex="2" /> - + {% endif %} @@ -79,6 +84,11 @@ if (window.location.protocol === "http:") { var warning = document.getElementById('insecure-login-warning'); warning.className = warning.className.replace(/\bhidden\b/, ''); } +// setup onSubmit feedback +$('form').submit(() => { + $('.feedback-container>input').attr('disabled', true); + $('.feedback-container>*').toggleClass('hidden'); + $('.feedback-widget>*').toggleClass('fa-pulse'); +}); - {% endblock %} From debd2974943c03fadb1943ab6d8efb9634f3dff5 Mon Sep 17 00:00:00 2001 From: Michael Blackmon Date: Mon, 20 Apr 2020 11:33:38 -0400 Subject: [PATCH 355/541] restrict submit handler to only operate on targeted form --- share/jupyterhub/templates/login.html | 9 +++++---- share/jupyterhub/templates/spawn.html | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/share/jupyterhub/templates/login.html b/share/jupyterhub/templates/login.html index cd585ded..5c00d31d 100644 --- a/share/jupyterhub/templates/login.html +++ b/share/jupyterhub/templates/login.html @@ -85,10 +85,11 @@ if (window.location.protocol === "http:") { warning.className = warning.className.replace(/\bhidden\b/, ''); } // setup onSubmit feedback -$('form').submit(() => { - $('.feedback-container>input').attr('disabled', true); - $('.feedback-container>*').toggleClass('hidden'); - $('.feedback-widget>*').toggleClass('fa-pulse'); +$('form').submit((e) => { + var form = $(e.target); + form.find('.feedback-container>input').attr('disabled', true); + form.find('.feedback-container>*').toggleClass('hidden'); + form.find('.feedback-widget>*').toggleClass('fa-pulse'); }); {% endblock %} diff --git a/share/jupyterhub/templates/spawn.html b/share/jupyterhub/templates/spawn.html index d44bdd0d..773a55e7 100644 --- a/share/jupyterhub/templates/spawn.html +++ b/share/jupyterhub/templates/spawn.html @@ -39,10 +39,11 @@ {{ super() }} {% endblock %} From 137591f4582dfe470cb2c4e708d796caea16149a Mon Sep 17 00:00:00 2001 From: Will Starms Date: Fri, 1 May 2020 19:09:19 -0500 Subject: [PATCH 356/541] remove fixed position, causes Z ordering issues with the bottom of the users list --- share/jupyterhub/static/less/admin.less | 1 - 1 file changed, 1 deletion(-) diff --git a/share/jupyterhub/static/less/admin.less b/share/jupyterhub/static/less/admin.less index 6189e4e9..86dd6bcd 100644 --- a/share/jupyterhub/static/less/admin.less +++ b/share/jupyterhub/static/less/admin.less @@ -3,7 +3,6 @@ i.sort-icon { } .version_footer { - position: fixed; bottom: 0; width: 100%; } From da34c6cb34db6d677d3cebd0cadbc76f585fa5af Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 6 May 2020 10:44:53 +0200 Subject: [PATCH 357/541] remove hardcoded path from pagination links allows pagination of other pages --- jupyterhub/pagination.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/jupyterhub/pagination.py b/jupyterhub/pagination.py index c64a3ded..7f2450ca 100644 --- a/jupyterhub/pagination.py +++ b/jupyterhub/pagination.py @@ -126,10 +126,12 @@ class Pagination: @property def links(self): - """Sets the links for the pagination. - Getting the input from calculate_pages_window(), generates the HTML code + """Get the links for the pagination. + Getting the input from calculate_pages_window(), generates the HTML code for the pages to render, plus the arrows to go onwards and backwards (if needed). """ + if self.total_pages == 1: + return [] pages_to_render = self.calculate_pages_window() @@ -139,9 +141,7 @@ class Pagination: if self.page > 1: prev_page = self.page - 1 links.append( - '
  • «
  • '.format( - prev_page=prev_page - ) + '
  • «
  • '.format(prev_page=prev_page) ) else: links.append( @@ -161,17 +161,13 @@ class Pagination: ) else: links.append( - '
  • {page}
  • '.format( - page=page - ) + '
  • {page}
  • '.format(page=page) ) if self.page >= 1 and self.page < self.total_pages: next_page = self.page + 1 links.append( - '
  • »
  • '.format( - next_page=next_page - ) + '
  • »
  • '.format(next_page=next_page) ) else: links.append( From 0d245fe4e4979ac3aae9f8b801ea3537b9111ccf Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 6 May 2020 10:47:08 +0200 Subject: [PATCH 358/541] move pagination info next to pagination links at the bottom --- share/jupyterhub/static/less/admin.less | 4 ++++ share/jupyterhub/templates/admin.html | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/share/jupyterhub/static/less/admin.less b/share/jupyterhub/static/less/admin.less index 6189e4e9..88e36fd0 100644 --- a/share/jupyterhub/static/less/admin.less +++ b/share/jupyterhub/static/less/admin.less @@ -2,6 +2,10 @@ i.sort-icon { margin-left: 4px; } +tr.pagination-row > td.pagination-page-info { + vertical-align: middle; +} + .version_footer { position: fixed; bottom: 0; diff --git a/share/jupyterhub/templates/admin.html b/share/jupyterhub/templates/admin.html index b98108de..a13ba6c8 100644 --- a/share/jupyterhub/templates/admin.html +++ b/share/jupyterhub/templates/admin.html @@ -18,9 +18,6 @@ {% block main %}
    -
    - Displaying users {{ pagination.info.start|safe }} - {{ pagination.info.end|safe }}. Total users {{ pagination.info.total|safe }} -
    @@ -104,16 +101,19 @@ {% endfor %} {% endfor %} + + + + + +
    + {% if pagination.links %} + + {% endif %} + + Displaying users {{ pagination.info.start|safe }} - {{ pagination.info.end|safe }} of {{ pagination.info.total|safe }} +
    - {% if pagination.links %} - - - - - - - - {% endif %}
    {% else %} - +
    Sign in
    From 6e988bf58781c65f559d32393cdf66633b71a1b1 Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 24 Jun 2020 13:29:42 +0200 Subject: [PATCH 395/541] call it allowed_users be clearer since it's users vs groups, etc. --- .../authenticators-users-basics.md | 16 ++--- docs/source/reference/config-ghoauth.md | 2 +- jupyterhub/app.py | 29 +++++---- jupyterhub/auth.py | 64 ++++++++++--------- jupyterhub/tests/test_app.py | 2 +- jupyterhub/tests/test_auth.py | 44 ++++++++----- 6 files changed, 85 insertions(+), 72 deletions(-) diff --git a/docs/source/getting-started/authenticators-users-basics.md b/docs/source/getting-started/authenticators-users-basics.md index a2647976..ec078fed 100644 --- a/docs/source/getting-started/authenticators-users-basics.md +++ b/docs/source/getting-started/authenticators-users-basics.md @@ -7,20 +7,20 @@ with an account and password on the system will be allowed to login. ## Create a set of allowed users You can restrict which users are allowed to login with a set, -`Authenticator.allowed`: +`Authenticator.allowed_users`: ```python -c.Authenticator.allowed = {'mal', 'zoe', 'inara', 'kaylee'} +c.Authenticator.allowed_users = {'mal', 'zoe', 'inara', 'kaylee'} ``` -Users in the allowed set are added to the Hub database when the Hub is +Users in the `allowed_users` set are added to the Hub database when the Hub is started. ## Configure admins (`admin_users`) Admin users of JupyterHub, `admin_users`, can add and remove users from -the user `allowed` set. `admin_users` can take actions on other users' +the user `allowed_users` set. `admin_users` can take actions on other users' behalf, such as stopping and restarting their servers. A set of initial admin users, `admin_users` can configured be as follows: @@ -28,7 +28,7 @@ A set of initial admin users, `admin_users` can configured be as follows: ```python c.Authenticator.admin_users = {'mal', 'zoe'} ``` -Users in the admin set are automatically added to the user `allowed` set, +Users in the admin set are automatically added to the user `allowed_users` set, if they are not already present. Each authenticator may have different ways of determining whether a user is an @@ -53,12 +53,12 @@ sure your users know if admin_access is enabled.** Users can be added to and removed from the Hub via either the admin panel or the REST API. When a user is **added**, the user will be -automatically added to the allowed set and database. Restarting the Hub -will not require manually updating the allowed set in your config file, +automatically added to the allowed users set and database. Restarting the Hub +will not require manually updating the allowed users set in your config file, as the users will be loaded from the database. After starting the Hub once, it is not sufficient to **remove** a user -from the allowed set in your config file. You must also remove the user +from the allowed users set in your config file. You must also remove the user from the Hub's database, either by deleting the user from JupyterHub's admin page, or you can clear the `jupyterhub.sqlite` database and start fresh. diff --git a/docs/source/reference/config-ghoauth.md b/docs/source/reference/config-ghoauth.md index 4183ddec..6ec46e1c 100644 --- a/docs/source/reference/config-ghoauth.md +++ b/docs/source/reference/config-ghoauth.md @@ -52,7 +52,7 @@ c.GitHubOAuthenticator.oauth_callback_url = os.environ['OAUTH_CALLBACK_URL'] c.LocalAuthenticator.create_system_users = True # specify users and admin -c.Authenticator.allowed = {'rgbkrk', 'minrk', 'jhamrick'} +c.Authenticator.allowed_users = {'rgbkrk', 'minrk', 'jhamrick'} c.Authenticator.admin_users = {'jhamrick', 'rgbkrk'} # uses the default spawner diff --git a/jupyterhub/app.py b/jupyterhub/app.py index ba549aa4..4135f8f0 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -1689,22 +1689,22 @@ class JupyterHub(Application): # the admin_users config variable will never be used after this point. # only the database values will be referenced. - allowed = [ + allowed_users = [ self.authenticator.normalize_username(name) - for name in self.authenticator.allowed + for name in self.authenticator.allowed_users ] - self.authenticator.allowed = set(allowed) # force normalization - for username in allowed: + self.authenticator.allowed_users = set(allowed_users) # force normalization + for username in allowed_users: if not self.authenticator.validate_username(username): raise ValueError("username %r is not valid" % username) - if not allowed: + if not allowed_users: self.log.info( - "Not using allowed user list. Any authenticated user will be allowed." + "Not using allowed_users. Any authenticated user will be allowed." ) # add allowed users to the db - for name in allowed: + for name in allowed_users: user = orm.User.find(db, name) if user is None: user = orm.User(name=name) @@ -1714,9 +1714,9 @@ class JupyterHub(Application): db.commit() # Notify authenticator of all users. - # This ensures Authenticator.allowed is up-to-date with the database. - # This lets .allowed be used to set up initial list, - # but changes to the allowed list can occur in the database, + # This ensures Authenticator.allowed_users is up-to-date with the database. + # This lets .allowed_users be used to set up initial list, + # but changes to the allowed_users set can occur in the database, # and persist across sessions. total_users = 0 for user in db.query(orm.User): @@ -1753,9 +1753,9 @@ class JupyterHub(Application): user.created = user.last_activity or datetime.utcnow() db.commit() - # The allowed set and the users in the db are now the same. + # The allowed_users set and the users in the db are now the same. # From this point on, any user changes should be done simultaneously - # to the allowed set and user db, unless the allowed set is empty (all users allowed). + # to the allowed_users set and user db, unless the allowed set is empty (all users allowed). TOTAL_USERS.set(total_users) @@ -1773,7 +1773,7 @@ class JupyterHub(Application): await maybe_future(self.authenticator.check_allowed(username, None)) ): raise ValueError( - "Username %r is not in Authenticator.allowed" % username + "Username %r is not in Authenticator.allowed_users" % username ) user = orm.User.find(db, name=username) if user is None: @@ -1801,7 +1801,8 @@ class JupyterHub(Application): await maybe_future(self.authenticator.check_allowed(name, None)) ): raise ValueError( - "Token user name %r is not in Authenticator.allowed" % name + "Token user name %r is not in Authenticator.allowed_users" + % name ) if not self.authenticator.validate_username(name): raise ValueError("Token user name %r is not valid" % name) diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index cfc82541..dfbfab79 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -101,9 +101,9 @@ class Authenticator(LoggingConfigurable): """ ).tag(config=True) - whitelist = Set(help="Deprecated, use `Authenticator.allowed`", config=True,) + whitelist = Set(help="Deprecated, use `Authenticator.allowed_users`", config=True,) - allowed = Set( + allowed_users = Set( help=""" Set of usernames that are allowed to log in. @@ -114,11 +114,11 @@ class Authenticator(LoggingConfigurable): If empty, does not perform any additional restriction. .. versionchanged:: 1.2 - `Authenticator.whitelist` renamed to `allowed` + `Authenticator.whitelist` renamed to `allowed_users` """ ).tag(config=True) - blocked = Set( + blocked_users = Set( help=""" Set of usernames that are not allowed to log in. @@ -131,13 +131,13 @@ class Authenticator(LoggingConfigurable): .. versionadded: 0.9 .. versionchanged:: 1.2 - `Authenticator.blacklist` renamed to `blocked` + `Authenticator.blacklist` renamed to `blocked_users` """ ).tag(config=True) _deprecated_aliases = { - "whitelist": ("allowed", "1.2"), - "blacklist": ("blocked", "1.2"), + "whitelist": ("allowed_users", "1.2"), + "blacklist": ("blocked_users", "1.2"), } @observe(*list(_deprecated_aliases)) @@ -160,15 +160,15 @@ class Authenticator(LoggingConfigurable): ) setattr(self, new_attr, change.new) - @observe('allowed') - def _check_allowed(self, change): + @observe('allowed_users') + def _check_allowed_users(self, change): short_names = [name for name in change['new'] if len(name) <= 1] if short_names: sorted_names = sorted(short_names) single = ''.join(sorted_names) string_set_typo = "set('%s')" % single self.log.warning( - "Allowed list contains single-character names: %s; did you mean set([%r]) instead of %s?", + "Allowed set contains single-character names: %s; did you mean set([%r]) instead of %s?", sorted_names[:8], single, string_set_typo, @@ -301,7 +301,7 @@ class Authenticator(LoggingConfigurable): # with correct subclass override priority! for old_name, new_name in ( ('check_whitelist', 'check_allowed'), - ('check_blacklist', 'check_blocked'), + ('check_blacklist', 'check_blocked_users'), ('check_group_whitelist', 'check_allowed_groups'), ): old_method = getattr(self, old_name, None) @@ -399,7 +399,7 @@ class Authenticator(LoggingConfigurable): """Check if a username is allowed to authenticate based on configuration Return True if username is allowed, False otherwise. - No allowed set means any username is allowed. + No allowed_users set means any username is allowed. Names are normalized *before* being checked against the allowed set. @@ -409,12 +409,12 @@ class Authenticator(LoggingConfigurable): .. versionchanged:: 1.2 Renamed check_whitelist to check_allowed """ - if not self.allowed: + if not self.allowed_users: # No allowed set means any name is allowed return True - return username in self.allowed + return username in self.allowed_users - def check_blocked(self, username, authentication=None): + def check_blocked_users(self, username, authentication=None): """Check if a username is blocked to authenticate based on Authenticator.blocked configuration Return True if username is allowed, False otherwise. @@ -428,12 +428,12 @@ class Authenticator(LoggingConfigurable): Signature updated to accept authentication data as second argument .. versionchanged:: 1.2 - Renamed check_blacklist to check_blocked + Renamed check_blacklist to check_blocked_users """ - if not self.blocked: + if not self.blocked_users: # No block list means any name is allowed return True - return username not in self.blocked + return username not in self.blocked_users async def get_authenticated_user(self, handler, data): """Authenticate the user who is attempting to log in @@ -450,7 +450,7 @@ class Authenticator(LoggingConfigurable): The various stages can be overridden separately: - `authenticate` turns formdata into a username - `normalize_username` normalizes the username - - `check_allowed` checks against the user allowed + - `check_allowed` checks against the allowed usernames .. versionchanged:: 0.8 return dict instead of username @@ -475,7 +475,9 @@ class Authenticator(LoggingConfigurable): self.log.warning("Disallowing invalid username %r.", username) return - blocked_pass = await maybe_future(self.check_blocked(username, authenticated)) + blocked_pass = await maybe_future( + self.check_blocked_users(username, authenticated) + ) allowed_pass = await maybe_future(self.check_allowed(username, authenticated)) if blocked_pass: @@ -550,7 +552,7 @@ class Authenticator(LoggingConfigurable): It must return the username on successful authentication, and return None on failed authentication. - Checking allowed/blocked is handled separately by the caller. + Checking allowed_users/blocked_users is handled separately by the caller. .. versionchanged:: 0.8 Allow `authenticate` to return a dict containing auth_state. @@ -591,10 +593,10 @@ class Authenticator(LoggingConfigurable): This method may be a coroutine. - By default, this just adds the user to the allowed set. + By default, this just adds the user to the allowed_users set. Subclasses may do more extensive things, such as adding actual unix users, - but they should call super to ensure the allowed set is updated. + but they should call super to ensure the allowed_users set is updated. Note that this should be idempotent, since it is called whenever the hub restarts for all users. @@ -604,19 +606,19 @@ class Authenticator(LoggingConfigurable): """ if not self.validate_username(user.name): raise ValueError("Invalid username: %s" % user.name) - if self.allowed: - self.allowed.add(user.name) + if self.allowed_users: + self.allowed_users.add(user.name) def delete_user(self, user): """Hook called when a user is deleted - Removes the user from the allowed set. - Subclasses should call super to ensure the allowed set is updated. + Removes the user from the allowed_users set. + Subclasses should call super to ensure the allowed_users set is updated. Args: user (User): The User wrapper object """ - self.allowed.discard(user.name) + self.allowed_users.discard(user.name) auto_login = Bool( False, @@ -709,7 +711,7 @@ import types # deprecate white/blacklist method names for _old_name, _new_name, _version in [ ("check_whitelist", "check_allowed", "1.2"), - ("check_blacklist", "check_blocked", "1.2"), + ("check_blacklist", "check_blocked_users", "1.2"), ]: setattr( Authenticator, _old_name, _deprecated_method(_old_name, _new_name, _version), @@ -788,9 +790,9 @@ class LocalAuthenticator(Authenticator): @observe('allowed_groups') def _allowed_groups_changed(self, change): """Log a warning if mutually exclusive user and group allowed sets are specified.""" - if self.allowed: + if self.allowed_users: self.log.warning( - "Ignoring Authenticator.allowed set because Authenticator.allowed_groups supplied!" + "Ignoring Authenticator.allowed_users set because Authenticator.allowed_groups supplied!" ) def check_allowed(self, username, authentication=None): diff --git a/jupyterhub/tests/test_app.py b/jupyterhub/tests/test_app.py index 55000bc7..e00c699e 100644 --- a/jupyterhub/tests/test_app.py +++ b/jupyterhub/tests/test_app.py @@ -93,7 +93,7 @@ def test_generate_config(): os.remove(cfg_file) assert cfg_file in out assert 'Spawner.cmd' in cfg_text - assert 'Authenticator.allowed' in cfg_text + assert 'Authenticator.allowed_users' in cfg_text async def test_init_tokens(request): diff --git a/jupyterhub/tests/test_auth.py b/jupyterhub/tests/test_auth.py index 0021b1da..51cd4c2a 100644 --- a/jupyterhub/tests/test_auth.py +++ b/jupyterhub/tests/test_auth.py @@ -141,7 +141,7 @@ async def test_pam_auth_admin_groups(): async def test_pam_auth_allowed(): - authenticator = MockPAMAuthenticator(allowed={'wash', 'kaylee'}) + authenticator = MockPAMAuthenticator(allowed_users={'wash', 'kaylee'}) authorized = await authenticator.get_authenticated_user( None, {'username': 'kaylee', 'password': 'kaylee'} ) @@ -186,41 +186,51 @@ async def test_pam_auth_blocked(): assert authorized['name'] == 'wash' # Blacklist basics - authenticator = MockPAMAuthenticator(blocked={'wash'}) + authenticator = MockPAMAuthenticator(blocked_users={'wash'}) authorized = await authenticator.get_authenticated_user( None, {'username': 'wash', 'password': 'wash'} ) assert authorized is None # User in both allowed and blocked: default deny. Make error someday? - authenticator = MockPAMAuthenticator(blocked={'wash'}, allowed={'wash', 'kaylee'}) + authenticator = MockPAMAuthenticator( + blocked_users={'wash'}, allowed_users={'wash', 'kaylee'} + ) authorized = await authenticator.get_authenticated_user( None, {'username': 'wash', 'password': 'wash'} ) assert authorized is None # User not in blocked set can log in - authenticator = MockPAMAuthenticator(blocked={'wash'}, allowed={'wash', 'kaylee'}) + authenticator = MockPAMAuthenticator( + blocked_users={'wash'}, allowed_users={'wash', 'kaylee'} + ) authorized = await authenticator.get_authenticated_user( None, {'username': 'kaylee', 'password': 'kaylee'} ) assert authorized['name'] == 'kaylee' # User in allowed, blocked irrelevent - authenticator = MockPAMAuthenticator(blocked={'mal'}, allowed={'wash', 'kaylee'}) + authenticator = MockPAMAuthenticator( + blocked_users={'mal'}, allowed_users={'wash', 'kaylee'} + ) authorized = await authenticator.get_authenticated_user( None, {'username': 'wash', 'password': 'wash'} ) assert authorized['name'] == 'wash' # User in neither list - authenticator = MockPAMAuthenticator(blocked={'mal'}, allowed={'wash', 'kaylee'}) + authenticator = MockPAMAuthenticator( + blocked_users={'mal'}, allowed_users={'wash', 'kaylee'} + ) authorized = await authenticator.get_authenticated_user( None, {'username': 'simon', 'password': 'simon'} ) assert authorized is None - authenticator = MockPAMAuthenticator(blocked=set(), allowed={'wash', 'kaylee'}) + authenticator = MockPAMAuthenticator( + blocked_users=set(), allowed_users={'wash', 'kaylee'} + ) authorized = await authenticator.get_authenticated_user( None, {'username': 'kaylee', 'password': 'kaylee'} ) @@ -256,7 +266,7 @@ async def test_pam_auth_no_such_group(): async def test_wont_add_system_user(): user = orm.User(name='lioness4321') - authenticator = auth.PAMAuthenticator(allowed={'mal'}) + authenticator = auth.PAMAuthenticator(allowed_users={'mal'}) authenticator.create_system_users = False with pytest.raises(KeyError): await authenticator.add_user(user) @@ -264,7 +274,7 @@ async def test_wont_add_system_user(): async def test_cant_add_system_user(): user = orm.User(name='lioness4321') - authenticator = auth.PAMAuthenticator(allowed={'mal'}) + authenticator = auth.PAMAuthenticator(allowed_users={'mal'}) authenticator.add_user_cmd = ['jupyterhub-fake-command'] authenticator.create_system_users = True @@ -290,7 +300,7 @@ async def test_cant_add_system_user(): async def test_add_system_user(): user = orm.User(name='lioness4321') - authenticator = auth.PAMAuthenticator(allowed={'mal'}) + authenticator = auth.PAMAuthenticator(allowed_users={'mal'}) authenticator.create_system_users = True authenticator.add_user_cmd = ['echo', '/home/USERNAME'] @@ -311,13 +321,13 @@ async def test_add_system_user(): async def test_delete_user(): user = orm.User(name='zoe') - a = MockPAMAuthenticator(allowed={'mal'}) + a = MockPAMAuthenticator(allowed_users={'mal'}) - assert 'zoe' not in a.allowed + assert 'zoe' not in a.allowed_users await a.add_user(user) - assert 'zoe' in a.allowed + assert 'zoe' in a.allowed_users a.delete_user(user) - assert 'zoe' not in a.allowed + assert 'zoe' not in a.allowed_users def test_urls(): @@ -472,10 +482,10 @@ def test_deprecated_config(caplog): log.name, logging.WARNING, 'Authenticator.whitelist is deprecated in JupyterHub 1.2, use ' - 'Authenticator.allowed instead', + 'Authenticator.allowed_users instead', ) ] - assert authenticator.allowed == {'user'} + assert authenticator.allowed_users == {'user'} def test_deprecated_methods(): @@ -496,7 +506,7 @@ def test_deprecated_config_subclass(): cfg.MyAuthenticator.whitelist = {'user'} with pytest.deprecated_call(): authenticator = MyAuthenticator(config=cfg) - assert authenticator.allowed == {'user'} + assert authenticator.allowed_users == {'user'} def test_deprecated_methods_subclass(): From cceb65203973126b9d0168bd13e6f8700e554164 Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 24 Jun 2020 20:19:44 +0200 Subject: [PATCH 396/541] TODO is TODONE Co-authored-by: Georgiana Elena --- jupyterhub/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index dfbfab79..48e02bda 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -297,7 +297,7 @@ class Authenticator(LoggingConfigurable): self._init_deprecated_methods() def _init_deprecated_methods(self): - # TODO: properly handle deprecated signature *and* name + # handles deprecated signature *and* name # with correct subclass override priority! for old_name, new_name in ( ('check_whitelist', 'check_allowed'), From 946ed844c5be53f03dbab741318537ffca5bcbfc Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Thu, 25 Jun 2020 19:41:46 +1200 Subject: [PATCH 397/541] Update jupyterhub/handlers/base.py Co-authored-by: Min RK --- jupyterhub/handlers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index b08a6ac7..75d0697d 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -678,7 +678,7 @@ class BaseHandler(RequestHandler): a list with "next" (to avoid redirect-loops) :rtype (str) """ - if not exclude: + if exclude is None: exclude = ['next'] if self.request.query: query_string = [ From ef4455bb677de153695b687e0dab5d4424c58742 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Wed, 24 Jun 2020 16:01:08 +1200 Subject: [PATCH 398/541] Closes #2182 display hamburger menu only if user variable is present (in responsive mode) --- share/jupyterhub/templates/page.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index bc701ce4..5953ac1c 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -106,12 +106,14 @@ {% endblock %} + {% if user %} + {% endif %}
    - {% call modal('Delete User', btn_class='btn-danger delete-button') %} Are you sure you want to delete user USER? @@ -175,6 +170,14 @@ {% endblock %} +{% block footer %} + +{% endblock %} + {% block script %} {{ super() }}