Compare commits

...

66 Commits

Author SHA1 Message Date
Min RK
2102c1fd1c 0.9.0rc1 2018-06-12 14:19:59 +02:00
Min RK
aa9676ec5e Merge pull request #1913 from rkdarst/announcement_text
Add customizable announcement text on home,login,logout,spawn
2018-06-12 14:14:21 +02:00
Min RK
5e93c7de4c announcement doc language
per willingc review
2018-06-12 13:48:42 +02:00
Min RK
d22626906b multiline conditionals setting announcement variable in templates
for readability per review by willingc
2018-06-12 13:48:24 +02:00
Min RK
5f91ed044e parametrize test_announcements 2018-06-12 13:47:55 +02:00
Min RK
5c3c7493c1 Merge pull request #1963 from willingc/hooks-doc
add a small section for pre/post spawn hooks
2018-06-11 15:27:39 +02:00
Carol Willing
1b7965092e remove backticks and long for rst format 2018-06-08 14:21:31 -07:00
Carol Willing
ef60be5a99 put backticks outside of link 2018-06-08 14:19:43 -07:00
Carol Willing
f78d652cd6 fix missing brackets 2018-06-08 14:18:14 -07:00
Carol Willing
3650575797 add a small section for pre/post spawn hooks 2018-06-08 14:13:45 -07:00
Tim Head
0f000f6d41 Merge pull request #1961 from willingc/doc-shib
Add link to authenticators used with Shibboleth
2018-06-08 18:17:08 +02:00
Carol Willing
643729ac0c Merge pull request #1962 from chicocvenancio/docs_mysql_dynamic
database docs utfmb4 collation and some versions of mysql/mariadb
2018-06-08 09:14:04 -07:00
Chico Venancio
91a67bf580 database docs: fix formatting 2018-06-08 13:09:09 -03:00
Chico Venancio
c75eddb730 database docs utfmb4 collation and some versions of mysql/mariadb 2018-06-08 12:55:02 -03:00
Carol Willing
0f5888ad6c Add link to authenticators used with Shibboleth 2018-06-08 08:22:11 -07:00
Carol Willing
8c48f3b856 Merge pull request #1960 from willingc/db-doc
add database doc section and edits to upgrading db
2018-06-08 08:08:51 -07:00
Carol Willing
6e7e18bc3c add @minrk review comments 2018-06-08 07:34:09 -07:00
Tim Head
3dfd7e5a84 Merge pull request #1958 from willingc/proxy-error
Add error message text
2018-06-08 15:19:27 +02:00
Carol Willing
19ecbf3734 add note about why no sqlite and nfs 2018-06-08 06:06:15 -07:00
Carol Willing
eac3e8ba90 add database doc section and edits to upgrading db 2018-06-08 05:51:00 -07:00
Carol Willing
a7a6829b69 add additional reference per @betatim review 2018-06-08 05:01:32 -07:00
Carol Willing
61299113c8 add error message text 2018-06-07 21:44:18 -07:00
Tim Head
21a57dfa0b Merge pull request #1949 from willingc/npm-doc
clarify that conda installs npm and proxy
2018-06-07 19:52:00 +02:00
Carol Willing
a7226a8231 changes per @minrk review 2018-06-07 09:10:04 -07:00
Min RK
6e3dd21f60 Merge pull request #1952 from willingc/docker-conda
bump miniconda to 4.5.1 in Dockerfile
2018-06-07 10:24:33 +02:00
Min RK
cf049730d4 Merge pull request #1954 from willingc/black-test
Blacken python doc build files
2018-06-07 10:24:14 +02:00
Min RK
cb9ce4d3af Merge pull request #1955 from dtaniwaki/handle-fatal-error
only relay headers from HTTPErrors
2018-06-07 10:22:38 +02:00
Daisuke Taniwaki
925ee1dfb2 Do not refer spawner on fatal errors 2018-06-07 14:53:46 +09:00
Daisuke Taniwaki
5d9122b26c Avoid setting unexpected headers 2018-06-07 14:53:34 +09:00
Carol Willing
6821ad0c59 blacken autodoc sphinx extension 2018-06-06 12:57:14 -07:00
Carol Willing
ff7851ee2e blacken conf.py 2018-06-06 12:52:30 -07:00
Carol Willing
6940ed85b1 bump miniconda to 4.5.1 2018-06-06 08:25:28 -07:00
Carol Willing
3d497a7f43 clarify that conda installs npm and proxy 2018-06-06 06:56:22 -07:00
Carol Willing
cc6968e225 Merge pull request #1942 from minrk/nginx-file
note where nginx config files are typically created.
2018-06-06 06:02:30 -07:00
Carol Willing
a6c517c344 Merge pull request #1947 from minrk/progress-stopping
Avoid showing spawn-pending page when user is stopping
2018-06-06 06:00:58 -07:00
Carol Willing
a3e08b7f52 Merge pull request #1948 from minrk/aclosing
Python 3.5.1 cannot close async iterators
2018-06-06 05:56:00 -07:00
Min RK
14c8d7dc46 Merge pull request #1946 from dtaniwaki/configure-max-inactive-duration
Configure max inactive duration
2018-06-06 12:54:55 +02:00
Daisuke Taniwaki
ac2590c679 Add active_user_window configuration 2018-06-06 19:00:34 +09:00
Min RK
ead13c6a11 further clarify that we are creating a new file, not editing nginx.confg 2018-06-06 12:00:21 +02:00
Min RK
5002ab2990 Python 3.5.1 cannot close async iterators
so provide a null aclosing async context manager that does nothing
2018-06-06 11:43:33 +02:00
Min RK
ab3e7293a4 disable my server link while stop is pending
makes it a little harder to request a spawn while stop is pending
2018-06-06 10:53:50 +02:00
Min RK
062af5e5cb Avoid showing spawn_pending page when pending action is stop
Separate stop_pending page when this occurs,
similar to the old spawn pending spinner without progress events
2018-06-06 10:53:05 +02:00
Carol Willing
92088570ea Merge pull request #1943 from minrk/getuser-delayed
delay call to getuser in token app
2018-06-05 10:18:08 -07:00
Min RK
604ccf515d delay call to getuser in token app
avoids issues with getuser preventing launch, e.g. in weird containers where the current user doesn’t exist
2018-06-05 17:52:00 +02:00
Min RK
ec9b244990 note where nginx config files are typically created. 2018-06-04 11:10:21 +02:00
Min RK
09acdc23b5 Merge pull request #1940 from dtaniwaki/fix-created-columne-error
Handle NULL created column of tokens table
2018-06-04 10:55:20 +02:00
Richard Darst
e7808b50af Add tests of page announcements
- Adds test_pages.py:test_page_contents, which currently tests just
  the page annoucement variables.
