Compare commits

..

4 Commits

Author SHA1 Message Date
Min RK
e94f5e043a release 0.9.3 2018-09-12 09:46:02 +02:00
Min RK
5456fb6356 remove spurious print from keepalive code
and send keepalive every 8 seconds

to protect against possibly aggressive proxies dropping connections after 10 seconds of inactivity
2018-09-12 09:46:02 +02:00
Min RK
fb75b9a392 write needs no await 2018-09-11 16:42:29 +02:00
Min RK
90d341e6f7 changelog for 0.9.3
Mainly small fixes, but the token page could be completely broken

This release will include the spawner.handler addition,
but not the oauthlib change currently in master
2018-09-11 16:42:21 +02:00
192 changed files with 4963 additions and 11201 deletions

View File

@@ -1,5 +1,4 @@
[run] [run]
parallel = True
branch = False branch = False
omit = omit =
jupyterhub/tests/* jupyterhub/tests/*

View File

@@ -10,12 +10,13 @@
# E402: module level import not at top of file # E402: module level import not at top of file
# I100: Import statements are in the wrong order # I100: Import statements are in the wrong order
# I101: Imported names are in the wrong order. Should be # I101: Imported names are in the wrong order. Should be
ignore = E, C, W, F401, F403, F811, F841, E402, I100, I101, D400 ignore = E, C, W, F401, F403, F811, F841, E402, I100, I101
builtins = c, get_config
exclude = exclude =
.cache, .cache,
.github, .github,
docs, docs,
examples,
jupyterhub/alembic*, jupyterhub/alembic*,
onbuild, onbuild,
scripts, scripts,

2
.gitignore vendored
View File

@@ -21,8 +21,6 @@ share/jupyterhub/static/css/style.min.css.map
*.egg-info *.egg-info
MANIFEST MANIFEST
.coverage .coverage
.coverage.*
htmlcov htmlcov
.idea/ .idea/
.pytest_cache .pytest_cache
pip-wheel-metadata

View File

@@ -1,20 +0,0 @@
repos:
- repo: https://github.com/asottile/reorder_python_imports
rev: v1.3.5
hooks:
- id: reorder-python-imports
language_version: python3.6
- repo: https://github.com/ambv/black
rev: 18.9b0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.1.0
hooks:
- id: end-of-file-fixer
- id: check-json
- id: check-yaml
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: requirements-txt-fixer
- id: flake8

View File

@@ -17,7 +17,6 @@ services:
# installing dependencies # installing dependencies
before_install: before_install:
- set -e
- nvm install 6; nvm use 6 - nvm install 6; nvm use 6
- npm install - npm install
- npm install -g configurable-http-proxy - npm install -g configurable-http-proxy
@@ -34,52 +33,27 @@ before_install:
fi fi
install: install:
- pip install --upgrade pip - pip install --upgrade pip
- pip install --upgrade --pre -r dev-requirements.txt . - pip install --pre -r dev-requirements.txt .
- pip freeze - pip freeze
# running tests # running tests
script: script:
- | - |
# run tests # run tests
if [[ -z "$TEST" ]]; then set -e
pytest -v --maxfail=2 --cov=jupyterhub jupyterhub/tests pytest -v --maxfail=2 --cov=jupyterhub jupyterhub/tests
fi
- |
# run autoformat
if [[ "$TEST" == "lint" ]]; then
pre-commit run --all-files
fi
- | - |
# build docs # build docs
if [[ "$TEST" == "docs" ]]; then
pushd docs pushd docs
pip install --upgrade -r requirements.txt pip install -r requirements.txt
pip install --upgrade alabaster_jupyterhub
make html make html
popd popd
fi
after_success: after_success:
- codecov - codecov
after_failure:
- |
# point to auto-lint-fix
if [[ "$TEST" == "lint" ]]; then
echo "You can install pre-commit hooks to automatically run formatting"
echo "on each commit with:"
echo " pre-commit install"
echo "or you can run by hand on staged files with"
echo " pre-commit run"
echo "or after-the-fact on already committed files with"
echo " pre-commit run --all-files"
fi
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- python: 3.6
env: TEST=lint
- python: 3.6
env: TEST=docs
- python: 3.6 - python: 3.6
env: JUPYTERHUB_TEST_SUBDOMAIN_HOST=http://localhost.jovyan.org:8000 env: JUPYTERHUB_TEST_SUBDOMAIN_HOST=http://localhost.jovyan.org:8000
- python: 3.6 - python: 3.6

View File

@@ -1,102 +1,98 @@
# Contributing to JupyterHub # Contributing
Welcome! As a [Jupyter](https://jupyter.org) project, Welcome! As a [Jupyter](https://jupyter.org) project, we follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html).
you can follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html).
Make sure to also follow [Project Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md)
for a friendly and welcoming collaborative environment.
## Setting up a development environment
JupyterHub requires Python >= 3.5 and nodejs.
As a Python project, a development install of JupyterHub follows standard practices for the basics (steps 1-2).
1. clone the repo ## Set up your development system
For a development install, clone the [repository](https://github.com/jupyterhub/jupyterhub)
and then install from source:
```bash ```bash
git clone https://github.com/jupyterhub/jupyterhub git clone https://github.com/jupyterhub/jupyterhub
```
2. do a development install with pip
```bash
cd jupyterhub cd jupyterhub
python3 -m pip install --editable .
```
3. install the development requirements,
which include things like testing tools
```bash
python3 -m pip install -r dev-requirements.txt
```
4. install configurable-http-proxy with npm:
```bash
npm install -g configurable-http-proxy npm install -g configurable-http-proxy
pip3 install -r dev-requirements.txt -e .
``` ```
5. set up pre-commit hooks for automatic code formatting, etc.
### Troubleshooting a development install
If the `pip3 install` command fails and complains about `lessc` being
unavailable, you may need to explicitly install some additional JavaScript
dependencies:
npm install
This will fetch client-side JavaScript dependencies necessary to compile CSS.
You may also need to manually update JavaScript and CSS after some development
updates, with:
```bash ```bash
pre-commit install python3 setup.py js # fetch updated client-side js
python3 setup.py css # recompile CSS from LESS sources
``` ```
You can also invoke the pre-commit hook manually at any time with ## Running the test suite
We use [pytest](http://doc.pytest.org/en/latest/) for running tests.
1. Set up a development install as described above.
2. Set environment variable for `ASYNC_TEST_TIMEOUT` to 15 seconds:
```bash ```bash
pre-commit run export ASYNC_TEST_TIMEOUT=15
``` ```
## Contributing 3. Run tests.
JupyterHub has adopted automatic code formatting so you shouldn't To run all the tests:
need to worry too much about your code style.
As long as your code is valid,
the pre-commit hook should take care of how it should look.
You can invoke the pre-commit hook by hand at any time with:
```bash ```bash
pre-commit run pytest -v jupyterhub/tests
``` ```
which should run any autoformatting on your code To run an individual test file (i.e. `test_api.py`):
and tell you about any errors it couldn't fix automatically.
You may also install [black integration](https://github.com/ambv/black#editor-integration)
into your text editor to format code automatically.
If you have already committed files before setting up the pre-commit
hook with `pre-commit install`, you can fix everything up using
`pre-commit run --all-files`. You need to make the fixing commit
yourself after that.
## Testing
It's a good idea to write tests to exercise any new features,
or that trigger any bugs that you have fixed to catch regressions.
You can run the tests with:
```bash ```bash
pytest -v pytest -v jupyterhub/tests/test_api.py
``` ```
in the repo directory. If you want to just run certain tests, ### Troubleshooting tests
check out the [pytest docs](https://pytest.readthedocs.io/en/latest/usage.html)
for how pytest can be called. If you see test failures because of timeouts, you may wish to increase the
For instance, to test only spawner-related things in the REST API: `ASYNC_TEST_TIMEOUT` used by the
[pytest-tornado-plugin](https://github.com/eugeniy/pytest-tornado/blob/c79f68de2222eb7cf84edcfe28650ebf309a4d0c/README.rst#markers)
from the default of 5 seconds:
```bash ```bash
pytest -v -k spawn jupyterhub/tests/test_api.py export ASYNC_TEST_TIMEOUT=15
``` ```
The tests live in `jupyterhub/tests` and are organized roughly into: If you see many test errors and failures, double check that you have installed
`configurable-http-proxy`.
1. `test_api.py` tests the REST API ## Building the Docs locally
2. `test_pages.py` tests loading the HTML pages
and other collections of tests for different components. 1. Install the development system as described above.
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. 2. Install the dependencies for documentation:
```bash
python3 -m pip install -r docs/requirements.txt
```
3. Build the docs:
```bash
cd docs
make clean
make html
```
4. View the docs:
```bash
open build/html/index.html
```

View File

@@ -35,8 +35,8 @@ RUN apt-get -y update && \
ENV LANG C.UTF-8 ENV LANG C.UTF-8
# install Python + NodeJS with conda # install Python + NodeJS with conda
RUN wget -q https://repo.continuum.io/miniconda/Miniconda3-4.5.11-Linux-x86_64.sh -O /tmp/miniconda.sh && \ RUN wget -q https://repo.continuum.io/miniconda/Miniconda3-4.5.1-Linux-x86_64.sh -O /tmp/miniconda.sh && \
echo 'e1045ee415162f944b6aebfe560b8fee */tmp/miniconda.sh' | md5sum -c - && \ echo '0c28787e3126238df24c5d4858bd0744 */tmp/miniconda.sh' | md5sum -c - && \
bash /tmp/miniconda.sh -f -b -p /opt/conda && \ bash /tmp/miniconda.sh -f -b -p /opt/conda && \
/opt/conda/bin/conda install --yes -c conda-forge \ /opt/conda/bin/conda install --yes -c conda-forge \
python=3.6 sqlalchemy tornado jinja2 traitlets requests pip pycurl \ python=3.6 sqlalchemy tornado jinja2 traitlets requests pip pycurl \

View File

@@ -0,0 +1 @@

View File

