Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e94f5e043a | ||
![]() |
5456fb6356 | ||
![]() |
fb75b9a392 | ||
![]() |
90d341e6f7 |
@@ -1,5 +1,4 @@
|
|||||||
[run]
|
[run]
|
||||||
parallel = True
|
|
||||||
branch = False
|
branch = False
|
||||||
omit =
|
omit =
|
||||||
jupyterhub/tests/*
|
jupyterhub/tests/*
|
||||||
|
5
.flake8
@@ -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
@@ -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
|
|
||||||
|
@@ -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
|
|
32
.travis.yml
@@ -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
|
||||||
|
136
CONTRIBUTING.md
@@ -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
|
||||||
|
```
|
||||||
|
@@ -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 \
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
10
README.md
@@ -12,12 +12,11 @@
|
|||||||
|
|
||||||
[](https://pypi.python.org/pypi/jupyterhub)
|
[](https://pypi.python.org/pypi/jupyterhub)
|
||||||
[](https://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
[](https://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
||||||
|
[](https://jupyterhub.readthedocs.io/en/0.7.2/?badge=0.7.2)
|
||||||
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
||||||
[](https://circleci.com/gh/jupyterhub/jupyterhub)
|
[](https://circleci.com/gh/jupyterhub/jupyterhub)
|
||||||
[](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
|
[](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
|
||||||
[](https://github.com/jupyterhub/jupyterhub/issues)
|
[](https://groups.google.com/forum/#!forum/jupyter)
|
||||||
[](https://discourse.jupyter.org/c/jupyterhub)
|
|
||||||
[](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)** |
|
||||||
|
@@ -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__))
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
|
||||||
|
@@ -7,3 +7,5 @@ ENV LANG=en_US.UTF-8
|
|||||||
|
|
||||||
USER nobody
|
USER nobody
|
||||||
CMD ["jupyterhub"]
|
CMD ["jupyterhub"]
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
|
|
||||||
|
@@ -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
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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.
|
||||||
|
@@ -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!
|
|
@@ -13,3 +13,4 @@ Module: :mod:`jupyterhub.app`
|
|||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
.. autoconfigurable:: JupyterHub
|
.. autoconfigurable:: JupyterHub
|
||||||
|
|
||||||
|
@@ -26,7 +26,3 @@ Module: :mod:`jupyterhub.auth`
|
|||||||
|
|
||||||
.. autoconfigurable:: PAMAuthenticator
|
.. autoconfigurable:: PAMAuthenticator
|
||||||
|
|
||||||
:class:`DummyAuthenticator`
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
.. autoconfigurable:: DummyAuthenticator
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -38,3 +38,4 @@ Module: :mod:`jupyterhub.services.auth`
|
|||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
.. autoclass:: HubOAuthCallbackHandler
|
.. autoclass:: HubOAuthCallbackHandler
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -34,3 +34,4 @@ Module: :mod:`jupyterhub.user`
|
|||||||
.. attribute:: spawner
|
.. attribute:: spawner
|
||||||
|
|
||||||
The user's :class:`~.Spawner` instance.
|
The user's :class:`~.Spawner` instance.
|
||||||
|
|
||||||
|
@@ -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`.
|
|
||||||
|
|
||||||

|
|
||||||
- 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
|
||||||
|
@@ -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'
|
||||||
|
@@ -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>`_.
|
|
@@ -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/>`_.
|
|
@@ -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.
|
|
@@ -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 haven’t
|
|
||||||
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, it’s 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
|
|
@@ -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.
|
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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).
|
|
||||||
|
@@ -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.
|
||||||
|
|
||||||
|
@@ -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:
|
||||||
|
|
||||||

|

|
||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 103 KiB |
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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``.
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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/'
|
|
||||||
```
|
|
||||||
|
@@ -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`.
|
||||||
|
@@ -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:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
as well as the admin page:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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`.
|
|
||||||
|
@@ -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.
|
|
@@ -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:
|
||||||
|
|
||||||

|

|
||||||
@@ -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
|
||||||
|
@@ -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)
|
|
@@ -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']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
@@ -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.
|
|
||||||
|
@@ -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.
|
||||||
|
@@ -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
|
||||||
|
98
docs/source/reference/upgrading.md
Normal 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`
|
@@ -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`
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
*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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
*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]]`
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
*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`
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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`
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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
|
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
14
docs/source/tutorials/index.rst
Normal 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
|
93
docs/source/tutorials/upgrade-dot-eight.rst
Normal 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.
|
@@ -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):
|
||||||
|
@@ -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.
|
||||||
|
@@ -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
|
||||||
|
@@ -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]
|
||||||
```
|
```
|
||||||
|
@@ -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(
|
||||||
|
@@ -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(),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@@ -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`:
|
||||||
|
@@ -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',
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
@@ -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()
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
)
|
)
|
||||||
|
@@ -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"],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -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))])
|
||||||
|
|
||||||
|
@@ -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"],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -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`
|
||||||
|
|
||||||
|
@@ -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 = [
|
||||||
|
@@ -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',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
@@ -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',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@@ -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
|
||||||
|
@@ -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',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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()
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
@@ -1,2 +1 @@
|
|||||||
from ._version import __version__
|
from ._version import version_info, __version__
|
||||||
from ._version import version_info
|
|
||||||
|
@@ -1,3 +1,2 @@
|
|||||||
from .app import main
|
from .app import main
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
@@ -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):
|
||||||
|
@@ -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
|
|
||||||
)
|
|
||||||
|
@@ -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:
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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'))
|
|
||||||
)
|
|
||||||
|
@@ -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'))
|
|
@@ -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():
|
||||||
|
@@ -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'
|
||||||
|