2018-06-03 01:18:48 +03:00
Richard Darst
9c27095744 Add customizable announcement text on home,login,logout,spawn
- Using the new template_vars setting (#1872), allow the variable
  `announcement` to create a header message on all the pages in the
  title, or the variables `announcement_{home,login,logout,spawn}` to
  set variables on these single pages.
- This is not the most powerful method of putting an announcement into
  the templates, because it requires a server restart to change.  But
  the invasiveness is very low, and allows minimal message
  without having to touch the templates themselves.
- Closes: #1836
2018-06-03 01:18:48 +03:00
Daisuke Taniwaki
690b07982e Handle NULL created column of api_tokens table 2018-06-02 23:55:21 +09:00
Min RK
784e5aa4ee Merge pull request #1926 from minrk/tilde-safe
tilde is a safe character in user URLs
2018-05-30 14:48:35 +02:00
Min RK
29187cab3a Merge pull request #1929 from minrk/pgbin
install psycopg2 from binary
2018-05-29 11:03:41 +02:00
Min RK
43a72807c6 install psycopg2 from binary
it has a new package name for the binary wheel
2018-05-29 10:41:53 +02:00
Min RK
1d1f6f1870 Merge pull request #1923 from nxg/doc-changes-1747
Documentation clarifications (adding explicitness).
2018-05-29 10:21:42 +02:00
Min RK
505a6eb4e3 ensure user subdomains are valid
escape with `_` instead of `%`.

This is not technically rigorous, as collisions are possible (users foo_40 and foo@ have the same domain)
and other domain restrictions are not applied (length, starting characters, etc.).
Username normalization can be used to apply stricter, more rigorous structure.
2018-05-29 10:19:21 +02:00
Min RK
cc49df8147 Merge pull request #1852 from summerswallow-whi/service-info
Attach an info field to the service
2018-05-28 14:57:10 +02:00
Min RK
98d60402b5 add service.info to rest api docs 2018-05-28 14:09:53 +02:00
Min RK
319e8a1062 update service models in tests 2018-05-28 14:09:44 +02:00
Min RK
0c5d564830 tilde is a safe character in user URLs
Chrome unconditionally reverts any not-strictly-necessary escaping in URLs (this seems wrong?)
2018-05-28 13:46:52 +02:00
Norman Gray
c0404cf9d9 Documentation clarifications (adding explicitness).
Addresses issue #1747.

These additions aren't perfect -- it's unfortunate that I've added
mention of reverse proxies on two separate pages.  I don't _think_
these can reasonably be put on the same page -- perhaps a cross
reference?
2018-05-27 18:49:40 +01:00
Min RK
f364661363 Merge pull request #1899 from adelcast/dev/adelcast/kill_proxy_tree
ConfigurableHTTPProxy.stop: kill child processes on Windows case
2018-05-25 15:25:53 +02:00
Min RK
f92d77b06d Merge pull request #1915 from rkdarst/respawn_error_msg
Clarify error message on implicit respawns.
2018-05-25 10:09:35 +02:00
Haw-minn Lu
2cf00e6aae Add info field to service model 2018-05-24 11:19:18 -07:00
Richard Darst
dfdb0cff2b Clarify error message on implicit respawns.
- This message is presented when the last spawn failed, along with a
  HTTP 500.  The current text is quite confusing, especially when the
  problem may just be solvable by trying to respawn again.
2018-05-24 16:07:26 +03:00
Alejandro del Castillo
d0dad84ffa ConfigurableHTTPProxy.stop: kill child processes on Windows case
On the Windows case, the configurable-http-proxy is spwaned using a
shell. To stop the proxy, we need to terminate both the main process
(shell) and its child (proxy).

Signed-off-by: Alejandro del Castillo <alejandro.delcastillo@ni.com>
2018-05-23 10:10:50 -05:00
Min RK
1745937f1a back to dev 2018-05-23 16:47:56 +02:00
Haw-minn Lu
a73e6f0bf8 Attach an info field to the service 2018-04-27 14:51:55 -07:00
36 changed files with 505 additions and 122 deletions

View File

@@ -29,7 +29,7 @@ before_install:
pip install 'mysql-connector<2.2'
elif [[ $JUPYTERHUB_TEST_DB_URL == postgresql* ]]; then
DB=postgres bash ci/init-db.sh
pip install psycopg2
pip install psycopg2-binary
fi
install:
- pip install --upgrade pip

View File

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

View File

@@ -50,37 +50,62 @@ for administration of the Hub and its users.
## Installation
### Check prerequisites
A Linux/Unix based system with the following:
- A Linux/Unix based system
- [Python](https://www.python.org/downloads/) 3.5 or greater
- [nodejs/npm](https://www.npmjs.com/)
- [Python](https://www.python.org/downloads/) 3.4 or greater
- [nodejs/npm](https://www.npmjs.com/) Install a recent version of
[nodejs/npm](https://docs.npmjs.com/getting-started/installing-node)
For example, install it on Linux (Debian/Ubuntu) using:
* If you are using **`conda`**, the nodejs and npm dependencies will be installed for
you by conda.
sudo apt-get install npm nodejs-legacy
* If you are using **`pip`**, install a recent version of
[nodejs/npm](https://docs.npmjs.com/getting-started/installing-node).
For example, install it on Linux (Debian/Ubuntu) using:
The `nodejs-legacy` package installs the `node` executable and is currently
required for npm to work on Debian/Ubuntu.
```
sudo apt-get install npm nodejs-legacy
```
The `nodejs-legacy` package installs the `node` executable and is currently
required for npm to work on Debian/Ubuntu.
- TLS certificate and key for HTTPS communication
- Domain name
### Install packages
#### Using `conda`
To install JupyterHub along with its dependencies including nodejs/npm:
```bash
conda install jupyterhub
```
If you plan to run notebook servers locally, install the Jupyter notebook
or JupyterLab:
```bash
conda install notebook
conda install jupyterlab
```
#### Using `pip`
JupyterHub can be installed with `pip`, and the proxy with `npm`:
```bash
npm install -g configurable-http-proxy
pip3 install jupyterhub
python3 -m pip install jupyterhub
```
If you plan to run notebook servers locally, you will need to install the
[Jupyter notebook](https://jupyter.readthedocs.io/en/latest/install.html)
package:
pip3 install --upgrade notebook
python3 -m pip install --upgrade notebook
### Run the Hub server

View File

@@ -689,6 +689,11 @@ definitions:
description: The command used to start the service (if managed)
items:
type: string
info:
type: object
description: |
Additional information a deployment can attach to a service.
JupyterHub does not use this field.
Token:
type: object
properties:

View File

@@ -35,12 +35,14 @@ author = u'Project Jupyter team'
# Autopopulate version
from os.path import dirname
docs = dirname(dirname(__file__))
root = dirname(docs)
sys.path.insert(0, root)
sys.path.insert(0, os.path.join(docs, 'sphinxext'))
import jupyterhub
# The short X.Y version.
version = '%i.%i' % jupyterhub.version_info[:2]
# The full version, including alpha/beta/rc tags.
@@ -56,12 +58,10 @@ default_role = 'literal'
# -- Source -------------------------------------------------------------
source_parsers = {
'.md': 'recommonmark.parser.CommonMarkParser',
}
source_parsers = {'.md': 'recommonmark.parser.CommonMarkParser'}
source_suffix = ['.rst', '.md']
#source_encoding = 'utf-8-sig'
# source_encoding = 'utf-8-sig'
# -- Options for HTML output ----------------------------------------------
@@ -96,7 +96,7 @@ html_sidebars = {
'navigation.html',
'relations.html',
'sourcelink.html',
],
]
}
htmlhelp_basename = 'JupyterHubdoc'
@@ -104,38 +104,40 @@ htmlhelp_basename = 'JupyterHubdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
#'papersize': 'letterpaper',
#'pointsize': '10pt',
#'preamble': '',
#'figure_align': 'htbp',
# 'papersize': 'letterpaper',
# 'pointsize': '10pt',
# 'preamble': '',
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'JupyterHub.tex', u'JupyterHub Documentation',
u'Project Jupyter team', 'manual'),
(
master_doc,
'JupyterHub.tex',
u'JupyterHub Documentation',
u'Project Jupyter team',
'manual',
)
]
#latex_logo = None
#latex_use_parts = False
#latex_show_pagerefs = False
#latex_show_urls = False
#latex_appendices = []
#latex_domain_indices = True
# latex_logo = None
# latex_use_parts = False
# latex_show_pagerefs = False
# latex_show_urls = False
# latex_appendices = []
# latex_domain_indices = True
# -- manual page output -------------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'jupyterhub', u'JupyterHub Documentation',
[author], 1)
]
man_pages = [(master_doc, 'jupyterhub', u'JupyterHub Documentation', [author], 1)]
#man_show_urls = False
# man_show_urls = False
# -- Texinfo output -----------------------------------------------------
@@ -144,15 +146,21 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'JupyterHub', u'JupyterHub Documentation',
author, 'JupyterHub', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
'JupyterHub',
u'JupyterHub Documentation',
author,
'JupyterHub',
'One line description of project.',
'Miscellaneous',
)
]
#texinfo_appendices = []
#texinfo_domain_indices = True
#texinfo_show_urls = 'footnote'
#texinfo_no_detailmenu = False
# texinfo_appendices = []
# texinfo_domain_indices = True
# texinfo_show_urls = 'footnote'
# texinfo_no_detailmenu = False
# -- Epub output --------------------------------------------------------
@@ -179,6 +187,7 @@ else:
# readthedocs.org uses their theme by default, so no need to specify it
# build rest-api, since RTD doesn't run make
from subprocess import check_call as sh
sh(['make', 'rest-api'], cwd=docs)
# -- Spell checking -------------------------------------------------------
@@ -190,4 +199,4 @@ except ImportError:
else:
extensions.append("sphinxcontrib.spelling")
spelling_word_list_filename='spelling_wordlist.txt'
spelling_word_list_filename = 'spelling_wordlist.txt'

