Compare commits

...

52 Commits

Author SHA1 Message Date
Min RK
3663d7c8fc release 1.1.0 2020-01-17 12:54:06 +01:00
Min RK
a30e6b539f changelog for 1.1.0 (#2898)
changelog for 1.1.0
2020-01-17 12:54:04 +01:00
Min RK
ca3982337e changelog for 1.1.0 2020-01-17 12:40:38 +01:00
Min RK
159b3553a9 Merge pull request #2881 from minrk/auth-state-earlier
trigger auth_state_hook prior to options form, add auth_state to template namespace
2020-01-17 12:35:33 +01:00
Min RK
6821e63b71 Merge pull request #2897 from consideRatio/combine-py38-and-bionic-ci-test
Optimize CI jobs and default to bionic
2020-01-17 12:32:10 +01:00
Erik Sundell
c1c13930f7 Optimize CI jobs and default to bionic 2020-01-17 12:19:39 +01:00
Min RK
58f18bffff _render_form is async 2020-01-17 12:08:20 +01:00
Min RK
b80906b8c8 make auth_state available to page templates 2020-01-17 10:55:07 +01:00
Min RK
07aa077eae Merge pull request #2882 from ociule/master
LocalProcessSpawner should work on windows by using psutil.pid_exists
2020-01-17 09:47:37 +01:00
Min RK
3f74c30288 Merge pull request #2887 from krinsman/master
Fix implementation of default server name
2020-01-16 19:05:30 +01:00
Min RK
141cb04b27 fix assertion in custom user_redirect_hook
custom hook means overrides server_name insertion
2020-01-16 18:05:53 +01:00
Min RK
8769864f24 missing imports in test_named_servers 2020-01-14 22:16:06 +01:00
Min RK
8ee72dd80f define default_server_name fixture 2020-01-14 22:15:14 +01:00
William Krinsman
455475724a Attempt to add tests documenting default named server feature. 2020-01-14 10:20:18 -08:00
William Krinsman
794be0de8e Fix implementation of default server name 2020-01-14 10:02:50 -08:00
Ovidiu Ciule
1f633e188d Updated doc 2020-01-14 14:40:07 +01:00
Ovidiu Ciule
df0745985b Made _signal more readable 2020-01-14 14:38:00 +01:00
Ovidiu Ciule
cad027f3fc Use psutil on windows only. 2020-01-14 14:37:44 +01:00
Min RK
61a844b413 Merge pull request #2889 from minrk/openssl-error
catch connection error for ssl failures
2020-01-14 11:26:44 +01:00
Min RK
319b404ef4 misread which error propagates up
it's a ConnectionError (requests, not stdlib)
2020-01-14 11:05:19 +01:00
Min RK
19fb7eb7cc catch openssl error for ssl failures
python 3.8 with more recent openssl seems to raise a different error
2020-01-14 10:48:48 +01:00
Georgiana Elena
cb3b0ce266 Merge pull request #2842 from mangecoeur/master
Added guide 'install jupyterlab the hard way' #2110
2020-01-10 15:34:33 +02:00
Ovidiu Ciule
82d8e9c433 Reordered commits 2020-01-10 14:30:15 +01:00
mangecoeur
86ee4cad59 add newline 2020-01-10 14:28:13 +01:00
mangecoeur
add9666fcd Update installation-guide-hard.md
Updated capitalisation of names. Addressed revisions.

Fleshed out the prerequists and explanation of access control.

Added part of configuration section to set JupyterLab as the default interface.

corrected need for sudo

Added warning to reverse-proxy section to recommend use of HTTPS and firewall.
2020-01-10 12:28:00 +01:00
mangecoeur
c93687eaad Update docs/source/installation-guide-hard.md
Co-Authored-By: Georgiana Elena <GeorgianaElena@users.noreply.github.com>
2020-01-10 11:32:27 +01:00
mangecoeur
d848873685 Update docs/source/installation-guide-hard.md
Co-Authored-By: Georgiana Elena <GeorgianaElena@users.noreply.github.com>
2020-01-10 11:32:18 +01:00
mangecoeur
c27576a41f Update docs/source/installation-guide-hard.md
Co-Authored-By: Georgiana Elena <GeorgianaElena@users.noreply.github.com>
2020-01-10 11:31:59 +01:00
Ovidiu Ciule
6d3ed95b84 Added missing dependency psutil. Already used in proxy.py#L690 2020-01-10 11:14:51 +01:00
Ovidiu Ciule
ff7cd082ff Just use psutil.pid_exists, which uses os.kill(pid, 0) on Linux as
before and win-specific code on win
2020-01-10 11:09:10 +01:00
Ovidiu Ciule
3582ecc9cc Added _is_single_user_process_alive to allow subclasses to reimplement
this without reimplementing the whole poll method.
2020-01-09 16:39:44 +01:00
Min RK
5f626268ef trigger auth_state_hook prior to options form
- allow auth_state_hook to be async
- trigger it prior to start and options_form serving, rather than on home page
2020-01-09 13:04:45 +01:00
Min RK
6227f92b5f fixup allow_failures (#2880)
fixup allow_failures
2020-01-09 12:45:09 +01:00
Min RK
020ba08635 fixup allow_failures
jobs format doesn't create jobs under allow_failures

the old syntax used to do that. Instead, it uses key, value matches
2020-01-09 12:33:57 +01:00
Min RK
2ad175816a Pass tests on Python 3.8 (#2879)
Pass tests on Python 3.8
2020-01-09 12:28:09 +01:00
Min RK
3d46083dcc Stop allowing failures on Python 3.8
and simplify matrix without cross-references
2020-01-09 11:50:07 +01:00
Min RK
dad1417b23 loosen assertion for process exit
Python 3.8 captures exit codes differently.
All we care about is that it exited.
2020-01-09 11:18:26 +01:00
Min RK
9a3c2409d1 Update README's badges (#2867)
Update README's badges
2020-01-09 11:02:25 +01:00
Richard Darst
0efb16793e Bugfix: pam_normalize_username didn't return username
- A trivial bug caused by my last change to #2397 - made possible by
  the fact we didn't have a way to reliable test PAM stuff.
- Thanks to @narnish for noticing.
- Closes: #2875
2020-01-02 17:04:21 +02:00
Erik Sundell
68ad36e945 Try dist:bionic with py3.8 2019-12-28 18:51:10 +01:00
Erik Sundell
989ed216a7 Add travis-ci job names 2019-12-28 18:51:10 +01:00
Erik Sundell
319113024d Rework .travis.yml 2019-12-28 18:51:10 +01:00
Erik Sundell
399f7e7b80 Remove deprecated part in .travis.yml 2019-12-28 18:51:10 +01:00
Erik Sundell
b4a6e5c2fe Test docs only in CircleCI 2019-12-28 18:51:10 +01:00
Erik Sundell
1949ab892a Make TravisCI single out allowed-failuers 2019-12-28 18:51:10 +01:00
Erik Sundell
1ec34b256c Fixup .travis.yml
- We now default to ubuntu bionic (18.04) and try once with ubuntu xenial
(16.04).
- We now always test Python 3.8 but allow it to fail, as compared to not
allowing it to fail and only testing it on tagged commits. This is a
bugfix I'd say.
- We now no longer test Python 3.5 and Python 3.6 dedicatedly without
any custom configuration like usage of subdomain, which allows us to
reduce the number of build jobs in a way I think makes a great sense to
compromise.
2019-12-28 18:51:10 +01:00
Erik Sundell
3c12a99415 Update README's badges
Some notes:
- Added a conda-forge and DockerHub badge
- Added logo's and made us conform with the team-compass badges section
as can be found here:
  https://jupyterhub-team-compass.readthedocs.io/en/latest/building-blocks/readme-badges.html
- Concluded that our CircleCI badge is good because it let's us overview
the repo's build systems, but that it is bad because it is only is about
documentation preview in PRs which isn't useful in a README's header in
a way.
- Noted there was a CircleCI token in the badge, that I believe is meant
to be used with private repo access rather than public repo access. I'm
not sure we need that but I made it a markdown/html comment for now.
- Decided to not manually add a line break between badges. I figured it
could make sense to break manually before the social badges instead of
automatically letting it wrap at some point, but we don't really know
the size of the window viewing so it felt like a bad idea to hardcode
that.
2019-12-28 14:56:56 +01:00
Richard Darst
a8ced3a7ad Dockerfile: Copy share/ to the final image
- When the Dockerfile was turned into a multi-stage build, it seems
  the share/ directory was not copied to the final image.  This
  resulted in certain components (static/components/, static/css/)
  being missing, which resulted in the JupyterHub share directory not
  being findable (in jupyterhub/_data.py).  This led to all kinds of
  weird havoc, like templates not being findable (#2852).
- I am still unsure if this is the right fix, please check this well.
- Closes: #2852
2019-12-28 13:14:00 +01:00
Richard Darst
1af7deaeb3 Dockerfile: add build-essential to builder image
- While debugging another problem, I noticed some failures to build
  the C extensions in the logs.  Adding build-essential should fix
  that (also as mentioned in the logs themselves).
- Extensions failed for tornado, sqlalchemy, and pyrsistent(pvectorc)
  and can be found by searching the previous output for "fail".
2019-12-28 13:12:11 +01:00
Erik Sundell
861a7c5c5e back to dev 2019-12-26 18:20:06 +01:00
mangecoeur
fb64b4f0a8 change title and small corrections 2019-12-13 10:41:42 +01:00
mangecoeur
3a810c4fc0 Added guide 'install jupyterlab the hard way' 2019-12-06 16:44:59 +01:00
18 changed files with 616 additions and 104 deletions

View File

@@ -1,20 +1,18 @@
dist: bionic
language: python
sudo: false
cache:
- pip
python:
- 3.6
- 3.5
- nightly
env:
global:
- MYSQL_HOST=127.0.0.1
- MYSQL_TCP_PORT=13306
# request additional services for the jobs to access
services:
- postgresql
- docker
# installing dependencies
# install dependencies for running pytest (but not linting)
before_install:
- set -e
- nvm install 6; nvm use 6
@@ -34,38 +32,34 @@ before_install:
DB=postgres bash ci/init-db.sh
pip install psycopg2-binary
fi
# install general dependencies
install:
- pip install --upgrade pip
- pip install --upgrade --pre -r dev-requirements.txt .
- pip freeze
# running tests
# run tests
script:
- |
# run tests
if [[ -z "$TEST" ]]; then
pytest -v --maxfail=2 --cov=jupyterhub jupyterhub/tests
fi
- |
# run autoformat
if [[ "$TEST" == "lint" ]]; then
pre-commit run --all-files
fi
- |
# build docs
if [[ "$TEST" == "docs" ]]; then
pushd docs
pip install --upgrade -r requirements.txt
pip install --upgrade alabaster_jupyterhub
make html
popd
fi
- pytest -v --maxfail=2 --cov=jupyterhub jupyterhub/tests
# collect test coverage information
after_success:
- codecov
after_failure:
# list the jobs
jobs:
include:
- name: autoformatting check
python: 3.6
# NOTE: It does not suffice to override to: null, [], or [""]. Travis will
# fall back to the default if we do.
before_install: echo "Do nothing before install."
script:
- pre-commit run --all-files
after_success: echo "Do nothing after success."
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"
@@ -73,28 +67,28 @@ after_failure:
echo " pre-commit run"
echo "or after-the-fact on already committed files with"
echo " pre-commit run --all-files"
fi
matrix:
fast_finish: true
include:
- python: 3.6
env: TEST=lint
- python: 3.6
env: TEST=docs
- python: 3.6
# When we run pytest, we want to run it with python>=3.5 as well as with
# various configurations. We increment the python version at the same time
# as we test new configurations in order to reduce the number of test jobs.
- name: python:3.5 + dist:xenial
python: 3.5
dist: xenial
- name: python:3.6 + subdomain
python: 3.6
env: JUPYTERHUB_TEST_SUBDOMAIN_HOST=http://localhost.jovyan.org:8000
- python: 3.6
- name: python:3.7 + mysql
python: 3.7
env:
- JUPYTERHUB_TEST_DB_URL=mysql+mysqlconnector://root@127.0.0.1:$MYSQL_TCP_PORT/jupyterhub
- python: 3.6
- name: python:3.8 + postgresql
python: 3.8
env:
- PGUSER=jupyterhub
- PGPASSWORD=hub[test/:?
# password in url is url-encoded (urllib.parse.quote($PGPASSWORD, safe=''))
# The password in url below is url-encoded with: urllib.parse.quote($PGPASSWORD, safe='')
- JUPYTERHUB_TEST_DB_URL=postgresql://jupyterhub:hub%5Btest%2F%3A%3F@127.0.0.1/jupyterhub
- python: 3.7
dist: xenial
- python: 3.8
if: tag IS present
- name: python:nightly
python: nightly
allow_failures:
- python: nightly
- name: python:nightly
fast_finish: true

View File

@@ -30,6 +30,7 @@ USER root
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update \
&& apt-get install -yq --no-install-recommends \
build-essential \
ca-certificates \
locales \
python3-dev \
@@ -89,6 +90,7 @@ RUN npm install -g configurable-http-proxy@^4.2.0 \
# install the wheels we built in the first stage
COPY --from=builder /src/jupyterhub/wheelhouse /tmp/wheelhouse
COPY --from=builder /src/jupyterhub/share /src/jupyterhub/share
RUN python3 -m pip install --no-cache /tmp/wheelhouse/*
RUN mkdir -p /srv/jupyterhub/

View File

@@ -10,14 +10,16 @@
# [JupyterHub](https://github.com/jupyterhub/jupyterhub)
[![PyPI](https://img.shields.io/pypi/v/jupyterhub.svg)](https://pypi.python.org/pypi/jupyterhub)
[![Documentation Status](https://readthedocs.org/projects/jupyterhub/badge/?version=latest)](https://jupyterhub.readthedocs.org/en/latest/?badge=latest)
[![Build Status](https://travis-ci.org/jupyterhub/jupyterhub.svg?branch=master)](https://travis-ci.org/jupyterhub/jupyterhub)
[![Circle CI](https://circleci.com/gh/jupyterhub/jupyterhub.svg?style=shield&circle-token=b5b65862eb2617b9a8d39e79340b0a6b816da8cc)](https://circleci.com/gh/jupyterhub/jupyterhub)
[![codecov.io](https://codecov.io/github/jupyterhub/jupyterhub/coverage.svg?branch=master)](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
[![GitHub](https://img.shields.io/badge/issue_tracking-github-blue.svg)](https://github.com/jupyterhub/jupyterhub/issues)
[![Discourse](https://img.shields.io/badge/help_forum-discourse-blue.svg)](https://discourse.jupyter.org/c/jupyterhub)
[![Gitter](https://img.shields.io/badge/social_chat-gitter-blue.svg)](https://gitter.im/jupyterhub/jupyterhub)
[![Latest PyPI version](https://img.shields.io/pypi/v/jupyterhub?logo=pypi)](https://pypi.python.org/pypi/jupyterhub)
[![Latest conda-forge version](https://img.shields.io/conda/vn/conda-forge/jupyterhub?logo=conda-forge)](https://www.npmjs.com/package/jupyterhub)
[![Documentation build status](https://img.shields.io/readthedocs/jupyterhub?logo=read-the-docs)](https://jupyterhub.readthedocs.org/en/latest/)
[![TravisCI build status](https://img.shields.io/travis/jupyterhub/jupyterhub/master?logo=travis)](https://travis-ci.org/jupyterhub/jupyterhub)
[![DockerHub build status](https://img.shields.io/docker/build/jupyterhub/jupyterhub?logo=docker&label=build)](https://hub.docker.com/r/jupyterhub/jupyterhub/tags)
[![CircleCI build status](https://img.shields.io/circleci/build/github/jupyterhub/jupyterhub?logo=circleci)](https://circleci.com/gh/jupyterhub/jupyterhub)<!-- CircleCI Token: b5b65862eb2617b9a8d39e79340b0a6b816da8cc -->
[![Test coverage of code](https://codecov.io/gh/jupyterhub/jupyterhub/branch/master/graph/badge.svg)](https://codecov.io/gh/jupyterhub/jupyterhub)
[![GitHub](https://img.shields.io/badge/issue_tracking-github-blue?logo=github)](https://github.com/jupyterhub/jupyterhub/issues)
[![Discourse](https://img.shields.io/badge/help_forum-discourse-blue?logo=discourse)](https://discourse.jupyter.org/c/jupyterhub)
[![Gitter](https://img.shields.io/badge/social_chat-gitter-blue?logo=gitter)](https://gitter.im/jupyterhub/jupyterhub)
With [JupyterHub](https://jupyterhub.readthedocs.io) you can create a
**multi-user Hub** which spawns, manages, and proxies multiple instances of the

View File

@@ -9,7 +9,7 @@ command line for details.
## 1.1
### [1.1.0b1] 2019-12-26
### [1.1.0] 2020-01-17
1.1 is a release with lots of accumulated fixes and improvements,
especially in performance, metrics, and customization.
@@ -35,6 +35,9 @@ Thanks to everyone who has contributed to this release!
#### New
- LocalProcessSpawner should work on windows by using psutil.pid_exists [#2882](https://github.com/jupyterhub/jupyterhub/pull/2882) ([@ociule](https://github.com/ociule))
- trigger auth_state_hook prior to options form, add auth_state to template namespace [#2881](https://github.com/jupyterhub/jupyterhub/pull/2881) ([@minrk](https://github.com/minrk))
- Added guide 'install jupyterlab the hard way' #2110 [#2842](https://github.com/jupyterhub/jupyterhub/pull/2842) ([@mangecoeur](https://github.com/mangecoeur))
- Add prometheus metric to measure hub startup time [#2799](https://github.com/jupyterhub/jupyterhub/pull/2799) ([@rajat404](https://github.com/rajat404))
- Add Spawner.auth_state_hook [#2555](https://github.com/jupyterhub/jupyterhub/pull/2555) ([@rcthomas](https://github.com/rcthomas))
- Link services from jupyterhub pages [#2763](https://github.com/jupyterhub/jupyterhub/pull/2763) ([@rcthomas](https://github.com/rcthomas))
@@ -57,6 +60,7 @@ Thanks to everyone who has contributed to this release!
#### Fixes
- Bugfix: pam_normalize_username didn't return username [#2876](https://github.com/jupyterhub/jupyterhub/pull/2876) ([@rkdarst](https://github.com/rkdarst))
- Cleanup if spawner stop fails [#2849](https://github.com/jupyterhub/jupyterhub/pull/2849) ([@gabber12](https://github.com/gabber12))
- Fix an issue occurring with the default spawner and `internal_ssl` enabled [#2785](https://github.com/jupyterhub/jupyterhub/pull/2785) ([@rpwagner](https://github.com/rpwagner))
- Fix named servers to not be spawnable unless activated [#2772](https://github.com/jupyterhub/jupyterhub/pull/2772) ([@bitnik](https://github.com/bitnik))
@@ -77,6 +81,16 @@ Thanks to everyone who has contributed to this release!
#### Maintenance
- Optimize CI jobs and default to bionic [#2897](https://github.com/jupyterhub/jupyterhub/pull/2897) ([@consideRatio](https://github.com/consideRatio))
- catch connection error for ssl failures [#2889](https://github.com/jupyterhub/jupyterhub/pull/2889) ([@minrk](https://github.com/minrk))
- Fix implementation of default server name [#2887](https://github.com/jupyterhub/jupyterhub/pull/2887) ([@krinsman](https://github.com/krinsman))
- fixup allow_failures [#2880](https://github.com/jupyterhub/jupyterhub/pull/2880) ([@minrk](https://github.com/minrk))
- Pass tests on Python 3.8 [#2879](https://github.com/jupyterhub/jupyterhub/pull/2879) ([@minrk](https://github.com/minrk))
- Fixup .travis.yml [#2868](https://github.com/jupyterhub/jupyterhub/pull/2868) ([@consideRatio](https://github.com/consideRatio))
- Update README's badges [#2867](https://github.com/jupyterhub/jupyterhub/pull/2867) ([@consideRatio](https://github.com/consideRatio))
- Dockerfile: add build-essential to builder image [#2866](https://github.com/jupyterhub/jupyterhub/pull/2866) ([@rkdarst](https://github.com/rkdarst))
- Dockerfile: Copy share/ to the final image [#2864](https://github.com/jupyterhub/jupyterhub/pull/2864) ([@rkdarst](https://github.com/rkdarst))
- chore: Dockerfile updates [#2853](https://github.com/jupyterhub/jupyterhub/pull/2853) ([@jgwerner](https://github.com/jgwerner))
- simplify Dockerfile [#2840](https://github.com/jupyterhub/jupyterhub/pull/2840) ([@minrk](https://github.com/minrk))
- docker: fix onbuild image arg [#2839](https://github.com/jupyterhub/jupyterhub/pull/2839) ([@minrk](https://github.com/minrk))
@@ -704,7 +718,8 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
First preview release
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.0.0...HEAD
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.1.0...HEAD
[1.1.0]: https://github.com/jupyterhub/jupyterhub/compare/1.0.0...1.1.0
[1.0.0]: https://github.com/jupyterhub/jupyterhub/compare/0.9.6...1.0.0
[0.9.6]: https://github.com/jupyterhub/jupyterhub/compare/0.9.4...0.9.6
[0.9.4]: https://github.com/jupyterhub/jupyterhub/compare/0.9.3...0.9.4

View File

@@ -0,0 +1,338 @@
# Install JupyterHub and JupyterLab from the ground up
The combination of [JupyterHub](https://jupyterhub.readthedocs.io) and [JupyterLab](https://jupyterlab.readthedocs.io)
is a great way to make shared computing resources available to a group.
These instructions are a guide for a manual, 'bare metal' install of [JupyterHub](https://jupyterhub.readthedocs.io)
and [JupyterLab](https://jupyterlab.readthedocs.io). This is ideal for running on a single server: build a beast
of a machine and share it within your lab, or use a virtual machine from any VPS or cloud provider.
This guide has similar goals to [The Littlest JupyterHub](https://the-littlest-jupyterhub.readthedocs.io) setup
script. However, instead of bundling all these step for you into one installer, we will perform every step manually.
This makes it easy to customize any part (e.g. if you want to run other services on the same system and need to make them
work together), as well as giving you full control and understanding of your setup.
## Prerequisites
Your own server with administrator (root) access. This could be a local machine, a remotely hosted one, or a cloud instance
or VPS. Each user who will access JupyterHub should have a standard user account on the machine. The install will be done
through the command line - useful if you log into your machine remotely using SSH.
This tutorial was tested on **Ubuntu 18.04**. No other Linux distributions have been tested, but the instructions
should be reasonably straightforward to adapt.
## Goals
JupyterLab enables access to a multiple 'kernels', each one being a given environment for a given language. The most
common is a Python environment, for scientific computing usually one managed by the `conda` package manager.
This guide will set up JupyterHub and JupyterLab seperately from the Python environment. In other words, we treat
JupyterHub+JupyterLab as a 'app' or webservice, which will connect to the kernels available on the system. Specifically:
- We will create an installation of JupyterHub and JupyterLab using a virtualenv under `/opt` using the system Python.
- We will install conda globally.
- We will create a shared conda environment which can be used (but not modified) by all users.
- We will show how users can create their own private conda environments, where they can install whatever they like.
The default JupyterHub Authenticator uses PAM to authenticate system users with their username and password. One can
[choose the authenticator](https://jupyterhub.readthedocs.io/en/stable/reference/authenticators.html#authenticators)
that best suits their needs. In this guide we will use the default Authenticator because it makes it easy for everyone to manage data
in their home folder and to mix and match different services and access methods (e.g. SSH) which all work using the
Linux system user accounts. Therefore, each user of JupyterHub will need a standard system user account.
Another goal of this guide is to use system provided packages wherever possible. This has the advantage that these packages
get automatic patches and security updates (be sure to turn on automatic updates in Ubuntu). This means less maintenance
work and a more reliable system.
## Part 1: JupyterHub and JupyterLab
### Setup the JupyterHub and JupyterLab in a virtual environment
First we create a virtual environment under '/opt/jupyterhub'. The '/opt' folder is where apps not belonging to the operating
system are [commonly installed](https://unix.stackexchange.com/questions/11544/what-is-the-difference-between-opt-and-usr-local).
Both jupyterlab and jupyterhub will be installed into this virtualenv. Create it with the command:
```sh
sudo python3 -m venv /opt/jupyterhub/
```
Now we use pip to install the required Python packages into the new virtual environment. Be sure to install
`wheel` first. Since we are separating the user interface from the computing kernels, we don't install
any Python scientific packages here. The only exception is `ipywidgets` because this is needed to allow connection
between interactive tools running in the kernel and the user interface.
Note that we use `/opt/jupyterhub/bin/python3 -m pip install` each time - this [makes sure](https://snarky.ca/why-you-should-use-python-m-pip/)
that the packages are installed to the correct virtual environment.
Perform the install using the following commands:
```sh
sudo /opt/jupyterhub/bin/python3 -m pip install wheel
sudo /opt/jupyterhub/bin/python3 -m pip install jupyterhub jupyterlab
sudo /opt/jupyterhub/bin/python3 -m pip install ipywidgets
```
JupyterHub also currently defaults to requiring `configurable-http-proxy`, which needs `nodejs` and `npm`. The versions
of these available in Ubuntu therefore need to be installed first (they are a bit old but this is ok for our needs):
```sh
sudo apt install nodejs npm
```
Then install `configurable-http-proxy`:
```sh
npm install -g configurable-http-proxy
```
### Create the configuration for JupyterHub
Now we start creating configuration files. To keep everything together, we put all the configuration into the folder
created for the virtualenv, under `/opt/jupyterhub/etc/`. For each thing needing configuration, we will create a further
subfolder and necessary files.
First create the folder for the JupyterHub configuration and navigate to it:
```sh
sudo mkdir -p /opt/jupyterhub/etc/jupyterhub/
cd /opt/jupyterhub/etc/jupyterhub/
```
Then generate the default configuration file
```sh
sudo /opt/jupyterhub/bin/jupyterhub --generate-config
```
This will produce the default configuration file `/opt/jupyterhub/etc/jupyterhub/jupyterhub_config.py`
You will need to edit the configuration file to make the JupyterLab interface by the default.
Set the following configuration option in your `jupyterhub_config.py` file:
```python
c.Spawner.default_url = '/lab'
```
Further configuration options may be found in the documentation.
### Setup Systemd service
We will setup JupyterHub to run as a system service using Systemd (which is responsible for managing all services and
servers that run on startup in Ubuntu). We will create a service file in a suitable location in the virtualenv folder
and then link it to the system services. First create the folder for the service file:
```sh
sudo mkdir -p /opt/jupyterhub/etc/systemd
```
Then create the following text file using your [favourite editor](https://micro-editor.github.io/) at
```sh
/opt/jupyterhub/etc/systemd/jupyterhub.service
```
Paste the following service unit definition into the file:
```
[Unit]
Description=JupyterHub
After=syslog.target network.target
[Service]
User=root
Environment="PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/jupyterhub/bin"
ExecStart=/opt/jupyterhub/bin/jupyterhub -f /opt/jupyterhub/etc/jupyterhub/jupyterhub_config.py
[Install]
WantedBy=multi-user.target
```
This sets up the environment to use the virtual environment we created, tells Systemd how to start jupyterhub using
the configuration file we created, specifies that jupyterhub will be started as the `root` user (needed so that it can
start jupyter on behalf of other logged in users), and specifies that jupyterhub should start on boot after the network
is enabled.
Finally, we need to make systemd aware of our service file. First we symlink our file into systemd's directory:
```sh
sudo ln -s /opt/jupyterhub/etc/systemd/jupyterhub.service /etc/systemd/system/jupyterhub.service
```
Then tell systemd to reload its configuration files
```sh
sudo systemctl daemon-reload
```
And finally enable the service
```sh
sudo systemctl enable jupyterhub.service
```
The service will start on reboot, but we can start it straight away using:
```sh
sudo systemctl start jupyterhub.service
```
...and check that it's running using:
```sh
sudo systemctl status jupyterhub.service
```
You should now be already be able to access jupyterhub using `<your servers ip>:8000` (assuming you haven't already set
up a firewall or something). However, when you log in the jupyter notebooks will be trying to use the Python virtualenv
that was created to install JupyterHub, this is not what we want. So on to part 2
## Part 2: Conda environments
### Install conda for the whole system
We will use `conda` to manage Python environments. We will install the officially maintained `conda` packages for Ubuntu,
this means they will get automatic updates with the rest of the system. Setup repo for the official Conda debian packages,
instructions are copied from [here](https://docs.conda.io/projects/conda/en/latest/user-guide/install/rpm-debian.html):
Install Anacononda public gpg key to trusted store
```sh
curl https://repo.anaconda.com/pkgs/misc/gpgkeys/anaconda.asc | gpg --dearmor > conda.gpg
sudo install -o root -g root -m 644 conda.gpg /etc/apt/trusted.gpg.d/
```
Add Debian repo
```sh
sudo echo "deb [arch=amd64] https://repo.anaconda.com/pkgs/misc/debrepo/conda stable main" > /etc/apt/sources.list.d/conda.list
```
Install conda
```sh
sudo apt update
sudo apt install conda
```
This will install conda into the folder `/opt/conda/`, with the conda command available at `/opt/conda/bin/conda`.
Finally, we can make conda more easily available to users by symlinking the conda shell setup script to the profile
'drop in' folder so that it gets run on login
```sh
sudo ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh
```
### Install a default conda environment for all users
First create a folder for conda envs (might exist already):
```sh
sudo mkdir /opt/conda/envs/
```
Then create a conda environment to your liking within that folder. Here we have called it 'python' because it will
be the obvious default - call it whatever you like. You can install whatever you like into this environment, but you MUST at least install `ipykernel`.
```sh
sudo /opt/conda/bin/conda create --prefix /opt/conda/envs/python python=3.7 ipykernel
```
Once your env is set up as desired, make it visible to Jupyter by installing the kernel spec. There are two options here:
1 ) Install into the JupyterHub virtualenv - this ensures it overrides the default python version. It will only be visible
to the JupyterHub installation we have just created. This is useful to avoid conda environments appearing where they are not expected.
```sh
sudo /opt/conda/envs/python/bin/python -m ipykernel install --prefix=/opt/jupyterhub/ --name 'python' --display-name "Python (default)"
```
2 ) Install it system-wide by putting it into `/usr/local`. It will be visible to any parallel install of JupyterHub or
JupyterLab, and will persist even if you later delete or modify the JupyterHub installation. This is useful if the kernels
might be used by other services, or if you want to modify the JupyterHub installation independently from the conda environments.
```sh
sudo /opt/conda/envs/python/bin/python -m ipykernel install --prefix /usr/local/ --name 'python' --display-name "Python (default)"
````
### Setting up users' own conda environments
There is relatively little for the administrator to do here, as users will have to set up their own environments using the shell.
On login they should run `conda init` or `/opt/conda/bin/conda`. The can then use conda to set up their environment,
although they must also install `ipykernel`. Once done, they can enable their kernel using:
```sh
/path/to/kernel/env/bin/python -m ipykernel install --name 'python-my-env' --display-name "Python My Env"
```
This will place the kernel spec into their home folder, where Jupyter will look for it on startup.
## Setting up a reverse proxy
The guide so far results in JupyterHub running on port 8000. It is not generally advisable to run open web services in
this way - instead, use a reverse proxy running on standard HTTP/HTTPS ports.
> **Important**: Be aware of the security implications especially if you are running a server that is accessible from the open internet
> i.e. not protected within an institutional intranet or private home/office network. You should set up a firewall and
> HTTPS encryption, which is outside of the scope of this guide. For HTTPS consider using [LetsEncrypt](https://letsencrypt.org/)
> or setting up a [self-signed certificate](https://www.digitalocean.com/community/tutorials/how-to-create-a-self-signed-ssl-certificate-for-nginx-in-ubuntu-18-04).
> Firewalls may be set up using `ufs` or `firewalld` and combined with `fail2ban`.
### Using Nginx
Nginx is a mature and established web server and reverse proxy and is easy to install using `sudo apt install nginx`.
Details on using Nginx as a reverse proxy can be found elsewhere. Here, we will only outline the additional steps needed
to setup JupyterHub with Nginx and host it at a given URL e.g. `<your-server-ip-or-url>/jupyter`.
This could be useful for example if you are running several services or web pages on the same server.
To achieve this needs a few tweaks to both the JupyterHub configuration and the Nginx config. First, edit the
configuration file `/opt/jupyterhub/etc/jupyterhub/jupyterhub_config.py` and add the line:
```python
c.JupyterHub.bind_url = 'http://:8000/jupyter'
```
where `/jupyter` will be the relative URL of the JupyterHub.
Now Nginx must be configured with a to pass all traffic from `/jupyter` to the the local address `127.0.0.1:8000`.
Add the following snippet to your nginx configuration file (e.g. `/etc/nginx/sites-available/default`).
```
location /jupyter/ {
# NOTE important to also set base url of jupyterhub to /jupyter in its config
proxy_pass http://127.0.0.1:8000;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# websocket headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
```
Nginx will not run if there are errors in the configuration, check your configuration using:
```sh
nginx -t
```
If there are no errors, you can restart the Nginx service for the new configuration to take effect.
```sh
sudo systemctl restart nginx.service
```
## Getting started using your new JupyterHub
Once you have setup JupyterHub and Nginx proxy as described, you can browse to your JupyterHub IP or URL
(e.g. if your server IP address is `123.456.789.1` and you decided to host JupyterHub at the `/jupyter` URL, browse
to `123.456.789.1/jupyter`). You will find a login page where you enter your Linux username and password. On login
you will be presented with the JupyterLab interface, with the file browser pane showing the contents of your users'
home directory on the server.

View File

@@ -11,3 +11,4 @@ running on your own infrastructure.
quickstart
quickstart-docker
installation-basics
installation-guide-hard

View File

@@ -74,7 +74,7 @@ It should return `None` if it is still running,
and an integer exit status, otherwise.
For the local process case, `Spawner.poll` uses `os.kill(PID, 0)`
to check if the local process is still running.
to check if the local process is still running. On Windows, it uses `psutil.pid_exists`.
### Spawner.stop

View File

@@ -6,8 +6,8 @@ version_info = (
1,
1,
0,
"b1", # release (b1, rc1, or "" for final or dev)
# "dev", # dev or nothing
# "", # release (b1, rc1, or "" for final or dev)
# "dev", # dev or nothing for beta/rc/stable releases
)
# pep 440 version: no dot before beta/rc, but before .dev

View File

@@ -223,7 +223,7 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
return True
@web.authenticated
def get(self):
async def get(self):
"""GET /oauth/authorization
Render oauth confirmation page:
@@ -251,8 +251,14 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
return
# Render oauth 'Authorize application...' page
auth_state = await self.current_user.get_auth_state()
self.write(
self.render_template("oauth.html", scopes=scopes, oauth_client=client)
self.render_template(
"oauth.html",
auth_state=auth_state,
scopes=scopes,
oauth_client=client,
)
)
# Errors that should be shown to the user on the provider website

View File

@@ -981,6 +981,7 @@ class PAMAuthenticator(LocalAuthenticator):
uid = pwd.getpwnam(username).pw_uid
username = pwd.getpwuid(uid).pw_name
username = self.username_map.get(username, username)
return username
else:
return super().normalize_username(username)

View File

@@ -1431,8 +1431,13 @@ class UserUrlHandler(BaseHandler):
url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name),
{"next": self.request.uri},
)
auth_state = await user.get_auth_state()
html = self.render_template(
"not_running.html", user=user, server_name=server_name, spawn_url=spawn_url
"not_running.html",
user=user,
server_name=server_name,
spawn_url=spawn_url,
auth_state=auth_state,
)
self.finish(html)
@@ -1526,10 +1531,26 @@ class UserRedirectHandler(BaseHandler):
)
if url is None:
user = self.current_user
user_url = url_path_join(user.url, path)
user_url = user.url
if self.app.default_server_name:
user_url = url_path_join(user_url, self.app.default_server_name)
user_url = url_path_join(user_url, path)
if self.request.query:
user_url = url_concat(user_url, parse_qsl(self.request.query))
if self.app.default_server_name:
url = url_concat(
url_path_join(
self.hub.base_url,
"spawn",
user.escaped_name,
self.app.default_server_name,
),
{"next": user_url},
)
else:
url = url_concat(
url_path_join(self.hub.base_url, "spawn", user.escaped_name),
{"next": user_url},

View File

@@ -68,10 +68,9 @@ class HomeHandler(BaseHandler):
url = url_path_join(self.hub.base_url, 'spawn', user.escaped_name)
auth_state = await user.get_auth_state()
user.spawner.run_auth_state_hook(auth_state)
html = self.render_template(
'home.html',
auth_state=auth_state,
user=user,
url=url,
allow_named_servers=self.allow_named_servers,
@@ -94,10 +93,12 @@ class SpawnHandler(BaseHandler):
default_url = None
def _render_form(self, for_user, spawner_options_form, message=''):
async def _render_form(self, for_user, spawner_options_form, message=''):
auth_state = await for_user.get_auth_state()
return self.render_template(
'spawn.html',
for_user=for_user,
auth_state=auth_state,
spawner_options_form=spawner_options_form,
error_message=message,
url=self.request.uri,
@@ -149,6 +150,7 @@ class SpawnHandler(BaseHandler):
server_name = ''
spawner = user.spawners[server_name]
# resolve `?next=...`, falling back on the spawn-pending url
# must not be /user/server for named servers,
# which may get handled by the default server if they aren't ready yet
@@ -175,10 +177,16 @@ class SpawnHandler(BaseHandler):
# Add handler to spawner here so you can access query params in form rendering.
spawner.handler = self
# auth_state may be an input to options form,
# so resolve the auth state hook here
auth_state = await user.get_auth_state()
await spawner.run_auth_state_hook(auth_state)
spawner_options_form = await spawner.get_options_form()
if spawner_options_form:
self.log.debug("Serving options form for %s", spawner._log_name)
form = self._render_form(
form = await self._render_form(
for_user=user, spawner_options_form=spawner_options_form
)
self.finish(form)
@@ -240,7 +248,7 @@ class SpawnHandler(BaseHandler):
"Failed to spawn single-user server with form", exc_info=True
)
spawner_options_form = await user.spawner.get_options_form()
form = self._render_form(
form = await self._render_form(
for_user=user, spawner_options_form=spawner_options_form, message=str(e)
)
self.finish(form)
@@ -301,6 +309,8 @@ class SpawnPendingHandler(BaseHandler):
# if spawning fails for any reason, point users to /hub/home to retry
self.extra_error_html = self.spawn_home_error
auth_state = await user.get_auth_state()
# First, check for previous failure.
if (
not spawner.active
@@ -320,6 +330,7 @@ class SpawnPendingHandler(BaseHandler):
html = self.render_template(
"not_running.html",
user=user,
auth_state=auth_state,
server_name=server_name,
spawn_url=spawn_url,
failed=True,
@@ -341,7 +352,11 @@ class SpawnPendingHandler(BaseHandler):
else:
page = "spawn_pending.html"
html = self.render_template(
page, user=user, spawner=spawner, progress_url=spawner._progress_url
page,
user=user,
spawner=spawner,
progress_url=spawner._progress_url,
auth_state=auth_state,
)
self.finish(html)
return
@@ -366,6 +381,7 @@ class SpawnPendingHandler(BaseHandler):
html = self.render_template(
"not_running.html",
user=user,
auth_state=auth_state,
server_name=server_name,
spawn_url=spawn_url,
)
@@ -385,7 +401,7 @@ class AdminHandler(BaseHandler):
@web.authenticated
@admin_only
def get(self):
async def get(self):
available = {'name', 'admin', 'running', 'last_activity'}
default_sort = ['admin', 'name']
mapping = {'running': orm.Spawner.server_id}
@@ -434,9 +450,11 @@ class AdminHandler(BaseHandler):
for u in users:
running.extend(s for s in u.spawners.values() if s.active)
auth_state = await self.current_user.get_auth_state()
html = self.render_template(
'admin.html',
current_user=self.current_user,
auth_state=auth_state,
admin_access=self.settings.get('admin_access', False),
users=users,
running=running,
@@ -452,7 +470,7 @@ class TokenPageHandler(BaseHandler):
"""Handler for page requesting new API tokens"""
@web.authenticated
def get(self):
async def get(self):
never = datetime(1900, 1, 1)
user = self.current_user
@@ -521,8 +539,12 @@ class TokenPageHandler(BaseHandler):
oauth_clients = sorted(oauth_clients, key=sort_key, reverse=True)
auth_state = await self.current_user.get_auth_state()
html = self.render_template(
'token.html', api_tokens=api_tokens, oauth_clients=oauth_clients
'token.html',
api_tokens=api_tokens,
oauth_clients=oauth_clients,
auth_state=auth_state,
)
self.finish(html)

View File

@@ -16,6 +16,8 @@ import warnings
from subprocess import Popen
from tempfile import mkdtemp
if os.name == 'nt':
import psutil
from async_generator import async_generator
from async_generator import yield_
from sqlalchemy import inspect
@@ -972,11 +974,11 @@ class Spawner(LoggingConfigurable):
except Exception:
self.log.exception("post_stop_hook failed with exception: %s", self)
def run_auth_state_hook(self, auth_state):
async def run_auth_state_hook(self, auth_state):
"""Run the auth_state_hook if defined"""
if self.auth_state_hook is not None:
try:
return self.auth_state_hook(self, auth_state)
await maybe_future(self.auth_state_hook(self, auth_state))
except Exception:
self.log.exception("auth_stop_hook failed with exception: %s", self)
@@ -1468,8 +1470,10 @@ class LocalProcessSpawner(Spawner):
self.clear_state()
return 0
# send signal 0 to check if PID exists
# this doesn't work on Windows, but that's okay because we don't support Windows.
# We use pustil.pid_exists on windows
if os.name == 'nt':
alive = psutil.pid_exists(self.pid)
else:
alive = await self._signal(0)
if not alive:
self.clear_state()
@@ -1486,11 +1490,10 @@ class LocalProcessSpawner(Spawner):
"""
try:
os.kill(self.pid, sig)
except OSError as e:
if e.errno == errno.ESRCH:
except ProcessLookupError:
return False # process is gone
else:
raise
except OSError as e:
raise # Can be EPERM or EINVAL
return True # process exists
async def stop(self, now=False):

View File

@@ -6,6 +6,7 @@ from unittest import mock
from urllib.parse import urlparse
import pytest
from requests.exceptions import ConnectionError
from requests.exceptions import SSLError
from tornado import gen
@@ -15,6 +16,9 @@ from .utils import async_requests
ssl_enabled = True
# possible errors raised by ssl failures
SSL_ERROR = (SSLError, ConnectionError)
@gen.coroutine
def wait_for_spawner(spawner, timeout=10):
@@ -41,7 +45,7 @@ def wait_for_spawner(spawner, timeout=10):
async def test_connection_hub_wrong_certs(app):
"""Connecting to the internal hub url fails without correct certs"""
with pytest.raises(SSLError):
with pytest.raises(SSL_ERROR):
kwargs = {'verify': False}
r = await async_requests.get(app.hub.url, **kwargs)
r.raise_for_status()
@@ -49,7 +53,7 @@ async def test_connection_hub_wrong_certs(app):
async def test_connection_proxy_api_wrong_certs(app):
"""Connecting to the proxy api fails without correct certs"""
with pytest.raises(SSLError):
with pytest.raises(SSL_ERROR):
kwargs = {'verify': False}
r = await async_requests.get(app.proxy.api_url, **kwargs)
r.raise_for_status()
@@ -68,7 +72,7 @@ async def test_connection_notebook_wrong_certs(app):
status = await spawner.poll()
assert status is None
with pytest.raises(SSLError):
with pytest.raises(SSL_ERROR):
kwargs = {'verify': False}
r = await async_requests.get(spawner.server.url, **kwargs)
r.raise_for_status()

View File

@@ -1,6 +1,8 @@
"""Tests for named servers"""
import asyncio
import json
from unittest import mock
from urllib.parse import urlencode
from urllib.parse import urlparse
import pytest
@@ -27,6 +29,17 @@ def named_servers(app):
yield
@pytest.fixture
def default_server_name(app, named_servers):
"""configure app to use a default server name"""
server_name = 'myserver'
try:
app.default_server_name = server_name
yield server_name
finally:
app.default_server_name = ''
async def test_default_server(app, named_servers):
"""Test the default /users/:user/server handler when named servers are enabled"""
username = 'rosie'
@@ -267,3 +280,91 @@ async def test_named_server_spawn_form(app, username, named_servers):
assert server_name in user.spawners
spawner = user.spawners[server_name]
spawner.user_options == {'energy': '938MeV', 'bounds': [-10, 10], 'notspecified': 5}
async def test_user_redirect_default_server_name(
app, username, named_servers, default_server_name
):
name = username
server_name = default_server_name
cookies = await app.login_user(name)
r = await api_request(app, 'users', username, 'servers', server_name, method='post')
r.raise_for_status()
assert r.status_code == 201
assert r.text == ''
r = await get_page('/user-redirect/tree/top/', app)
r.raise_for_status()
print(urlparse(r.url))
path = urlparse(r.url).path
assert path == url_path_join(app.base_url, '/hub/login')
query = urlparse(r.url).query
assert query == urlencode(
{'next': url_path_join(app.hub.base_url, '/user-redirect/tree/top/')}
)
r = await get_page('/user-redirect/notebooks/test.ipynb', app, cookies=cookies)
r.raise_for_status()
print(urlparse(r.url))
path = urlparse(r.url).path
while '/spawn-pending/' in path:
await asyncio.sleep(0.1)
r = await async_requests.get(r.url, cookies=cookies)
path = urlparse(r.url).path
assert path == url_path_join(
app.base_url, '/user/{}/{}/notebooks/test.ipynb'.format(name, server_name)
)
async def test_user_redirect_hook_default_server_name(
app, username, named_servers, default_server_name
):
"""
Test proper behavior of user_redirect_hook when c.JupyterHub.default_server_name is set
"""
name = username
server_name = default_server_name
cookies = await app.login_user(name)
r = await api_request(app, 'users', username, 'servers', server_name, method='post')
r.raise_for_status()
assert r.status_code == 201
assert r.text == ''
async def dummy_redirect(path, request, user, base_url):
assert base_url == app.base_url
assert path == 'redirect-to-terminal'
assert request.uri == url_path_join(
base_url, 'hub', 'user-redirect', 'redirect-to-terminal'
)
# exclude custom server_name
# custom hook is respected exactly
url = url_path_join(user.url, '/terminals/1')
return url
app.user_redirect_hook = dummy_redirect
r = await get_page('/user-redirect/redirect-to-terminal', app)
r.raise_for_status()
print(urlparse(r.url))
path = urlparse(r.url).path
assert path == url_path_join(app.base_url, '/hub/login')
query = urlparse(r.url).query
assert query == urlencode(
{'next': url_path_join(app.hub.base_url, '/user-redirect/redirect-to-terminal')}
)
# We don't actually want to start the server by going through spawn - just want to make sure
# the redirect is to the right place
r = await get_page(
'/user-redirect/redirect-to-terminal',
app,
cookies=cookies,
allow_redirects=False,
)
r.raise_for_status()
redirected_url = urlparse(r.headers['Location'])
assert redirected_url.path == url_path_join(
app.base_url, 'user', username, 'terminals/1'
)

View File

@@ -76,7 +76,8 @@ async def test_spawner(db, request):
assert status is None
await spawner.stop()
status = await spawner.poll()
assert status == 1
assert status is not None
assert isinstance(status, int)
async def wait_for_spawner(spawner, timeout=10):

View File

@@ -389,14 +389,9 @@ class User:
Full name.domain/path if using subdomains, otherwise just my /base/url
"""
if self.settings.get('subdomain_host'):
url = '{host}{path}'.format(host=self.host, path=self.base_url)
return '{host}{path}'.format(host=self.host, path=self.base_url)
else:
url = self.base_url
if self.settings.get('default_server_name'):
return url_path_join(url, self.settings.get('default_server_name'))
else:
return url
return self.base_url
def server_url(self, server_name=''):
"""Get the url for a server with a given name"""
@@ -535,12 +530,17 @@ class User:
# trigger pre-spawn hook on authenticator
authenticator = self.authenticator
try:
spawner._start_pending = True
if authenticator:
# pre_spawn_start can thow errors that can lead to a redirect loop
# if left uncaught (see https://github.com/jupyterhub/jupyterhub/issues/2683)
await maybe_future(authenticator.pre_spawn_start(self, spawner))
spawner._start_pending = True
# trigger auth_state hook
auth_state = await self.get_auth_state()
await spawner.run_auth_state_hook(auth_state)
# update spawner start time, and activity for both spawner and user
self.last_activity = (
spawner.orm_spawner.started

View File

@@ -7,6 +7,7 @@ jupyter_telemetry
oauthlib>=3.0
pamela
prometheus_client>=0.0.21
psutil>=5.6.5; sys_platform == 'win32'
python-dateutil
requests
SQLAlchemy>=1.1