mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-09 19:13:03 +00:00
Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
aa132cade7 | ||
![]() |
dd35ffbe86 | ||
![]() |
8edcf8be81 | ||
![]() |
29b02b7bcb | ||
![]() |
0383bc27b2 | ||
![]() |
65d5102b49 | ||
![]() |
8a226e6f46 | ||
![]() |
0bd34e0a10 | ||
![]() |
186107d959 | ||
![]() |
91b07b7ea4 | ||
![]() |
f5b30fd2b4 | ||
![]() |
0234396c2c | ||
![]() |
a43d677ae4 | ||
![]() |
dcfe71e7f0 | ||
![]() |
5d41376c2e | ||
![]() |
dd083359ec | ||
![]() |
e6d54960ba | ||
![]() |
a9295bc5c2 | ||
![]() |
2015c701fa | ||
![]() |
3e9c18f50a | ||
![]() |
7cac874afc | ||
![]() |
a7b6bd8d32 | ||
![]() |
1649a98656 | ||
![]() |
ecbe51f60f | ||
![]() |
fed14abed3 | ||
![]() |
94978ea9e0 | ||
![]() |
bf6999e439 | ||
![]() |
020ee7378f | ||
![]() |
e4a0569961 | ||
![]() |
4ff525d5bd | ||
![]() |
37a31b01b2 | ||
![]() |
1604cb1b0b | ||
![]() |
45702ac18c | ||
![]() |
c81e9d60e4 | ||
![]() |
224865b894 | ||
![]() |
3b3bc8224b | ||
![]() |
c56dc2ea6f | ||
![]() |
62202bbb74 | ||
![]() |
7ba28c0207 | ||
![]() |
9392a29dad | ||
![]() |
72ab8f99ec | ||
![]() |
fcf32c7e50 | ||
![]() |
da451d6552 | ||
![]() |
662b1a4d4a | ||
![]() |
732adea997 | ||
![]() |
7e1dbf3515 | ||
![]() |
65b92ec246 | ||
![]() |
dc42ee4779 | ||
![]() |
c04441c1b2 |
@@ -2,6 +2,7 @@
|
||||
language: python
|
||||
sudo: false
|
||||
python:
|
||||
- 3.6-dev
|
||||
- 3.5
|
||||
- 3.4
|
||||
- 3.3
|
||||
|
@@ -131,11 +131,11 @@ Some examples, meant as illustration and testing of this concept:
|
||||
----
|
||||
|
||||
## Docker
|
||||
A ready to go [docker image for JupyterHub](https://hub.docker.com/r/jupyterhub/jupyterhub/) gives a straightforward deployment of JupyterHub.
|
||||
A starter [docker image for JupyterHub](https://hub.docker.com/r/jupyterhub/jupyterhub/) gives a baseline deployment of JupyterHub.
|
||||
|
||||
*Note: This `jupyterhub/jupyterhub` docker image is only an image for running the Hub service itself.
|
||||
It does not require the other Jupyter components, such as Notebook installation, which are needed by the single-user servers.
|
||||
To run the single-user servers, which may be on the same system as the Hub or not, Jupyter Notebook version 4 or greater must be installed.*
|
||||
**Important:** This `jupyterhub/jupyterhub` image contains only the Hub itself, with no configuration. In general, one needs
|
||||
to make a derivative image, with at least a `jupyterhub_config.py` setting up an Authenticator and/or a Spawner. To run the
|
||||
single-user servers, which may be on the same system as the Hub or not, Jupyter Notebook version 4 or greater must be installed.
|
||||
|
||||
#### Starting JupyterHub with docker
|
||||
The JupyterHub docker image can be started with the following command:
|
||||
|
@@ -8,7 +8,7 @@
|
||||
"author": "",
|
||||
"license": "BSD-3-Clause",
|
||||
"devDependencies": {
|
||||
"bootprint": "^0.8.5",
|
||||
"bootprint": "^0.10.0",
|
||||
"bootprint-openapi": "^0.17.0"
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,33 @@ command line for details.
|
||||
|
||||
## 0.7
|
||||
|
||||
### [0.7.2] - 2017-01-09
|
||||
|
||||
#### Added
|
||||
|
||||
- Support service environment variables and defaults in `jupyterhub-singleuser`
|
||||
for easier deployment of notebook servers as a Service.
|
||||
- Add `--group` parameter for deploying `jupyterhub-singleuser` as a Service with group authentication.
|
||||
- Include URL parameters when redirecting through `/user-redirect/`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix group authentication for HubAuthenticated services
|
||||
|
||||
### [0.7.1] - 2017-01-02
|
||||
|
||||
#### Added
|
||||
|
||||
- `Spawner.will_resume` for signaling that a single-user server is paused instead of stopped.
|
||||
This is needed for cases like `DockerSpawner.remove_containers = False`,
|
||||
where the first API token is re-used for subsequent spawns.
|
||||
- Warning on startup about single-character usernames,
|
||||
caused by common `set('string')` typo in config.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Removed spurious warning about empty `next_url`, which is AOK.
|
||||
|
||||
### [0.7.0] - 2016-12-2
|
||||
|
||||
#### Added
|
||||
@@ -118,8 +145,9 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
|
||||
First preview release
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.7.0...HEAD
|
||||
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.6.1...0.7.0
|
||||
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.7.1...HEAD
|
||||
[0.7.1]: https://github.com/jupyterhub/jupyterhub/compare/0.7.0...0.7.1
|
||||
[0.7.0]: https://github.com/jupyterhub/jupyterhub/compare/0.6.1...0.7.0
|
||||
[0.6.1]: https://github.com/jupyterhub/jupyterhub/compare/0.6.0...0.6.1
|
||||
[0.6.0]: https://github.com/jupyterhub/jupyterhub/compare/0.5.0...0.6.0
|
||||
[0.5]: https://github.com/jupyterhub/jupyterhub/compare/0.4.1...0.5.0
|
||||
|
@@ -405,6 +405,9 @@ You can restrict which users are allowed to login with `Authenticator.whitelist`
|
||||
c.Authenticator.whitelist = {'mal', 'zoe', 'inara', 'kaylee'}
|
||||
```
|
||||
|
||||
Users listed in the whitelist are added to the Hub database when the Hub is
|
||||
started.
|
||||
|
||||
### Managing Hub administrators
|
||||
|
||||
Admin users of JupyterHub have the ability to take actions on users' behalf,
|
||||
@@ -427,12 +430,17 @@ Note: additional configuration examples are provided in this guide's
|
||||
|
||||
### Add or remove users from the Hub
|
||||
|
||||
Users can be added and removed to the Hub via the admin panel or REST API. These users will be
|
||||
added to the whitelist and database. Restarting the Hub will not require manually updating the
|
||||
whitelist in your config file, as the users will be loaded from the database. This means that
|
||||
after starting the Hub once, it is not sufficient to remove users from the whitelist in your
|
||||
config file. You must also remove them from the database, either by discarding the database file,
|
||||
or via the admin UI.
|
||||
Users can be added to and removed from the Hub via either the admin panel or
|
||||
REST API.
|
||||
|
||||
If a user is **added**, the user will be automatically added to the whitelist
|
||||
and database. Restarting the Hub will not require manually updating the
|
||||
whitelist 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 whitelist in your config file. You must also remove the user from the Hub's
|
||||
database, either by deleting the user from the admin page, or you can clear
|
||||
the `jupyterhub.sqlite` database and start fresh.
|
||||
|
||||
The default `PAMAuthenticator` is one case of a special kind of authenticator, called a
|
||||
`LocalAuthenticator`, indicating that it manages users on the local system. When you add a user to
|
||||
|
@@ -67,4 +67,4 @@ Note: The Swagger specification is being renamed the [OpenAPI Initiative][].
|
||||
|
||||
[on swagger's petstore]: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default
|
||||
[OpenAPI Initiative]: https://www.openapis.org/
|
||||
[JupyterHub REST API]: ./api/index.html
|
||||
[JupyterHub REST API]: ./_static/rest-api/index.html
|
||||
|
@@ -54,7 +54,7 @@ If a service is also to be managed by the Hub, it has a few extra options:
|
||||
externally.
|
||||
- If a command is specified for launching the Service, the Service will
|
||||
be started and managed by the Hub.
|
||||
- `env: dict` - environment variables to add to the current env
|
||||
- `environment: dict` - additional environment variables for the Service.
|
||||
- `user: str` - the name of a system user to manage the Service. If
|
||||
unspecified, run as the same user as the Hub.
|
||||
|
||||
@@ -99,7 +99,7 @@ c.JupyterHub.services = [
|
||||
A Hub-Managed Service may also be configured with additional optional
|
||||
parameters, which describe the environment needed to start the Service process:
|
||||
|
||||
- `env: dict` - additional environment variables for the Service.
|
||||
- `environment: dict` - additional environment variables for the Service.
|
||||
- `user: str` - name of the user to run the server if different from the Hub.
|
||||
Requires Hub to be root.
|
||||
- `cwd: path` directory in which to run the Service, if different from the
|
||||
|
@@ -249,3 +249,26 @@ jupyter kernelspec list
|
||||
```bash
|
||||
jupyterhub --debug
|
||||
```
|
||||
|
||||
## Toree integration with HDFS rack awareness script
|
||||
|
||||
The Apache Toree kernel will an issue, when running with JupyterHub, if the standard HDFS
|
||||
rack awareness script is used. This will materialize in the logs as a repeated WARN:
|
||||
|
||||
```bash
|
||||
16/11/29 16:24:20 WARN ScriptBasedMapping: Exception running /etc/hadoop/conf/topology_script.py some.ip.address
|
||||
ExitCodeException exitCode=1: File "/etc/hadoop/conf/topology_script.py", line 63
|
||||
print rack
|
||||
^
|
||||
SyntaxError: Missing parentheses in call to 'print'
|
||||
|
||||
at `org.apache.hadoop.util.Shell.runCommand(Shell.java:576)`
|
||||
```
|
||||
|
||||
In order to resolve this issue, there are two potential options.
|
||||
|
||||
1. Update HDFS core-site.xml, so the parameter "net.topology.script.file.name" points to a custom
|
||||
script (e.g. /etc/hadoop/conf/custom_topology_script.py). Copy the original script and change the first line point
|
||||
to a python two installation (e.g. /usr/bin/python).
|
||||
2. In spark-env.sh add a Python 2 installation to your path (e.g. export PATH=/opt/anaconda2/bin:$PATH).
|
||||
|
||||
|
25
examples/service-notebook/README.md
Normal file
25
examples/service-notebook/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Running a shared notebook as a service
|
||||
|
||||
This directory contains two examples of running a shared notebook server as a service,
|
||||
one as a 'managed' service, and one as an external service with supervisor.
|
||||
|
||||
These examples require jupyterhub >= 0.7.2.
|
||||
|
||||
A single-user notebook server is run as a service,
|
||||
and uses groups to authenticate a collection of users with the Hub.
|
||||
|
||||
In these examples, a JupyterHub group `'shared'` is created,
|
||||
and a notebook server is spawned at `/services/shared-notebook`.
|
||||
Any user in the `'shared'` group will be able to access the notebook server at `/services/shared-notebook/`.
|
||||
|
||||
In both examples, you will want to select the name of the group,
|
||||
and the name of the shared-notebook service.
|
||||
|
||||
In the external example, some extra steps are required to set up supervisor:
|
||||
|
||||
1. select a system user to run the service. This is a user on the system, and does not need to be a Hub user. Add this to the user field in `shared-notebook.conf`, replacing `someuser`.
|
||||
2. generate a secret token for authentication, and replace the `super-secret` fields in `shared-notebook-service` and `jupyterhub_config.py`
|
||||
3. install `shared-notebook-service` somewhere on your system, and update `/path/to/shared-notebook-service` to the absolute path of this destination
|
||||
3. copy `shared-notebook.conf` to `/etc/supervisor/conf.d/`
|
||||
4. `supervisorctl reload`
|
||||
|
24
examples/service-notebook/external/jupyterhub_config.py
vendored
Normal file
24
examples/service-notebook/external/jupyterhub_config.py
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# our user list
|
||||
c.Authenticator.whitelist = [
|
||||
'minrk',
|
||||
'ellisonbg',
|
||||
'willingc',
|
||||
]
|
||||
|
||||
# ellisonbg and willingc have access to a shared server:
|
||||
|
||||
c.JupyterHub.load_groups = {
|
||||
'shared': [
|
||||
'ellisonbg',
|
||||
'willingc',
|
||||
]
|
||||
}
|
||||
|
||||
# start the notebook server as a service
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'shared-notebook',
|
||||
'url': 'http://127.0.0.1:9999',
|
||||
'api_token': 'super-secret',
|
||||
}
|
||||
]
|
9
examples/service-notebook/external/shared-notebook-service
vendored
Executable file
9
examples/service-notebook/external/shared-notebook-service
vendored
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash -l
|
||||
set -e
|
||||
|
||||
export JUPYTERHUB_API_TOKEN=super-secret
|
||||
export JUPYTERHUB_SERVICE_URL=http://127.0.0.1:9999
|
||||
export JUPYTERHUB_SERVICE_NAME=shared-notebook
|
||||
|
||||
jupyterhub-singleuser \
|
||||
--group='shared'
|
14
examples/service-notebook/external/shared-notebook.conf
vendored
Normal file
14
examples/service-notebook/external/shared-notebook.conf
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
[program:jupyterhub-shared-notebook]
|
||||
user=someuser
|
||||
command=bash -l /path/to/shared-notebook-service
|
||||
directory=/home/someuser
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=1
|
||||
exitcodes=0,2
|
||||
stopsignal=TERM
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/jupyterhub-service-shared-notebook.log
|
||||
stdout_logfile_maxbytes=1MB
|
||||
stdout_logfile_backups=10
|
||||
stdout_capture_maxbytes=1MB
|
32
examples/service-notebook/managed/jupyterhub_config.py
Normal file
32
examples/service-notebook/managed/jupyterhub_config.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# our user list
|
||||
c.Authenticator.whitelist = [
|
||||
'minrk',
|
||||
'ellisonbg',
|
||||
'willingc',
|
||||
]
|
||||
|
||||
# ellisonbg and willingc have access to a shared server:
|
||||
|
||||
c.JupyterHub.load_groups = {
|
||||
'shared': [
|
||||
'ellisonbg',
|
||||
'willingc',
|
||||
]
|
||||
}
|
||||
|
||||
service_name = 'shared-notebook'
|
||||
service_port = 9999
|
||||
group_name = 'shared'
|
||||
|
||||
# start the notebook server as a service
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': service_name,
|
||||
'url': 'http://127.0.0.1:{}'.format(service_port),
|
||||
'command': [
|
||||
'jupyterhub-singleuser',
|
||||
'--group=shared',
|
||||
'--debug',
|
||||
],
|
||||
}
|
||||
]
|
@@ -454,7 +454,7 @@ class JupyterHub(Application):
|
||||
'name': 'formgrader',
|
||||
'url': 'http://127.0.0.1:1234',
|
||||
'token': 'super-secret',
|
||||
'env':
|
||||
'environment':
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
@@ -56,6 +56,17 @@ class Authenticator(LoggingConfigurable):
|
||||
"""
|
||||
).tag(config=True)
|
||||
|
||||
@observe('whitelist')
|
||||
def _check_whitelist(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("whitelist contains single-character names: %s; did you mean set([%r]) instead of %s?",
|
||||
sorted_names[:8], single, string_set_typo,
|
||||
)
|
||||
|
||||
custom_html = Unicode(
|
||||
help="""
|
||||
HTML form to be overridden by authenticators if they want a custom authentication form.
|
||||
|
@@ -571,6 +571,9 @@ class UserRedirectHandler(BaseHandler):
|
||||
def get(self, path):
|
||||
user = self.get_current_user()
|
||||
url = url_path_join(user.url, path)
|
||||
if self.request.query:
|
||||
# FIXME: use urlunparse instead?
|
||||
url += '?' + self.request.query
|
||||
self.redirect(url)
|
||||
|
||||
|
||||
|
@@ -28,7 +28,7 @@ class RootHandler(BaseHandler):
|
||||
"""
|
||||
def get(self):
|
||||
next_url = self.get_argument('next', '')
|
||||
if not next_url.startswith('/'):
|
||||
if next_url and not next_url.startswith('/'):
|
||||
self.log.warning("Disallowing redirect outside JupyterHub: %r", next_url)
|
||||
next_url = ''
|
||||
if next_url and next_url.startswith(url_path_join(self.base_url, 'user/')):
|
||||
|
@@ -174,6 +174,7 @@ class HubAuth(Configurable):
|
||||
raise HTTPError(500, msg)
|
||||
|
||||
if r.status_code == 404:
|
||||
app_log.warning("No Hub user identified for request")
|
||||
data = None
|
||||
elif r.status_code == 403:
|
||||
app_log.error("I don't have permission to verify cookies, my auth token may have expired: [%i] %s", r.status_code, r.reason)
|
||||
@@ -186,6 +187,7 @@ class HubAuth(Configurable):
|
||||
raise HTTPError(500, "Failed to check authorization")
|
||||
else:
|
||||
data = r.json()
|
||||
app_log.debug("Received request from Hub user %s", data)
|
||||
self.cookie_cache[encrypted_cookie] = data
|
||||
return data
|
||||
|
||||
@@ -274,14 +276,18 @@ class HubAuthenticated(object):
|
||||
Returns:
|
||||
user_model (dict): The user model if the user should be allowed, None otherwise.
|
||||
"""
|
||||
name = user_model['name']
|
||||
if self.hub_users is None and self.hub_groups is None:
|
||||
# no whitelist specified, allow any authenticated Hub user
|
||||
app_log.debug("Allowing Hub user %s (all Hub users allowed)", name)
|
||||
return user_model
|
||||
name = user_model['name']
|
||||
if self.hub_users and name in self.hub_users:
|
||||
# user in whitelist
|
||||
app_log.debug("Allowing whitelisted Hub user %s", name)
|
||||
return user_model
|
||||
elif self.hub_groups and set(user_model['groups']).union(self.hub_groups):
|
||||
elif self.hub_groups and set(user_model['groups']).intersection(self.hub_groups):
|
||||
allowed_groups = set(user_model['groups']).intersection(self.hub_groups)
|
||||
app_log.debug("Allowing Hub user %s in group(s) %s", name, ','.join(sorted(allowed_groups)))
|
||||
# group in whitelist
|
||||
return user_model
|
||||
else:
|
||||
@@ -294,8 +300,12 @@ class HubAuthenticated(object):
|
||||
Returns:
|
||||
user_model (dict): The user model, if a user is identified, None if authentication fails.
|
||||
"""
|
||||
if hasattr(self, '_hub_auth_user_cache'):
|
||||
return self._hub_auth_user_cache
|
||||
user_model = self.hub_auth.get_user(self)
|
||||
if not user_model:
|
||||
self._hub_auth_user_cache = None
|
||||
return
|
||||
return self.check_hub_user(user_model)
|
||||
self._hub_auth_user_cache = self.check_hub_user(user_model)
|
||||
return self._hub_auth_user_cache
|
||||
|
||||
|
@@ -124,8 +124,8 @@ class Service(LoggingConfigurable):
|
||||
Only use this if the service should be a subprocess.
|
||||
If command is not specified, it is assumed to be managed
|
||||
by a
|
||||
- env: dict
|
||||
environment variables to add to the current env
|
||||
- environment: dict
|
||||
Additional environment variables for the service.
|
||||
- user: str
|
||||
The name of a system user to become.
|
||||
If unspecified, run as the same user as the Hub.
|
||||
|
67
jupyterhub/singleuser.py
Normal file → Executable file
67
jupyterhub/singleuser.py
Normal file → Executable file
@@ -5,6 +5,7 @@
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from jinja2 import ChoiceLoader, FunctionLoader
|
||||
|
||||
@@ -21,6 +22,7 @@ from traitlets import (
|
||||
Unicode,
|
||||
CUnicode,
|
||||
default,
|
||||
observe,
|
||||
validate,
|
||||
TraitError,
|
||||
)
|
||||
@@ -47,6 +49,11 @@ class HubAuthenticatedHandler(HubAuthenticated):
|
||||
@property
|
||||
def hub_users(self):
|
||||
return { self.settings['user'] }
|
||||
@property
|
||||
def hub_groups(self):
|
||||
if self.settings['group']:
|
||||
return { self.settings['group'] }
|
||||
return set()
|
||||
|
||||
|
||||
class JupyterHubLoginHandler(LoginHandler):
|
||||
@@ -76,6 +83,7 @@ class JupyterHubLogoutHandler(LogoutHandler):
|
||||
aliases = dict(notebook_aliases)
|
||||
aliases.update({
|
||||
'user' : 'SingleUserNotebookApp.user',
|
||||
'group': 'SingleUserNotebookApp.group',
|
||||
'cookie-name': 'HubAuth.cookie_name',
|
||||
'hub-prefix': 'SingleUserNotebookApp.hub_prefix',
|
||||
'hub-host': 'SingleUserNotebookApp.hub_host',
|
||||
@@ -118,6 +126,7 @@ def _exclude_home(path_list):
|
||||
if not p.startswith(home):
|
||||
yield p
|
||||
|
||||
|
||||
class SingleUserNotebookApp(NotebookApp):
|
||||
"""A Subclass of the regular NotebookApp that is aware of the parent multiuser context."""
|
||||
description = dedent("""
|
||||
@@ -131,12 +140,48 @@ class SingleUserNotebookApp(NotebookApp):
|
||||
version = __version__
|
||||
classes = NotebookApp.classes + [HubAuth]
|
||||
|
||||
user = CUnicode(config=True)
|
||||
def _user_changed(self, name, old, new):
|
||||
self.log.name = new
|
||||
hub_prefix = Unicode().tag(config=True)
|
||||
user = CUnicode().tag(config=True)
|
||||
group = CUnicode().tag(config=True)
|
||||
@observe('user')
|
||||
def _user_changed(self, change):
|
||||
self.log.name = change.new
|
||||
|
||||
hub_host = Unicode().tag(config=True)
|
||||
|
||||
hub_prefix = Unicode('/hub/').tag(config=True)
|
||||
@default('hub_prefix')
|
||||
def _hub_prefix_default(self):
|
||||
base_url = os.environ.get('JUPYTERHUB_BASE_URL') or '/'
|
||||
return base_url + 'hub/'
|
||||
|
||||
hub_api_url = Unicode().tag(config=True)
|
||||
@default('hub_api_url')
|
||||
def _hub_api_url_default(self):
|
||||
return os.environ.get('JUPYTERHUB_API_URL') or 'http://127.0.0.1:8081/hub/api'
|
||||
|
||||
# defaults for some configurables that may come from service env variables:
|
||||
@default('base_url')
|
||||
def _base_url_default(self):
|
||||
return os.environ.get('JUPYTERHUB_SERVICE_PREFIX') or '/'
|
||||
|
||||
@default('cookie_name')
|
||||
def _cookie_name_default(self):
|
||||
if os.environ.get('JUPYTERHUB_SERVICE_NAME'):
|
||||
# if I'm a service, use the services cookie name
|
||||
return 'jupyterhub-services'
|
||||
|
||||
@default('port')
|
||||
def _port_default(self):
|
||||
if os.environ.get('JUPYTERHUB_SERVICE_URL'):
|
||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||
return url.port
|
||||
|
||||
@default('ip')
|
||||
def _ip_default(self):
|
||||
if os.environ.get('JUPYTERHUB_SERVICE_URL'):
|
||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||
return url.hostname
|
||||
|
||||
aliases = aliases
|
||||
flags = flags
|
||||
|
||||
@@ -221,11 +266,18 @@ class SingleUserNotebookApp(NotebookApp):
|
||||
super(SingleUserNotebookApp, self).start()
|
||||
|
||||
def init_hub_auth(self):
|
||||
if not os.environ.get('JPY_API_TOKEN'):
|
||||
self.exit("JPY_API_TOKEN env is required to run jupyterhub-singleuser. Did you launch it manually?")
|
||||
api_token = None
|
||||
if os.getenv('JPY_API_TOKEN'):
|
||||
# Deprecated env variable (as of 0.7.2)
|
||||
api_token = os.environ.pop('JPY_API_TOKEN')
|
||||
if os.getenv('JUPYTERHUB_API_TOKEN'):
|
||||
api_token = os.environ.pop('JUPYTERHUB_API_TOKEN')
|
||||
|
||||
if not api_token:
|
||||
self.exit("JUPYTERHUB_API_TOKEN env is required to run jupyterhub-singleuser. Did you launch it manually?")
|
||||
self.hub_auth = HubAuth(
|
||||
parent=self,
|
||||
api_token=os.environ.pop('JPY_API_TOKEN'),
|
||||
api_token=api_token,
|
||||
api_url=self.hub_api_url,
|
||||
)
|
||||
|
||||
@@ -234,6 +286,7 @@ class SingleUserNotebookApp(NotebookApp):
|
||||
self.init_hub_auth()
|
||||
s = self.tornado_settings
|
||||
s['user'] = self.user
|
||||
s['group'] = self.group
|
||||
s['hub_prefix'] = self.hub_prefix
|
||||
s['hub_host'] = self.hub_host
|
||||
s['hub_auth'] = self.hub_auth
|
||||
|
@@ -52,6 +52,17 @@ class Spawner(LoggingConfigurable):
|
||||
authenticator = Any()
|
||||
api_token = Unicode()
|
||||
|
||||
will_resume = Bool(False,
|
||||
help="""Whether the Spawner will resume on next start
|
||||
|
||||
|
||||
Default is False where each launch of the Spawner will be a new instance.
|
||||
If True, an existing Spawner will resume instead of starting anew
|
||||
(e.g. resuming a Docker container),
|
||||
and API tokens in use when the Spawner stops will not be deleted.
|
||||
"""
|
||||
)
|
||||
|
||||
ip = Unicode('127.0.0.1',
|
||||
help="""
|
||||
The IP address (or hostname) the single-user server should listen on.
|
||||
@@ -179,7 +190,7 @@ class Spawner(LoggingConfigurable):
|
||||
Environment variables that end up in the single-user server's process come from 3 sources:
|
||||
- This `environment` configurable
|
||||
- The JupyterHub process' environment variables that are whitelisted in `env_keep`
|
||||
- Variables to establish contact between the single-user notebook and the hub (such as JPY_API_TOKEN)
|
||||
- Variables to establish contact between the single-user notebook and the hub (such as JUPYTERHUB_API_TOKEN)
|
||||
|
||||
The `enviornment` configurable should be set by JupyterHub administrators to add
|
||||
installation specific environment variables. It is a dict where the key is the name of the environment
|
||||
@@ -404,6 +415,8 @@ class Spawner(LoggingConfigurable):
|
||||
else:
|
||||
env[key] = value
|
||||
|
||||
env['JUPYTERHUB_API_TOKEN'] = self.api_token
|
||||
# deprecated (as of 0.7.2), for old versions of singleuser
|
||||
env['JPY_API_TOKEN'] = self.api_token
|
||||
|
||||
# Put in limit and guarantee info if they exist.
|
||||
|
@@ -101,7 +101,8 @@ def test_hub_auth():
|
||||
def test_hub_authenticated(request):
|
||||
auth = HubAuth(cookie_name='jubal')
|
||||
mock_model = {
|
||||
'name': 'jubalearly'
|
||||
'name': 'jubalearly',
|
||||
'groups': ['lions'],
|
||||
}
|
||||
cookie_url = url_path_join(auth.api_url, "authorizations/cookie", auth.cookie_name)
|
||||
good_url = url_path_join(cookie_url, "early")
|
||||
@@ -194,6 +195,25 @@ def test_hub_authenticated(request):
|
||||
assert r.status_code == 302
|
||||
assert auth.login_url in r.headers['Location']
|
||||
|
||||
# pass group whitelist
|
||||
TestHandler.hub_groups = {'lions'}
|
||||
r = requests.get('http://127.0.0.1:%i' % port,
|
||||
cookies={'jubal': 'early'},
|
||||
allow_redirects=False,
|
||||
)
|
||||
r.raise_for_status()
|
||||
assert r.status_code == 200
|
||||
|
||||
# no pass group whitelist
|
||||
TestHandler.hub_groups = {'tigers'}
|
||||
r = requests.get('http://127.0.0.1:%i' % port,
|
||||
cookies={'jubal': 'early'},
|
||||
allow_redirects=False,
|
||||
)
|
||||
r.raise_for_status()
|
||||
assert r.status_code == 302
|
||||
assert auth.login_url in r.headers['Location']
|
||||
|
||||
|
||||
def test_service_cookie_auth(app, mockservice_url):
|
||||
cookies = app.login_user('badger')
|
||||
|
@@ -234,6 +234,12 @@ class User(HasTraits):
|
||||
# prior to 0.7, spawners had to store this info in user.server themselves.
|
||||
# Handle < 0.7 behavior with a warning, assuming info was stored in db by the Spawner.
|
||||
self.log.warning("DEPRECATION: Spawner.start should return (ip, port) in JupyterHub >= 0.7")
|
||||
if spawner.api_token != api_token:
|
||||
# Spawner re-used an API token, discard the unused api_token
|
||||
orm_token = orm.APIToken.find(self.db, api_token)
|
||||
if orm_token is not None:
|
||||
self.db.delete(orm_token)
|
||||
self.db.commit()
|
||||
except Exception as e:
|
||||
if isinstance(e, gen.TimeoutError):
|
||||
self.log.warning("{user}'s server failed to start in {s} seconds, giving up".format(
|
||||
@@ -313,10 +319,13 @@ class User(HasTraits):
|
||||
if self.server:
|
||||
# cleanup server entry from db
|
||||
self.db.delete(self.server)
|
||||
self.server = None
|
||||
if not spawner.will_resume:
|
||||
# find and remove the API token if the spawner isn't
|
||||
# going to re-use it next time
|
||||
orm_token = orm.APIToken.find(self.db, api_token)
|
||||
if orm_token:
|
||||
self.db.delete(orm_token)
|
||||
self.server = None
|
||||
self.db.commit()
|
||||
finally:
|
||||
self.stop_pending = False
|
||||
|
@@ -6,7 +6,8 @@
|
||||
version_info = (
|
||||
0,
|
||||
7,
|
||||
0,
|
||||
2,
|
||||
# 'dev',
|
||||
)
|
||||
|
||||
__version__ = '.'.join(map(str, version_info))
|
||||
|
Reference in New Issue
Block a user