mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 18:14:10 +00:00
Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3663d7c8fc | ||
![]() |
a30e6b539f | ||
![]() |
ca3982337e | ||
![]() |
159b3553a9 | ||
![]() |
6821e63b71 | ||
![]() |
c1c13930f7 | ||
![]() |
58f18bffff | ||
![]() |
b80906b8c8 | ||
![]() |
07aa077eae | ||
![]() |
3f74c30288 | ||
![]() |
141cb04b27 | ||
![]() |
8769864f24 | ||
![]() |
8ee72dd80f | ||
![]() |
455475724a | ||
![]() |
794be0de8e | ||
![]() |
1f633e188d | ||
![]() |
df0745985b | ||
![]() |
cad027f3fc | ||
![]() |
61a844b413 | ||
![]() |
319b404ef4 | ||
![]() |
19fb7eb7cc | ||
![]() |
cb3b0ce266 | ||
![]() |
82d8e9c433 | ||
![]() |
86ee4cad59 | ||
![]() |
add9666fcd | ||
![]() |
c93687eaad | ||
![]() |
d848873685 | ||
![]() |
c27576a41f | ||
![]() |
6d3ed95b84 | ||
![]() |
ff7cd082ff | ||
![]() |
3582ecc9cc | ||
![]() |
5f626268ef | ||
![]() |
6227f92b5f | ||
![]() |
020ba08635 | ||
![]() |
2ad175816a | ||
![]() |
3d46083dcc | ||
![]() |
dad1417b23 | ||
![]() |
9a3c2409d1 | ||
![]() |
0efb16793e | ||
![]() |
68ad36e945 | ||
![]() |
989ed216a7 | ||
![]() |
319113024d | ||
![]() |
399f7e7b80 | ||
![]() |
b4a6e5c2fe | ||
![]() |
1949ab892a | ||
![]() |
1ec34b256c | ||
![]() |
3c12a99415 | ||
![]() |
a8ced3a7ad | ||
![]() |
1af7deaeb3 | ||
![]() |
861a7c5c5e | ||
![]() |
fb64b4f0a8 | ||
![]() |
3a810c4fc0 |
86
.travis.yml
86
.travis.yml
@@ -1,20 +1,18 @@
|
|||||||
|
dist: bionic
|
||||||
language: python
|
language: python
|
||||||
sudo: false
|
|
||||||
cache:
|
cache:
|
||||||
- pip
|
- pip
|
||||||
python:
|
|
||||||
- 3.6
|
|
||||||
- 3.5
|
|
||||||
- nightly
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- MYSQL_HOST=127.0.0.1
|
- MYSQL_HOST=127.0.0.1
|
||||||
- MYSQL_TCP_PORT=13306
|
- MYSQL_TCP_PORT=13306
|
||||||
|
|
||||||
|
# request additional services for the jobs to access
|
||||||
services:
|
services:
|
||||||
- postgresql
|
- postgresql
|
||||||
- docker
|
- docker
|
||||||
|
|
||||||
# installing dependencies
|
# install dependencies for running pytest (but not linting)
|
||||||
before_install:
|
before_install:
|
||||||
- set -e
|
- set -e
|
||||||
- nvm install 6; nvm use 6
|
- nvm install 6; nvm use 6
|
||||||
@@ -34,38 +32,34 @@ before_install:
|
|||||||
DB=postgres bash ci/init-db.sh
|
DB=postgres bash ci/init-db.sh
|
||||||
pip install psycopg2-binary
|
pip install psycopg2-binary
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# install general dependencies
|
||||||
install:
|
install:
|
||||||
- pip install --upgrade pip
|
- pip install --upgrade pip
|
||||||
- pip install --upgrade --pre -r dev-requirements.txt .
|
- pip install --upgrade --pre -r dev-requirements.txt .
|
||||||
- pip freeze
|
- pip freeze
|
||||||
|
|
||||||
# running tests
|
# run tests
|
||||||
script:
|
script:
|
||||||
- |
|
- pytest -v --maxfail=2 --cov=jupyterhub jupyterhub/tests
|
||||||
# run tests
|
|
||||||
if [[ -z "$TEST" ]]; then
|
# collect test coverage information
|
||||||
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
|
|
||||||
after_success:
|
after_success:
|
||||||
- codecov
|
- 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 "You can install pre-commit hooks to automatically run formatting"
|
||||||
echo "on each commit with:"
|
echo "on each commit with:"
|
||||||
echo " pre-commit install"
|
echo " pre-commit install"
|
||||||
@@ -73,28 +67,28 @@ after_failure:
|
|||||||
echo " pre-commit run"
|
echo " pre-commit run"
|
||||||
echo "or after-the-fact on already committed files with"
|
echo "or after-the-fact on already committed files with"
|
||||||
echo " pre-commit run --all-files"
|
echo " pre-commit run --all-files"
|
||||||
fi
|
# When we run pytest, we want to run it with python>=3.5 as well as with
|
||||||
matrix:
|
# various configurations. We increment the python version at the same time
|
||||||
fast_finish: true
|
# as we test new configurations in order to reduce the number of test jobs.
|
||||||
include:
|
- name: python:3.5 + dist:xenial
|
||||||
- python: 3.6
|
python: 3.5
|
||||||
env: TEST=lint
|
dist: xenial
|
||||||
- python: 3.6
|
- name: python:3.6 + subdomain
|
||||||
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
|
- name: python:3.7 + mysql
|
||||||
|
python: 3.7
|
||||||
env:
|
env:
|
||||||
- JUPYTERHUB_TEST_DB_URL=mysql+mysqlconnector://root@127.0.0.1:$MYSQL_TCP_PORT/jupyterhub
|
- 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:
|
env:
|
||||||
- PGUSER=jupyterhub
|
- PGUSER=jupyterhub
|
||||||
- PGPASSWORD=hub[test/:?
|
- 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
|
- JUPYTERHUB_TEST_DB_URL=postgresql://jupyterhub:hub%5Btest%2F%3A%3F@127.0.0.1/jupyterhub
|
||||||
- python: 3.7
|
- name: python:nightly
|
||||||
dist: xenial
|
python: nightly
|
||||||
- python: 3.8
|
|
||||||
if: tag IS present
|
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- python: nightly
|
- name: python:nightly
|
||||||
|
fast_finish: true
|
||||||
|
@@ -30,6 +30,7 @@ USER root
|
|||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -yq --no-install-recommends \
|
&& apt-get install -yq --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
locales \
|
locales \
|
||||||
python3-dev \
|
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
|
# install the wheels we built in the first stage
|
||||||
COPY --from=builder /src/jupyterhub/wheelhouse /tmp/wheelhouse
|
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 python3 -m pip install --no-cache /tmp/wheelhouse/*
|
||||||
|
|
||||||
RUN mkdir -p /srv/jupyterhub/
|
RUN mkdir -p /srv/jupyterhub/
|
||||||
|
18
README.md
18
README.md
@@ -10,14 +10,16 @@
|
|||||||
# [JupyterHub](https://github.com/jupyterhub/jupyterhub)
|
# [JupyterHub](https://github.com/jupyterhub/jupyterhub)
|
||||||
|
|
||||||
|
|
||||||
[](https://pypi.python.org/pypi/jupyterhub)
|
[](https://pypi.python.org/pypi/jupyterhub)
|
||||||
[](https://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
[](https://www.npmjs.com/package/jupyterhub)
|
||||||
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
[](https://jupyterhub.readthedocs.org/en/latest/)
|
||||||
[](https://circleci.com/gh/jupyterhub/jupyterhub)
|
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
||||||
[](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
|
[](https://hub.docker.com/r/jupyterhub/jupyterhub/tags)
|
||||||
[](https://github.com/jupyterhub/jupyterhub/issues)
|
[](https://circleci.com/gh/jupyterhub/jupyterhub)<!-- CircleCI Token: b5b65862eb2617b9a8d39e79340b0a6b816da8cc -->
|
||||||
[](https://discourse.jupyter.org/c/jupyterhub)
|
[](https://codecov.io/gh/jupyterhub/jupyterhub)
|
||||||
[](https://gitter.im/jupyterhub/jupyterhub)
|
[](https://github.com/jupyterhub/jupyterhub/issues)
|
||||||
|
[](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
|
||||||
|
@@ -9,7 +9,7 @@ command line for details.
|
|||||||
|
|
||||||
## 1.1
|
## 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,
|
1.1 is a release with lots of accumulated fixes and improvements,
|
||||||
especially in performance, metrics, and customization.
|
especially in performance, metrics, and customization.
|
||||||
@@ -35,6 +35,9 @@ Thanks to everyone who has contributed to this release!
|
|||||||
|
|
||||||
#### New
|
#### 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 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))
|
- 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))
|
- 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
|
#### 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))
|
- 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 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))
|
- 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
|
#### 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))
|
- 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))
|
- 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))
|
- 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
|
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
|
[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.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
|
[0.9.4]: https://github.com/jupyterhub/jupyterhub/compare/0.9.3...0.9.4
|
||||||
|
338
docs/source/installation-guide-hard.md
Normal file
338
docs/source/installation-guide-hard.md
Normal 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.
|
@@ -11,3 +11,4 @@ running on your own infrastructure.
|
|||||||
quickstart
|
quickstart
|
||||||
quickstart-docker
|
quickstart-docker
|
||||||
installation-basics
|
installation-basics
|
||||||
|
installation-guide-hard
|
||||||
|
@@ -74,7 +74,7 @@ It should return `None` if it is still running,
|
|||||||
and an integer exit status, otherwise.
|
and an integer exit status, otherwise.
|
||||||
|
|
||||||
For the local process case, `Spawner.poll` uses `os.kill(PID, 0)`
|
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
|
### Spawner.stop
|
||||||
|
|
||||||
|
@@ -6,8 +6,8 @@ version_info = (
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
"b1", # release (b1, rc1, or "" for final or dev)
|
# "", # release (b1, rc1, or "" for final or dev)
|
||||||
# "dev", # dev or nothing
|
# "dev", # dev or nothing for beta/rc/stable releases
|
||||||
)
|
)
|
||||||
|
|
||||||
# pep 440 version: no dot before beta/rc, but before .dev
|
# pep 440 version: no dot before beta/rc, but before .dev
|
||||||
|
@@ -223,7 +223,7 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@web.authenticated
|
@web.authenticated
|
||||||
def get(self):
|
async def get(self):
|
||||||
"""GET /oauth/authorization
|
"""GET /oauth/authorization
|
||||||
|
|
||||||
Render oauth confirmation page:
|
Render oauth confirmation page:
|
||||||
@@ -251,8 +251,14 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Render oauth 'Authorize application...' page
|
# Render oauth 'Authorize application...' page
|
||||||
|
auth_state = await self.current_user.get_auth_state()
|
||||||
self.write(
|
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
|
# Errors that should be shown to the user on the provider website
|
||||||
|
@@ -981,6 +981,7 @@ class PAMAuthenticator(LocalAuthenticator):
|
|||||||
uid = pwd.getpwnam(username).pw_uid
|
uid = pwd.getpwnam(username).pw_uid
|
||||||
username = pwd.getpwuid(uid).pw_name
|
username = pwd.getpwuid(uid).pw_name
|
||||||
username = self.username_map.get(username, username)
|
username = self.username_map.get(username, username)
|
||||||
|
return username
|
||||||
else:
|
else:
|
||||||
return super().normalize_username(username)
|
return super().normalize_username(username)
|
||||||
|
|
||||||
|
@@ -1431,8 +1431,13 @@ class UserUrlHandler(BaseHandler):
|
|||||||
url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name),
|
url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name),
|
||||||
{"next": self.request.uri},
|
{"next": self.request.uri},
|
||||||
)
|
)
|
||||||
|
auth_state = await user.get_auth_state()
|
||||||
html = self.render_template(
|
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)
|
self.finish(html)
|
||||||
|
|
||||||
@@ -1526,10 +1531,26 @@ class UserRedirectHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
if url is None:
|
if url is None:
|
||||||
user = self.current_user
|
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:
|
if self.request.query:
|
||||||
user_url = url_concat(user_url, parse_qsl(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 = url_concat(
|
||||||
url_path_join(self.hub.base_url, "spawn", user.escaped_name),
|
url_path_join(self.hub.base_url, "spawn", user.escaped_name),
|
||||||
{"next": user_url},
|
{"next": user_url},
|
||||||
|
@@ -68,10 +68,9 @@ class HomeHandler(BaseHandler):
|
|||||||
url = url_path_join(self.hub.base_url, 'spawn', user.escaped_name)
|
url = url_path_join(self.hub.base_url, 'spawn', user.escaped_name)
|
||||||
|
|
||||||
auth_state = await user.get_auth_state()
|
auth_state = await user.get_auth_state()
|
||||||
user.spawner.run_auth_state_hook(auth_state)
|
|
||||||
|
|
||||||
html = self.render_template(
|
html = self.render_template(
|
||||||
'home.html',
|
'home.html',
|
||||||
|
auth_state=auth_state,
|
||||||
user=user,
|
user=user,
|
||||||
url=url,
|
url=url,
|
||||||
allow_named_servers=self.allow_named_servers,
|
allow_named_servers=self.allow_named_servers,
|
||||||
@@ -94,10 +93,12 @@ class SpawnHandler(BaseHandler):
|
|||||||
|
|
||||||
default_url = None
|
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(
|
return self.render_template(
|
||||||
'spawn.html',
|
'spawn.html',
|
||||||
for_user=for_user,
|
for_user=for_user,
|
||||||
|
auth_state=auth_state,
|
||||||
spawner_options_form=spawner_options_form,
|
spawner_options_form=spawner_options_form,
|
||||||
error_message=message,
|
error_message=message,
|
||||||
url=self.request.uri,
|
url=self.request.uri,
|
||||||
@@ -149,6 +150,7 @@ class SpawnHandler(BaseHandler):
|
|||||||
server_name = ''
|
server_name = ''
|
||||||
|
|
||||||
spawner = user.spawners[server_name]
|
spawner = user.spawners[server_name]
|
||||||
|
|
||||||
# resolve `?next=...`, falling back on the spawn-pending url
|
# resolve `?next=...`, falling back on the spawn-pending url
|
||||||
# must not be /user/server for named servers,
|
# must not be /user/server for named servers,
|
||||||
# which may get handled by the default server if they aren't ready yet
|
# 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.
|
# Add handler to spawner here so you can access query params in form rendering.
|
||||||
spawner.handler = self
|
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()
|
spawner_options_form = await spawner.get_options_form()
|
||||||
if spawner_options_form:
|
if spawner_options_form:
|
||||||
self.log.debug("Serving options form for %s", spawner._log_name)
|
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
|
for_user=user, spawner_options_form=spawner_options_form
|
||||||
)
|
)
|
||||||
self.finish(form)
|
self.finish(form)
|
||||||
@@ -240,7 +248,7 @@ class SpawnHandler(BaseHandler):
|
|||||||
"Failed to spawn single-user server with form", exc_info=True
|
"Failed to spawn single-user server with form", exc_info=True
|
||||||
)
|
)
|
||||||
spawner_options_form = await user.spawner.get_options_form()
|
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)
|
for_user=user, spawner_options_form=spawner_options_form, message=str(e)
|
||||||
)
|
)
|
||||||
self.finish(form)
|
self.finish(form)
|
||||||
@@ -301,6 +309,8 @@ class SpawnPendingHandler(BaseHandler):
|
|||||||
# if spawning fails for any reason, point users to /hub/home to retry
|
# if spawning fails for any reason, point users to /hub/home to retry
|
||||||
self.extra_error_html = self.spawn_home_error
|
self.extra_error_html = self.spawn_home_error
|
||||||
|
|
||||||
|
auth_state = await user.get_auth_state()
|
||||||
|
|
||||||
# First, check for previous failure.
|
# First, check for previous failure.
|
||||||
if (
|
if (
|
||||||
not spawner.active
|
not spawner.active
|
||||||
@@ -320,6 +330,7 @@ class SpawnPendingHandler(BaseHandler):
|
|||||||
html = self.render_template(
|
html = self.render_template(
|
||||||
"not_running.html",
|
"not_running.html",
|
||||||
user=user,
|
user=user,
|
||||||
|
auth_state=auth_state,
|
||||||
server_name=server_name,
|
server_name=server_name,
|
||||||
spawn_url=spawn_url,
|
spawn_url=spawn_url,
|
||||||
failed=True,
|
failed=True,
|
||||||
@@ -341,7 +352,11 @@ class SpawnPendingHandler(BaseHandler):
|
|||||||
else:
|
else:
|
||||||
page = "spawn_pending.html"
|
page = "spawn_pending.html"
|
||||||
html = self.render_template(
|
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)
|
self.finish(html)
|
||||||
return
|
return
|
||||||
@@ -366,6 +381,7 @@ class SpawnPendingHandler(BaseHandler):
|
|||||||
html = self.render_template(
|
html = self.render_template(
|
||||||
"not_running.html",
|
"not_running.html",
|
||||||
user=user,
|
user=user,
|
||||||
|
auth_state=auth_state,
|
||||||
server_name=server_name,
|
server_name=server_name,
|
||||||
spawn_url=spawn_url,
|
spawn_url=spawn_url,
|
||||||
)
|
)
|
||||||
@@ -385,7 +401,7 @@ class AdminHandler(BaseHandler):
|
|||||||
|
|
||||||
@web.authenticated
|
@web.authenticated
|
||||||
@admin_only
|
@admin_only
|
||||||
def get(self):
|
async def get(self):
|
||||||
available = {'name', 'admin', 'running', 'last_activity'}
|
available = {'name', 'admin', 'running', 'last_activity'}
|
||||||
default_sort = ['admin', 'name']
|
default_sort = ['admin', 'name']
|
||||||
mapping = {'running': orm.Spawner.server_id}
|
mapping = {'running': orm.Spawner.server_id}
|
||||||
@@ -434,9 +450,11 @@ class AdminHandler(BaseHandler):
|
|||||||
for u in users:
|
for u in users:
|
||||||
running.extend(s for s in u.spawners.values() if s.active)
|
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(
|
html = self.render_template(
|
||||||
'admin.html',
|
'admin.html',
|
||||||
current_user=self.current_user,
|
current_user=self.current_user,
|
||||||
|
auth_state=auth_state,
|
||||||
admin_access=self.settings.get('admin_access', False),
|
admin_access=self.settings.get('admin_access', False),
|
||||||
users=users,
|
users=users,
|
||||||
running=running,
|
running=running,
|
||||||
@@ -452,7 +470,7 @@ class TokenPageHandler(BaseHandler):
|
|||||||
"""Handler for page requesting new API tokens"""
|
"""Handler for page requesting new API tokens"""
|
||||||
|
|
||||||
@web.authenticated
|
@web.authenticated
|
||||||
def get(self):
|
async def get(self):
|
||||||
never = datetime(1900, 1, 1)
|
never = datetime(1900, 1, 1)
|
||||||
|
|
||||||
user = self.current_user
|
user = self.current_user
|
||||||
@@ -521,8 +539,12 @@ class TokenPageHandler(BaseHandler):
|
|||||||
|
|
||||||
oauth_clients = sorted(oauth_clients, key=sort_key, reverse=True)
|
oauth_clients = sorted(oauth_clients, key=sort_key, reverse=True)
|
||||||
|
|
||||||
|
auth_state = await self.current_user.get_auth_state()
|
||||||
html = self.render_template(
|
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)
|
self.finish(html)
|
||||||
|
|
||||||
|
@@ -16,6 +16,8 @@ import warnings
|
|||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
|
|
||||||
|
if os.name == 'nt':
|
||||||
|
import psutil
|
||||||
from async_generator import async_generator
|
from async_generator import async_generator
|
||||||
from async_generator import yield_
|
from async_generator import yield_
|
||||||
from sqlalchemy import inspect
|
from sqlalchemy import inspect
|
||||||
@@ -972,11 +974,11 @@ class Spawner(LoggingConfigurable):
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception("post_stop_hook failed with exception: %s", self)
|
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"""
|
"""Run the auth_state_hook if defined"""
|
||||||
if self.auth_state_hook is not None:
|
if self.auth_state_hook is not None:
|
||||||
try:
|
try:
|
||||||
return self.auth_state_hook(self, auth_state)
|
await maybe_future(self.auth_state_hook(self, auth_state))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception("auth_stop_hook failed with exception: %s", self)
|
self.log.exception("auth_stop_hook failed with exception: %s", self)
|
||||||
|
|
||||||
@@ -1468,8 +1470,10 @@ class LocalProcessSpawner(Spawner):
|
|||||||
self.clear_state()
|
self.clear_state()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# send signal 0 to check if PID exists
|
# We use pustil.pid_exists on windows
|
||||||
# this doesn't work on Windows, but that's okay because we don't support Windows.
|
if os.name == 'nt':
|
||||||
|
alive = psutil.pid_exists(self.pid)
|
||||||
|
else:
|
||||||
alive = await self._signal(0)
|
alive = await self._signal(0)
|
||||||
if not alive:
|
if not alive:
|
||||||
self.clear_state()
|
self.clear_state()
|
||||||
@@ -1486,11 +1490,10 @@ class LocalProcessSpawner(Spawner):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
os.kill(self.pid, sig)
|
os.kill(self.pid, sig)
|
||||||
except OSError as e:
|
except ProcessLookupError:
|
||||||
if e.errno == errno.ESRCH:
|
|
||||||
return False # process is gone
|
return False # process is gone
|
||||||
else:
|
except OSError as e:
|
||||||
raise
|
raise # Can be EPERM or EINVAL
|
||||||
return True # process exists
|
return True # process exists
|
||||||
|
|
||||||
async def stop(self, now=False):
|
async def stop(self, now=False):
|
||||||
|
@@ -6,6 +6,7 @@ from unittest import mock
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
from requests.exceptions import SSLError
|
from requests.exceptions import SSLError
|
||||||
from tornado import gen
|
from tornado import gen
|
||||||
|
|
||||||
@@ -15,6 +16,9 @@ from .utils import async_requests
|
|||||||
|
|
||||||
ssl_enabled = True
|
ssl_enabled = True
|
||||||
|
|
||||||
|
# possible errors raised by ssl failures
|
||||||
|
SSL_ERROR = (SSLError, ConnectionError)
|
||||||
|
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def wait_for_spawner(spawner, timeout=10):
|
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):
|
async def test_connection_hub_wrong_certs(app):
|
||||||
"""Connecting to the internal hub url fails without correct certs"""
|
"""Connecting to the internal hub url fails without correct certs"""
|
||||||
with pytest.raises(SSLError):
|
with pytest.raises(SSL_ERROR):
|
||||||
kwargs = {'verify': False}
|
kwargs = {'verify': False}
|
||||||
r = await async_requests.get(app.hub.url, **kwargs)
|
r = await async_requests.get(app.hub.url, **kwargs)
|
||||||
r.raise_for_status()
|
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):
|
async def test_connection_proxy_api_wrong_certs(app):
|
||||||
"""Connecting to the proxy api fails without correct certs"""
|
"""Connecting to the proxy api fails without correct certs"""
|
||||||
with pytest.raises(SSLError):
|
with pytest.raises(SSL_ERROR):
|
||||||
kwargs = {'verify': False}
|
kwargs = {'verify': False}
|
||||||
r = await async_requests.get(app.proxy.api_url, **kwargs)
|
r = await async_requests.get(app.proxy.api_url, **kwargs)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -68,7 +72,7 @@ async def test_connection_notebook_wrong_certs(app):
|
|||||||
status = await spawner.poll()
|
status = await spawner.poll()
|
||||||
assert status is None
|
assert status is None
|
||||||
|
|
||||||
with pytest.raises(SSLError):
|
with pytest.raises(SSL_ERROR):
|
||||||
kwargs = {'verify': False}
|
kwargs = {'verify': False}
|
||||||
r = await async_requests.get(spawner.server.url, **kwargs)
|
r = await async_requests.get(spawner.server.url, **kwargs)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
"""Tests for named servers"""
|
"""Tests for named servers"""
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
from urllib.parse import urlencode
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -27,6 +29,17 @@ def named_servers(app):
|
|||||||
yield
|
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):
|
async def test_default_server(app, named_servers):
|
||||||
"""Test the default /users/:user/server handler when named servers are enabled"""
|
"""Test the default /users/:user/server handler when named servers are enabled"""
|
||||||
username = 'rosie'
|
username = 'rosie'
|
||||||
@@ -267,3 +280,91 @@ async def test_named_server_spawn_form(app, username, named_servers):
|
|||||||
assert server_name in user.spawners
|
assert server_name in user.spawners
|
||||||
spawner = user.spawners[server_name]
|
spawner = user.spawners[server_name]
|
||||||
spawner.user_options == {'energy': '938MeV', 'bounds': [-10, 10], 'notspecified': 5}
|
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'
|
||||||
|
)
|
||||||
|
@@ -76,7 +76,8 @@ async def test_spawner(db, request):
|
|||||||
assert status is None
|
assert status is None
|
||||||
await spawner.stop()
|
await spawner.stop()
|
||||||
status = await spawner.poll()
|
status = await spawner.poll()
|
||||||
assert status == 1
|
assert status is not None
|
||||||
|
assert isinstance(status, int)
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_spawner(spawner, timeout=10):
|
async def wait_for_spawner(spawner, timeout=10):
|
||||||
|
@@ -389,14 +389,9 @@ class User:
|
|||||||
Full name.domain/path if using subdomains, otherwise just my /base/url
|
Full name.domain/path if using subdomains, otherwise just my /base/url
|
||||||
"""
|
"""
|
||||||
if self.settings.get('subdomain_host'):
|
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:
|
else:
|
||||||
url = self.base_url
|
return self.base_url
|
||||||
|
|
||||||
if self.settings.get('default_server_name'):
|
|
||||||
return url_path_join(url, self.settings.get('default_server_name'))
|
|
||||||
else:
|
|
||||||
return url
|
|
||||||
|
|
||||||
def server_url(self, server_name=''):
|
def server_url(self, server_name=''):
|
||||||
"""Get the url for a server with a given name"""
|
"""Get the url for a server with a given name"""
|
||||||
@@ -535,12 +530,17 @@ class User:
|
|||||||
# trigger pre-spawn hook on authenticator
|
# trigger pre-spawn hook on authenticator
|
||||||
authenticator = self.authenticator
|
authenticator = self.authenticator
|
||||||
try:
|
try:
|
||||||
|
spawner._start_pending = True
|
||||||
|
|
||||||
if authenticator:
|
if authenticator:
|
||||||
# pre_spawn_start can thow errors that can lead to a redirect loop
|
# pre_spawn_start can thow errors that can lead to a redirect loop
|
||||||
# if left uncaught (see https://github.com/jupyterhub/jupyterhub/issues/2683)
|
# if left uncaught (see https://github.com/jupyterhub/jupyterhub/issues/2683)
|
||||||
await maybe_future(authenticator.pre_spawn_start(self, spawner))
|
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
|
# update spawner start time, and activity for both spawner and user
|
||||||
self.last_activity = (
|
self.last_activity = (
|
||||||
spawner.orm_spawner.started
|
spawner.orm_spawner.started
|
||||||
|
@@ -7,6 +7,7 @@ jupyter_telemetry
|
|||||||
oauthlib>=3.0
|
oauthlib>=3.0
|
||||||
pamela
|
pamela
|
||||||
prometheus_client>=0.0.21
|
prometheus_client>=0.0.21
|
||||||
|
psutil>=5.6.5; sys_platform == 'win32'
|
||||||
python-dateutil
|
python-dateutil
|
||||||
requests
|
requests
|
||||||
SQLAlchemy>=1.1
|
SQLAlchemy>=1.1
|
||||||
|
Reference in New Issue
Block a user