View File

@@ -35,6 +35,10 @@ Configuring only the main IP and port of JupyterHub should be sufficient for
most deployments of JupyterHub. However, more customized scenarios may need
additional networking details to be configured.
Note that `c.JupyterHub.ip` and `c.JupyterHub.port` are single values,
not tuples or lists JupyterHub listens to only a single IP address and
port.
## Set the Proxy's REST API communication URL (optional)
By default, this REST API listens on port 8081 of `localhost` only.
@@ -86,3 +90,12 @@ configuration for, e.g. docker, is:
c.JupyterHub.hub_ip = '0.0.0.0' # listen on all interfaces
c.JupyterHub.hub_connect_ip = '10.0.1.4' # ip as seen on the docker network. Can also be a hostname.
```
## Adjusting the hub's URL
The hub will most commonly be running on a hostname of its own. If it
is not for example, if the hub is being reverse-proxied and being
exposed at a URL such as `https://proxy.example.org/jupyter/` then
you will need to tell JupyterHub the base URL of the service. In such
a case, it is both necessary and sufficient to set
`c.JupyterHub.base_url = '/jupyter/'` in the configuration.

View File

@@ -72,8 +72,13 @@ would be the needed configuration:
If SSL termination happens outside of the Hub
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In certain cases, e.g. behind `SSL termination in NGINX <https://www.nginx.com/resources/admin-guide/nginx-ssl-termination/>`_,
allowing no SSL running on the hub may be the desired configuration option.
In certain cases, for example if the hub is running behind a reverse proxy, and
`SSL termination is being provided by NGINX <https://www.nginx.com/resources/admin-guide/nginx-ssl-termination/>`_,
it is reasonable to run the hub without SSL.
To achieve this, simply omit the configuration settings
``c.JupyterHub.ssl_key`` and ``c.JupyterHub.ssl_cert``
(setting them to ``None`` does not have the same effect, and is an error).
.. _cookie-secret:

View File

@@ -5,20 +5,27 @@
Before installing JupyterHub, you will need:
- a Linux/Unix based system
- [Python](https://www.python.org/downloads/) 3.4 or greater. An understanding
- [Python](https://www.python.org/downloads/) 3.5 or greater. An understanding
of using [`pip`](https://pip.pypa.io/en/stable/) or
[`conda`](https://conda.io/docs/get-started.html) for
installing Python packages is helpful.
- [nodejs/npm](https://www.npmjs.com/). [Install nodejs/npm](https://docs.npmjs.com/getting-started/installing-node),
using your operating system's package manager. For example, install on Linux
Debian/Ubuntu using:
using your operating system's package manager.
```bash
sudo apt-get install npm nodejs-legacy
```
* If you are using **`conda`**, the nodejs and npm dependencies will be installed for
you by conda.
* If you are using **`pip`**, install a recent version of
[nodejs/npm](https://docs.npmjs.com/getting-started/installing-node).
For example, install it on Linux (Debian/Ubuntu) using:
```
sudo apt-get install npm nodejs-legacy
```
The `nodejs-legacy` package installs the `node` executable and is currently
required for npm to work on Debian/Ubuntu.
The `nodejs-legacy` package installs the `node` executable and is currently
required for `npm` to work on Debian/Ubuntu.
- TLS certificate and key for HTTPS communication
- Domain name

View File

@@ -38,6 +38,8 @@ with any provider, is also available.
- ldapauthenticator for LDAP
- tmpauthenticator for temporary accounts
- For Shibboleth, [jhub_shibboleth_auth](https://github.com/gesiscss/jhub_shibboleth_auth)
and [jhub_remote_user_authenticator](https://github.com/cwaldbieser/jhub_remote_user_authenticator)
## Technical Overview of Authentication
@@ -206,7 +208,13 @@ class MyAuthenticator(Authenticator):
spawner.environment['UPSTREAM_TOKEN'] = auth_state['upstream_token']
```
## pre_spawn_start and post_spawn_stop hooks
Authenticators uses two hooks, [pre_spawn_start(user, spawner)][] and
[post_spawn_stop(user, spawner)][] to add pass additional state information
between the authenticator and a spawner. These hooks are typically used auth-related
startup, i.e. opening a PAM session, and auth-related cleanup, i.e. closing a
PAM session.
## JupyterHub as an OAuth provider

View File

@@ -30,8 +30,10 @@ openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
## nginx
The **`nginx` server config file** is fairly standard fare except for the two
`location` blocks within the `HUB.DOMAIN.TLD` config file:
This **`nginx` config file** is fairly standard fare except for the two
`location` blocks within the main section for HUB.DOMAIN.tld.
To create a new site for jupyterhub in your nginx config, make a new file
in `sites.enabled`, e.g. `/etc/nginx/sites.enabled/jupyterhub.conf`:
```bash
# top-level http config for websocket headers

View File

@@ -0,0 +1,62 @@
# The Hub's Database
JupyterHub uses a database to store information about users, services, and other
data needed for operating the Hub.
## Default SQLite database
The default database for JupyterHub is a [SQLite](https://sqlite.org) database.
We have chosen SQLite as JupyterHub's default for its lightweight simplicity
in certain uses such as testing, small deployments and workshops.
For production systems, SQLite has some disadvantages when used with JupyterHub:
- `upgrade-db` may not work, and you may need to start with a fresh database
- `downgrade-db` **will not** work if you want to rollback to an earlier
version, so backup the `jupyterhub.sqlite` file before upgrading
The sqlite documentation provides a helpful page about [when to use SQLite and
where traditional RDBMS may be a better choice](https://sqlite.org/whentouse.html).
## Using an RDBMS (PostgreSQL, MySQL)
When running a long term deployment or a production system, we recommend using
a traditional RDBMS database, such as [PostgreSQL](https://www.postgresql.org)
or [MySQL](https://www.mysql.com), that supports the SQL `ALTER TABLE`
statement.
## Notes and Tips
### SQLite
The SQLite database should not be used on NFS. SQLite uses reader/writer locks
to control access to the database. This locking mechanism might not work
correctly if the database file is kept on an NFS filesystem. This is because
`fcntl()` file locking is broken on many NFS implementations. Therefore, you
should avoid putting SQLite database files on NFS since it will not handle well
multiple processes which might try to access the file at the same time.
### PostgreSQL
We recommend using PostgreSQL for production if you are unsure whether to use
MySQL or PostgreSQL or if you do not have a strong preference. There is
additional configuration required for MySQL that is not needed for PostgreSQL.
### MySQL / MariaDB
- You should use the `pymysql` sqlalchemy provider (the other one, MySQLdb,
isn't available for py3).
- You also need to set `pool_recycle` to some value (typically 60 - 300)
which depends on your MySQL setup. This is necessary since MySQL kills
connections serverside if they've been idle for a while, and the connection
from the hub will be idle for longer than most connections. This behavior
will lead to frustrating 'the connection has gone away' errors from
sqlalchemy if `pool_recycle` is not set.
- If you use `utf8mb4` collation with MySQL earlier than 5.7.7 or MariaDB
earlier than 10.2.1 you may get an `1709, Index column size too large` error.
To fix this you need to set `innodb_large_prefix` to enabled and
`innodb_file_format` to `Barracuda` to allow for the index sizes jupyterhub
uses. `row_format` will be set to `DYNAMIC` as long as those options are set
correctly. Later versions of MariaDB and MySQL should set these values by
default, as well as have a default `DYNAMIC` `row_format` and pose no trouble
to users.

View File

@@ -11,6 +11,7 @@ Technical Reference
services
proxy
rest
database
upgrading
templates
config-examples

View File

@@ -59,3 +59,35 @@ text about the server starting up, place this content in a file named
<p>Patience is a virtue.</p>
{% endblock %}
```
## Page Announcements
To add announcements to be displayed on a page, you have two options:
- Extend the page templates as described above
- Use configuration variables
### Announcement Configuration Variables
If you set the configuration variable `JupyterHub.template_vars =
{'announcement': 'some_text}`, the given `some_text` will be placed on
the top of all pages. The more specific variables
`announcement_login`, `announcement_spawn`, `announcement_home`, and
`announcement_logout` are more specific and only show on their
respective pages (overriding the global `announcement` variable).
Note that changing these varables require a restart, unlike direct
template extension.
You can get the same effect by extending templates, which allows you
to update the messages without restarting. Set
`c.JupyterHub.template_paths` as mentioned above, and then create a
template (for example, `login.html`) with:
```html
{% extends "templates/login.html" %}
{% set announcement = 'some message' %}
```
Extending `page.html` puts the message on all pages, but note that
extending `page.html` take precedence over an extension of a specific
page (unlike the variable-based approach above).

View File

@@ -2,30 +2,22 @@
From time to time, you may wish to upgrade JupyterHub to take advantage
of new releases. Much of this process is automated using scripts,
such as those generated by alembic for database upgrades. Before upgrading a
JupyterHub deployment, it's critical to backup your data and configurations
before shutting down the JupyterHub process and server.
such as those generated by alembic for database upgrades. Whether you
are using the default SQLite database or an RDBMS, such as PostgreSQL or
MySQL, the process follows similar steps.
## Databases: SQLite (default) or RDBMS (PostgreSQL, MySQL)
**Before upgrading a JupyterHub deployment**, it's critical to backup your data
and configurations before shutting down the JupyterHub process and server.
The default database for JupyterHub is a [SQLite](https://sqlite.org) database.
We have chosen SQLite as JupyterHub's default for its lightweight simplicity
in certain uses such as testing, small deployments and workshops.
## Note about upgrading the SQLite database
When running a long term deployment or a production system, we recommend using
a traditional RDBMS database, such as [PostgreSQL](https://www.postgresql.org)
or [MySQL](https://www.mysql.com), that supports the SQL `ALTER TABLE`
statement.
For production systems, SQLite has some disadvantages when used with JupyterHub:
When used in production systems, SQLite has some disadvantages when it
comes to upgrading JupyterHub. These are:
- `upgrade-db` may not work, and you may need to start with a fresh database
- `downgrade-db` **will not** work if you want to rollback to an earlier
version, so backup the `jupyterhub.sqlite` file before upgrading
The sqlite documentation provides a helpful page about [when to use sqlite and
where traditional RDBMS may be a better choice](https://sqlite.org/whentouse.html).
## The upgrade process
Five fundamental process steps are needed when upgrading JupyterHub and its

View File

@@ -7,14 +7,18 @@ from sphinx.ext.autodoc import ClassDocumenter, AttributeDocumenter
class ConfigurableDocumenter(ClassDocumenter):
"""Specialized Documenter subclass for traits with config=True"""
objtype = 'configurable'
directivetype = 'class'
def get_object_members(self, want_all):
"""Add traits with .tag(config=True) to members list"""
check, members = super().get_object_members(want_all)
get_traits = self.object.class_own_traits if self.options.inherited_members \
else self.object.class_traits
get_traits = (
self.object.class_own_traits
if self.options.inherited_members
else self.object.class_traits
)
trait_members = []
for name, trait in sorted(get_traits(config=True).items()):
# put help in __doc__ where autodoc will look for it
@@ -42,10 +46,7 @@ class TraitDocumenter(AttributeDocumenter):
default_s = ''
else:
default_s = repr(default)
sig = ' = {}({})'.format(
self.object.__class__.__name__,
default_s,
)
sig = ' = {}({})'.format(self.object.__class__.__name__, default_s)
return super().add_directive_header(sig)

View File

@@ -7,7 +7,7 @@ version_info = (
0,
9,
0,
"b3", # release (b1, rc1)
"rc1", # release (b1, rc1)
# "dev", # dev
)

View File

@@ -93,12 +93,15 @@ class APIHandler(BaseHandler):
self.db.rollback()
self.set_header('Content-Type', 'application/json')
# allow setting headers from exceptions
# since exception handler clears headers
headers = getattr(exception, 'headers', None)
if headers:
for key, value in headers.items():
self.set_header(key, value)
if isinstance(exception, web.HTTPError):
# allow setting headers from exceptions
# since exception handler clears headers
headers = getattr(exception, 'headers', None)
if headers:
for key, value in headers.items():
self.set_header(key, value)
# Content-Length must be recalculated.
self.clear_header('Content-Length')
self.write(json.dumps({
'status': status_code,

View File

@@ -23,6 +23,7 @@ def service_model(service):
'prefix': service.server.base_url if service.server else '',
'command': service.command,
'pid': service.proc.pid if service.proc else 0,
'info': service.info
}
class ServiceListAPIHandler(APIHandler):

View File

@@ -140,7 +140,10 @@ class NewToken(Application):
ab01cd23ef45
"""
name = Unicode(getuser())
name = Unicode()
@default('name')
def _default_name(self):
return getuser()
aliases = token_aliases
classes = []
@@ -274,6 +277,9 @@ class JupyterHub(Application):
service_check_interval = Integer(60,
help="Interval (in seconds) at which to check connectivity of services with web endpoints."
).tag(config=True)
active_user_window = Integer(30 * 60,
help="Duration (in seconds) to determine the number of active users."
).tag(config=True)
data_files_path = Unicode(DATA_FILES_PATH,
help="The location of jupyterhub data files (e.g. /usr/local/share/jupyterhub)"
@@ -1786,8 +1792,7 @@ class JupyterHub(Application):
spawner.last_activity = max(spawner.last_activity, dt)
else:
spawner.last_activity = dt
# FIXME: Make this configurable duration. 30 minutes for now!
if (now - user.last_activity).total_seconds() < 30 * 60:
if (now - user.last_activity).total_seconds() < self.active_user_window:
active_users_count += 1
self.statsd.gauge('users.running', users_count)
self.statsd.gauge('users.active', active_users_count)

View File

@@ -40,7 +40,8 @@ reasons = {
'timeout': "Failed to reach your server."
" Please try again later."
" Contact admin if the issue persists.",
'error': "Failed to start your server. Please contact admin.",
'error': "Failed to start your server on the last attempt. "
" Please contact admin if the issue persists.",
}
# constant, not configurable
@@ -826,19 +827,27 @@ class BaseHandler(RequestHandler):
)
self.set_header('Content-Type', 'text/html')
# allow setting headers from exceptions
# since exception handler clears headers
headers = getattr(exception, 'headers', None)
if headers:
for key, value in headers.items():
self.set_header(key, value)
if isinstance(exception, web.HTTPError):
# allow setting headers from exceptions
# since exception handler clears headers
headers = getattr(exception, 'headers', None)
if headers:
for key, value in headers.items():
self.set_header(key, value)
# Content-Length must be recalculated.
self.clear_header('Content-Length')
# render the template
try:
html = self.render_template('%s.html' % status_code, **ns)
except TemplateNotFound:
self.log.debug("No template for %d", status_code)
html = self.render_template('error.html', **ns)
try:
html = self.render_template('error.html', **ns)
except:
# In this case, any side effect must be avoided.
ns['no_spawner_check'] = True
html = self.render_template('error.html', **ns)
self.write(html)
@@ -941,7 +950,7 @@ class UserSpawnHandler(BaseHandler):
raise copy.copy(exc).with_traceback(exc.__traceback__)
# check for pending spawn
if spawner.pending and spawner._spawn_future:
if spawner.pending == 'spawn' and spawner._spawn_future:
# wait on the pending spawn
self.log.debug("Waiting for %s pending %s", spawner._log_name, spawner.pending)
try:
@@ -951,14 +960,20 @@ class UserSpawnHandler(BaseHandler):
pass
# we may have waited above, check pending again:
# page could be pending spawn *or* stop
if spawner.pending:
self.log.info("%s is pending %s", spawner._log_name, spawner.pending)
# spawn has started, but not finished
self.statsd.incr('redirects.user_spawn_pending', 1)
url_parts = []
if spawner.pending == "stop":
page = "stop_pending.html"
else:
page = "spawn_pending.html"
html = self.render_template(
"spawn_pending.html",
page,
user=user,
spawner=spawner,
progress_url=spawner._progress_url,
)
self.finish(html)

View File

@@ -488,7 +488,10 @@ class ConfigurableHTTPProxy(Proxy):
except FileNotFoundError as e:
self.log.error(
"Failed to find proxy %r\n"
"The proxy can be installed with `npm install -g configurable-http-proxy`"
"The proxy can be installed with `npm install -g configurable-http-proxy`."
"To install `npm`, install nodejs which includes `npm`."
"If you see an `EACCES` error or permissions error, refer to the `npm` "
"documentation on How To Prevent Permissions Errors."
% self.command
)
raise
@@ -516,13 +519,26 @@ class ConfigurableHTTPProxy(Proxy):
self._check_running_callback = pc
pc.start()
def _kill_proc_tree(self, pid):
import psutil
parent = psutil.Process(pid)
children = parent.children(recursive=True)
for child in children:
child.kill()
psutil.wait_procs(children, timeout=5)
def stop(self):
self.log.info("Cleaning up proxy[%i]...", self.proxy_process.pid)
if self._check_running_callback is not None:
self._check_running_callback.stop()
if self.proxy_process.poll() is None:
try:
self.proxy_process.terminate()
if os.name == 'nt':
# On Windows we spawned a shell on Popen, so we need to
# terminate all child processes as well
self._kill_proc_tree(self.proxy_process.pid)
else:
self.proxy_process.terminate()
except Exception as e:
self.log.error("Failed to terminate proxy process: %s", e)

View File

@@ -175,6 +175,13 @@ class Service(LoggingConfigurable):
If unspecified, an API token will be generated for managed services.
"""
).tag(input=True)
info = Dict(
help="""Provide a place to include miscellaneous information about the service,
provided through the configuration
"""
).tag(input=True)
# Managed service API:
spawner = Any()

View File

@@ -1525,6 +1525,7 @@ def test_get_services(app, mockservice_url):
'pid': mockservice.proc.pid,
'prefix': mockservice.server.base_url,
'url': mockservice.url,
'info': {},
}
}
@@ -1551,6 +1552,7 @@ def test_get_service(app, mockservice_url):
'pid': mockservice.proc.pid,
'prefix': mockservice.server.base_url,
'url': mockservice.url,
'info': {},
}
r = yield api_request(app, 'services/%s' % mockservice.name,

View File

@@ -17,6 +17,7 @@ from .mocking import FormSpawner, public_url, public_host
from .test_api import api_request, add_user
from .utils import async_requests
def get_page(path, app, hub=True, **kw):
if hub:
prefix = app.hub.base_url
@@ -517,3 +518,55 @@ def test_oauth_token_page(app):
def test_proxy_error(app, error_status):
r = yield get_page('/error/%i' % error_status, app)
assert r.status_code == 200
@pytest.mark.gen_test
@pytest.mark.parametrize(
"announcements",
[
"",
"spawn",
"spawn,home,login",
"login,logout",
]
)
def test_announcements(app, announcements):
"""Test announcements on various pages"""
# Default announcement - same on all pages
ann01 = "ANNOUNCE01"
template_vars = {"announcement": ann01}
announcements = announcements.split(",")
for name in announcements:
template_vars["announcement_" + name] = "ANN_" + name
def assert_announcement(name, text):
if name in announcements:
assert template_vars["announcement_" + name] in text
assert ann01 not in text
else:
assert ann01 in text
cookies = yield app.login_user("jones")
with mock.patch.dict(
app.tornado_settings,
{"template_vars": template_vars, "spawner_class": FormSpawner},
):
r = yield get_page("login", app)
r.raise_for_status()
assert_announcement("login", r.text)
r = yield get_page("spawn", app, cookies=cookies)
r.raise_for_status()
assert_announcement("spawn", r.text)
r = yield get_page("home", app, cookies=cookies) # hub/home
r.raise_for_status()
assert_announcement("home", r.text)
# need auto_login=True to get logout page
auto_login = app.authenticator.auto_login
app.authenticator.auto_login = True
try:
r = yield get_page("logout", app, cookies=cookies)
finally:
app.authenticator.auto_login = auto_login
r.raise_for_status()
assert_announcement("logout", r.text)

View File

@@ -11,6 +11,7 @@ import sys
import tempfile
import time
from unittest import mock
from urllib.parse import urlparse
import pytest
from tornado import gen
@@ -20,7 +21,8 @@ from .. import orm
from .. import spawner as spawnermod
from ..spawner import LocalProcessSpawner, Spawner
from ..user import User
from ..utils import new_token
from ..utils import new_token, url_path_join
from .mocking import public_url
from .test_api import add_user
from .utils import async_requests
@@ -304,7 +306,7 @@ def test_spawner_reuse_api_token(db, app):
@pytest.mark.gen_test
def test_spawner_insert_api_token(app):
"""Token provided by spawner is not in the db
Insert token into db as a user-provided token.
"""
# setup: new user, double check that they don't have any tokens registered
@@ -379,3 +381,28 @@ def test_spawner_delete_server(app):
# verify that both ORM and top-level references are None
assert spawner.orm_spawner.server is None
assert spawner.server is None
@pytest.mark.parametrize(
"name",
[
"has@x",
"has~x",
"has%x",
"has%40x",
]
)
@pytest.mark.gen_test
def test_spawner_routing(app, name):
"""Test routing of names with special characters"""
db = app.db
with mock.patch.dict(app.config.LocalProcessSpawner, {'cmd': [sys.executable, '-m', 'jupyterhub.tests.mocksu']}):
user = add_user(app.db, app, name=name)
yield user.spawn()
yield wait_for_spawner(user.spawner)
yield app.proxy.add_user(user)
url = url_path_join(public_url(app, user), "test/url")
r = yield async_requests.get(url, allow_redirects=False)
r.raise_for_status()
assert r.url == url
assert r.text == urlparse(url).path

View File

@@ -282,7 +282,7 @@ class User:
@property
def escaped_name(self):
"""My name, escaped for use in URLs, cookies, etc."""
return quote(self.name, safe='@')
return quote(self.name, safe='@~')
@property
def proxy_spec(self):
@@ -295,15 +295,17 @@ class User:
@property
def domain(self):
"""Get the domain for my server."""
# FIXME: escaped_name probably isn't escaped enough in general for a domain fragment
return self.escaped_name + '.' + self.settings['domain']
# use underscore as escape char for domains
return quote(self.name).replace('%', '_').lower() + '.' + self.settings['domain']
@property
def host(self):
"""Get the *host* for my server (proto://domain[:port])"""
# FIXME: escaped_name probably isn't escaped enough in general for a domain fragment
parsed = urlparse(self.settings['subdomain_host'])
h = '%s://%s.%s' % (parsed.scheme, self.escaped_name, parsed.netloc)
h = '%s://%s' % (parsed.scheme, self.domain)
if parsed.port:
h += ':%i' % parsed.port
return h
@property

View File

@@ -19,7 +19,7 @@ import threading
import uuid
import warnings
from async_generator import aclosing, async_generator, yield_
from async_generator import aclosing, asynccontextmanager, async_generator, yield_
from tornado import gen, ioloop, web
from tornado.platform.asyncio import to_asyncio_future
from tornado.httpclient import AsyncHTTPClient, HTTPError
@@ -452,6 +452,21 @@ def maybe_future(obj):
return to_asyncio_future(gen.maybe_future(obj))
@asynccontextmanager
@async_generator
async def not_aclosing(coro):
"""An empty context manager for Python < 3.5.2
which lacks the `aclose` method on async iterators
"""
await yield_(await coro)
if sys.version_info < (3, 5, 2):
# Python 3.5.1 is missing the aclose method on async iterators,
# so we can't close them
aclosing = not_aclosing
@async_generator
async def iterate_until(deadline_future, generator):
"""An async generator that yields items from a generator

View File

@@ -1,19 +1,29 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
require(["jquery", "jhapi"], function ($, JHAPI) {
"use strict";
require(["jquery", "jhapi"], function($, JHAPI) {
"use strict";
var base_url = window.jhdata.base_url;
var user = window.jhdata.user;
var api = new JHAPI(base_url);
var base_url = window.jhdata.base_url;
var user = window.jhdata.user;
var api = new JHAPI(base_url);
$("#stop").click(function () {
api.stop_server(user, {
success: function () {
$("#stop").hide();
}
});
$("#stop").click(function() {
$("#start")
.attr("disabled", true)
.attr("title", "Your server is stopping")
.click(function() {
return false;
});
api.stop_server(user, {
success: function() {
$("#start")
.text("Start My Server")
.attr("title", "Start your server")
.attr("disabled", false)
.off("click");
$("#stop").hide();
}
});
});
});

View File

@@ -12,7 +12,7 @@ require(["jquery", "jhapi", "moment"], function($, JHAPI, moment) {
// convert ISO datestamps to nice momentjs ones
el = $(el);
let m = moment(new Date(el.text().trim()));
el.text(m.isValid() ? m.fromNow() : "Never");
el.text(m.isValid() ? m.fromNow() : el.text());
});
$("#request-token-form").submit(function() {

View File

@@ -1,4 +1,7 @@
{% extends "page.html" %}
{% if announcement_home %}
{% set announcement = announcement_home %}
{% endif %}
{% block main %}
@@ -8,8 +11,8 @@
{% if user.running %}
<a id="stop" role="button" class="btn btn-lg btn-danger">Stop My Server</a>
{% endif %}
<a id="start"role="button" class="btn btn-lg btn-primary" href="{{ url }}">
{% if not user.running %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ url }}">
{% if not user.active %}
Start
{% endif %}
My Server

View File

@@ -1,4 +1,7 @@
{% extends "page.html" %}
{% if announcement_login %}
{% set announcement = announcement_login %}
{% endif %}
{% block login_widget %}
{% endblock %}

View File

@@ -1,8 +1,14 @@
{% extends "page.html" %}
{% if announcement_logout %}
{% set announcement = announcement_logout %}
{% endif %}
{% block main %}
<div id="logout-main" class="container">
<p>
Successfully logged out.
</p>
</div>
{% endblock %}

View File

@@ -70,7 +70,7 @@
{% else %}
admin_access: false,
{% endif %}
{% if user and user.spawner.options_form %}
{% if not no_spawner_check and user and user.spawner.options_form %}
options_form: true,
{% else %}
options_form: false,
@@ -140,6 +140,16 @@
</nav>
{% endblock %}
{% block announcement %}
{% if announcement %}
<div class="container text-center announcement">
{{ announcement | safe }}
</div>
{% endif %}
{% endblock %}
{% block main %}
{% endblock %}

View File

@@ -1,4 +1,7 @@
{% extends "page.html" %}
{% if announcement_spawn %}
{% set announcement = announcement_spawn %}
{% endif %}
{% block main %}

View File

@@ -0,0 +1,32 @@
{% extends "page.html" %}
{% block main %}
<div class="container">
<div class="row">
<div class="text-center">
{% block message %}
<p>Your server is stopping.</p>
<p>You will be able to start it again once it has finished stopping.</p>
{% endblock message %}
<p><i class="fa fa-spinner fa-pulse fa-fw fa-3x" aria-hidden="true"></i></p>
<a role="button" id="refresh" class="btn btn-lg btn-primary" href="#">refresh</a>
</div>
</div>
</div>
{% endblock %}
{% block script %}
{{ super() }}
<script type="text/javascript">
require(["jquery"], function ($) {
$("#refresh").click(function () {
window.location.reload();
})
setTimeout(function () {
window.location.reload();
}, 5000);
});
</script>
{% endblock %}

View File

@@ -73,7 +73,11 @@
{%- endif -%}
</td>
<td class="time-col col-sm-3">
{%- if token.created -%}
{{ token.created.isoformat() + 'Z' }}
{%- else -%}
N/A
{%- endif -%}
</td>
<td class="col-sm-1 text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
@@ -118,7 +122,11 @@
{%- endif -%}
</td>
<td class="time-col col-sm-3">
{%- if client['created'] -%}
{{ client['created'].isoformat() + 'Z' }}
{%- else -%}
N/A
{%- endif -%}
</td>
<td class="col-sm-1 text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</a>