@@ -12,12 +12,11 @@
[![PyPI](https://img.shields.io/pypi/v/jupyterhub.svg)](https://pypi.python.org/pypi/jupyterhub) [![PyPI](https://img.shields.io/pypi/v/jupyterhub.svg)](https://pypi.python.org/pypi/jupyterhub)
[![Documentation Status](https://readthedocs.org/projects/jupyterhub/badge/?version=latest)](https://jupyterhub.readthedocs.org/en/latest/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/jupyterhub/badge/?version=latest)](https://jupyterhub.readthedocs.org/en/latest/?badge=latest)
[![Documentation Status](http://readthedocs.org/projects/jupyterhub/badge/?version=0.7.2)](https://jupyterhub.readthedocs.io/en/0.7.2/?badge=0.7.2)
[![Build Status](https://travis-ci.org/jupyterhub/jupyterhub.svg?branch=master)](https://travis-ci.org/jupyterhub/jupyterhub) [![Build Status](https://travis-ci.org/jupyterhub/jupyterhub.svg?branch=master)](https://travis-ci.org/jupyterhub/jupyterhub)
[![Circle CI](https://circleci.com/gh/jupyterhub/jupyterhub.svg?style=shield&circle-token=b5b65862eb2617b9a8d39e79340b0a6b816da8cc)](https://circleci.com/gh/jupyterhub/jupyterhub) [![Circle CI](https://circleci.com/gh/jupyterhub/jupyterhub.svg?style=shield&circle-token=b5b65862eb2617b9a8d39e79340b0a6b816da8cc)](https://circleci.com/gh/jupyterhub/jupyterhub)
[![codecov.io](https://codecov.io/github/jupyterhub/jupyterhub/coverage.svg?branch=master)](https://codecov.io/github/jupyterhub/jupyterhub?branch=master) [![codecov.io](https://codecov.io/github/jupyterhub/jupyterhub/coverage.svg?branch=master)](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
[![GitHub](https://img.shields.io/badge/issue_tracking-github-blue.svg)](https://github.com/jupyterhub/jupyterhub/issues) [![Google Group](https://img.shields.io/badge/google-group-blue.svg)](https://groups.google.com/forum/#!forum/jupyter)
[![Discourse](https://img.shields.io/badge/help_forum-discourse-blue.svg)](https://discourse.jupyter.org/c/jupyterhub)
[![Gitter](https://img.shields.io/badge/social_chat-gitter-blue.svg)](https://gitter.im/jupyterhub/jupyterhub)
With [JupyterHub](https://jupyterhub.readthedocs.io) you can create a With [JupyterHub](https://jupyterhub.readthedocs.io) you can create a
**multi-user Hub** which spawns, manages, and proxies multiple instances of the **multi-user Hub** which spawns, manages, and proxies multiple instances of the
@@ -205,9 +204,6 @@ and the [`CONTRIBUTING.md`](CONTRIBUTING.md). The `CONTRIBUTING.md` file
explains how to set up a development installation, how to run the test suite, explains how to set up a development installation, how to run the test suite,
and how to contribute to documentation. and how to contribute to documentation.
For a high-level view of the vision and next directions of the project, see the
[JupyterHub community roadmap](docs/source/contributing/roadmap.md).
### A note about platform support ### A note about platform support
JupyterHub is supported on Linux/Unix based systems. JupyterHub is supported on Linux/Unix based systems.
@@ -242,8 +238,6 @@ our JupyterHub [Gitter](https://gitter.im/jupyterhub/jupyterhub) channel.
- [Documentation for Project Jupyter](http://jupyter.readthedocs.io/en/latest/index.html) | [PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf) - [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 website](https://jupyter.org)
JupyterHub follows the Jupyter [Community Guides](https://jupyter.readthedocs.io/en/latest/community/content-community.html).
--- ---
**[Technical Overview](#technical-overview)** | **[Technical Overview](#technical-overview)** |

View File

@@ -1,16 +1,19 @@
#!/usr/bin/env python #!/usr/bin/env python
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
""" """
bower-lite bower-lite
Since Bower's on its way out, Since Bower's on its way out,
stage frontend dependencies from node_modules into components stage frontend dependencies from node_modules into components
""" """
import json import json
import os import os
import shutil
from os.path import join from os.path import join
import shutil
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))

View File

@@ -21,7 +21,7 @@ esac
set -x set -x
for SUFFIX in '' _upgrade_072 _upgrade_081 _upgrade_094; do for SUFFIX in '' _upgrade_072 _upgrade_081; do
$SQL "DROP DATABASE jupyterhub${SUFFIX};" 2>/dev/null || true $SQL "DROP DATABASE jupyterhub${SUFFIX};" 2>/dev/null || true
$SQL "CREATE DATABASE jupyterhub${SUFFIX} ${EXTRA_CREATE};" $SQL "CREATE DATABASE jupyterhub${SUFFIX} ${EXTRA_CREATE};"
done done

View File

@@ -1,17 +1,14 @@
-r requirements.txt -r requirements.txt
mock
beautifulsoup4
codecov
cryptography
pytest-cov
pytest-tornado
pytest>=3.3
notebook
requests-mock
virtualenv
# temporary pin of attrs for jsonschema 0.3.0a1 # temporary pin of attrs for jsonschema 0.3.0a1
# seems to be a pip bug # seems to be a pip bug
attrs>=17.4.0 attrs>=17.4.0
beautifulsoup4
codecov
coverage
cryptography
html5lib # needed for beautifulsoup
mock
notebook
pre-commit
pytest-asyncio
pytest-cov
pytest>=3.3
requests-mock
virtualenv

View File

@@ -7,3 +7,5 @@ ENV LANG=en_US.UTF-8
USER nobody USER nobody
CMD ["jupyterhub"] CMD ["jupyterhub"]

View File

@@ -18,3 +18,4 @@ Dockerfile.alpine contains base image for jupyterhub. It does not work independ
* Use dummy authenticator for ease of testing. Update following in jupyterhub_config file * Use dummy authenticator for ease of testing. Update following in jupyterhub_config file
- c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator' - c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator'
- c.DummyAuthenticator.password = "your strong password" - c.DummyAuthenticator.password = "your strong password"

View File

@@ -15,11 +15,8 @@ dependencies:
- traitlets>=4.1 - traitlets>=4.1
- sphinx>=1.7 - sphinx>=1.7
- pip: - pip:
- entrypoints - python-oauth2
- oauthlib>=2.0 - recommonmark==0.4.0
- recommonmark==0.5.0
- async_generator - async_generator
- prometheus_client - prometheus_client
- attrs>=17.4.0 - attrs>=17.4.0
- sphinx-copybutton
- alabaster_jupyterhub

View File

@@ -1,7 +1,5 @@
# ReadTheDocs uses the `environment.yaml` so make sure to update that as well # ReadTheDocs uses the `environment.yaml` so make sure to update that as well
# if you change this file # if you change this file
-r ../requirements.txt -r ../requirements.txt
alabaster_jupyterhub
recommonmark==0.5.0
sphinx-copybutton
sphinx>=1.7 sphinx>=1.7
recommonmark==0.4.0

View File

@@ -89,7 +89,7 @@ paths:
post: post:
summary: Create multiple users summary: Create multiple users
parameters: parameters:
- name: body - name: data
in: body in: body
required: true required: true
schema: schema:
@@ -147,7 +147,7 @@ paths:
in: path in: path
required: true required: true
type: string type: string
- name: body - name: data
in: body in: body
required: true required: true
description: Updated user info. At least one key to be updated (name or admin) is required. description: Updated user info. At least one key to be updated (name or admin) is required.
@@ -176,60 +176,6 @@ paths:
responses: responses:
'204': '204':
description: The user has been deleted description: The user has been deleted
/users/{name}/activity:
post:
summary:
Notify Hub of activity for a given user.
description:
Notify the Hub of activity by the user,
e.g. accessing a service or (more likely)
actively using a server.
parameters:
- name: name
description: username
in: path
required: true
type: string
- body:
in: body
schema:
type: object
properties:
last_activity:
type: string
format: date-time
description: |
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:
'<server name>':
description: |
Activity for a single server.
type: object
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'
/users/{name}/server: /users/{name}/server:
post: post:
summary: Start a user's single-user notebook server summary: Start a user's single-user notebook server
@@ -239,15 +185,6 @@ paths:
in: path in: path
required: true required: true
type: string type: string
- options:
description: |
Spawn options can be passed as a JSON body
when spawning via the API instead of spawn form.
The structure of the options
will depend on the Spawner's configuration.
in: body
required: false
type: object
responses: responses:
'201': '201':
description: The user's notebook server has started description: The user's notebook server has started
@@ -280,15 +217,6 @@ paths:
in: path in: path
required: true required: true
type: string type: string
- options:
description: |
Spawn options can be passed as a JSON body
when spawning via the API instead of spawn form.
The structure of the options
will depend on the Spawner's configuration.
in: body
required: false
type: object
responses: responses:
'201': '201':
description: The user's notebook named-server has started description: The user's notebook named-server has started
@@ -307,13 +235,6 @@ paths:
in: path in: path
required: true required: true
type: string type: string
- name: remove
description: |
Whether to fully remove the server, rather than just stop it.
Removing a server deletes things like the state of the stopped server.
in: body
required: false
type: boolean
responses: responses:
'204': '204':
description: The user's notebook named-server has stopped description: The user's notebook named-server has stopped
@@ -424,7 +345,7 @@ paths:
in: path in: path
required: true required: true
type: string type: string
- name: body - name: data
in: body in: body
required: true required: true
description: The users to add to the group description: The users to add to the group
@@ -449,7 +370,7 @@ paths:
in: path in: path
required: true required: true
type: string type: string
- name: body - name: data
in: body in: body
required: true required: true
description: The users to remove from the group description: The users to remove from the group
@@ -507,7 +428,7 @@ paths:
summary: Notify the Hub about a new proxy summary: Notify the Hub about a new proxy
description: Notifies the Hub of a new proxy to use. description: Notifies the Hub of a new proxy to use.
parameters: parameters:
- name: body - name: data
in: body in: body
required: true required: true
description: Any values that have changed for the new proxy. All keys are optional. description: Any values that have changed for the new proxy. All keys are optional.

View File

@@ -1,159 +0,0 @@
.. _admin/upgrading:
====================
Upgrading JupyterHub
====================
JupyterHub offers easy upgrade pathways between minor versions. This
document describes how to do these upgrades.
If you are using :ref:`a JupyterHub distribution <index/distributions>`, you
should consult the distribution's documentation on how to upgrade. This
document is if you have set up your own JupyterHub without using a
distribution.
It is long because is pretty detailed! Most likely, upgrading
JupyterHub is painless, quick and with minimal user interruption.
Read the Changelog
==================
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
read the changelogs for those too!
Notify your users
=================
If you are using the default configuration where ``configurable-http-proxy``
is managed by JupyterHub, your users will see service disruption during
the upgrade process. You should notify them, and pick a time to do the
upgrade where they will be least disrupted.
If you are using a different proxy, or running ``configurable-http-proxy``
independent of JupyterHub, your users will be able to continue using notebook
servers they had already launched, but will not be able to launch new servers
nor sign in.
Backup database & config
========================
Before doing an upgrade, it is critical to back up:
#. Your JupyterHub database (sqlite by default, or MySQL / Postgres
if you used those). If you are using sqlite (the default), you
should backup the ``jupyterhub.sqlite`` file.
#. Your ``jupyterhub_config.py`` file.
#. Your user's home directories. This is unlikely to be affected directly by
a JupyterHub upgrade, but we recommend a backup since user data is very
critical.
Shutdown JupyterHub
===================
Shutdown the JupyterHub process. This would vary depending on how you
have set up JupyterHub to run. Most likely, it is using a process
supervisor of some sort (``systemd`` or ``supervisord`` or even ``docker``).
Use the supervisor specific command to stop the JupyterHub process.
Upgrade JupyterHub packages
===========================
There are two environments where the ``jupyterhub`` package is installed:
#. The *hub environment*, which is where the JupyterHub server process
runs. This is started with the ``jupyterhub`` command, and is what
people generally think of as JupyterHub.
#. The *notebook user environments*. This is where the user notebook
servers are launched from, and is probably custom to your own
installation. This could be just one environment (different from the
hub environment) that is shared by all users, one environment
per user, or same environment as the hub environment. The hub
launched the ``jupyterhub-singleuser`` command in this environment,
which in turn starts the notebook server.
You need to make sure the version of the ``jupyterhub`` package matches
in both these environments. If you installed ``jupyterhub`` with pip,
you can upgrade it with:
.. code-block:: bash
python3 -m pip install --upgrade jupyterhub==<version>
Where ``<version>`` is the version of JupyterHub you are upgrading to.
If you used ``conda`` to install ``jupyterhub``, you should upgrade it
with:
.. code-block:: bash
conda install -c conda-forge jupyterhub==<version>
Where ``<version>`` is the version of JupyterHub you are upgrading to.
You should also check for new releases of the authenticator & spawner you
are using. You might wish to upgrade those packages too along with JupyterHub,
or upgrade them separately.
Upgrade JupyterHub database
===========================
Once new packages are installed, you need to upgrade the JupyterHub
database. From the hub environment, in the same directory as your
``jupyterhub_config.py`` file, you should run:
.. code-block:: bash
jupyterhub upgrade-db
This should find the location of your database, and run necessary upgrades
for it.
SQLite database disadvantages
-----------------------------
SQLite has some disadvantages when it comes to upgrading JupyterHub. These
are:
- ``upgrade-db`` may not work, and you may need delete your database
and start with a fresh one.
- ``downgrade-db`` **will not** work if you want to rollback to an
earlier version, so backup the ``jupyterhub.sqlite`` file before
upgrading
What happens if I delete my database?
-------------------------------------
Losing the Hub database is often not a big deal. Information that
resides only in the Hub database includes:
- active login tokens (user cookies, service tokens)
- users added via JupyterHub UI, instead of config files
- info about running servers
If the following conditions are true, you should be fine clearing the
Hub database and starting over:
- users specified in config file, or login using an external
authentication provider (Google, GitHub, LDAP, etc)
- user servers are stopped during upgrade
- don't mind causing users to login again after upgrade
Start JupyterHub
================
Once the database upgrade is completed, start the ``jupyterhub``
process again.
#. Log-in and start the server to make sure things work as
expected.
#. Check the logs for any errors or deprecation warnings. You
might have to update your ``jupyterhub_config.py`` file to
deal with any deprecated options.
Congratulations, your JupyterHub has been upgraded!

View File

@@ -13,3 +13,4 @@ Module: :mod:`jupyterhub.app`
------------------- -------------------
.. autoconfigurable:: JupyterHub .. autoconfigurable:: JupyterHub

View File

@@ -26,7 +26,3 @@ Module: :mod:`jupyterhub.auth`
.. autoconfigurable:: PAMAuthenticator .. autoconfigurable:: PAMAuthenticator
:class:`DummyAuthenticator`
---------------------------
.. autoconfigurable:: DummyAuthenticator

View File

@@ -20,3 +20,4 @@ Module: :mod:`jupyterhub.proxy`
.. autoconfigurable:: ConfigurableHTTPProxy .. autoconfigurable:: ConfigurableHTTPProxy
:members: debug, auth_token, check_running_interval, api_url, command :members: debug, auth_token, check_running_interval, api_url, command

View File

@@ -14,3 +14,4 @@ Module: :mod:`jupyterhub.services.service`
.. autoconfigurable:: Service .. autoconfigurable:: Service
:members: name, admin, url, api_token, managed, kind, command, cwd, environment, user, oauth_client_id, server, prefix, proxy_spec :members: name, admin, url, api_token, managed, kind, command, cwd, environment, user, oauth_client_id, server, prefix, proxy_spec

View File

@@ -38,3 +38,4 @@ Module: :mod:`jupyterhub.services.auth`
-------------------------------- --------------------------------
.. autoclass:: HubOAuthCallbackHandler .. autoclass:: HubOAuthCallbackHandler

View File

@@ -13,9 +13,10 @@ Module: :mod:`jupyterhub.spawner`
---------------- ----------------
.. autoconfigurable:: Spawner .. autoconfigurable:: Spawner
:members: options_from_form, poll, start, stop, get_args, get_env, get_state, template_namespace, format_string, create_certs, move_certs :members: options_from_form, poll, start, stop, get_args, get_env, get_state, template_namespace, format_string
:class:`LocalProcessSpawner` :class:`LocalProcessSpawner`
---------------------------- ----------------------------
.. autoconfigurable:: LocalProcessSpawner .. autoconfigurable:: LocalProcessSpawner

View File

@@ -34,3 +34,4 @@ Module: :mod:`jupyterhub.user`
.. attribute:: spawner .. attribute:: spawner
The user's :class:`~.Spawner` instance. The user's :class:`~.Spawner` instance.

View File

@@ -7,156 +7,8 @@ command line for details.
## [Unreleased] ## [Unreleased]
## 1.0
### [1.0.0] 2018-03-XX
JupyterHub 1.0 is a major milestone for JupyterHub.
Huge thanks to the many people who have contributed to this release,
whether it was through discussion, testing, documentation, or development.
#### Major new features
- Support TLS encryption and authentication of all internal communication.
Spawners must implement `.move_certs` method to make certificates available
to the notebook server if it is not local to the Hub.
- There is now full UI support for managing named servers.
With named servers, each jupyterhub user may have access to more than one named server. For example, a professor may access a server named `research` and another named `teaching`.
![named servers on the home page](./images/named-servers-home.png)
- Authenticators can now expire and refresh authentication data by implementing
`Authenticator.refresh_user(user)`.
This allows things like OAuth data and access tokens to be refreshed.
When used together with `Authenticator.refresh_pre_spawn = True`,
auth refresh can be forced prior to Spawn,
allowing the Authenticator to *require* that authentication data is fresh
immediately before the user's server is launched.
```eval_rst
.. seealso::
- :meth:`.Authenticator.refresh_user`
- :meth:`.Spawner.create_certs`
- :meth:`.Spawner.move_certs`
```
#### New features
- allow custom spawners, authenticators, and proxies to register themselves via 'entry points', enabling more convenient configuration such as:
```python
c.JupyterHub.authenticator_class = 'github'
c.JupyterHub.spawner_class = 'docker'
c.JupyterHub.proxy_class = 'traefik_etcd'
```
- Spawners are passed the tornado Handler object that requested their spawn (as `self.handler`),
so they can do things like make decisions based on query arguments in the request.
- SimpleSpawner and DummyAuthenticator, which are useful for testing, have been merged into JupyterHub itself:
```python
# For testing purposes only. Should not be used in production.
c.JupyterHub.authenticator_class = 'dummy'
c.JupyterHub.spawner_class = 'simple'
```
These classes are **not** appropriate for production use. Only testing.
- Add health check endpoint at `/hub/health`
- Several prometheus metrics have been added (thanks to [Outreachy](https://www.outreachy.org/) applicants!)
- A new API for registering user activity.
To prepare for the addition of [alternate proxy implementations](https://github.com/jupyterhub/traefik-proxy),
responsibility for tracking activity is taken away from the proxy
and moved to the notebook server (which already has activity tracking features).
Activity is now tracked by pushing it to the Hub from user servers instead of polling the
proxy API.
- Dynamic `options_form` callables may now return an empty string
which will result in no options form being rendered.
- `Spawner.user_options` is persisted to the database to be re-used,
so that a server spawned once via the form can be re-spawned via the API
with the same options.
- Added `c.PAMAuthenticator.pam_normalize_username` option for round-tripping
usernames through PAM to retrieve the normalized form.
- Added `c.JupyterHub.named_server_limit_per_user` configuration to limit
the number of named servers each user can have.
The default is 0, for no limit.
- API requests to HubAuthenticated services (e.g. single-user servers)
may pass a token in the `Authorization` header,
matching authentication with the Hub API itself.
- Added `Authenticator.is_admin(handler, authentication)` method
and `Authenticator.admin_groups` configuration for automatically
determining that a member of a group should be considered an admin.
- New `c.Authenticator.post_auth_hook` configuration
that can be any callable of the form `async def hook(authenticator, handler, authentication=None):`.
This hook may transform the return value of `Authenticator.authenticate()`
and return a new authentication dictionary,
e.g. specifying admin privileges, group membership,
or custom white/blacklisting logic.
This hook is called *after* existing normalization and whitelist checking.
- `Spawner.options_from_form` may now be async
- Added `JupyterHub.shutdown_on_logout` option to trigger shutdown of a user's
servers when they log out.
#### Changes
- Authentication methods such as `check_whitelist` should now take an additional
`authentication` argument
that will be a dictionary (default: None) of authentication data,
as returned by `Authenticator.authenticate()`:
```python
def check_whitelist(self, username, authentication=None):
...
```
`authentication` should have a default value of None
for backward-compatibility with jupyterhub < 1.0.
- Prometheus metrics page is now authenticated.
Any authenticated user may see the prometheus metrics.
To disable prometheus authentication,
set `JupyterHub.authenticate_prometheus = False`.
- Visits to `/user/:name` no longer trigger an implicit launch of the user's server.
Instead, a page is shown indicating that the server is not running
with a link to request the spawn.
- API requests to `/user/:name` for a not-running server will have status 503 instead of 404.
- OAuth includes a confirmation page when attempting to visit another user's server,
so that users can choose to cancel authentication with the single-user server.
Confirmation is still skipped when accessing your own server.
#### Fixed
- Various fixes to improve Windows compatibility
(default Authenticator and Spawner still do not support Windows, but other Spawners may)
- Fixed compatibility with Oracle db
- Fewer redirects following a visit to the default `/` url
- Error when progress is requested before progress is ready
- Error when API requests are made to a not-running server without authentication
#### Development changes
There have been several changes to the development process that shouldn't
generally affect users of JupyterHub, but may affect contributors.
In general, see `CONTRIBUTING.md` for contribution info or ask if you have questions.
- JupyterHub has adopted `black` as a code autoformatter and `pre-commit`
as a tool for automatically running code formatting on commit.
This is meant to make it *easier* to contribute to JupyterHub,
so let us know if it's having the opposite effect.
- JupyterHub has switched its test suite to using `pytest-asyncio` from `pytest-tornado`.
- OAuth is now implemented internally using `oauthlib` instead of `python-oauth2`. This should have no effect on behavior.
## 0.9 ## 0.9
### [0.9.4] 2018-09-24
JupyterHub 0.9.4 is a small bugfix release.
- Fixes an issue that required all running user servers to be restarted
when performing an upgrade from 0.8 to 0.9.
- Fixes content-type for API endpoints back to `application/json`.
It was `text/html` in 0.9.0-0.9.3.
### [0.9.3] 2018-09-12 ### [0.9.3] 2018-09-12
JupyterHub 0.9.3 contains small bugfixes and improvements JupyterHub 0.9.3 contains small bugfixes and improvements
@@ -565,9 +417,7 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
First preview release First preview release
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.0.0...HEAD [Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.9.3...HEAD
[1.0.0]: https://github.com/jupyterhub/jupyterhub/compare/0.9.4...HEAD
[0.9.4]: https://github.com/jupyterhub/jupyterhub/compare/0.9.3...0.9.4
[0.9.3]: https://github.com/jupyterhub/jupyterhub/compare/0.9.2...0.9.3 [0.9.3]: https://github.com/jupyterhub/jupyterhub/compare/0.9.2...0.9.3
[0.9.2]: https://github.com/jupyterhub/jupyterhub/compare/0.9.1...0.9.2 [0.9.2]: https://github.com/jupyterhub/jupyterhub/compare/0.9.1...0.9.2
[0.9.1]: https://github.com/jupyterhub/jupyterhub/compare/0.9.0...0.9.1 [0.9.1]: https://github.com/jupyterhub/jupyterhub/compare/0.9.0...0.9.1

View File

@@ -1,8 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import sys
import os import os
import shlex import shlex
import sys
# For conversion from markdown to html
import recommonmark.parser
# Set paths # Set paths
sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('.'))
@@ -18,7 +21,6 @@ extensions = [
'sphinx.ext.intersphinx', 'sphinx.ext.intersphinx',
'sphinx.ext.napoleon', 'sphinx.ext.napoleon',
'autodoc_traits', 'autodoc_traits',
'sphinx_copybutton',
] ]
templates_path = ['_templates'] templates_path = ['_templates']
@@ -56,16 +58,6 @@ default_role = 'literal'
# -- Source ------------------------------------------------------------- # -- Source -------------------------------------------------------------
import recommonmark
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_transform(AutoStructify)
source_parsers = {'.md': 'recommonmark.parser.CommonMarkParser'} source_parsers = {'.md': 'recommonmark.parser.CommonMarkParser'}
source_suffix = ['.rst', '.md'] source_suffix = ['.rst', '.md']
@@ -74,10 +66,7 @@ source_suffix = ['.rst', '.md']
# -- Options for HTML output ---------------------------------------------- # -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. # The theme to use for HTML and HTML Help pages.
import alabaster_jupyterhub html_theme = 'alabaster'
html_theme = 'alabaster_jupyterhub'
html_theme_path = [alabaster_jupyterhub.get_html_theme_path()]
html_logo = '_static/images/logo/logo.png' html_logo = '_static/images/logo/logo.png'
html_favicon = '_static/images/logo/favicon.ico' html_favicon = '_static/images/logo/favicon.ico'

View File

@@ -1,25 +0,0 @@
.. _contributing/community:
================================
Community communication channels
================================
We use `Gitter <https://gitter.im>`_ for online, real-time text chat. The
primary channel for JupyterHub is `jupyterhub/jupyterhub <https://gitter.im/jupyterhub/jupyterhub>`_.
Remember that our community is distributed across the world in various
timezones, so be patient if you do not get an answer immediately!
GitHub issues are used for most long-form project discussions, bug reports
and feature requests. Issues related to a specific authenticator or
spawner should be directed to the appropriate repository for the
authenticator or spawner. If you are using a specific JupyterHub
distribution (such as `Zero to JupyterHub on Kubernetes <http://github.com/jupyterhub/zero-to-jupyterhub-k8s>`_
or `The Littlest JupyterHub <http://github.com/jupyterhub/the-littlest-jupyterhub/>`_),
you should open issues directly in their repository. If you can not
find a repository to open your issue in, do not worry! Create it in the `main
JupyterHub repository <https://github.com/jupyterhub/jupyterhub/>`_ and our
community will help you figure it out.
A `mailing list <https://groups.google.com/forum/#!forum/jupyter>`_ for all
of Project Jupyter exists, along with one for `teaching with Jupyter
<https://groups.google.com/forum/#!forum/jupyter-education>`_.

View File

@@ -1,78 +0,0 @@
.. _contributing/docs:
==========================
Contributing Documentation
==========================
Documentation is often more important than code. This page helps
you get set up on how to contribute documentation to JupyterHub.
Building documentation locally
==============================
We use `sphinx <http://sphinx-doc.org>`_ to build our documentation. It takes
our documentation source files (written in `markdown
<https://daringfireball.net/projects/markdown/>`_ or `reStructuredText
<http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_ &
stored under the ``docs/source`` directory) and converts it into various
formats for people to read. To make sure the documentation you write or
change renders correctly, it is good practice to test it locally.
#. Make sure you have successfuly completed :ref:`contributing/setup`.
#. Install the packages required to build the docs.
.. code-block:: bash
python3 -m pip install -r docs/requirements.txt
#. Build the html version of the docs. This is the most commonly used
output format, so verifying it renders as you should is usually good
enough.
.. code-block:: bash
cd docs
make html
This step will display any syntax or formatting errors in the documentation,
along with the filename / line number in which they occurred. Fix them,
and re-run the ``make html`` command to re-render the documentation.
#. View the rendered documentation by opening ``build/html/index.html`` in
a web browser.
.. tip::
On macOS, you can open a file from the terminal with ``open <path-to-file>``.
On Linux, you can do the same with ``xdg-open <path-to-file>``.
.. _contributing/docs/conventions:
Documentation conventions
=========================
This section lists various conventions we use in our documentation. This is a
living document that grows over time, so feel free to add to it / change it!
Our entire documentation does not yet fully conform to these conventions yet,
so help in making it so would be appreciated!
``pip`` invocation
------------------
There are many ways to invoke a ``pip`` command, we recommend the following
approach:
.. code-block:: bash
python3 -m pip
This invokes pip explicitly using the python3 binary that you are
currently using. This is the **recommended way** to invoke pip
in our documentation, since it is least likely to cause problems
with python3 and pip being from different environments.
For more information on how to invoke ``pip`` commands, see
`the pip documentation <https://pip.pypa.io/en/stable/>`_.

View File

@@ -1,98 +0,0 @@
# The JupyterHub roadmap
This roadmap collects "next steps" for JupyterHub. It is about creating a
shared understanding of the project's vision and direction amongst
the community of users, contributors, and maintainers.
The goal is to communicate priorities and upcoming release plans.
It is not a aimed at limiting contributions to what is listed here.
## Using the roadmap
### Sharing Feedback on the Roadmap
All of the community is encouraged to provide feedback as well as share new
ideas with the community. Please do so by submitting an issue. If you want to
have an informal conversation first use one of the other communication channels.
After submitting the issue, others from the community will probably
respond with questions or comments they have to clarify the issue. The
maintainers will help identify what a good next step is for the issue.
### What do we mean by "next step"?
When submitting an issue, think about what "next step" category best describes
your issue:
* **now**, concrete/actionable step that is ready for someone to start work on.
These might be items that have a link to an issue or more abstract like
"decrease typos and dead links in the documentation"
* **soon**, less concrete/actionable step that is going to happen soon,
discussions around the topic are coming close to an end at which point it can
move into the "now" category
* **later**, abstract ideas or tasks, need a lot of discussion or
experimentation to shape the idea so that it can be executed. Can also
contain concrete/actionable steps that have been postponed on purpose
(these are steps that could be in "now" but the decision was taken to work on
them later)
### Reviewing and Updating the Roadmap
The roadmap will get updated as time passes (next review by 1st December) based
on discussions and ideas captured as issues.
This means this list should not be exhaustive, it should only represent
the "top of the stack" of ideas. It should
not function as a wish list, collection of feature requests or todo list.
For those please create a
[new issue](https://github.com/jupyterhub/jupyterhub/issues/new).
The roadmap should give the reader an idea of what is happening next, what needs
input and discussion before it can happen and what has been postponed.
## The roadmap proper
### Project vision
JupyterHub is a dependable tool used by humans that reduces the complexity of
creating the environment in which a piece of software can be executed.
### Now
These "Now" items are considered active areas of focus for the project:
* HubShare - a sharing service for use with JupyterHub.
* Users should be able to:
- Push a project to other users.
- Get a checkout of a project from other users.
- Push updates to a published project.
- Pull updates from a published project.
- Manage conflicts/merges by simply picking a version (our/theirs)
- Get a checkout of a project from the internet. These steps are completely different from saving notebooks/files.
- Have directories that are managed by git completely separately from our stuff.
- Look at pushed content that they have access to without an explicit pull.
- Define and manage teams of users.
- Adding/removing a user to/from a team gives/removes them access to all projects that team has access to.
- Build other services, such as static HTML publishing and dashboarding on top of these things.
### Soon
These "Soon" items are under discussion. Once an item reaches the point of an
actionable plan, the item will be moved to the "Now" section. Typically,
these will be moved at a future review of the roadmap.
* resource monitoring and management:
- (prometheus?) API for resource monitoring
- tracking activity on single-user servers instead of the proxy
- notes and activity tracking per API token
- UI for managing named servers
### Later
The "Later" items are things that are at the back of the project's mind. At this
time there is no active plan for an item. The project would like to find the
resources and time to discuss these ideas.
- real-time collaboration
- Enter into real-time collaboration mode for a project that starts a shared execution context.
- Once the single-user notebook package supports realtime collaboration,
implement sharing mechanism integrated into the Hub.

View File

@@ -1,177 +0,0 @@
.. _contributing/setup:
================================
Setting up a development install
================================
System requirements
===================
JupyterHub can only run on MacOS or Linux operating systems. If you are
using Windows, we recommend using `VirtualBox <https://virtualbox.org>`_
or a similar system to run `Ubuntu Linux <https://ubuntu.com>`_ for
development.
Install Python
--------------
JupyterHub is written in the `Python <https://python.org>`_ programming language, and
requires you have at least version 3.5 installed locally. If you havent
installed Python before, the recommended way to install it is to use
`miniconda <https://conda.io/miniconda.html>`_. Remember to get the Python 3 version,
and **not** the Python 2 version!
Install nodejs
--------------
``configurable-http-proxy``, the default proxy implementation for
JupyterHub, is written in Javascript to run on `NodeJS
<https://nodejs.org/en/>`_. If you have not installed nodejs before, we
recommend installing it in the ``miniconda`` environment you set up for
Python. You can do so with ``conda install nodejs``.
Install git
-----------
JupyterHub uses `git <https://git-scm.com>`_ & `GitHub <https://github.com>`_
for development & collaboration. You need to `install git
<https://git-scm.com/book/en/v2/Getting-Started-Installing-Git>`_ to work on
JupyterHub. We also recommend getting a free account on GitHub.com.
Setting up a development install
================================
When developing JupyterHub, you need to make changes to the code & see
their effects quickly. You need to do a developer install to make that
happen.
1. Clone the `JupyterHub git repository <https://github.com/jupyterhub/jupyterhub>`_
to your computer.
.. code:: bash
git clone https://github.com/jupyterhub/jupyterhub
cd jupyterhub
2. Make sure the ``python`` you installed and the ``npm`` you installed
are available to you on the command line.
.. code:: bash
python -V
This should return a version number greater than or equal to 3.5.
.. code:: bash
npm -v
This should return a version number greater than or equal to 5.0.
3. Install ``configurable-http-proxy``. This is required to run
JupyterHub.
.. code:: bash
npm install -g configurable-http-proxy
If you get an error that says ``Error: EACCES: permission denied``,
you might need to prefix the command with ``sudo``. If you do not
have access to sudo, you may instead run the following commands:
.. code:: bash
npm install configurable-http-proxy
export PATH=$PATH:$(pwd)/node_modules/.bin
The second line needs to be run every time you open a new terminal.
4. Install the python packages required for JupyterHub development.
.. code:: bash
python3 -m pip install -r dev-requirements.txt
python3 -m pip install -r requirements.txt
5. Install the development version of JupyterHub. This lets you edit
JupyterHub code in a text editor & restart the JupyterHub process to
see your code changes immediately.
.. code:: bash
python3 -m pip install --editable .
6. You are now ready to start JupyterHub!
.. code:: bash
jupyterhub
7. You can access JupyterHub from your browser at
``http://localhost:8000`` now.
Happy developing!
Using DummyAuthenticator & SimpleSpawner
========================================
To simplify testing of JupyterHub, its helpful to use
:class:`~jupyterhub.auth.DummyAuthenticator` instead of the default JupyterHub
authenticator and `SimpleSpawner <https://github.com/jupyterhub/simplespawner>`_
instead of the default spawner.
There is a sample configuration file that does this in
``testing/jupyterhub_config.py``. To launch jupyterhub with this
configuration:
.. code:: bash
pip install jupyterhub-simplespawner
jupyterhub -f testing/jupyterhub_config.py
The default JupyterHub `authenticator
<https://jupyterhub.readthedocs.io/en/stable/reference/authenticators.html#the-default-pam-authenticator>`_
& `spawner
<https://jupyterhub.readthedocs.io/en/stable/api/spawner.html#localprocessspawner>`_
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
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.
Troubleshooting
===============
This section lists common ways setting up your development environment may
fail, and how to fix them. Please add to the list if you encounter yet
another way it can fail!
``lessc`` not found
-------------------
If the ``python3 -m pip install --editable .`` command fails and complains about
``lessc`` being unavailable, you may need to explicitly install some
additional JavaScript dependencies:
.. code:: bash
npm install
This will fetch client-side JavaScript dependencies necessary to compile
CSS.
You may also need to manually update JavaScript and CSS after some
development updates, with:
.. code:: bash
python3 setup.py js # fetch updated client-side js
python3 setup.py css # recompile CSS from LESS sources

View File

@@ -1,78 +0,0 @@
.. _contributing/tests:
==================
Testing JupyterHub
==================
Unit test help validate that JupyterHub works the way we think it does,
and continues to do so when changes occur. They also help communicate
precisely what we expect our code to do.
JupyterHub uses `pytest <https://pytest.org>`_ for all our tests. You
can find them under ``jupyterhub/tests`` directory in the git repository.
Running the tests
==================
#. Make sure you have completed :ref:`contributing/setup`. You should be able
to start ``jupyterhub`` from the commandline & access it from your
web browser. This ensures that the dev environment is properly set
up for tests to run.
#. You can run all tests in JupyterHub
.. code-block:: bash
pytest --async-test-timeout 15 -v jupyterhub/tests
This should display progress as it runs all the tests, printing
information about any test failures as they occur.
The ``--async-test-timeout`` parameter is used by `pytest-tornado
<https://github.com/eugeniy/pytest-tornado#markers>`_ to set the
asynchronous test timeout to 15 seconds rather than the default 5,
since some of our tests take longer than 5s to execute.
#. You can also run tests in just a specific file:
.. code-block:: bash
pytest --async-test-timeout 15 -v jupyterhub/tests/<test-file-name>
#. To run a specific test only, you can do:
.. code-block:: bash
pytest --async-test-timeout 15 -v jupyterhub/tests/<test-file-name>::<test-name>
This runs the test with function name ``<test-name>`` defined in
``<test-file-name>``. This is very useful when you are iteratively
developing a single test.
For example, to run the test ``test_shutdown`` in the file ``test_api.py``,
you would run:
.. code-block:: bash
pytest -v jupyterhub/tests/test_api.py::test_shutdown
Troubleshooting Test Failures
=============================
All the tests are failing
-------------------------
Make sure you have completed all the steps in :ref:`contributing/setup` sucessfully, and
can launch ``jupyterhub`` from the terminal.
Tests are timing out
--------------------
The ``--async-test-timeout`` parameter to ``pytest`` is used by
`pytest-tornado <https://github.com/eugeniy/pytest-tornado#markers>`_ to set
the asynchronous test timeout to a higher value than the default of 5s,
since some of our tests take longer than 5s to execute. If the tests
are still timing out, try increasing that value even more. You can
also set an environment variable ``ASYNC_TEST_TIMEOUT`` instead of
passing ``--async-test-timeout`` to each invocation of pytest.

View File

@@ -85,13 +85,8 @@ easy to do with RStudio too.
- https://datascience.business.illinois.edu - https://datascience.business.illinois.edu
### IllustrisTNG Simulation Project
- [JupyterHub/Lab-based analysis platform, part of the TNG public data release](http://www.tng-project.org/data/)
### MIT and Lincoln Labs ### MIT and Lincoln Labs
- https://supercloud.mit.edu/
### Michigan State University ### Michigan State University
@@ -105,11 +100,6 @@ easy to do with RStudio too.
- https://dsa.missouri.edu/faq/ - https://dsa.missouri.edu/faq/
### Paderborn University
- [Data Science (DICE) group](https://dice.cs.uni-paderborn.de/)
- [nbgraderutils](https://github.com/dice-group/nbgraderutils): Use JupyterHub + nbgrader + iJava kernel for online Java exercises. Used in lecture Statistical Natural Language Processing.
### University of Rochester CIRC ### University of Rochester CIRC
- [JupyterHub Userguide](https://info.circ.rochester.edu/Web_Applications/JupyterHub.html) - Slurm, beehive - [JupyterHub Userguide](https://info.circ.rochester.edu/Web_Applications/JupyterHub.html) - Slurm, beehive
@@ -151,6 +141,7 @@ easy to do with RStudio too.
[Everware](https://github.com/everware) Reproducible and reusable science powered by jupyterhub and docker. Like nbviewer, but executable. CERN, Geneva [website](http://everware.xyz/) [Everware](https://github.com/everware) Reproducible and reusable science powered by jupyterhub and docker. Like nbviewer, but executable. CERN, Geneva [website](http://everware.xyz/)
### Microsoft Azure ### Microsoft Azure
- https://docs.microsoft.com/en-us/azure/machine-learning/machine-learning-data-science-linux-dsvm-intro - https://docs.microsoft.com/en-us/azure/machine-learning/machine-learning-data-science-linux-dsvm-intro
@@ -160,7 +151,9 @@ easy to do with RStudio too.
- https://getcarina.com/blog/learning-how-to-whale/ - https://getcarina.com/blog/learning-how-to-whale/
- http://carolynvanslyck.com/talk/carina/jupyterhub/#/ - http://carolynvanslyck.com/talk/carina/jupyterhub/#/
### jcloud.io
- Open to public JupyterHub server
- https://jcloud.io
## Miscellaneous ## Miscellaneous
- https://medium.com/@ybarraud/setting-up-jupyterhub-with-sudospawner-and-anaconda-844628c0dbee#.rm3yt87e1 - https://medium.com/@ybarraud/setting-up-jupyterhub-with-sudospawner-and-anaconda-844628c0dbee#.rm3yt87e1

View File

@@ -31,15 +31,6 @@ c.Authenticator.admin_users = {'mal', 'zoe'}
Users in the admin list are automatically added to the user `whitelist`, Users in the admin list are automatically added to the user `whitelist`,
if they are not already present. if they are not already present.
Each authenticator may have different ways of determining whether a user is an
administrator. By default JupyterHub use the PAMAuthenticator which provide the
`admin_groups` option and can determine administrator status base on a user
groups. For example we can let any users in the `wheel` group be admin:
```python
c.PAMAuthenticator.admin_groups = {'wheel'}
```
## Give admin access to other users' notebook servers (`admin_access`) ## Give admin access to other users' notebook servers (`admin_access`)
Since the default `JupyterHub.admin_access` setting is False, the admins Since the default `JupyterHub.admin_access` setting is False, the admins
@@ -104,16 +95,5 @@ popular services:
A generic implementation, which you can use for OAuth authentication A generic implementation, which you can use for OAuth authentication
with any provider, is also available. with any provider, is also available.
## Use DummyAuthenticator for testing
The :class:`~jupyterhub.auth.DummyAuthenticator` is a simple authenticator that
allows for any username/password unless if a global password has been set. If
set, it will allow for any username as long as the correct password is provided.
To set a global password, add this to the config file:
```python
c.DummyAuthenticator.password = "some_password"
```
[PAM]: https://en.wikipedia.org/wiki/Pluggable_authentication_module [PAM]: https://en.wikipedia.org/wiki/Pluggable_authentication_module
[OAuthenticator]: https://github.com/jupyterhub/oauthenticator [OAuthenticator]: https://github.com/jupyterhub/oauthenticator

View File

@@ -1,7 +1,7 @@
# Configuration Basics # Configuration Basics
The section contains basic information about configuring settings for a JupyterHub The section contains basic information about configuring settings for a JupyterHub
deployment. The [Technical Reference](../reference/index) deployment. The [Technical Reference](../reference/index.html)
documentation provides additional details. documentation provides additional details.
This section will help you learn how to: This section will help you learn how to:
@@ -44,7 +44,7 @@ jupyterhub -f /etc/jupyterhub/jupyterhub_config.py
``` ```
The IPython documentation provides additional information on the The IPython documentation provides additional information on the
[config system](http://ipython.readthedocs.io/en/stable/development/config) [config system](http://ipython.readthedocs.io/en/stable/development/config.html)
that Jupyter uses. that Jupyter uses.
## Configure using command line options ## Configure using command line options
@@ -77,24 +77,11 @@ jupyterhub --Spawner.notebook_dir='~/assignments'
## Configure for various deployment environments ## Configure for various deployment environments
The default authentication and process spawning mechanisms can be replaced, and The default authentication and process spawning mechanisms can be replaced, and
specific [authenticators](./authenticators-users-basics) and specific [authenticators](./authenticators-users-basics.html) and
[spawners](./spawners-basics) can be set in the configuration file. [spawners](./spawners-basics.html) can be set in the configuration file.
This enables JupyterHub to be used with a variety of authentication methods or This enables JupyterHub to be used with a variety of authentication methods or
process control and deployment environments. [Some examples](../reference/config-examples), process control and deployment environments. [Some examples](../reference/config-examples.html),
meant as illustration, are: meant as illustration, are:
- Using GitHub OAuth instead of PAM with [OAuthenticator](https://github.com/jupyterhub/oauthenticator) - Using GitHub OAuth instead of PAM with [OAuthenticator](https://github.com/jupyterhub/oauthenticator)
- Spawning single-user servers with Docker, using the [DockerSpawner](https://github.com/jupyterhub/dockerspawner) - Spawning single-user servers with Docker, using the [DockerSpawner](https://github.com/jupyterhub/dockerspawner)
## Run the proxy separately
This is *not* strictly necessary, but useful in many cases. If you
use a custom proxy (e.g. Traefik), this also not needed.
Connections to user servers go through the proxy, and *not* the hub
itself. If the proxy stays running when the hub restarts (for
maintenance, re-configuration, etc.), then use connections are not
interrupted. For simplicity, by default the hub starts the proxy
automatically, so if the hub restarts, the proxy restarts, and user
connections are interrupted. It is easy to run the proxy separately,
for information see [the separate proxy page](../reference/separate-proxy).

View File

@@ -124,7 +124,7 @@ hex-encoded string. You can set it this way:
.. code-block:: bash .. code-block:: bash
export JPY_COOKIE_SECRET=$(openssl rand -hex 32) export JPY_COOKIE_SECRET=`openssl rand -hex 32`
For security reasons, this environment variable should only be visible to the For security reasons, this environment variable should only be visible to the
Hub. If you set it dynamically as above, all users will be logged out each time Hub. If you set it dynamically as above, all users will be logged out each time
@@ -173,7 +173,7 @@ using the ``CONFIGPROXY_AUTH_TOKEN`` environment variable:
.. code-block:: bash .. code-block:: bash
export CONFIGPROXY_AUTH_TOKEN=$(openssl rand -hex 32) export CONFIGPROXY_AUTH_TOKEN='openssl rand -hex 32'
This environment variable needs to be visible to the Hub and Proxy. This environment variable needs to be visible to the Hub and Proxy.

View File

@@ -14,7 +14,7 @@ document will:
- explain some basic information about API tokens - explain some basic information about API tokens
- clarify that API tokens can be used to authenticate to - clarify that API tokens can be used to authenticate to
single-user servers as of [version 0.8.0](../changelog) single-user servers as of [version 0.8.0](../changelog.html)
- show how the [cull_idle_servers][] script can be: - show how the [cull_idle_servers][] script can be:
- used in a Hub-managed service - used in a Hub-managed service
- run as a standalone script - run as a standalone script
@@ -29,14 +29,14 @@ Hub via the REST API.
To run such an external service, an API token must be created and To run such an external service, an API token must be created and
provided to the service. provided to the service.
As of [version 0.6.0](../changelog), the preferred way of doing As of [version 0.6.0](../changelog.html), the preferred way of doing
this is to first generate an API token: this is to first generate an API token:
```bash ```bash
openssl rand -hex 32 openssl rand -hex 32
``` ```
In [version 0.8.0](../changelog), a TOKEN request page for In [version 0.8.0](../changelog.html), a TOKEN request page for
generating an API token is available from the JupyterHub user interface: generating an API token is available from the JupyterHub user interface:
![Request API TOKEN page](../images/token-request.png) ![Request API TOKEN page](../images/token-request.png)
@@ -88,7 +88,7 @@ c.JupyterHub.services = [
{ {
'name': 'cull-idle', 'name': 'cull-idle',
'admin': True, 'admin': True,
'command': [sys.executable, 'cull_idle_servers.py', '--timeout=3600'], 'command': 'python3 cull_idle_servers.py --timeout=3600'.split(),
} }
] ]
``` ```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -1,4 +1,3 @@
==========
JupyterHub JupyterHub
========== ==========
@@ -29,140 +28,75 @@ JupyterHub performs the following functions:
For convenient administration of the Hub, its users, and services, For convenient administration of the Hub, its users, and services,
JupyterHub also provides a `REST API`_. JupyterHub also provides a `REST API`_.
The JupyterHub team and Project Jupyter value our community, and JupyterHub
follows the Jupyter `Community Guides <https://jupyter.readthedocs.io/en/latest/community/content-community.html>`_.
Contents Contents
======== --------
.. _index/distributions: **Installation Guide**
Distributions * :doc:`installation-guide`
------------- * :doc:`quickstart`
* :doc:`quickstart-docker`
* :doc:`installation-basics`
A JupyterHub **distribution** is tailored towards a particular set of **Getting Started**
use cases. These are generally easier to set up than setting up
JupyterHub from scratch, assuming they fit your use case.
The two popular ones are: * :doc:`getting-started/index`
* :doc:`getting-started/config-basics`
* :doc:`getting-started/networking-basics`
* :doc:`getting-started/security-basics`
* :doc:`getting-started/authenticators-users-basics`
* :doc:`getting-started/spawners-basics`
* :doc:`getting-started/services-basics`
* `Zero to JupyterHub on Kubernetes <http://z2jh.jupyter.org>`_, for **Technical Reference**
running JupyterHub on top of `Kubernetes <https://k8s.io>`_. This
can scale to large number of machines & users.
* `The Littlest JupyterHub <http://tljh.jupyter.org>`_, for an easy
to set up & run JupyterHub supporting 1-100 users on a single machine.
Installation Guide * :doc:`reference/index`
------------------ * :doc:`reference/technical-overview`
* :doc:`reference/websecurity`
* :doc:`reference/authenticators`
* :doc:`reference/spawners`
* :doc:`reference/services`
* :doc:`reference/rest`
* :doc:`reference/upgrading`
* :doc:`reference/templates`
* :doc:`reference/config-user-env`
* :doc:`reference/config-examples`
* :doc:`reference/config-ghoauth`
* :doc:`reference/config-proxy`
* :doc:`reference/config-sudo`
.. toctree:: **API Reference**
:maxdepth: 1
installation-guide * :doc:`api/index`
quickstart
quickstart-docker
installation-basics
Getting Started **Tutorials**
---------------
.. toctree:: * :doc:`tutorials/index`
:maxdepth: 1 * :doc:`tutorials/upgrade-dot-eight`
* `Zero to JupyterHub with Kubernetes <https://zero-to-jupyterhub.readthedocs.io/en/latest/>`_
getting-started/index **Troubleshooting**
getting-started/config-basics
getting-started/networking-basics
getting-started/security-basics
getting-started/authenticators-users-basics
getting-started/spawners-basics
getting-started/services-basics
Technical Reference * :doc:`troubleshooting`
-------------------
.. toctree:: **About JupyterHub**
:maxdepth: 1
reference/index * :doc:`contributor-list`
reference/technical-overview * :doc:`gallery-jhub-deployments`
reference/websecurity
reference/authenticators
reference/spawners
reference/services
reference/rest
reference/templates
reference/config-user-env
reference/config-examples
reference/config-ghoauth
reference/config-proxy
reference/config-sudo
Contributing **Changelog**
------------
We want you to contribute to JupyterHub in ways that are most exciting * :doc:`changelog`
& useful to you. We value documentation, testing, bug reporting & code equally,
and are glad to have your contributions in whatever form you wish :)
Our `Code of Conduct <https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md>`_
(`reporting guidelines <https://github.com/jupyter/governance/blob/master/conduct/reporting_online.md>`_)
helps keep our community welcoming to as many people as possible.
.. toctree::
:maxdepth: 1
contributing/community
contributing/setup
contributing/docs
contributing/tests
contributing/roadmap
Upgrading JupyterHub
--------------------
We try to make upgrades between minor versions as painless as possible.
.. toctree::
:maxdepth: 1
admin/upgrading
changelog
API Reference
-------------
.. toctree::
:maxdepth: 1
api/index
Troubleshooting
---------------
.. toctree::
:maxdepth: 1
troubleshooting
About JupyterHub
----------------
.. toctree::
:maxdepth: 1
contributor-list
changelog
gallery-jhub-deployments
Indices and tables Indices and tables
================== ------------------
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex` * :ref:`modindex`
Questions? Suggestions? Questions? Suggestions?
======================= -----------------------
- `Jupyter mailing list <https://groups.google.com/forum/#!forum/jupyter>`_ - `Jupyter mailing list <https://groups.google.com/forum/#!forum/jupyter>`_
- `Jupyter website <https://jupyter.org>`_ - `Jupyter website <https://jupyter.org>`_
@@ -170,7 +104,7 @@ Questions? Suggestions?
.. _contents: .. _contents:
Full Table of Contents Full Table of Contents
====================== ----------------------
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
@@ -179,6 +113,7 @@ Full Table of Contents
getting-started/index getting-started/index
reference/index reference/index
api/index api/index
tutorials/index
troubleshooting troubleshooting
contributor-list contributor-list
gallery-jhub-deployments gallery-jhub-deployments

View File

@@ -6,7 +6,7 @@ JupyterHub is supported on Linux/Unix based systems. To use JupyterHub, you need
a Unix server (typically Linux) running somewhere that is accessible to your a Unix server (typically Linux) running somewhere that is accessible to your
team on the network. The JupyterHub server can be on an internal network at your team on the network. The JupyterHub server can be on an internal network at your
organization, or it can run on the public internet (in which case, take care organization, or it can run on the public internet (in which case, take care
with the Hub's [security](./getting-started/security-basics)). with the Hub's [security](./security-basics.html)).
JupyterHub officially **does not** support Windows. You may be able to use JupyterHub officially **does not** support Windows. You may be able to use
JupyterHub on Windows if you use a Spawner and Authenticator that work on JupyterHub on Windows if you use a Spawner and Authenticator that work on

View File

@@ -25,7 +25,7 @@ Starting JupyterHub with docker
The JupyterHub docker image can be started with the following command:: The JupyterHub docker image can be started with the following command::
docker run -d -p 8000:8000 --name jupyterhub jupyterhub/jupyterhub jupyterhub docker run -d --name jupyterhub jupyterhub/jupyterhub jupyterhub
This command will create a container named ``jupyterhub`` that you can This command will create a container named ``jupyterhub`` that you can
**stop and resume** with ``docker stop/start``. **stop and resume** with ``docker stop/start``.

View File

@@ -5,8 +5,8 @@ Hub and single user notebook servers.
## The default PAM Authenticator ## The default PAM Authenticator
JupyterHub ships with the default [PAM][]-based Authenticator, for JupyterHub ships only with the default [PAM][]-based Authenticator,
logging in with local user accounts via a username and password. for logging in with local user accounts via a username and password.
## The OAuthenticator ## The OAuthenticator
@@ -34,17 +34,12 @@ popular services:
A generic implementation, which you can use for OAuth authentication A generic implementation, which you can use for OAuth authentication
with any provider, is also available. with any provider, is also available.
## The Dummy Authenticator
When testing, it may be helpful to use the
:class:`~jupyterhub.auth.DummyAuthenticator`. This allows for any username and
password unless if a global password has been set. Once set, any username will
still be accepted but the correct password will need to be provided.
## Additional Authenticators ## Additional Authenticators
A partial list of other authenticators is available on the - ldapauthenticator for LDAP
[JupyterHub wiki](https://github.com/jupyterhub/jupyterhub/wiki/Authenticators). - tmpauthenticator for temporary accounts
- For Shibboleth, [jhub_shibboleth_auth](https://github.com/gesiscss/jhub_shibboleth_auth)
and [jhub_remote_user_authenticator](https://github.com/cwaldbieser/jhub_remote_user_authenticator)
## Technical Overview of Authentication ## Technical Overview of Authentication
@@ -75,6 +70,7 @@ Writing an Authenticator that looks up passwords in a dictionary
requires only overriding this one method: requires only overriding this one method:
```python ```python
from tornado import gen
from IPython.utils.traitlets import Dict from IPython.utils.traitlets import Dict
from jupyterhub.auth import Authenticator from jupyterhub.auth import Authenticator
@@ -84,7 +80,8 @@ class DictionaryAuthenticator(Authenticator):
help="""dict of username:password for authentication""" help="""dict of username:password for authentication"""
) )
async def authenticate(self, handler, data): @gen.coroutine
def authenticate(self, handler, data):
if self.passwords.get(data['username']) == data['password']: if self.passwords.get(data['username']) == data['password']:
return data['username'] return data['username']
``` ```
@@ -106,16 +103,6 @@ c.Authenticator.username_map = {
} }
``` ```
When using `PAMAuthenticator`, you can set
`c.PAMAuthenticator.pam_normalize_username = True`, which will
normalize usernames using PAM (basically round-tripping them: username
to uid to username), which is useful in case you use some external
service that allows multiple usernames mapping to the same user (such
as ActiveDirectory, yes, this really happens). When
`pam_normalize_username` is on, usernames are *not* normalized to
lowercase.
#### Validate usernames #### Validate usernames
In most cases, there is a very limited set of acceptable usernames. In most cases, there is a very limited set of acceptable usernames.
@@ -151,41 +138,6 @@ See a list of custom Authenticators [on the wiki](https://github.com/jupyterhub/
If you are interested in writing a custom authenticator, you can read If you are interested in writing a custom authenticator, you can read
[this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/authenticators.html). [this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/authenticators.html).
### Registering custom Authenticators via entry points
As of JupyterHub 1.0, custom authenticators can register themselves via
the `jupyterhub.authenticators` entry point metadata.
To do this, in your `setup.py` add:
```python
setup(
...
entry_points={
'jupyterhub.authenticators': [
'myservice = mypackage:MyAuthenticator',
],
},
)
```
If you have added this metadata to your package,
users can select your authenticator with the configuration:
```python
c.JupyterHub.authenticator_class = 'myservice'
```
instead of the full
```python
c.JupyterHub.authenticator_class = 'mypackage:MyAuthenticator'
```
previously required.
Additionally, configurable attributes for your authenticator will
appear in jupyterhub help output and auto-generated configuration files
via `jupyterhub --generate-config`.
### Authentication state ### Authentication state

View File

@@ -190,23 +190,3 @@ Listen 443
</Location> </Location>
</VirtualHost> </VirtualHost>
``` ```
In case of the need to run the jupyterhub under /jhub/ or other location please use the below configurations:
- JupyterHub running locally at http://127.0.0.1:8000/jhub/ or other location
httpd.conf amendments:
```bash
RewriteRule /jhub/(.*) ws://127.0.0.1:8000/jhub/$1 [P,L]
RewriteRule /jhub/(.*) http://127.0.0.1:8000/jhub/$1 [P,L]
ProxyPass /jhub/ http://127.0.0.1:8000/jhub/
ProxyPassReverse /jhub/ http://127.0.0.1:8000/jhub/
```
jupyterhub_config.py amendments:
```bash
--The public facing URL of the whole JupyterHub application.
--This is the address on which the proxy will bind. Sets protocol, ip, base_url
c.JupyterHub.bind_url = 'http://127.0.0.1:8000/jhub/'
```

View File

@@ -37,7 +37,7 @@ Next, you will need [sudospawner](https://github.com/jupyter/sudospawner)
to enable monitoring the single-user servers with sudo: to enable monitoring the single-user servers with sudo:
```bash ```bash
sudo python3 -m pip install sudospawner sudo pip install sudospawner
``` ```
Now we have to configure sudo to allow the Hub user (`rhea`) to launch Now we have to configure sudo to allow the Hub user (`rhea`) to launch
@@ -219,14 +219,14 @@ $ sudo -u rhea jupyterhub --JupyterHub.spawner_class=sudospawner.SudoSpawner
And try logging in. And try logging in.
## Troubleshooting: SELinux ### Troubleshooting: SELinux
If you still get a generic `Permission denied` `PermissionError`, it's possible SELinux is blocking you. If you still get a generic `Permission denied` `PermissionError`, it's possible SELinux is blocking you.
Here's how you can make a module to allow this. Here's how you can make a module to allow this.
First, put this in a file named `sudo_exec_selinux.te`: First, put this in a file sudo_exec_selinux.te:
```bash ```bash
module sudo_exec_selinux 1.1; module sudo_exec 1.1;
require { require {
type unconfined_t; type unconfined_t;
@@ -246,9 +246,9 @@ $ semodule_package -o sudo_exec_selinux.pp -m sudo_exec_selinux.mod
$ semodule -i sudo_exec_selinux.pp $ semodule -i sudo_exec_selinux.pp
``` ```
## Troubleshooting: PAM session errors ### Troubleshooting: PAM session errors
If the PAM authentication doesn't work and you see errors for If the PAM authentication doesn't work and you see errors for
`login:session-auth`, or similar, considering updating to a more recent version `login:session-auth`, or similar, considering updating to `master`
of jupyterhub and disabling the opening of PAM sessions with and/or incorporating this commit https://github.com/jupyter/jupyterhub/commit/40368b8f555f04ffdd662ffe99d32392a088b1d2
`c.PAMAuthenticator.open_sessions=False`. and configuration option, `c.PAMAuthenticator.open_sessions = False`.

View File

@@ -145,37 +145,3 @@ In both cases, you want to *avoid putting configuration in user home
directories* because users can change those configuration settings. Also, directories* because users can change those configuration settings. Also,
home directories typically persist once they are created, so they are home directories typically persist once they are created, so they are
difficult for admins to update later. difficult for admins to update later.
## Named servers
By default, in a JupyterHub deployment each user has exactly one server.
JupyterHub can, however, have multiple servers per user.
This is most useful in deployments where users can configure the environment
in which their server will start (e.g. resource requests on an HPC cluster),
so that a given user can have multiple configurations running at the same time,
without having to stop and restart their one server.
To allow named servers:
```python
c.JupyterHub.allow_named_servers = True
```
Named servers were implemented in the REST API in JupyterHub 0.8,
and JupyterHub 1.0 introduces UI for managing named servers via the user home page:
![named servers on the home page](../images/named-servers-home.png)
as well as the admin page:
![named servers on the admin page](../images/named-servers-admin.png)
Named servers can be accessed, created, started, stopped, and deleted
from these pages. Activity tracking is now per-server as well.
The number of named servers per user can be limited by setting
```python
c.JupyterHub.named_server_limit_per_user = 5
```

View File

@@ -5,15 +5,14 @@ Technical Reference
:maxdepth: 2 :maxdepth: 2
technical-overview technical-overview
urls
websecurity websecurity
authenticators authenticators
spawners spawners
services services
proxy proxy
separate-proxy
rest rest
database database
upgrading
templates templates
config-user-env config-user-env
config-examples config-examples

View File

@@ -45,12 +45,15 @@ If your proxy should be launched when the Hub starts, you must define how
to start and stop your proxy: to start and stop your proxy:
```python ```python
from tornado import gen
class MyProxy(Proxy): class MyProxy(Proxy):
... ...
async def start(self): @gen.coroutine
def start(self):
"""Start the proxy""" """Start the proxy"""
async def stop(self): @gen.coroutine
def stop(self):
"""Stop the proxy""" """Stop the proxy"""
``` ```
@@ -59,18 +62,6 @@ These methods **may** be coroutines.
`c.Proxy.should_start` is a configurable flag that determines whether the `c.Proxy.should_start` is a configurable flag that determines whether the
Hub should call these methods when the Hub itself starts and stops. 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
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
hub via the `jupyterhub_config.py` file by providing a `dict` of named paths
to the `external_authorities` option. The hub will include all certificates
provided in that `dict` in the trust bundle utilized by all internal
components.
### Purely external proxies ### Purely external proxies
Probably most custom proxies will be externally managed, Probably most custom proxies will be externally managed,
@@ -109,7 +100,8 @@ Python wrapper may have to handle storing the `data` piece itself, e.g in a
simple file or database. simple file or database.
```python ```python
async def add_route(self, routespec, target, data): @gen.coroutine
def add_route(self, routespec, target, data):
"""Proxy `routespec` to `target`. """Proxy `routespec` to `target`.
Store `data` associated with the routespec Store `data` associated with the routespec
@@ -120,7 +112,7 @@ async def add_route(self, routespec, target, data):
Adding a route for a user looks like this: Adding a route for a user looks like this:
```python ```python
await proxy.add_route('/user/pgeorgiou/', 'http://127.0.0.1:1227', proxy.add_route('/user/pgeorgiou/', 'http://127.0.0.1:1227',
{'user': 'pgeorgiou'}) {'user': 'pgeorgiou'})
``` ```
@@ -130,7 +122,8 @@ await proxy.add_route('/user/pgeorgiou/', 'http://127.0.0.1:1227',
`delete_route` should still succeed, but a warning may be issued. `delete_route` should still succeed, but a warning may be issued.
```python ```python
async def delete_route(self, routespec): @gen.coroutine
def delete_route(self, routespec):
"""Delete the route""" """Delete the route"""
``` ```
@@ -142,7 +135,8 @@ routes. The return value for this function should be a dictionary, keyed by
`add_route` (`routespec`, `target`, `data`) `add_route` (`routespec`, `target`, `data`)
```python ```python
async def get_all_routes(self): @gen.coroutine
def get_all_routes(self):
"""Return all routes, keyed by routespec""" """Return all routes, keyed by routespec"""
``` ```
@@ -185,38 +179,3 @@ tracked, and services such as cull-idle will not work.
Now that `notebook-5.0` tracks activity internally, we can retrieve activity Now that `notebook-5.0` tracks activity internally, we can retrieve activity
information from the single-user servers instead, removing the need to track information from the single-user servers instead, removing the need to track
activity in the proxy. But this is not yet implemented in JupyterHub 0.8.0. activity in the proxy. But this is not yet implemented in JupyterHub 0.8.0.
### Registering custom Proxies via entry points
As of JupyterHub 1.0, custom proxy implementations can register themselves via
the `jupyterhub.proxies` entry point metadata.
To do this, in your `setup.py` add:
```python
setup(
...
entry_points={
'jupyterhub.proxies': [
'mything = mypackage:MyProxy',
],
},
)
```
If you have added this metadata to your package,
users can select your authenticator with the configuration:
```python
c.JupyterHub.proxy_class = 'mything'
```
instead of the full
```python
c.JupyterHub.proxy_class = 'mypackage:MyProxy'
```
previously required.
Additionally, configurable attributes for your proxy will
appear in jupyterhub help output and auto-generated configuration files
via `jupyterhub --generate-config`.

View File

@@ -1,14 +0,0 @@
:orphan:
===================
JupyterHub REST API
===================
.. this doc exists as a resolvable link target
.. which _static files are not
.. meta::
:http-equiv=refresh: 0;url=../_static/rest-api/index.html
The rest API docs are `here <../_static/rest-api/index.html>`_
if you are not redirected automatically.

View File

@@ -27,7 +27,7 @@ Hub.
To send requests using JupyterHub API, you must pass an API token with To send requests using JupyterHub API, you must pass an API token with
the request. the request.
As of [version 0.6.0](../changelog.md), the preferred way of As of [version 0.6.0](../changelog.html), the preferred way of
generating an API token is: generating an API token is:
```bash ```bash
@@ -48,7 +48,7 @@ jupyterhub token <username>
This command generates a random string to use as a token and registers This command generates a random string to use as a token and registers
it for the given user with the Hub's database. it for the given user with the Hub's database.
In [version 0.8.0](../changelog.md), a TOKEN request page for In [version 0.8.0](../changelog.html), a TOKEN request page for
generating an API token is available from the JupyterHub user interface: generating an API token is available from the JupyterHub user interface:
![Request API TOKEN page](../images/token-request.png) ![Request API TOKEN page](../images/token-request.png)
@@ -158,6 +158,11 @@ The same servers can be stopped by substituting `DELETE` for `POST` above.
### Some caveats for using named-servers ### Some caveats for using named-servers
The named-server capabilities are not fully implemented for JupyterHub as yet.
While it's possible to start/stop a server via the API, the UI on the
JupyterHub control-panel has not been implemented, and so it may not be obvious
to those viewing the panel that a named-server may be running for a given user.
For named-servers via the API to work, the spawner used to spawn these servers For named-servers via the API to work, the spawner used to spawn these servers
will need to be able to handle the case of multiple servers per user and ensure will need to be able to handle the case of multiple servers per user and ensure
uniqueness of names, particularly if servers are spawned via docker containers uniqueness of names, particularly if servers are spawned via docker containers
@@ -173,5 +178,5 @@ Note: The Swagger specification is being renamed the [OpenAPI Initiative][].
[interactive style on swagger's petstore]: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default [interactive style 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/ [OpenAPI Initiative]: https://www.openapis.org/
[JupyterHub REST API]: ./rest-api [JupyterHub REST API]: ../_static/rest-api/index.html
[Jupyter Notebook REST API]: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml [Jupyter Notebook REST API]: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml

View File

@@ -1,80 +0,0 @@
# Running proxy separately from the hub
## Background
The thing which users directly connect to is the proxy, by default
`configurable-http-proxy`. The proxy either redirects users to the
hub (for login and managing servers), or to their own single-user
servers. Thus, as long as the proxy stays running, access to existing
servers continues, even if the hub itself restarts or goes down.
When you first configure the hub, you may not even realize this
because the proxy is automatically managed by the hub. This is great
for getting started and even most use, but everytime you restart the
hub, all user connections also get restarted. But it's also simple to
run the proxy as a service separate from the hub, so that you are free
to reconfigure the hub while only interrupting users who are currently
actively starting the hub.
The default JupyterHub proxy is
[configurable-http-proxy](https://github.com/jupyterhub/configurable-http-proxy),
and that page has some docs. If you are using a different proxy, such
as Traefik, these instructions are probably not relevant to you.
## Configuration options
`c.JupyterHub.cleanup_servers = False` should be set, which tells the
hub to not stop servers when the hub restarts (this is useful even if
you don't run the proxy separately).
`c.ConfigurableHTTPProxy.should_start = False` should be set, which
tells the hub that the proxy should not be started (because you start
it yourself).
`c.ConfigurableHTTPProxy.auth_token = "CONFIGPROXY_AUTH_TOKEN"` should be set to a
token for authenticating communication with the proxy.
`c.ConfigurableHTTPProxy.api_url = 'http://localhost:8001'` should be
set to the URL which the hub uses to connect *to the proxy's API*.
## Proxy configuration
You need to configure a service to start the proxy. An example
command line for this is `configurable-http-proxy --ip=127.0.0.1
--port=8000 --api-ip=127.0.0.1 --api-port=8001
--default-target=http://localhost:8081
--error-target=http://localhost:8081/hub/error`. (Details for how to
do this is out of scope for this tutorial - for example it might be a
systemd service on within another docker cotainer). The proxy has no
configuration files, all configuration is via the command line and
environment variables.
`--api-ip` and `--api-port` (which tells the proxy where to listen) should match the hub's `ConfigurableHTTPProxy.api_url`.
`--ip`, `-port`, and other options configure the *user* connections to the proxy.
`--default-target` and `--error-target` should point to the hub, and used when users navigate to the proxy originally.
You must define the environment variable `CONFIGPROXY_AUTH_TOKEN` to
match the token given to `c.ConfigurableHTTPProxy.auth_token`.
You should check the [configurable-http-proxy
options](https://github.com/jupyterhub/configurable-http-proxy) to see
what other options are needed, for example SSL options. Note that
these are configured in the hub if the hub is starting the proxy - you
need to move the options to here.
## Docker image
You can use [jupyterhub configurable-http-proxy docker
image](https://hub.docker.com/r/jupyterhub/configurable-http-proxy/)
to run the proxy.
## See also
* [jupyterhub configurable-http-proxy](https://github.com/jupyterhub/configurable-http-proxy)

View File

@@ -93,7 +93,7 @@ c.JupyterHub.services = [
{ {
'name': 'cull-idle', 'name': 'cull-idle',
'admin': True, 'admin': True,
'command': [sys.executable, '/path/to/cull-idle.py', '--timeout'] 'command': ['python', '/path/to/cull-idle.py', '--timeout']
} }
] ]
``` ```

View File

@@ -10,7 +10,6 @@ and a custom Spawner needs to be able to take three actions:
## Examples ## Examples
Custom Spawners for JupyterHub can be found on the [JupyterHub wiki](https://github.com/jupyterhub/jupyterhub/wiki/Spawners). Custom Spawners for JupyterHub can be found on the [JupyterHub wiki](https://github.com/jupyterhub/jupyterhub/wiki/Spawners).
Some examples include: Some examples include:
@@ -175,42 +174,6 @@ When `Spawner.start` is called, this dictionary is accessible as `self.user_opti
If you are interested in building a custom spawner, you can read [this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/spawners.html). If you are interested in building a custom spawner, you can read [this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/spawners.html).
### Registering custom Spawners via entry points
As of JupyterHub 1.0, custom Spawners can register themselves via
the `jupyterhub.spawners` entry point metadata.
To do this, in your `setup.py` add:
```python
setup(
...
entry_points={
'jupyterhub.spawners': [
'myservice = mypackage:MySpawner',
],
},
)
```
If you have added this metadata to your package,
users can select your authenticator with the configuration:
```python
c.JupyterHub.spawner_class = 'myservice'
```
instead of the full
```python
c.JupyterHub.spawner_class = 'mypackage:MySpawner'
```
previously required.
Additionally, configurable attributes for your spawner will
appear in jupyterhub help output and auto-generated configuration files
via `jupyterhub --generate-config`.
## Spawners, resource limits, and guarantees (Optional) ## Spawners, resource limits, and guarantees (Optional)
Some spawners of the single-user notebook servers allow setting limits or Some spawners of the single-user notebook servers allow setting limits or
@@ -260,30 +223,3 @@ in the single-user notebook server when a guarantee is being provided.
**The spawner's underlying system or cluster is responsible for enforcing these **The spawner's underlying system or cluster is responsible for enforcing these
limits and providing these guarantees.** If these values are set to `None`, no limits and providing these guarantees.** If these values are set to `None`, no
limits or guarantees are provided, and no environment values are set. limits or guarantees are provided, and no environment values are set.
### Encryption
Communication between the `Proxy`, `Hub`, and `Notebook` can be secured by
turning on `internal_ssl` in `jupyterhub_config.py`. For a custom spawner to
utilize these certs, there are two methods of interest on the base `Spawner`
class: `.create_certs` and `.move_certs`.
The first method, `.create_certs` will sign a key-cert pair using an internally
trusted authority for notebooks. During this process, `.create_certs` can
apply `ip` and `dns` name information to the cert via an `alt_names` `kwarg`.
This is used for certificate authentication (verification). Without proper
verification, the `Notebook` will be unable to communicate with the `Hub` and
vice versa when `internal_ssl` is enabled. For example, given a deployment
using the `DockerSpawner` which will start containers with `ips` from the
`docker` subnet pool, the `DockerSpawner` would need to instead choose a
container `ip` prior to starting and pass that to `.create_certs` (TODO: edit).
In general though, this method will not need to be changed and the default
`ip`/`dns` (localhost) info will suffice.
When `.create_certs` is run, it will `.create_certs` in a default, central
location specified by `c.JupyterHub.internal_certs_location`. For `Spawners`
that need access to these certs elsewhere (i.e. on another host altogether),
the `.move_certs` method can be overridden to move the certs appropriately.
Again, using `DockerSpawner` as an example, this would entail moving certs
to a directory that will get mounted into the container this spawner starts.

View File

@@ -49,14 +49,14 @@ The proxy is the only process that listens on a public interface. The Hub sits
behind the proxy at `/hub`. Single-user servers sit behind the proxy at behind the proxy at `/hub`. Single-user servers sit behind the proxy at
`/user/[username]`. `/user/[username]`.
Different **[authenticators](./authenticators.md)** control access Different **[authenticators](./authenticators.html)** control access
to JupyterHub. The default one (PAM) uses the user accounts on the server where to JupyterHub. The default one (PAM) uses the user accounts on the server where
JupyterHub is running. If you use this, you will need to create a user account JupyterHub is running. If you use this, you will need to create a user account
on the system for each user on your team. Using other authenticators, you can on the system for each user on your team. Using other authenticators, you can
allow users to sign in with e.g. a GitHub account, or with any single-sign-on allow users to sign in with e.g. a GitHub account, or with any single-sign-on
system your organization has. system your organization has.
Next, **[spawners](./spawners.md)** control how JupyterHub starts Next, **[spawners](./spawners.html)** control how JupyterHub starts
the individual notebook server for each user. The default spawner will the individual notebook server for each user. The default spawner will
start a notebook server on the same machine running under their system username. start a notebook server on the same machine running under their system username.
The other main option is to start each server in a separate container, often The other main option is to start each server in a separate container, often
@@ -66,10 +66,10 @@ using Docker.
When a user accesses JupyterHub, the following events take place: When a user accesses JupyterHub, the following events take place:
- Login data is handed to the [Authenticator](./authenticators.md) instance for - Login data is handed to the [Authenticator](./authenticators.html) instance for
validation validation
- The Authenticator returns the username if the login information is valid - The Authenticator returns the username if the login information is valid
- A single-user notebook server instance is [spawned](./spawners.md) for the - A single-user notebook server instance is [spawned](./spawners.html) for the
logged-in user logged-in user
- When the single-user notebook server starts, the proxy is notified to forward - When the single-user notebook server starts, the proxy is notified to forward
requests to `/user/[username]/*` to the single-user notebook server. requests to `/user/[username]/*` to the single-user notebook server.
@@ -111,7 +111,7 @@ working directory:
This file needs to persist so that a **Hub** server restart will avoid This file needs to persist so that a **Hub** server restart will avoid
invalidating cookies. Conversely, deleting this file and restarting the server invalidating cookies. Conversely, deleting this file and restarting the server
effectively invalidates all login cookies. The cookie secret file is discussed effectively invalidates all login cookies. The cookie secret file is discussed
in the [Cookie Secret section of the Security Settings document](../getting-started/security-basics.md). in the [Cookie Secret section of the Security Settings document](../getting-started/security-basics.html).
The location of these files can be specified via configuration settings. It is The location of these files can be specified via configuration settings. It is
recommended that these files be stored in standard UNIX filesystem locations, recommended that these files be stored in standard UNIX filesystem locations,
@@ -122,9 +122,9 @@ all security and runtime files.
There are two basic extension points for JupyterHub: There are two basic extension points for JupyterHub:
- How users are authenticated by [Authenticators](./authenticators.md) - How users are authenticated by [Authenticators](./authenticators.html)
- How user's single-user notebook server processes are started by - How user's single-user notebook server processes are started by
[Spawners](./spawners.md) [Spawners](./spawners.html)
Each is governed by a customizable class, and JupyterHub ships with basic Each is governed by a customizable class, and JupyterHub ships with basic
defaults for each. defaults for each.

View File

@@ -25,19 +25,19 @@ supplement the material in the block. The
make extensive use of blocks, which allows you to customize parts of the make extensive use of blocks, which allows you to customize parts of the
interface easily. interface easily.
In general, a child template can extend a base template, `page.html`, by beginning with: In general, a child template can extend a base template, `base.html`, by beginning with:
```html ```html
{% extends "page.html" %} {% extends "base.html" %}
``` ```
This works, unless you are trying to extend the default template for the same This works, unless you are trying to extend the default template for the same
file name. Starting in version 0.9, you may refer to the base file with a file name. Starting in version 0.9, you may refer to the base file with a
`templates/` prefix. Thus, if you are writing a custom `page.html`, start the `templates/` prefix. Thus, if you are writing a custom `base.html`, start the
file with this block: file with this block:
```html ```html
{% extends "templates/page.html" %} {% extends "templates/base.html" %}
``` ```
By defining `block`s with same name as in the base template, child templates By defining `block`s with same name as in the base template, child templates

View File

@@ -0,0 +1,98 @@
# Upgrading JupyterHub and its database
From time to time, you may wish to upgrade JupyterHub to take advantage
of new releases. Much of this process is automated using scripts,
such as those generated by alembic for database upgrades. Whether you
are using the default SQLite database or an RDBMS, such as PostgreSQL or
MySQL, the process follows similar steps.
**Before upgrading a JupyterHub deployment**, it's critical to backup your data
and configurations before shutting down the JupyterHub process and server.
## Note about upgrading the SQLite database
When used in production systems, SQLite has some disadvantages when it
comes to upgrading JupyterHub. These are:
- `upgrade-db` may not work, and you may need to start with a fresh database
- `downgrade-db` **will not** work if you want to rollback to an earlier
version, so backup the `jupyterhub.sqlite` file before upgrading
## The upgrade process
Five fundamental process steps are needed when upgrading JupyterHub and its
database:
1. Backup JupyterHub database
2. Backup JupyterHub configuration file
3. Shutdown the Hub
4. Upgrade JupyterHub
5. Upgrade the database using run `jupyterhub upgrade-db`
Let's take a closer look at each step in the upgrade process as well as some
additional information about JupyterHub databases.
### Backup JupyterHub database
To prevent unintended loss of data or configuration information, you should
back up the JupyterHub database (the default SQLite database or a RDBMS
database using PostgreSQL, MySQL, or others supported by SQLAlchemy):
- If using the default SQLite database, back up the `jupyterhub.sqlite`
database.
- If using an RDBMS database such as PostgreSQL, MySQL, or other supported by
SQLAlchemy, back up the JupyterHub database.
Losing the Hub database is often not a big deal. Information that resides only
in the Hub database includes:
- active login tokens (user cookies, service tokens)
- users added via GitHub UI, instead of config files
- info about running servers
If the following conditions are true, you should be fine clearing the Hub
database and starting over:
- users specified in config file
- user servers are stopped during upgrade
- don't mind causing users to login again after upgrade
### Backup JupyterHub configuration file
Additionally, backing up your configuration file, `jupyterhub_config.py`, to
a secure location.
### Shutdown JupyterHub
Prior to shutting down JupyterHub, you should notify the Hub users of the
scheduled downtime. This gives users the opportunity to finish any outstanding
work in process.
Next, shutdown the JupyterHub service.
### Upgrade JupyterHub
Follow directions that correspond to your package manager, `pip` or `conda`,
for the new JupyterHub release. These directions will guide you to the
specific command. In general, `pip install -U jupyterhub` or
`conda upgrade jupyterhub`
### Upgrade JupyterHub databases
To run the upgrade process for JupyterHub databases, enter:
```
jupyterhub upgrade-db
```
## Upgrade checklist
1. Backup JupyterHub database:
- `jupyterhub.sqlite` when using the default sqlite database
- Your JupyterHub database when using an RDBMS
2. Backup JupyterHub configuration file: `jupyterhub_config.py`
3. Shutdown the Hub
4. Upgrade JupyterHub
- `pip install -U jupyterhub` when using `pip`
- `conda upgrade jupyterhub` when using `conda`
5. Upgrade the database using run `jupyterhub upgrade-db`

View File

@@ -1,255 +0,0 @@
# JupyterHub URL scheme
This document describes how JupyterHub routes requests.
This does not include the [REST API](./rest.md) urls.
In general, all URLs can be prefixed with `c.JupyterHub.base_url` to
run the whole JupyterHub application on a prefix.
All authenticated handlers redirect to `/hub/login` to login users
prior to being redirected back to the originating page.
The returned request should preserve all query parameters.
## `/`
The top-level request is always a simple redirect to `/hub/`,
to be handled by the default JupyterHub handler.
In general, all requests to `/anything` that do not start with `/hub/`
but are routed to the Hub, will be redirected to `/hub/anything` before being handled by the Hub.
## `/hub/`
This is an authenticated URL.
This handler redirects users to the default URL of the application,
which defaults to the user's default server.
That is, it redirects to `/hub/spawn` if the user's server is not running,
or the server itself (`/user/:name`) if the server is running.
This default url behavior can be customized in two ways:
To redirect users to the JupyterHub home page (`/hub/home`)
instead of spawning their server,
set `redirect_to_server` to False:
```python
c.JupyterHub.redirect_to_server = False
```
This might be useful if you have a Hub where you expect
users to be managing multiple server configurations
and automatic spawning is not desirable.
Second, you can customise the landing page to any page you like,
such as a custom service you have deployed e.g. with course information:
```python
c.JupyterHub.default_url = '/services/my-landing-service'
```
## `/hub/home`
![The Hub home page with named servers enabled](../images/named-servers-home.png)
By default, the Hub home page has just one or two buttons
for starting and stopping the user's server.
If named servers are enabled, there will be some additional
tools for management of named servers.
*Version added: 1.0* named server UI is new in 1.0.
## `/hub/login`
This is the JupyterHub login page.
If you have a form-based username+password login,
such as the default PAMAuthenticator,
this page will render the login form.
![A login form](../images/login-form.png)
If login is handled by an external service,
e.g. with OAuth, this page will have a button,
declaring "Login with ..." which users can click
to login with the chosen service.
![A login redirect button](../images/login-button.png)
If you want to skip the user-interaction to initiate logging in
via the button, you can set
```python
c.Authenticator.auto_login = True
```
This can be useful when the user is "already logged in" via some mechanism,
but a handshake via redirects is necessary to complete the authentication with JupyterHub.
## `/hub/logout`
Visiting `/hub/logout` clears cookies from the current browser.
Note that **logging out does not stop a user's server(s)** by default.
If you would like to shutdown user servers on logout,
you can enable this behavior with:
```python
c.JupyterHub.shutdown_on_logout = True
```
Be careful with this setting because logging out one browser
does not mean the user is no longer actively using their server from another machine.
## `/user/:username[/:servername]`
If a user's server is running, this URL is handled by the user's given server,
not the Hub.
The username is the first part and, if using named servers,
the server name is the second part.
If the user's server is *not* running, this will be redirected to `/hub/user/:username/...`
## `/hub/user/:username[/:servername]`
This URL indicates a request for a user server that is not running
(because `/user/...` would have been handled by the notebook server
if the specified server were running).
Handling this URL is the most complicated condition in JupyterHub,
because there can be many states:
1. server is not active
a. user matches
b. user doesn't match
2. server is ready
3. server is pending, but not ready
If the server is pending spawn,
the browser will be redirected to `/hub/spawn-pending/:username/:servername`
to see a progress page while waiting for the server to be ready.
If the server is not active at all,
a page will be served with a link to `/hub/spawn/:username/:servername`.
Following that link will launch the requested server.
The HTTP status will be 503 in this case because a request has been made for a server that is not running.
If the server is ready, it is assumed that the proxy has not yet registered the route.
Some checks are performed and a delay is added before redirecting back to `/user/:username/:servername/...`.
If something is really wrong, this can result in a redirect loop.
Visiting this page will never result in triggering the spawn of servers
without additional user action (i.e. clicking the link on the page)
![Visiting a URL for a server that's not running](../images/not-running.png)
*Version changed: 1.0*
Prior to 1.0, this URL itself was responsible for spawning servers,
and served the progress page if it was pending,
redirected to running servers, and
This was useful because it made sure that requested servers were restarted after they stopped,
but could also be harmful because unused servers would continuously be restarted if e.g.
an idle JupyterLab frontend were open pointed at it,
which constantly makes polling requests.
### Special handling of API requests
Requests to `/user/:username[/:servername]/api/...` are assumed to be
from applications connected to stopped servers.
These are failed with 503 and an informative JSON error message
indicating how to spawn the server.
This is meant to help applications such as JupyterLab
that are connected to a server that has stopped.
*Version changed: 1.0*
JupyterHub 0.9 failed these API requests with status 404,
but 1.0 uses 503.
## `/user-redirect/...`
This URL is for sharing a URL that will redirect a user
to a path on their own default server.
This is useful when users have the same file at the same URL on their servers,
and you want a single link to give to any user that will open that file on their server.
e.g. a link to `/user-redirect/notebooks/Index.ipynb`
will send user `hortense` to `/user/hortense/notebooks/Index.ipynb`
**DO NOT** share links to your own server with other users.
This will not work in general,
unless you grant those users access to your server.
**Contributions welcome:** The JupyterLab "shareable link" should share this link
when run with JupyterHub, but it does not.
See [jupyterlab-hub](https://github.com/jupyterhub/jupyterlab-hub)
where this should probably be done and
[this issue in JupyterLab](https://github.com/jupyterlab/jupyterlab/issues/5388)
that is intended to make it possible.
## Spawning
### `/hub/spawn[/:username[/:servername]]`
Requesting `/hub/spawn` will spawn the default server for the current user.
If `username` and optionally `servername` are specified,
then the specified server for the specified user will be spawned.
Once spawn has been requested,
the browser is redirected to `/hub/spawn-pending/...`.
If `Spawner.options_form` is used,
this will render a form,
and a POST request will trigger the actual spawn and redirect.
![The spawn form](../images/spawn-form.png)
*Version added: 1.0*
1.0 adds the ability to specify username and servername.
Prior to 1.0, only `/hub/spawn` was recognized for the default server.
*Version changed: 1.0*
Prior to 1.0, this page redirected back to `/hub/user/:username`,
which was responsible for triggering spawn and rendering progress, etc.
### `/hub/spawn-pending[/:username[/:servername]]`
![The spawn pending page](../images/spawn-pending.png)
*Version added: 1.0* this URL is new in JupyterHub 1.0.
This page renders the progress view for the given spawn request.
Once the server is ready,
the browser is redirected to the running server at `/user/:username/:servername/...`.
If this page is requested at any time after the specified server is ready,
the browser will be redirected to the running server.
Requesting this page will never trigger any side effects.
If the server is not running (e.g. because the spawn has failed),
the spawn failure message (if applicable) will be displayed,
and the page will show a link back to `/hub/spawn/...`.
## `/hub/token`
![The token management page](../images/token-page.png)
On this page, users can manage their JupyterHub API tokens.
They can revoke access and request new tokens for writing scripts
against the [JupyterHub REST API](./rest.md).
## `/hub/admin`
![The admin panel](../images/named-servers-admin.png)
Administrators can take various administrative actions from this page:
1. add/remove users
2. grant admin privileges
3. start/stop user servers
4. shutdown JupyterHub itself

View File

@@ -99,23 +99,6 @@ single-user server, and not the environment(s) in which the user's kernel(s)
may run. Installing additional packages in the kernel environment does not may run. Installing additional packages in the kernel environment does not
pose additional risk to the web application's security. pose additional risk to the web application's security.
### Encrypt internal connections with SSL/TLS
By default, all communication on the server, between the proxy, hub, and single
-user notebooks is performed unencrypted. Setting the `internal_ssl` flag in
`jupyterhub_config.py` secures the aforementioned routes. Turning this
feature on does require that the enabled `Spawner` can use the certificates
generated by the `Hub` (the default `LocalProcessSpawner` can, for instance).
It is also important to note that this encryption **does not** (yet) cover the
`zmq tcp` sockets between the Notebook client and kernel. While users cannot
submit arbitrary commands to another user's kernel, they can bind to these
sockets and listen. When serving untrusted users, this eavesdropping can be
mitigated by setting `KernelManager.transport` to `ipc`. This applies standard
Unix permissions to the communication sockets thereby restricting
communication to the socket owner. The `internal_ssl` option will eventually
extend to securing the `tcp` sockets as well.
## Security audits ## Security audits
We recommend that you do periodic reviews of your deployment's security. It's We recommend that you do periodic reviews of your deployment's security. It's

View File

@@ -204,7 +204,7 @@ from there instead of the internet.
For instance, you can install JupyterHub with pip and configurable-http-proxy For instance, you can install JupyterHub with pip and configurable-http-proxy
with npmbox: with npmbox:
python3 -m pip wheel jupyterhub pip wheel jupyterhub
npmbox configurable-http-proxy npmbox configurable-http-proxy
### I want access to the whole filesystem, but still default users to their home directory ### I want access to the whole filesystem, but still default users to their home directory
@@ -236,7 +236,7 @@ then you can change the default URL to `/lab`.
For instance: For instance:
python3 -m pip install jupyterlab pip install jupyterlab
jupyter serverextension enable --py jupyterlab --sys-prefix jupyter serverextension enable --py jupyterlab --sys-prefix
The important thing is that jupyterlab is installed and enabled in the The important thing is that jupyterlab is installed and enabled in the

View File

@@ -0,0 +1,14 @@
Tutorials
=========
This section provides links to documentation that helps a user do a specific
task.
* :doc:`upgrade-dot-eight`
* `Zero to JupyterHub with Kubernetes <https://zero-to-jupyterhub.readthedocs.io/en/latest/>`_
.. toctree::
:maxdepth: 1
:hidden:
upgrade-dot-eight

View File

@@ -0,0 +1,93 @@
.. _upgrade-dot-eight:
Upgrading to JupyterHub version 0.8
===================================
This document will assist you in upgrading an existing JupyterHub deployment
from version 0.7 to version 0.8.
Upgrade checklist
-----------------
0. Review the release notes. Review any deprecated features and pay attention
to any backwards incompatible changes
1. Backup JupyterHub database:
- ``jupyterhub.sqlite`` when using the default sqlite database
- Your JupyterHub database when using an RDBMS
2. Backup the existing JupyterHub configuration file: ``jupyterhub_config.py``
3. Shutdown the Hub
4. Upgrade JupyterHub
- ``pip install -U jupyterhub`` when using ``pip``
- ``conda upgrade jupyterhub`` when using ``conda``
5. Upgrade the database using run ```jupyterhub upgrade-db``
6. Update the JupyterHub configuration file ``jupyterhub_config.py``
Backup JupyterHub database
--------------------------
To prevent unintended loss of data or configuration information, you should
back up the JupyterHub database (the default SQLite database or a RDBMS
database using PostgreSQL, MySQL, or others supported by SQLAlchemy):
- If using the default SQLite database, back up the ``jupyterhub.sqlite``
database.
- If using an RDBMS database such as PostgreSQL, MySQL, or other supported by
SQLAlchemy, back up the JupyterHub database.
.. note::
Losing the Hub database is often not a big deal. Information that resides only
in the Hub database includes:
- active login tokens (user cookies, service tokens)
- users added via GitHub UI, instead of config files
- info about running servers
If the following conditions are true, you should be fine clearing the Hub
database and starting over:
- users specified in config file
- user servers are stopped during upgrade
- don't mind causing users to login again after upgrade
Backup JupyterHub configuration file
------------------------------------
Backup up your configuration file, ``jupyterhub_config.py``, to a secure
location.
Shutdown JupyterHub
-------------------
- Prior to shutting down JupyterHub, you should notify the Hub users of the
scheduled downtime.
- Shutdown the JupyterHub service.
Upgrade JupyterHub
------------------
Follow directions that correspond to your package manager, ``pip`` or ``conda``,
for the new JupyterHub release:
- ``pip install -U jupyterhub`` for ``pip``
- ``conda upgrade jupyterhub`` for ``conda``
Upgrade the proxy, authenticator, or spawner if needed.
Upgrade JupyterHub database
---------------------------
To run the upgrade process for JupyterHub databases, enter::
jupyterhub upgrade-db
Update the JupyterHub configuration file
----------------------------------------
Create a new JupyterHub configuration file or edit a copy of the existing
file ``jupyterhub_config.py``.
Start JupyterHub
----------------
Start JupyterHub with the same command that you used before the upgrade.

View File

@@ -1,9 +1,8 @@
"""autodoc extension for configurable traits""" """autodoc extension for configurable traits"""
from traitlets import TraitType, Undefined
from sphinx.domains.python import PyClassmember from sphinx.domains.python import PyClassmember
from sphinx.ext.autodoc import AttributeDocumenter from sphinx.ext.autodoc import ClassDocumenter, AttributeDocumenter
from sphinx.ext.autodoc import ClassDocumenter
from traitlets import TraitType
from traitlets import Undefined
class ConfigurableDocumenter(ClassDocumenter): class ConfigurableDocumenter(ClassDocumenter):

View File

@@ -59,31 +59,7 @@ def create_dir_hook(spawner):
c.Spawner.pre_spawn_hook = create_dir_hook c.Spawner.pre_spawn_hook = create_dir_hook
``` ```
### Example #2 - Run `mkhomedir_helper` ### Example #2 - Run a shell script
Many Linux distributions provide a script that is responsible for user homedir bootstrapping: `/sbin/mkhomedir_helper`. To make use of it, you can use
```python
def create_dir_hook(spawner):
username = spawner.user.name
if not os.path.exists(os.path.join('/volumes/jupyterhub', username)):
subprocess.call(["sudo", "/sbin/mkhomedir_helper", spawner.user.name])
# attach the hook function to the spawner
c.Spawner.pre_spawn_hook = create_dir_hook
```
and make sure to add
```
jupyterhub ALL = (root) NOPASSWD: /sbin/mkhomedir_helper
```
in a new file in `/etc/sudoers.d`, or simply in `/etc/sudoers`.
All new home directories will be created from `/etc/skel`, so make sure to place any custom homedir-contents in there.
### Example #3 - Run a shell script
You can specify a plain ole' shell script (or any other executable) to be run You can specify a plain ole' shell script (or any other executable) to be run
by the bootstrap process. by the bootstrap process.

View File

@@ -5,10 +5,8 @@ create a directory for the user before the spawner starts
# pylint: disable=import-error # pylint: disable=import-error
import os import os
import shutil import shutil
from jupyter_client.localinterfaces import public_ips from jupyter_client.localinterfaces import public_ips
def create_dir_hook(spawner): def create_dir_hook(spawner):
""" Create directory """ """ Create directory """
username = spawner.user.name # get the username username = spawner.user.name # get the username
@@ -18,7 +16,6 @@ def create_dir_hook(spawner):
# now do whatever you think your user needs # now do whatever you think your user needs
# ... # ...
def clean_dir_hook(spawner): def clean_dir_hook(spawner):
""" Delete directory """ """ Delete directory """
username = spawner.user.name # get the username username = spawner.user.name # get the username
@@ -26,7 +23,6 @@ def clean_dir_hook(spawner):
if os.path.exists(temp_path) and os.path.isdir(temp_path): if os.path.exists(temp_path) and os.path.isdir(temp_path):
shutil.rmtree(temp_path) shutil.rmtree(temp_path)
# attach the hook functions to the spawner # attach the hook functions to the spawner
# pylint: disable=undefined-variable # pylint: disable=undefined-variable
c.Spawner.pre_spawn_hook = create_dir_hook c.Spawner.pre_spawn_hook = create_dir_hook

View File

@@ -15,7 +15,7 @@ c.JupyterHub.services = [
{ {
'name': 'cull-idle', 'name': 'cull-idle',
'admin': True, 'admin': True,
'command': [sys.executable, 'cull_idle_servers.py', '--timeout=3600'], 'command': 'python3 cull_idle_servers.py --timeout=3600'.split(),
} }
] ]
``` ```
@@ -36,6 +36,6 @@ Generate an API token and store it in the `JUPYTERHUB_API_TOKEN` environment
variable. Run `cull_idle_servers.py` manually. variable. Run `cull_idle_servers.py` manually.
```bash ```bash
export JUPYTERHUB_API_TOKEN=$(jupyterhub token) export JUPYTERHUB_API_TOKEN=`jupyterhub token`
python3 cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api] python3 cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api]
``` ```

View File

@@ -16,13 +16,13 @@ You can run this as a service managed by JupyterHub with this in your config::
{ {
'name': 'cull-idle', 'name': 'cull-idle',
'admin': True, 'admin': True,
'command': [sys.executable, 'cull_idle_servers.py', '--timeout=3600'], 'command': 'python3 cull_idle_servers.py --timeout=3600'.split(),
} }
] ]
Or run it manually by generating an API token and storing it in `JUPYTERHUB_API_TOKEN`: Or run it manually by generating an API token and storing it in `JUPYTERHUB_API_TOKEN`:
export JUPYTERHUB_API_TOKEN=$(jupyterhub token) export JUPYTERHUB_API_TOKEN=`jupyterhub token`
python3 cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api] python3 cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api]
This script uses the same ``--timeout`` and ``--max-age`` values for This script uses the same ``--timeout`` and ``--max-age`` values for
@@ -31,11 +31,11 @@ users and servers, you should add this script to the services list
twice, just with different ``name``s, different values, and one with twice, just with different ``name``s, different values, and one with
the ``--cull-users`` option. the ``--cull-users`` option.
""" """
from datetime import datetime, timezone
from functools import partial
import json import json
import os import os
from datetime import datetime
from datetime import timezone
from functools import partial
try: try:
from urllib.parse import quote from urllib.parse import quote
@@ -85,21 +85,23 @@ def format_td(td):
@coroutine @coroutine
def cull_idle( def cull_idle(url, api_token, inactive_limit, cull_users=False, max_age=0, concurrency=10):
url, api_token, inactive_limit, cull_users=False, max_age=0, concurrency=10
):
"""Shutdown idle single-user servers """Shutdown idle single-user servers
If cull_users, inactive *users* will be deleted as well. If cull_users, inactive *users* will be deleted as well.
""" """
auth_header = {'Authorization': 'token %s' % api_token} auth_header = {
req = HTTPRequest(url=url + '/users', headers=auth_header) 'Authorization': 'token %s' % api_token,
}
req = HTTPRequest(
url=url + '/users',
headers=auth_header,
)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
client = AsyncHTTPClient() client = AsyncHTTPClient()
if concurrency: if concurrency:
semaphore = Semaphore(concurrency) semaphore = Semaphore(concurrency)
@coroutine @coroutine
def fetch(req): def fetch(req):
"""client.fetch wrapped in a semaphore to limit concurrency""" """client.fetch wrapped in a semaphore to limit concurrency"""
@@ -108,7 +110,6 @@ def cull_idle(
return (yield client.fetch(req)) return (yield client.fetch(req))
finally: finally:
yield semaphore.release() yield semaphore.release()
else: else:
fetch = client.fetch fetch = client.fetch
@@ -128,8 +129,8 @@ def cull_idle(
log_name = '%s/%s' % (user['name'], server_name) log_name = '%s/%s' % (user['name'], server_name)
if server.get('pending'): if server.get('pending'):
app_log.warning( app_log.warning(
"Not culling server %s with pending %s", log_name, server['pending'] "Not culling server %s with pending %s",
) log_name, server['pending'])
return False return False
# jupyterhub < 0.9 defined 'server.url' once the server was ready # jupyterhub < 0.9 defined 'server.url' once the server was ready
@@ -141,8 +142,8 @@ def cull_idle(
if not server.get('ready', bool(server['url'])): if not server.get('ready', bool(server['url'])):
app_log.warning( app_log.warning(
"Not culling not-ready not-pending server %s: %s", log_name, server "Not culling not-ready not-pending server %s: %s",
) log_name, server)
return False return False
if server.get('started'): if server.get('started'):
@@ -162,13 +163,12 @@ def cull_idle(
# for running servers # for running servers
inactive = age inactive = age
should_cull = ( should_cull = (inactive is not None and
inactive is not None and inactive.total_seconds() >= inactive_limit inactive.total_seconds() >= inactive_limit)
)
if should_cull: if should_cull:
app_log.info( app_log.info(
"Culling server %s (inactive for %s)", log_name, format_td(inactive) "Culling server %s (inactive for %s)",
) log_name, format_td(inactive))
if max_age and not should_cull: if max_age and not should_cull:
# only check started if max_age is specified # only check started if max_age is specified
@@ -177,34 +177,32 @@ def cull_idle(
if age is not None and age.total_seconds() >= max_age: if age is not None and age.total_seconds() >= max_age:
app_log.info( app_log.info(
"Culling server %s (age: %s, inactive for %s)", "Culling server %s (age: %s, inactive for %s)",
log_name, log_name, format_td(age), format_td(inactive))
format_td(age),
format_td(inactive),
)
should_cull = True should_cull = True
if not should_cull: if not should_cull:
app_log.debug( app_log.debug(
"Not culling server %s (age: %s, inactive for %s)", "Not culling server %s (age: %s, inactive for %s)",
log_name, log_name, format_td(age), format_td(inactive))
format_td(age),
format_td(inactive),
)
return False return False
if server_name: if server_name:
# culling a named server # culling a named server
delete_url = url + "/users/%s/servers/%s" % ( delete_url = url + "/users/%s/servers/%s" % (
quote(user['name']), quote(user['name']), quote(server['name'])
quote(server['name']),
) )
else: else:
delete_url = url + '/users/%s/server' % quote(user['name']) delete_url = url + '/users/%s/server' % quote(user['name'])
req = HTTPRequest(url=delete_url, method='DELETE', headers=auth_header) req = HTTPRequest(
url=delete_url, method='DELETE', headers=auth_header,
)
resp = yield fetch(req) resp = yield fetch(req)
if resp.code == 202: if resp.code == 202:
app_log.warning("Server %s is slow to stop", log_name) app_log.warning(
"Server %s is slow to stop",
log_name,
)
# return False to prevent culling user with pending shutdowns # return False to prevent culling user with pending shutdowns
return False return False
return True return True
@@ -247,9 +245,7 @@ def cull_idle(
if still_alive: if still_alive:
app_log.debug( app_log.debug(
"Not culling user %s with %i servers still alive", "Not culling user %s with %i servers still alive",
user['name'], user['name'], still_alive)
still_alive,
)
return False return False
should_cull = False should_cull = False
@@ -269,11 +265,12 @@ def cull_idle(
# which introduces the 'created' field which is never None # which introduces the 'created' field which is never None
inactive = age inactive = age
should_cull = ( should_cull = (inactive is not None and
inactive is not None and inactive.total_seconds() >= inactive_limit inactive.total_seconds() >= inactive_limit)
)
if should_cull: if should_cull:
app_log.info("Culling user %s (inactive for %s)", user['name'], inactive) app_log.info(
"Culling user %s (inactive for %s)",
user['name'], inactive)
if max_age and not should_cull: if max_age and not should_cull:
# only check created if max_age is specified # only check created if max_age is specified
@@ -282,23 +279,19 @@ def cull_idle(
if age is not None and age.total_seconds() >= max_age: if age is not None and age.total_seconds() >= max_age:
app_log.info( app_log.info(
"Culling user %s (age: %s, inactive for %s)", "Culling user %s (age: %s, inactive for %s)",
user['name'], user['name'], format_td(age), format_td(inactive))
format_td(age),
format_td(inactive),
)
should_cull = True should_cull = True
if not should_cull: if not should_cull:
app_log.debug( app_log.debug(
"Not culling user %s (created: %s, last active: %s)", "Not culling user %s (created: %s, last active: %s)",
user['name'], user['name'], format_td(age), format_td(inactive))
format_td(age),
format_td(inactive),
)
return False return False
req = HTTPRequest( req = HTTPRequest(
url=url + '/users/%s' % user['name'], method='DELETE', headers=auth_header url=url + '/users/%s' % user['name'],
method='DELETE',
headers=auth_header,
) )
yield fetch(req) yield fetch(req)
return True return True
@@ -323,30 +316,20 @@ if __name__ == '__main__':
help="The JupyterHub API URL", help="The JupyterHub API URL",
) )
define('timeout', default=600, help="The idle timeout (in seconds)") define('timeout', default=600, help="The idle timeout (in seconds)")
define( define('cull_every', default=0,
'cull_every', help="The interval (in seconds) for checking for idle servers to cull")
default=0, define('max_age', default=0,
help="The interval (in seconds) for checking for idle servers to cull", help="The maximum age (in seconds) of servers that should be culled even if they are active")
) define('cull_users', default=False,
define(
'max_age',
default=0,
help="The maximum age (in seconds) of servers that should be culled even if they are active",
)
define(
'cull_users',
default=False,
help="""Cull users in addition to servers. help="""Cull users in addition to servers.
This is for use in temporary-user cases such as tmpnb.""", This is for use in temporary-user cases such as tmpnb.""",
) )
define( define('concurrency', default=10,
'concurrency',
default=10,
help="""Limit the number of concurrent requests made to the Hub. help="""Limit the number of concurrent requests made to the Hub.
Deleting a lot of users at the same time can slow down the Hub, Deleting a lot of users at the same time can slow down the Hub,
so limit the number of API requests we have outstanding at any given time. so limit the number of API requests we have outstanding at any given time.
""", """
) )
parse_command_line() parse_command_line()
@@ -360,8 +343,7 @@ if __name__ == '__main__':
app_log.warning( app_log.warning(
"Could not load pycurl: %s\n" "Could not load pycurl: %s\n"
"pycurl is recommended if you have a large number of users.", "pycurl is recommended if you have a large number of users.",
e, e)
)
loop = IOLoop.current() loop = IOLoop.current()
cull = partial( cull = partial(

View File

@@ -1,11 +1,8 @@
import sys
# run cull-idle as a service # run cull-idle as a service
c.JupyterHub.services = [ c.JupyterHub.services = [
{ {
'name': 'cull-idle', 'name': 'cull-idle',
'admin': True, 'admin': True,
'command': [sys.executable, 'cull_idle_servers.py', '--timeout=3600'], 'command': 'python3 cull_idle_servers.py --timeout=3600'.split(),
} }
] ]

View File

@@ -18,7 +18,7 @@ implementations in other web servers or languages.
1. generate an API token: 1. generate an API token:
export JUPYTERHUB_API_TOKEN=$(openssl rand -hex 32) export JUPYTERHUB_API_TOKEN=`openssl rand -hex 32`
2. launch a version of the the whoami service. 2. launch a version of the the whoami service.
For `whoami-oauth`: For `whoami-oauth`:

View File

@@ -4,9 +4,7 @@ import os
# this could come from anywhere # this could come from anywhere
api_token = os.getenv("JUPYTERHUB_API_TOKEN") api_token = os.getenv("JUPYTERHUB_API_TOKEN")
if not api_token: if not api_token:
raise ValueError( raise ValueError("Make sure to `export JUPYTERHUB_API_TOKEN=$(openssl rand -hex 32)`")
"Make sure to `export JUPYTERHUB_API_TOKEN=$(openssl rand -hex 32)`"
)
# tell JupyterHub to register the service as an external oauth client # tell JupyterHub to register the service as an external oauth client
@@ -16,5 +14,5 @@ c.JupyterHub.services = [
'oauth_client_id': "whoami-oauth-client-test", 'oauth_client_id': "whoami-oauth-client-test",
'api_token': api_token, 'api_token': api_token,
'oauth_redirect_uri': 'http://127.0.0.1:5555/oauth_callback', 'oauth_redirect_uri': 'http://127.0.0.1:5555/oauth_callback',
} },
] ]

View File

@@ -3,19 +3,18 @@
Implements OAuth handshake manually Implements OAuth handshake manually
so all URLs and requests necessary for OAuth with JupyterHub should be in one place so all URLs and requests necessary for OAuth with JupyterHub should be in one place
""" """
import json import json
import os import os
import sys import sys
from urllib.parse import urlencode from urllib.parse import urlencode, urlparse
from urllib.parse import urlparse
from tornado import log
from tornado import web
from tornado.auth import OAuth2Mixin from tornado.auth import OAuth2Mixin
from tornado.httpclient import AsyncHTTPClient from tornado.httpclient import AsyncHTTPClient, HTTPRequest
from tornado.httpclient import HTTPRequest
from tornado.httputil import url_concat from tornado.httputil import url_concat
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado import log
from tornado import web
class JupyterHubLoginHandler(web.RequestHandler): class JupyterHubLoginHandler(web.RequestHandler):
@@ -33,11 +32,11 @@ class JupyterHubLoginHandler(web.RequestHandler):
code=code, code=code,
redirect_uri=self.settings['redirect_uri'], redirect_uri=self.settings['redirect_uri'],
) )
req = HTTPRequest( req = HTTPRequest(self.settings['token_url'], method='POST',
self.settings['token_url'],
method='POST',
body=urlencode(params).encode('utf8'), body=urlencode(params).encode('utf8'),
headers={'Content-Type': 'application/x-www-form-urlencoded'}, headers={
'Content-Type': 'application/x-www-form-urlencoded',
},
) )
response = await AsyncHTTPClient().fetch(req) response = await AsyncHTTPClient().fetch(req)
data = json.loads(response.body.decode('utf8', 'replace')) data = json.loads(response.body.decode('utf8', 'replace'))
@@ -56,16 +55,14 @@ class JupyterHubLoginHandler(web.RequestHandler):
# we are the login handler, # we are the login handler,
# begin oauth process which will come back later with an # begin oauth process which will come back later with an
# authorization_code # authorization_code
self.redirect( self.redirect(url_concat(
url_concat(
self.settings['authorize_url'], self.settings['authorize_url'],
dict( dict(
redirect_uri=self.settings['redirect_uri'], redirect_uri=self.settings['redirect_uri'],
client_id=self.settings['client_id'], client_id=self.settings['client_id'],
response_type='code', response_type='code',
),
)
) )
))
class WhoAmIHandler(web.RequestHandler): class WhoAmIHandler(web.RequestHandler):
@@ -88,7 +85,10 @@ class WhoAmIHandler(web.RequestHandler):
"""Retrieve the user for a given token, via /hub/api/user""" """Retrieve the user for a given token, via /hub/api/user"""
req = HTTPRequest( req = HTTPRequest(
self.settings['user_url'], headers={'Authorization': f'token {token}'} self.settings['user_url'],
headers={
'Authorization': f'token {token}'
},
) )
response = await AsyncHTTPClient().fetch(req) response = await AsyncHTTPClient().fetch(req)
return json.loads(response.body.decode('utf8', 'replace')) return json.loads(response.body.decode('utf8', 'replace'))
@@ -110,23 +110,23 @@ def main():
token_url = hub_api + '/oauth2/token' token_url = hub_api + '/oauth2/token'
user_url = hub_api + '/user' user_url = hub_api + '/user'
app = web.Application( app = web.Application([
[('/oauth_callback', JupyterHubLoginHandler), ('/', WhoAmIHandler)], ('/oauth_callback', JupyterHubLoginHandler),
('/', WhoAmIHandler),
],
login_url='/oauth_callback', login_url='/oauth_callback',
cookie_secret=os.urandom(32), cookie_secret=os.urandom(32),
api_token=os.environ['JUPYTERHUB_API_TOKEN'], api_token=os.environ['JUPYTERHUB_API_TOKEN'],
client_id=os.environ['JUPYTERHUB_CLIENT_ID'], client_id=os.environ['JUPYTERHUB_CLIENT_ID'],
redirect_uri=os.environ['JUPYTERHUB_SERVICE_URL'].rstrip('/') redirect_uri=os.environ['JUPYTERHUB_SERVICE_URL'].rstrip('/') + '/oauth_callback',
+ '/oauth_callback',
authorize_url=authorize_url, authorize_url=authorize_url,
token_url=token_url, token_url=token_url,
user_url=user_url, user_url=user_url,
) )
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL']) url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
log.app_log.info( log.app_log.info("Running basic whoami service on %s",
"Running basic whoami service on %s", os.environ['JUPYTERHUB_SERVICE_URL'] os.environ['JUPYTERHUB_SERVICE_URL'])
)
app.listen(url.port, url.hostname) app.listen(url.port, url.hostname)
IOLoop.current().start() IOLoop.current().start()

View File

@@ -8,10 +8,10 @@ c.Authenticator.whitelist = {'ganymede', 'io', 'rhea'}
# These environment variables are automatically supplied by the linked postgres # These environment variables are automatically supplied by the linked postgres
# container. # container.
import os import os;
pg_pass = os.getenv('POSTGRES_ENV_JPY_PSQL_PASSWORD') pg_pass = os.getenv('POSTGRES_ENV_JPY_PSQL_PASSWORD')
pg_host = os.getenv('POSTGRES_PORT_5432_TCP_ADDR') pg_host = os.getenv('POSTGRES_PORT_5432_TCP_ADDR')
c.JupyterHub.db_url = 'postgresql://jupyterhub:{}@{}:5432/jupyterhub'.format( c.JupyterHub.db_url = 'postgresql://jupyterhub:{}@{}:5432/jupyterhub'.format(
pg_pass, pg_host pg_pass,
pg_host,
) )

View File

@@ -11,7 +11,7 @@ configuration file something like:
{ {
'name': 'announcement', 'name': 'announcement',
'url': 'http://127.0.0.1:8888', 'url': 'http://127.0.0.1:8888',
'command': [sys.executable, "-m", "announcement"], 'command': ["python", "-m", "announcement"],
} }
] ]

View File

@@ -1,14 +1,11 @@
import argparse import argparse
import datetime import datetime
import json import json
import os import os
from tornado import escape
from tornado import gen
from tornado import ioloop
from tornado import web
from jupyterhub.services.auth import HubAuthenticated from jupyterhub.services.auth import HubAuthenticated
from tornado import escape, gen, ioloop, web
class AnnouncementRequestHandler(HubAuthenticated, web.RequestHandler): class AnnouncementRequestHandler(HubAuthenticated, web.RequestHandler):
@@ -24,7 +21,6 @@ class AnnouncementRequestHandler(HubAuthenticated, web.RequestHandler):
@web.authenticated @web.authenticated
def post(self): def post(self):
"""Update announcement""" """Update announcement"""
user = self.get_current_user()
doc = escape.json_decode(self.request.body) doc = escape.json_decode(self.request.body)
self.storage["announcement"] = doc["announcement"] self.storage["announcement"] = doc["announcement"]
self.storage["timestamp"] = datetime.datetime.now().isoformat() self.storage["timestamp"] = datetime.datetime.now().isoformat()
@@ -56,19 +52,19 @@ def main():
def parse_arguments(): def parse_arguments():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument("--api-prefix", "-a",
"--api-prefix",
"-a",
default=os.environ.get("JUPYTERHUB_SERVICE_PREFIX", "/"), default=os.environ.get("JUPYTERHUB_SERVICE_PREFIX", "/"),
help="application API prefix", help="application API prefix")
) parser.add_argument("--port", "-p",
parser.add_argument( default=8888,
"--port", "-p", default=8888, help="port for API to listen on", type=int help="port for API to listen on",
) type=int)
return parser.parse_args() return parser.parse_args()
def create_application(api_prefix="/", handler=AnnouncementRequestHandler, **kwargs): def create_application(api_prefix="/",
handler=AnnouncementRequestHandler,
**kwargs):
storage = dict(announcement="", timestamp="", user="") storage = dict(announcement="", timestamp="", user="")
return web.Application([(api_prefix, handler, dict(storage=storage))]) return web.Application([(api_prefix, handler, dict(storage=storage))])

View File

@@ -1,4 +1,3 @@
import sys
# To run the announcement service managed by the hub, add this. # To run the announcement service managed by the hub, add this.
@@ -6,7 +5,7 @@ c.JupyterHub.services = [
{ {
'name': 'announcement', 'name': 'announcement',
'url': 'http://127.0.0.1:8888', 'url': 'http://127.0.0.1:8888',
'command': [sys.executable, "-m", "announcement"], 'command': ["python", "-m", "announcement"],
} }
] ]

View File

@@ -22,3 +22,4 @@ In the external example, some extra steps are required to set up supervisor:
3. install `shared-notebook-service` somewhere on your system, and update `/path/to/shared-notebook-service` to the absolute path of this destination 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/` 3. copy `shared-notebook.conf` to `/etc/supervisor/conf.d/`
4. `supervisorctl reload` 4. `supervisorctl reload`

View File

@@ -1,9 +1,18 @@
# our user list # our user list
c.Authenticator.whitelist = ['minrk', 'ellisonbg', 'willingc'] c.Authenticator.whitelist = [
'minrk',
'ellisonbg',
'willingc',
]
# ellisonbg and willingc have access to a shared server: # ellisonbg and willingc have access to a shared server:
c.JupyterHub.load_groups = {'shared': ['ellisonbg', 'willingc']} c.JupyterHub.load_groups = {
'shared': [
'ellisonbg',
'willingc',
]
}
# start the notebook server as a service # start the notebook server as a service
c.JupyterHub.services = [ c.JupyterHub.services = [

View File

@@ -1,9 +1,18 @@
# our user list # our user list
c.Authenticator.whitelist = ['minrk', 'ellisonbg', 'willingc'] c.Authenticator.whitelist = [
'minrk',
'ellisonbg',
'willingc',
]
# ellisonbg and willingc have access to a shared server: # ellisonbg and willingc have access to a shared server:
c.JupyterHub.load_groups = {'shared': ['ellisonbg', 'willingc']} c.JupyterHub.load_groups = {
'shared': [
'ellisonbg',
'willingc',
]
}
service_name = 'shared-notebook' service_name = 'shared-notebook'
service_port = 9999 service_port = 9999
@@ -14,6 +23,10 @@ c.JupyterHub.services = [
{ {
'name': service_name, 'name': service_name,
'url': 'http://127.0.0.1:{}'.format(service_port), 'url': 'http://127.0.0.1:{}'.format(service_port),
'command': ['jupyterhub-singleuser', '--group=shared', '--debug'], 'command': [
'jupyterhub-singleuser',
'--group=shared',
'--debug',
],
} }
] ]

View File

@@ -6,12 +6,16 @@ c.JupyterHub.services = [
'name': 'whoami', 'name': 'whoami',
'url': 'http://127.0.0.1:10101', 'url': 'http://127.0.0.1:10101',
'command': ['flask', 'run', '--port=10101'], 'command': ['flask', 'run', '--port=10101'],
'environment': {'FLASK_APP': 'whoami-flask.py'}, 'environment': {
'FLASK_APP': 'whoami-flask.py',
}
}, },
{ {
'name': 'whoami-oauth', 'name': 'whoami-oauth',
'url': 'http://127.0.0.1:10201', 'url': 'http://127.0.0.1:10201',
'command': ['flask', 'run', '--port=10201'], 'command': ['flask', 'run', '--port=10201'],
'environment': {'FLASK_APP': 'whoami-oauth.py'}, 'environment': {
'FLASK_APP': 'whoami-oauth.py',
}
}, },
] ]

View File

@@ -1,4 +1,4 @@
export CONFIGPROXY_AUTH_TOKEN=$(openssl rand -hex 32) export CONFIGPROXY_AUTH_TOKEN=`openssl rand -hex 32`
# start JupyterHub # start JupyterHub
jupyterhub --ip=127.0.0.1 jupyterhub --ip=127.0.0.1

View File

@@ -2,29 +2,29 @@
""" """
whoami service authentication with the Hub whoami service authentication with the Hub
""" """
from functools import wraps
import json import json
import os import os
from functools import wraps
from urllib.parse import quote from urllib.parse import quote
from flask import Flask from flask import Flask, redirect, request, Response
from flask import redirect
from flask import request
from flask import Response
from jupyterhub.services.auth import HubAuth from jupyterhub.services.auth import HubAuth
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/') prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
auth = HubAuth(api_token=os.environ['JUPYTERHUB_API_TOKEN'], cache_max_age=60) auth = HubAuth(
api_token=os.environ['JUPYTERHUB_API_TOKEN'],
cache_max_age=60,
)
app = Flask(__name__) app = Flask(__name__)
def authenticated(f): def authenticated(f):
"""Decorator for authenticating with the Hub""" """Decorator for authenticating with the Hub"""
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
cookie = request.cookies.get(auth.cookie_name) cookie = request.cookies.get(auth.cookie_name)
@@ -40,7 +40,6 @@ def authenticated(f):
else: else:
# redirect to login url on failed auth # redirect to login url on failed auth
return redirect(auth.login_url + '?next=%s' % quote(request.path)) return redirect(auth.login_url + '?next=%s' % quote(request.path))
return decorated return decorated
@@ -48,5 +47,7 @@ def authenticated(f):
@authenticated @authenticated
def whoami(user): def whoami(user):
return Response( return Response(
json.dumps(user, indent=1, sort_keys=True), mimetype='application/json' json.dumps(user, indent=1, sort_keys=True),
mimetype='application/json',
) )

View File

@@ -2,29 +2,28 @@
""" """
whoami service authentication with the Hub whoami service authentication with the Hub
""" """
from functools import wraps
import json import json
import os import os
from functools import wraps
from flask import Flask from flask import Flask, redirect, request, Response, make_response
from flask import make_response
from flask import redirect
from flask import request
from flask import Response
from jupyterhub.services.auth import HubOAuth from jupyterhub.services.auth import HubOAuth
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/') prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
auth = HubOAuth(api_token=os.environ['JUPYTERHUB_API_TOKEN'], cache_max_age=60) auth = HubOAuth(
api_token=os.environ['JUPYTERHUB_API_TOKEN'],
cache_max_age=60,
)
app = Flask(__name__) app = Flask(__name__)
def authenticated(f): def authenticated(f):
"""Decorator for authenticating with the Hub via OAuth""" """Decorator for authenticating with the Hub via OAuth"""
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
token = request.cookies.get(auth.cookie_name) token = request.cookies.get(auth.cookie_name)
@@ -40,7 +39,6 @@ def authenticated(f):
response = make_response(redirect(auth.login_url + '&state=%s' % state)) response = make_response(redirect(auth.login_url + '&state=%s' % state))
response.set_cookie(auth.state_cookie_name, state) response.set_cookie(auth.state_cookie_name, state)
return response return response
return decorated return decorated
@@ -48,10 +46,10 @@ def authenticated(f):
@authenticated @authenticated
def whoami(user): def whoami(user):
return Response( return Response(
json.dumps(user, indent=1, sort_keys=True), mimetype='application/json' json.dumps(user, indent=1, sort_keys=True),
mimetype='application/json',
) )
@app.route(prefix + 'oauth_callback') @app.route(prefix + 'oauth_callback')
def oauth_callback(): def oauth_callback():
code = request.args.get('code', None) code = request.args.get('code', None)

View File

@@ -4,22 +4,18 @@ This example service serves `/services/whoami/`,
authenticated with the Hub, authenticated with the Hub,
showing the user their own info. showing the user their own info.
""" """
from getpass import getuser
import json import json
import os import os
from getpass import getuser
from urllib.parse import urlparse from urllib.parse import urlparse
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.web import Application from tornado.httpserver import HTTPServer
from tornado.web import authenticated from tornado.web import RequestHandler, Application, authenticated
from tornado.web import RequestHandler
from jupyterhub.services.auth import HubOAuthCallbackHandler from jupyterhub.services.auth import HubOAuthenticated, HubOAuthCallbackHandler
from jupyterhub.services.auth import HubOAuthenticated
from jupyterhub.utils import url_path_join from jupyterhub.utils import url_path_join
class WhoAmIHandler(HubOAuthenticated, RequestHandler): class WhoAmIHandler(HubOAuthenticated, RequestHandler):
# hub_users can be a set of users who are allowed to access the service # hub_users can be a set of users who are allowed to access the service
# `getuser()` here would mean only the user who started the service # `getuser()` here would mean only the user who started the service
@@ -33,21 +29,12 @@ class WhoAmIHandler(HubOAuthenticated, RequestHandler):
self.set_header('content-type', 'application/json') self.set_header('content-type', 'application/json')
self.write(json.dumps(user_model, indent=1, sort_keys=True)) self.write(json.dumps(user_model, indent=1, sort_keys=True))
def main(): def main():
app = Application( app = Application([
[
(os.environ['JUPYTERHUB_SERVICE_PREFIX'], WhoAmIHandler), (os.environ['JUPYTERHUB_SERVICE_PREFIX'], WhoAmIHandler),
( (url_path_join(os.environ['JUPYTERHUB_SERVICE_PREFIX'], 'oauth_callback'), HubOAuthCallbackHandler),
url_path_join(
os.environ['JUPYTERHUB_SERVICE_PREFIX'], 'oauth_callback'
),
HubOAuthCallbackHandler,
),
(r'.*', WhoAmIHandler), (r'.*', WhoAmIHandler),
], ], cookie_secret=os.urandom(32))
cookie_secret=os.urandom(32),
)
http_server = HTTPServer(app) http_server = HTTPServer(app)
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL']) url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
@@ -56,6 +43,5 @@ def main():
IOLoop.current().start() IOLoop.current().start()
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@@ -2,16 +2,14 @@
This serves `/services/whoami/`, authenticated with the Hub, showing the user their own info. This serves `/services/whoami/`, authenticated with the Hub, showing the user their own info.
""" """
from getpass import getuser
import json import json
import os import os
from getpass import getuser
from urllib.parse import urlparse from urllib.parse import urlparse
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.web import Application from tornado.httpserver import HTTPServer
from tornado.web import authenticated from tornado.web import RequestHandler, Application, authenticated
from tornado.web import RequestHandler
from jupyterhub.services.auth import HubAuthenticated from jupyterhub.services.auth import HubAuthenticated
@@ -29,14 +27,11 @@ class WhoAmIHandler(HubAuthenticated, RequestHandler):
self.set_header('content-type', 'application/json') self.set_header('content-type', 'application/json')
self.write(json.dumps(user_model, indent=1, sort_keys=True)) self.write(json.dumps(user_model, indent=1, sort_keys=True))
def main(): def main():
app = Application( app = Application([
[
(os.environ['JUPYTERHUB_SERVICE_PREFIX'] + '/?', WhoAmIHandler), (os.environ['JUPYTERHUB_SERVICE_PREFIX'] + '/?', WhoAmIHandler),
(r'.*', WhoAmIHandler), (r'.*', WhoAmIHandler),
] ])
)
http_server = HTTPServer(app) http_server = HTTPServer(app)
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL']) url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
@@ -45,6 +40,5 @@ def main():
IOLoop.current().start() IOLoop.current().start()
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@@ -5,7 +5,6 @@ import shlex
from jupyterhub.spawner import LocalProcessSpawner from jupyterhub.spawner import LocalProcessSpawner
class DemoFormSpawner(LocalProcessSpawner): class DemoFormSpawner(LocalProcessSpawner):
def _options_form_default(self): def _options_form_default(self):
default_env = "YOURNAME=%s\n" % self.user.name default_env = "YOURNAME=%s\n" % self.user.name
@@ -14,9 +13,7 @@ class DemoFormSpawner(LocalProcessSpawner):
<input name="args" placeholder="e.g. --debug"></input> <input name="args" placeholder="e.g. --debug"></input>
<label for="env">Environment variables (one per line)</label> <label for="env">Environment variables (one per line)</label>
<textarea name="env">{env}</textarea> <textarea name="env">{env}</textarea>
""".format( """.format(env=default_env)
env=default_env
)
def options_from_form(self, formdata): def options_from_form(self, formdata):
options = {} options = {}
@@ -46,5 +43,4 @@ class DemoFormSpawner(LocalProcessSpawner):
env.update(self.user_options['env']) env.update(self.user_options['env'])
return env return env
c.JupyterHub.spawner_class = DemoFormSpawner c.JupyterHub.spawner_class = DemoFormSpawner

View File

@@ -1,2 +1 @@
from ._version import __version__ from ._version import version_info, __version__
from ._version import version_info

View File

@@ -1,3 +1,2 @@
from .app import main from .app import main
main() main()

View File

@@ -5,7 +5,6 @@ def get_data_files():
"""Walk up until we find share/jupyterhub""" """Walk up until we find share/jupyterhub"""
import sys import sys
from os.path import join, abspath, dirname, exists, split from os.path import join, abspath, dirname, exists, split
path = abspath(dirname(__file__)) path = abspath(dirname(__file__))
starting_points = [path] starting_points = [path]
if not path.startswith(sys.prefix): if not path.startswith(sys.prefix):

View File

@@ -1,12 +1,13 @@
"""JupyterHub version info""" """JupyterHub version info"""
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
version_info = ( version_info = (
1,
0, 0,
0, 9,
"b1", # release (b1, rc1, or "" for final or dev) 3,
"", # release (b1, rc1, or "" for final or dev)
# "dev", # dev or nothing # "dev", # dev or nothing
) )
@@ -22,23 +23,16 @@ __version__ = ".".join(map(str, version_info[:3])) + ".".join(version_info[3:])
def _check_version(hub_version, singleuser_version, log): def _check_version(hub_version, singleuser_version, log):
"""Compare Hub and single-user server versions""" """Compare Hub and single-user server versions"""
if not hub_version: if not hub_version:
log.warning( log.warning("Hub has no version header, which means it is likely < 0.8. Expected %s", __version__)
"Hub has no version header, which means it is likely < 0.8. Expected %s",
__version__,
)
return return
if not singleuser_version: if not singleuser_version:
log.warning( log.warning("Single-user server has no version header, which means it is likely < 0.8. Expected %s", __version__)
"Single-user server has no version header, which means it is likely < 0.8. Expected %s",
__version__,
)
return return
# compare minor X.Y versions # compare minor X.Y versions
if hub_version != singleuser_version: if hub_version != singleuser_version:
from distutils.version import LooseVersion as V from distutils.version import LooseVersion as V
hub_major_minor = V(hub_version).version[:2] hub_major_minor = V(hub_version).version[:2]
singleuser_major_minor = V(singleuser_version).version[:2] singleuser_major_minor = V(singleuser_version).version[:2]
extra = "" extra = ""
@@ -56,6 +50,4 @@ def _check_version(hub_version, singleuser_version, log):
singleuser_version, singleuser_version,
) )
else: else:
log.debug( log.debug("jupyterhub and jupyterhub-singleuser both on version %s" % hub_version)
"jupyterhub and jupyterhub-singleuser both on version %s" % hub_version
)

View File

@@ -1,10 +1,9 @@
import logging
import sys import sys
from logging.config import fileConfig
from alembic import context from alembic import context
from sqlalchemy import engine_from_config from sqlalchemy import engine_from_config, pool
from sqlalchemy import pool import logging
from logging.config import fileConfig
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
@@ -15,7 +14,6 @@ config = context.config
if 'jupyterhub' in sys.modules: if 'jupyterhub' in sys.modules:
from traitlets.config import MultipleInstanceError from traitlets.config import MultipleInstanceError
from jupyterhub.app import JupyterHub from jupyterhub.app import JupyterHub
app = None app = None
if JupyterHub.initialized(): if JupyterHub.initialized():
try: try:
@@ -34,7 +32,6 @@ else:
# add your model's MetaData object here for 'autogenerate' support # add your model's MetaData object here for 'autogenerate' support
from jupyterhub import orm from jupyterhub import orm
target_metadata = orm.Base.metadata target_metadata = orm.Base.metadata
# other values from the config, defined by the needs of env.py, # other values from the config, defined by the needs of env.py,
@@ -56,7 +53,8 @@ def run_migrations_offline():
""" """
url = config.get_main_option("sqlalchemy.url") url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True) context.configure(
url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
@@ -72,16 +70,17 @@ def run_migrations_online():
connectable = engine_from_config( connectable = engine_from_config(
config.get_section(config.config_ini_section), config.get_section(config.config_ini_section),
prefix='sqlalchemy.', prefix='sqlalchemy.',
poolclass=pool.NullPool, poolclass=pool.NullPool)
)
with connectable.connect() as connection: with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata) context.configure(
connection=connection,
target_metadata=target_metadata
)
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
if context.is_offline_mode(): if context.is_offline_mode():
run_migrations_offline() run_migrations_offline()
else: else:

View File

@@ -5,6 +5,7 @@ Revises:
Create Date: 2016-04-11 16:05:34.873288 Create Date: 2016-04-11 16:05:34.873288
""" """
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '19c0846f6344' revision = '19c0846f6344'
down_revision = None down_revision = None

View File

@@ -5,6 +5,7 @@ Revises: 3ec6993fe20c
Create Date: 2017-12-07 14:43:51.500740 Create Date: 2017-12-07 14:43:51.500740
""" """
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '1cebaf56856c' revision = '1cebaf56856c'
down_revision = '3ec6993fe20c' down_revision = '3ec6993fe20c'
@@ -12,7 +13,6 @@ branch_labels = None
depends_on = None depends_on = None
import logging import logging
logger = logging.getLogger('alembic') logger = logging.getLogger('alembic')
from alembic import op from alembic import op

View File

@@ -12,6 +12,7 @@ Revises: af4cbdb2d13c
Create Date: 2017-07-28 16:44:40.413648 Create Date: 2017-07-28 16:44:40.413648
""" """
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '3ec6993fe20c' revision = '3ec6993fe20c'
down_revision = 'af4cbdb2d13c' down_revision = 'af4cbdb2d13c'
@@ -43,9 +44,7 @@ def upgrade():
except sa.exc.OperationalError: except sa.exc.OperationalError:
# this won't be a problem moving forward, but downgrade will fail # this won't be a problem moving forward, but downgrade will fail
if op.get_context().dialect.name == 'sqlite': if op.get_context().dialect.name == 'sqlite':
logger.warning( logger.warning("sqlite cannot drop columns. Leaving unused old columns in place.")
"sqlite cannot drop columns. Leaving unused old columns in place."
)
else: else:
raise raise
@@ -55,13 +54,15 @@ def upgrade():
def downgrade(): def downgrade():
# drop all the new tables # drop all the new tables
engine = op.get_bind().engine engine = op.get_bind().engine
for table in ('oauth_clients', 'oauth_codes', 'oauth_access_tokens', 'spawners'): for table in ('oauth_clients',
'oauth_codes',
'oauth_access_tokens',
'spawners'):
if engine.has_table(table): if engine.has_table(table):
op.drop_table(table) op.drop_table(table)
op.drop_column('users', 'encrypted_auth_state') op.drop_column('users', 'encrypted_auth_state')
op.add_column('users', sa.Column('auth_state', JSONDict)) op.add_column('users', sa.Column('auth_state', JSONDict))
op.add_column( op.add_column('users', sa.Column('_server_id', sa.Integer, sa.ForeignKey('servers.id')))
'users', sa.Column('_server_id', sa.Integer, sa.ForeignKey('servers.id'))
)

View File

@@ -1,26 +0,0 @@
"""persist user_options
Revision ID: 4dc2d5a8c53c
Revises: 896818069c98
Create Date: 2019-02-28 14:14:27.423927
"""
# revision identifiers, used by Alembic.
revision = '4dc2d5a8c53c'
down_revision = '896818069c98'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
from jupyterhub.orm import JSONDict
def upgrade():
tables = op.get_bind().engine.table_names()
if 'spawners' in tables:
op.add_column('spawners', sa.Column('user_options', JSONDict()))
def downgrade():
op.drop_column('spawners', sa.Column('user_options'))

View File

@@ -5,6 +5,7 @@ Revises: 1cebaf56856c
Create Date: 2017-12-19 15:21:09.300513 Create Date: 2017-12-19 15:21:09.300513
""" """
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '56cc5a70207e' revision = '56cc5a70207e'
down_revision = '1cebaf56856c' down_revision = '1cebaf56856c'
@@ -15,48 +16,22 @@ from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
import logging import logging
logger = logging.getLogger('alembic') logger = logging.getLogger('alembic')
def upgrade(): def upgrade():
tables = op.get_bind().engine.table_names() tables = op.get_bind().engine.table_names()
op.add_column('api_tokens', sa.Column('created', sa.DateTime(), nullable=True)) op.add_column('api_tokens', sa.Column('created', sa.DateTime(), nullable=True))
op.add_column( op.add_column('api_tokens', sa.Column('last_activity', sa.DateTime(), nullable=True))
'api_tokens', sa.Column('last_activity', sa.DateTime(), nullable=True) op.add_column('api_tokens', sa.Column('note', sa.Unicode(length=1023), nullable=True))
)
op.add_column(
'api_tokens', sa.Column('note', sa.Unicode(length=1023), nullable=True)
)
if 'oauth_access_tokens' in tables: if 'oauth_access_tokens' in tables:
op.add_column( op.add_column('oauth_access_tokens', sa.Column('created', sa.DateTime(), nullable=True))
'oauth_access_tokens', sa.Column('created', sa.DateTime(), nullable=True) op.add_column('oauth_access_tokens', sa.Column('last_activity', sa.DateTime(), nullable=True))
)
op.add_column(
'oauth_access_tokens',
sa.Column('last_activity', sa.DateTime(), nullable=True),
)
if op.get_context().dialect.name == 'sqlite': if op.get_context().dialect.name == 'sqlite':
logger.warning( logger.warning("sqlite cannot use ALTER TABLE to create foreign keys. Upgrade will be incomplete.")
"sqlite cannot use ALTER TABLE to create foreign keys. Upgrade will be incomplete."
)
else: else:
op.create_foreign_key( op.create_foreign_key(None, 'oauth_access_tokens', 'oauth_clients', ['client_id'], ['identifier'], ondelete='CASCADE')
None, op.create_foreign_key(None, 'oauth_codes', 'oauth_clients', ['client_id'], ['identifier'], ondelete='CASCADE')
'oauth_access_tokens',
'oauth_clients',
['client_id'],
['identifier'],
ondelete='CASCADE',
)
op.create_foreign_key(
None,
'oauth_codes',
'oauth_clients',
['client_id'],
['identifier'],
ondelete='CASCADE',
)
def downgrade(): def downgrade():

View File

@@ -5,6 +5,7 @@ Revises: d68c98b66cd4
Create Date: 2018-05-07 11:35:58.050542 Create Date: 2018-05-07 11:35:58.050542
""" """
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '896818069c98' revision = '896818069c98'
down_revision = 'd68c98b66cd4' down_revision = 'd68c98b66cd4'

Some files were not shown because too many files have changed in this diff Show More