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
|
||||
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
|
||||
|
@@ -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/
|
||||
|
18
README.md
18
README.md
@@ -10,14 +10,16 @@
|
||||
# [JupyterHub](https://github.com/jupyterhub/jupyterhub)
|
||||
|
||||
|
||||
[](https://pypi.python.org/pypi/jupyterhub)
|
||||
[](https://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
||||
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
||||
[](https://circleci.com/gh/jupyterhub/jupyterhub)
|
||||
[](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
|
||||
[](https://github.com/jupyterhub/jupyterhub/issues)
|
||||
[](https://discourse.jupyter.org/c/jupyterhub)
|
||||
[](https://gitter.im/jupyterhub/jupyterhub)
|
||||
[](https://pypi.python.org/pypi/jupyterhub)
|
||||
[](https://www.npmjs.com/package/jupyterhub)
|
||||
[](https://jupyterhub.readthedocs.org/en/latest/)
|
||||
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
||||
[](https://hub.docker.com/r/jupyterhub/jupyterhub/tags)
|
||||
[](https://circleci.com/gh/jupyterhub/jupyterhub)<!-- CircleCI Token: b5b65862eb2617b9a8d39e79340b0a6b816da8cc -->
|
||||
[](https://codecov.io/gh/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
|
||||
**multi-user Hub** which spawns, manages, and proxies multiple instances of the
|
||||
|
@@ -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
|
||||
|
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-docker
|
||||
installation-basics
|
||||
installation-guide-hard
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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},
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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):
|
||||
|
@@ -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()
|
||||
|
@@ -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'
|
||||
)
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user