Compare commits

...

104 Commits

Author SHA1 Message Date
Min RK
6f15113e2a link and date for 0.9.0 2018-06-15 15:36:48 +02:00
Min RK
f3f08c9caa 0.9.0 2018-06-15 15:23:25 +02:00
Min RK
c495c4731a Merge pull request #1983 from willingc/test-tilde
add test case for user with tilde
2018-06-15 14:48:49 +02:00
Min RK
e08a50ef66 Merge pull request #1988 from gesiscss/redirects
fix AddSlashHandler for hub_prefix without trailing /
2018-06-15 14:48:14 +02:00
Min RK
fbcd792062 Merge pull request #1984 from chicocvenancio/tilde_safe_in_proxy
mark tilde as safe in proxy routespec quoting FIX:#1982
2018-06-15 14:38:38 +02:00
Min RK
bb81ce0160 also test @ handling in proxy.check_routes
@ and ~ should be the same
2018-06-15 14:33:31 +02:00
Kenan Erdogan
315087d67c fix AddSlashHandler for hub_prefix without trailing / 2018-06-15 13:36:05 +02:00
Chico Venancio
31e6a15a85 mark tilde as safe in proxy routespec quoting FIX:#1982 2018-06-14 18:18:52 -03:00
Carol Willing
aed99d8d19 add test case for user with tilde 2018-06-14 13:24:05 -07:00
Min RK
bedac5f148 Merge pull request #1980 from willingc/pypi-meta
Add info to display at pypi site
2018-06-14 11:51:29 +02:00
Carol Willing
376aa13981 correct link 2018-06-13 14:37:27 -07:00
Carol Willing
4bc8b48763 add info to display at pypi site 2018-06-13 14:35:23 -07:00
Carol Willing
21496890f6 Remove stray bullet that I missed in review 2018-06-13 11:10:41 -07:00
Carol Willing
70dcd50e44 Merge pull request #1976 from minrk/changelog-more
last few things in changelog
2018-06-13 11:09:30 -07:00
Min RK
24094567e5 Merge pull request #1977 from kpfleming/patch-1
Correct 'conda' installation instructions
2018-06-13 15:44:27 +02:00
Kevin P. Fleming
6bd0febbe1 Correct 'conda' installation instructions
JupyterHub packages are in the 'conda-forge' channel of Anaconda packages; if the Anaconda installation doesn't already have 'conda-forge' enabled, `conda install jupyterhub` fails.

Rather than adding instructions to enable 'conda-forge' in Anaconda, this patch modifies the installation command to specify that channel.
2018-06-13 09:42:05 -04:00
Min RK
57075aba52 Add last few entries in changelog for 0.9 2018-06-13 15:15:18 +02:00
Min RK
f0260aae52 add missing expiry fields in rest-api doc 2018-06-13 15:15:09 +02:00
Min RK
edd8e21f71 Merge pull request #1969 from willingc/edit-userenv
Edit and reflow user environment reference
2018-06-13 09:49:23 +02:00
Min RK
681d3ce2d8 Merge pull request #1971 from willingc/contributor-list
Update contributor list for 0.9 release
2018-06-13 09:45:37 +02:00
Carol Willing
97e792ccde Update issue templates 2018-06-12 15:47:05 -07:00
Carol Willing
a5a0543b2a Delete old issue template 2018-06-12 15:42:46 -07:00
Carol Willing
5a810ccba3 Update issue templates 2018-06-12 15:41:30 -07:00
Carol Willing
0a6b2cdadc Merge pull request #1973 from jupyterhub/willingc-patch-1
Create CODE_OF_CONDUCT.md
2018-06-12 15:33:37 -07:00
Carol Willing
08903e7af8 Create PULL_REQUEST_TEMPLATE.md 2018-06-12 15:29:54 -07:00
Carol Willing
78439329c0 Merge pull request #1972 from willingc/insights
Move issue template one level down in .github directory
2018-06-12 15:28:34 -07:00
Carol Willing
4dfd6bc4b9 Create CODE_OF_CONDUCT.md 2018-06-12 15:25:27 -07:00
Carol Willing
574cc39b5f set up pull request template directory 2018-06-12 15:16:02 -07:00
Carol Willing
6fb43a8241 update issue templaate location to current github recommendation 2018-06-12 15:13:39 -07:00
Carol Willing
84c82fe382 update the contributor list for 0.9 2018-06-12 14:51:51 -07:00
Carol Willing
5e45e76f5b update contributors for 0.9 2018-06-12 14:36:00 -07:00
Carol Willing
92fd819cd6 Merge pull request #1970 from JasonJWilliamsNY/hub-not-found-at-localhost
Hub not found at localhost
2018-06-12 14:04:09 -07:00
Jason Williams
cb5ef0c302 Update troubleshooting.md 2018-06-12 17:01:37 -04:00
Jason Williams
34fab033fe Jupyterhub on Docker add workaround for unable to connect to localhost
Added a command that worked for me to fix the situation that localhost:8000 is unable to reach the hub even though the published command for Docker exposes the correct port.
2018-06-12 16:59:17 -04:00
Carol Willing
37f4c4429e edit and reflow user environment reference 2018-06-12 08:47:22 -07:00
Carol Willing
293410ec94 Merge pull request #1967 from minrk/config-docs
docs: configuring user environments
2018-06-12 07:55:53 -07:00
Min RK
ed6ee27dcd docs: configuring user environments
covers system-wide installation, kernelspec registration, and the differences between containers and host systems
2018-06-12 14:34:26 +02:00
Min RK
ca16ddb7ad back to dev 2018-06-12 14:21:16 +02:00
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
49 changed files with 802 additions and 159 deletions

37
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,37 @@
---
name: Bug report
about: Create a report to help us improve
---
Hi! Thanks for using JupyterHub.
If you are reporting an issue with JupyterHub, please use the [GitHub issue](https://github.com/jupyterhub/jupyterhub/issues) search feature to check if your issue has been asked already. If it has, please add your comments to the existing issue.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
- Running `jupyter troubleshoot` from the command line, if possible, and posting
its output would also be helpful.
- Running in `--debug` mode can also be helpful for troubleshooting.

View File

@@ -0,0 +1,7 @@
---
name: Installation and configuration issues
about: Installation and configuration assistance
---
If you are having issues with installation or configuration, you may ask for help on the JupyterHub gitter channel or file an issue here.

0
.github/PULL_REQUEST_TEMPLATE/.keep vendored Normal file
View File

View File

@@ -1,29 +0,0 @@
Hi! Thanks for using JupyterHub.
If you are reporting an issue with JupyterHub:
- Please use the [GitHub issue](https://github.com/jupyterhub/jupyterhub/issues)
search feature to check if your issue has been asked already. If it has,
please add your comments to the existing issue.
- Where applicable, please fill out the details below to help us troubleshoot
the issue that you are facing. Please be as thorough as you are able to
provide details on the issue.
**How to reproduce the issue**
**What you expected to happen**
**What actually happens**
**Share what version of JupyterHub you are using**
Running `jupyter troubleshoot` from the command line, if possible, and posting
its output would also be helpful.
```
Insert jupyter troubleshoot output here
```

View File

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

1
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1 @@
Please refer to [Project Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md).

View File

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

1
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1 @@

View File

@@ -50,37 +50,62 @@ for administration of the Hub and its users.
## Installation ## Installation
### Check prerequisites ### 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 * If you are using **`conda`**, the nodejs and npm dependencies will be installed for
- [nodejs/npm](https://www.npmjs.com/) Install a recent version of you by conda.
[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 * 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 - TLS certificate and key for HTTPS communication
- Domain name - Domain name
### Install packages ### Install packages
#### Using `conda`
To install JupyterHub along with its dependencies including nodejs/npm:
```bash
conda install -c conda-forge 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`: JupyterHub can be installed with `pip`, and the proxy with `npm`:
```bash ```bash
npm install -g configurable-http-proxy 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 If you plan to run notebook servers locally, you will need to install the
[Jupyter notebook](https://jupyter.readthedocs.io/en/latest/install.html) [Jupyter notebook](https://jupyter.readthedocs.io/en/latest/install.html)
package: package:
pip3 install --upgrade notebook python3 -m pip install --upgrade notebook
### Run the Hub server ### Run the Hub server

View File

@@ -252,6 +252,17 @@ paths:
$ref: '#/definitions/Token' $ref: '#/definitions/Token'
post: post:
summary: Create a new token for the user summary: Create a new token for the user
parameters:
- name: expires_in
type: number
required: false
in: body
description: lifetime (in seconds) after which the requested token will expire.
- name: note
type: string
required: false
in: body
description: A note attached to the token for future bookkeeping
responses: responses:
'201': '201':
description: The newly created token description: The newly created token
@@ -689,6 +700,11 @@ definitions:
description: The command used to start the service (if managed) description: The command used to start the service (if managed)
items: items:
type: string type: string
info:
type: object
description: |
Additional information a deployment can attach to a service.
JupyterHub does not use this field.
Token: Token:
type: object type: object
properties: properties:
@@ -711,6 +727,10 @@ definitions:
type: string type: string
format: date-time format: date-time
description: Timestamp when this token was created description: Timestamp when this token was created
expires_at:
type: string
format: date-time
description: Timestamp when this token expires. Null if there is no expiry.
last_activity: last_activity:
type: string type: string
format: date-time format: date-time

View File

@@ -9,7 +9,7 @@ command line for details.
## 0.9 ## 0.9
### 0.9.0 ### [0.9.0] 2018-06-15
JupyterHub 0.9 is a major upgrade of JupyterHub. JupyterHub 0.9 is a major upgrade of JupyterHub.
There are several changes to the database schema, There are several changes to the database schema,
@@ -93,6 +93,12 @@ and tornado < 5.0.
- Add session-id cookie, enabling immediate revocation of login tokens. - Add session-id cookie, enabling immediate revocation of login tokens.
- Authenticators may specify that users are admins by specifying the `admin` key when return the user model as a dict. - Authenticators may specify that users are admins by specifying the `admin` key when return the user model as a dict.
- Added "Start All" button to admin page for launching all user servers at once. - Added "Start All" button to admin page for launching all user servers at once.
- Services have an `info` field which is a dictionary.
This is accessible via the REST API.
- `JupyterHub.extra_handlers` allows defining additonal tornado RequestHandlers attached to the Hub.
- API tokens may now expire.
Expiry is available in the REST model as `expires_at`,
and settable when creating API tokens by specifying `expires_in`.
#### Fixed #### Fixed
@@ -113,6 +119,11 @@ and tornado < 5.0.
- Various fixes in race conditions and performance improvements with the default proxy. - Various fixes in race conditions and performance improvements with the default proxy.
- Fixes for CORS headers - Fixes for CORS headers
- Stop setting `.form-control` on spawner form inputs unconditionally. - Stop setting `.form-control` on spawner form inputs unconditionally.
- Better recovery from database errors and database connection issues
without having to restart the Hub.
- Fix handling of `~` character in usernames.
- Fix jupyterhub startup when `getpass.getuser()` would fail,
e.g. due to missing entry in passwd file in containers.
## 0.8 ## 0.8
@@ -368,7 +379,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/0.8.1...HEAD [Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.9.0...HEAD
[0.9.0]: https://github.com/jupyterhub/jupyterhub/compare/0.8.1...0.9.0
[0.8.1]: https://github.com/jupyterhub/jupyterhub/compare/0.8.0...0.8.1 [0.8.1]: https://github.com/jupyterhub/jupyterhub/compare/0.8.0...0.8.1
[0.8.0]: https://github.com/jupyterhub/jupyterhub/compare/0.7.2...0.8.0 [0.8.0]: https://github.com/jupyterhub/jupyterhub/compare/0.7.2...0.8.0
[0.7.2]: https://github.com/jupyterhub/jupyterhub/compare/0.7.1...0.7.2 [0.7.2]: https://github.com/jupyterhub/jupyterhub/compare/0.7.1...0.7.2

View File

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

View File

@@ -3,38 +3,65 @@
Project Jupyter thanks the following people for their help and Project Jupyter thanks the following people for their help and
contribution on JupyterHub: contribution on JupyterHub:
- adelcast
- Analect - Analect
- anderbubble - anderbubble
- anikitml
- ankitksharma
- apetresc - apetresc
- athornton
- barrachri - barrachri
- BerserkerTroll
- betatim - betatim
- Carreau - Carreau
- cfournie
- charnpreetsingh - charnpreetsingh
- chicovenancio
- cikao
- ckald - ckald
- cmoscardi
- consideRatio
- cqzlxl
- CRegenschein - CRegenschein
- cwaldbieser - cwaldbieser
- danielballen - danielballen
- danoventa - danoventa
- daradib - daradib
- darky2004
- datapolitan - datapolitan
- dblockow-d2dcrc - dblockow-d2dcrc
- DeepHorizons - DeepHorizons
- DerekHeldtWerle
- dhirschfeld - dhirschfeld
- dietmarw - dietmarw
- dingc3
- dmartzol - dmartzol
- DominicFollettSmith - DominicFollettSmith
- dsblank - dsblank
- dtaniwaki
- echarles
- ellisonbg - ellisonbg
- emmanuel
- evanlinde - evanlinde
- Fokko - Fokko
- fperez - fperez
- franga2000
- GladysNalvarte
- glenak1911
- gweis
- iamed18 - iamed18
- jamescurtin
- JamiesHQ - JamiesHQ
- JasonJWilliamsNY
- jbweston - jbweston
- jdavidheiser - jdavidheiser
- jencabral - jencabral
- jhamrick - jhamrick
- jkinkead
- johnkpark
- josephtate - josephtate
- jzf2101
- karfai
- kinuax - kinuax
- KrishnaPG - KrishnaPG
- kroq-gar78 - kroq-gar78
@@ -44,27 +71,44 @@ contribution on JupyterHub:
- minrk - minrk
- mistercrunch - mistercrunch
- Mistobaan - Mistobaan
- mpacer
- mwmarkland - mwmarkland
- ndly
- nthiery - nthiery
- nxg
- ObiWahn - ObiWahn
- ozancaglayan - ozancaglayan
- paccorsi
- parente - parente
- PeterDaveHello - PeterDaveHello
- peterruppel - peterruppel
- phill84
- pjamason - pjamason
- prasadkatti - prasadkatti
- rafael-ladislau - rafael-ladislau
- rcthomas
- rgbkrk - rgbkrk
- rkdarst
- robnagler - robnagler
- rschroll
- ryanlovett - ryanlovett
- sangramga
- Scrypy - Scrypy
- schon
- shreddd - shreddd
- Siecje
- smiller5678
- spoorthyv - spoorthyv
- ssanderson - ssanderson
- summerswallow
- syutbai
- takluyver - takluyver
- temogen - temogen
- ThomasMChen - ThomasMChen
- Thoralf Gutierrez
- timfreund
- TimShawver - TimShawver
- tklever
- Todd-Z-Li - Todd-Z-Li
- toobaz - toobaz
- tsaeger - tsaeger

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 most deployments of JupyterHub. However, more customized scenarios may need
additional networking details to be configured. 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) ## Set the Proxy's REST API communication URL (optional)
By default, this REST API listens on port 8081 of `localhost` only. 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_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. 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 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/>`_, In certain cases, for example if the hub is running behind a reverse proxy, and
allowing no SSL running on the hub may be the desired configuration option. `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: .. _cookie-secret:

View File

@@ -5,20 +5,27 @@
Before installing JupyterHub, you will need: Before installing JupyterHub, you will need:
- a Linux/Unix based system - 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 of using [`pip`](https://pip.pypa.io/en/stable/) or
[`conda`](https://conda.io/docs/get-started.html) for [`conda`](https://conda.io/docs/get-started.html) for
installing Python packages is helpful. installing Python packages is helpful.
- [nodejs/npm](https://www.npmjs.com/). [Install nodejs/npm](https://docs.npmjs.com/getting-started/installing-node), - [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 using your operating system's package manager.
Debian/Ubuntu using:
```bash * If you are using **`conda`**, the nodejs and npm dependencies will be installed for
sudo apt-get install npm nodejs-legacy 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 - TLS certificate and key for HTTPS communication
- Domain name - Domain name

View File

@@ -38,6 +38,8 @@ with any provider, is also available.
- ldapauthenticator for LDAP - ldapauthenticator for LDAP
- tmpauthenticator for temporary accounts - tmpauthenticator for temporary accounts
- For Shibboleth, [jhub_shibboleth_auth](https://github.com/gesiscss/jhub_shibboleth_auth)
and [jhub_remote_user_authenticator](https://github.com/cwaldbieser/jhub_remote_user_authenticator)
## Technical Overview of Authentication ## Technical Overview of Authentication
@@ -206,7 +208,13 @@ class MyAuthenticator(Authenticator):
spawner.environment['UPSTREAM_TOKEN'] = auth_state['upstream_token'] 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 ## JupyterHub as an OAuth provider

View File

@@ -30,8 +30,10 @@ openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
## nginx ## nginx
The **`nginx` server config file** is fairly standard fare except for the two This **`nginx` config file** is fairly standard fare except for the two
`location` blocks within the `HUB.DOMAIN.TLD` config file: `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 ```bash
# top-level http config for websocket headers # top-level http config for websocket headers

View File

@@ -0,0 +1,147 @@
# Configuring user environments
Deploying JupyterHub means you are providing Jupyter notebook environments for
multiple users. Often, this includes a desire to configure the user
environment in some way.
Since the `jupyterhub-singleuser` server extends the standard Jupyter notebook
server, most configuration and documentation that applies to Jupyter Notebook
applies to the single-user environments. Configuration of user environments
typically does not occur through JupyterHub itself, but rather through system-
wide configuration of Jupyter, which is inherited by `jupyterhub-singleuser`.
**Tip:** When searching for configuration tips for JupyterHub user
environments, try removing JupyterHub from your search because there are a lot
more people out there configuring Jupyter than JupyterHub and the
configuration is the same.
This section will focus on user environments, including:
- Installing packages
- Configuring Jupyter and IPython
- Installing kernelspecs
- Using containers vs. multi-user hosts
## Installing packages
To make packages available to users, you generally will install packages
system-wide or in a shared environment.
This installation location should always be in the same environment that
`jupyterhub-singleuser` itself is installed in, and must be *readable and
executable* by your users. If you want users to be able to install additional
packages, it must also be *writable* by your users.
If you are using a standard system Python install, you would use:
```bash
sudo python3 -m pip install numpy
```
to install the numpy package in the default system Python 3 environment
(typically `/usr/local`).
You may also use conda to install packages. If you do, you should make sure
that the conda environment has appropriate permissions for users to be able to
run Python code in the env.
## Configuring Jupyter and IPython
[Jupyter](https://jupyter-notebook.readthedocs.io/en/stable/config_overview.html)
and [IPython](https://ipython.readthedocs.io/en/stable/development/config.html)
have their own configuration systems.
As a JupyterHub administrator, you will typically want to install and configure
environments for all JupyterHub users. For example, you wish for each student in
a class to have the same user environment configuration.
Jupyter and IPython support **"system-wide"** locations for configuration, which
is the logical place to put global configuration that you want to affect all
users. It's generally more efficient to configure user environments "system-wide",
and it's a good idea to avoid creating files in users' home directories.
The typical locations for these config files are:
- **system-wide** in `/etc/{jupyter|ipython}`
- **env-wide** (environment wide) in `{sys.prefix}/etc/{jupyter|ipython}`.
### Example: Enable an extension system-wide
For example, to enable the `cython` IPython extension for all of your users,
create the file `/etc/ipython/ipython_config.py`:
```python
c.InteractiveShellApp.extensions.append("cython")
```
### Example: Enable a Jupyter notebook configuration setting for all users
To enable Jupyter notebook's internal idle-shutdown behavior (requires
notebook ≥ 5.4), set the following in the `/etc/jupyter/jupyter_notebook_config.py`
file:
```python
# shutdown the server after no activity for an hour
c.NotebookApp.shutdown_no_activity_timeout = 60 * 60
# shutdown kernels after no activity for 20 minutes
c.MappingKernelManager.cull_idle_timeout = 20 * 60
# check for idle kernels every two minutes
c.MappingKernelManager.cull_interval = 2 * 60
```
## Installing kernelspecs
You may have multiple Jupyter kernels installed and want to make sure that
they are available to all of your users. This means installing kernelspecs
either system-wide (e.g. in /usr/local/) or in the `sys.prefix` of JupyterHub
itself.
Jupyter kernelspec installation is system wide by default, but some kernels
may default to installing kernelspecs in your home directory. These will need
to be moved system-wide to ensure that they are accessible.
You can see where your kernelspecs are with:
```bash
jupyter kernelspec list
```
### Example: Installing kernels system-wide
Assuming I have a Python 2 and Python 3 environment that I want to make
sure are available, I can install their specs system-wide (in /usr/local) with:
```bash
/path/to/python3 -m IPython kernel install --prefix=/usr/local
/path/to/python2 -m IPython kernel install --prefix=/usr/local
```
## Multi-user hosts vs. Containers
There are two broad categories of user environments that depend on what
Spawner you choose:
- Multi-user hosts (shared sytem)
- Container-based
How you configure user environments for each category can differ a bit
depending on what Spawner you are using.
The first category is a **shared system (multi-user host)** where
each user has a JupyterHub account and a home directory as well as being
a real system user. In this example, shared configuration and installation
must be in a 'system-wide' location, such as `/etc/` or `/usr/local`
or a custom prefix such as `/opt/conda`.
When JupyterHub uses **container-based** Spawners (e.g. KubeSpawner or
DockerSpawner), the 'system-wide' environment is really the container image
which you are using for users.
In both cases, you want to *avoid putting configuration in user home
directories* because users can change those configuration settings. Also,
home directories typically persist once they are created, so they are
difficult for admins to update later.

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,8 +11,10 @@ Technical Reference
services services
proxy proxy
rest rest
database
upgrading upgrading
templates templates
config-user-env
config-examples config-examples
config-ghoauth config-ghoauth
config-proxy config-proxy

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> <p>Patience is a virtue.</p>
{% endblock %} {% 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 From time to time, you may wish to upgrade JupyterHub to take advantage
of new releases. Much of this process is automated using scripts, of new releases. Much of this process is automated using scripts,
such as those generated by alembic for database upgrades. Before upgrading a such as those generated by alembic for database upgrades. Whether you
JupyterHub deployment, it's critical to backup your data and configurations are using the default SQLite database or an RDBMS, such as PostgreSQL or
before shutting down the JupyterHub process and server. 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. ## Note about upgrading the SQLite database
We have chosen SQLite as JupyterHub's default for its lightweight simplicity
in certain uses such as testing, small deployments and workshops.
When running a long term deployment or a production system, we recommend using When used in production systems, SQLite has some disadvantages when it
a traditional RDBMS database, such as [PostgreSQL](https://www.postgresql.org) comes to upgrading JupyterHub. These are:
or [MySQL](https://www.mysql.com), that supports the SQL `ALTER TABLE`
statement.
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 - `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 - `downgrade-db` **will not** work if you want to rollback to an earlier
version, so backup the `jupyterhub.sqlite` file before upgrading 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 ## The upgrade process
Five fundamental process steps are needed when upgrading JupyterHub and its Five fundamental process steps are needed when upgrading JupyterHub and its

View File

@@ -9,6 +9,7 @@ problem and how to resolve it.
- sudospawner fails to run - sudospawner fails to run
- What is the default behavior when none of the lists (admin, whitelist, - What is the default behavior when none of the lists (admin, whitelist,
group whitelist) are set? group whitelist) are set?
- JupyterHub Docker container not accessible at localhost
[*Errors*](#errors) [*Errors*](#errors)
- 500 error after spawning my single-user server - 500 error after spawning my single-user server
@@ -63,6 +64,17 @@ this to a particular set of users, and the admin_users lets you specify who
among them may use the admin interface (not necessary, unless you need to do among them may use the admin interface (not necessary, unless you need to do
things like inspect other users' servers, or modify the userlist at runtime). things like inspect other users' servers, or modify the userlist at runtime).
### JupyterHub Docker container not accessible at localhost
Even though the command to start your Docker container exposes port 8000
(`docker run -p 8000:8000 -d --name jupyterhub jupyterhub/jupyterhub jupyterhub`),
it is possible that the IP address itself is not accessible/visible. As a result
when you try http://localhost:8000 in your browser, you are unable to connect
even though the container is running properly. One workaround is to explicitly
tell Jupyterhub to start at `0.0.0.0` which is visible to everyone. Try this
command:
`docker run -p 8000:8000 -d --name jupyterhub jupyterhub/jupyterhub jupyterhub --ip 0.0.0.0 --port 8000`
## Errors ## Errors

View File

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

View File

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

View File

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

View File

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

View File

@@ -140,7 +140,10 @@ class NewToken(Application):
ab01cd23ef45 ab01cd23ef45
""" """
name = Unicode(getuser()) name = Unicode()
@default('name')
def _default_name(self):
return getuser()
aliases = token_aliases aliases = token_aliases
classes = [] classes = []
@@ -274,6 +277,9 @@ class JupyterHub(Application):
service_check_interval = Integer(60, service_check_interval = Integer(60,
help="Interval (in seconds) at which to check connectivity of services with web endpoints." help="Interval (in seconds) at which to check connectivity of services with web endpoints."
).tag(config=True) ).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, data_files_path = Unicode(DATA_FILES_PATH,
help="The location of jupyterhub data files (e.g. /usr/local/share/jupyterhub)" help="The location of jupyterhub data files (e.g. /usr/local/share/jupyterhub)"
@@ -970,8 +976,6 @@ class JupyterHub(Application):
self.handlers = self.add_url_prefix(self.hub_prefix, h) self.handlers = self.add_url_prefix(self.hub_prefix, h)
# some extra handlers, outside hub_prefix # some extra handlers, outside hub_prefix
self.handlers.extend([ self.handlers.extend([
# add trailing / to `/hub`
(self.hub_prefix.rstrip('/'), handlers.AddSlashHandler),
# add trailing / to ``/user|services/:name` # add trailing / to ``/user|services/:name`
(r"%s(user|services)/([^/]+)" % self.base_url, handlers.AddSlashHandler), (r"%s(user|services)/([^/]+)" % self.base_url, handlers.AddSlashHandler),
(r"(?!%s).*" % self.hub_prefix, handlers.PrefixRedirectHandler), (r"(?!%s).*" % self.hub_prefix, handlers.PrefixRedirectHandler),
@@ -1786,8 +1790,7 @@ class JupyterHub(Application):
spawner.last_activity = max(spawner.last_activity, dt) spawner.last_activity = max(spawner.last_activity, dt)
else: else:
spawner.last_activity = dt spawner.last_activity = dt
# FIXME: Make this configurable duration. 30 minutes for now! if (now - user.last_activity).total_seconds() < self.active_user_window:
if (now - user.last_activity).total_seconds() < 30 * 60:
active_users_count += 1 active_users_count += 1
self.statsd.gauge('users.running', users_count) self.statsd.gauge('users.running', users_count)
self.statsd.gauge('users.active', active_users_count) self.statsd.gauge('users.active', active_users_count)

View File

@@ -40,7 +40,8 @@ reasons = {
'timeout': "Failed to reach your server." 'timeout': "Failed to reach your server."
" Please try again later." " Please try again later."
" Contact admin if the issue persists.", " 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 # constant, not configurable
@@ -826,19 +827,27 @@ class BaseHandler(RequestHandler):
) )
self.set_header('Content-Type', 'text/html') self.set_header('Content-Type', 'text/html')
# allow setting headers from exceptions if isinstance(exception, web.HTTPError):
# since exception handler clears headers # allow setting headers from exceptions
headers = getattr(exception, 'headers', None) # since exception handler clears headers
if headers: headers = getattr(exception, 'headers', None)
for key, value in headers.items(): if headers:
self.set_header(key, value) for key, value in headers.items():
self.set_header(key, value)
# Content-Length must be recalculated.
self.clear_header('Content-Length')
# render the template # render the template
try: try:
html = self.render_template('%s.html' % status_code, **ns) html = self.render_template('%s.html' % status_code, **ns)
except TemplateNotFound: except TemplateNotFound:
self.log.debug("No template for %d", status_code) 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) self.write(html)
@@ -941,7 +950,7 @@ class UserSpawnHandler(BaseHandler):
raise copy.copy(exc).with_traceback(exc.__traceback__) raise copy.copy(exc).with_traceback(exc.__traceback__)
# check for pending spawn # check for pending spawn
if spawner.pending and spawner._spawn_future: if spawner.pending == 'spawn' and spawner._spawn_future:
# wait on the pending spawn # wait on the pending spawn
self.log.debug("Waiting for %s pending %s", spawner._log_name, spawner.pending) self.log.debug("Waiting for %s pending %s", spawner._log_name, spawner.pending)
try: try:
@@ -951,14 +960,20 @@ class UserSpawnHandler(BaseHandler):
pass pass
# we may have waited above, check pending again: # we may have waited above, check pending again:
# page could be pending spawn *or* stop
if spawner.pending: if spawner.pending:
self.log.info("%s is pending %s", spawner._log_name, spawner.pending) self.log.info("%s is pending %s", spawner._log_name, spawner.pending)
# spawn has started, but not finished # spawn has started, but not finished
self.statsd.incr('redirects.user_spawn_pending', 1) self.statsd.incr('redirects.user_spawn_pending', 1)
url_parts = [] url_parts = []
if spawner.pending == "stop":
page = "stop_pending.html"
else:
page = "spawn_pending.html"
html = self.render_template( html = self.render_template(
"spawn_pending.html", page,
user=user, user=user,
spawner=spawner,
progress_url=spawner._progress_url, progress_url=spawner._progress_url,
) )
self.finish(html) self.finish(html)
@@ -1104,6 +1119,7 @@ class AddSlashHandler(BaseHandler):
self.redirect(urlunparse(dest)) self.redirect(urlunparse(dest))
default_handlers = [ default_handlers = [
(r'', AddSlashHandler), # add trailing / to `/hub`
(r'/user/([^/]+)(/.*)?', UserSpawnHandler), (r'/user/([^/]+)(/.*)?', UserSpawnHandler),
(r'/user-redirect/(.*)?', UserRedirectHandler), (r'/user-redirect/(.*)?', UserRedirectHandler),
(r'/security/csp-report', CSPReportHandler), (r'/security/csp-report', CSPReportHandler),

View File

@@ -336,7 +336,7 @@ class ProxyErrorHandler(BaseHandler):
default_handlers = [ default_handlers = [
(r'/?', RootHandler), (r'/', RootHandler),
(r'/home', HomeHandler), (r'/home', HomeHandler),
(r'/admin', AdminHandler), (r'/admin', AdminHandler),
(r'/spawn', SpawnHandler), (r'/spawn', SpawnHandler),

View File

@@ -488,7 +488,10 @@ class ConfigurableHTTPProxy(Proxy):
except FileNotFoundError as e: except FileNotFoundError as e:
self.log.error( self.log.error(
"Failed to find proxy %r\n" "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 % self.command
) )
raise raise
@@ -516,13 +519,26 @@ class ConfigurableHTTPProxy(Proxy):
self._check_running_callback = pc self._check_running_callback = pc
pc.start() 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): def stop(self):
self.log.info("Cleaning up proxy[%i]...", self.proxy_process.pid) self.log.info("Cleaning up proxy[%i]...", self.proxy_process.pid)
if self._check_running_callback is not None: if self._check_running_callback is not None:
self._check_running_callback.stop() self._check_running_callback.stop()
if self.proxy_process.poll() is None: if self.proxy_process.poll() is None:
try: 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: except Exception as e:
self.log.error("Failed to terminate proxy process: %s", e) self.log.error("Failed to terminate proxy process: %s", e)
@@ -558,7 +574,7 @@ class ConfigurableHTTPProxy(Proxy):
""" """
# chp stores routes in unescaped form. # chp stores routes in unescaped form.
# restore escaped-form we created it with. # restore escaped-form we created it with.
routespec = quote(chp_path, safe='@/') routespec = quote(chp_path, safe='@/~')
if self.host_routing: if self.host_routing:
# host routes don't start with / # host routes don't start with /
routespec = routespec.lstrip('/') routespec = routespec.lstrip('/')

View File

@@ -175,6 +175,13 @@ class Service(LoggingConfigurable):
If unspecified, an API token will be generated for managed services. If unspecified, an API token will be generated for managed services.
""" """
).tag(input=True) ).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: # Managed service API:
spawner = Any() spawner = Any()

View File

@@ -1525,6 +1525,7 @@ def test_get_services(app, mockservice_url):
'pid': mockservice.proc.pid, 'pid': mockservice.proc.pid,
'prefix': mockservice.server.base_url, 'prefix': mockservice.server.base_url,
'url': mockservice.url, 'url': mockservice.url,
'info': {},
} }
} }
@@ -1551,6 +1552,7 @@ def test_get_service(app, mockservice_url):
'pid': mockservice.proc.pid, 'pid': mockservice.proc.pid,
'prefix': mockservice.server.base_url, 'prefix': mockservice.server.base_url,
'url': mockservice.url, 'url': mockservice.url,
'info': {},
} }
r = yield api_request(app, 'services/%s' % mockservice.name, 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 .test_api import api_request, add_user
from .utils import async_requests from .utils import async_requests
def get_page(path, app, hub=True, **kw): def get_page(path, app, hub=True, **kw):
if hub: if hub:
prefix = app.hub.base_url prefix = app.hub.base_url
@@ -517,3 +518,55 @@ def test_oauth_token_page(app):
def test_proxy_error(app, error_status): def test_proxy_error(app, error_status):
r = yield get_page('/error/%i' % error_status, app) r = yield get_page('/error/%i' % error_status, app)
assert r.status_code == 200 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

@@ -154,6 +154,8 @@ def test_external_proxy(request):
'zoe', 'zoe',
'50fia', '50fia',
'秀樹', '秀樹',
'~TestJH',
'has@',
]) ])
def test_check_routes(app, username, disable_check_routes): def test_check_routes(app, username, disable_check_routes):
proxy = app.proxy proxy = app.proxy

View File

@@ -11,6 +11,7 @@ import sys
import tempfile import tempfile
import time import time
from unittest import mock from unittest import mock
from urllib.parse import urlparse
import pytest import pytest
from tornado import gen from tornado import gen
@@ -20,7 +21,8 @@ from .. import orm
from .. import spawner as spawnermod from .. import spawner as spawnermod
from ..spawner import LocalProcessSpawner, Spawner from ..spawner import LocalProcessSpawner, Spawner
from ..user import User 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 .test_api import add_user
from .utils import async_requests from .utils import async_requests
@@ -304,7 +306,7 @@ def test_spawner_reuse_api_token(db, app):
@pytest.mark.gen_test @pytest.mark.gen_test
def test_spawner_insert_api_token(app): def test_spawner_insert_api_token(app):
"""Token provided by spawner is not in the db """Token provided by spawner is not in the db
Insert token into db as a user-provided token. Insert token into db as a user-provided token.
""" """
# setup: new user, double check that they don't have any tokens registered # 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 # verify that both ORM and top-level references are None
assert spawner.orm_spawner.server is None assert spawner.orm_spawner.server is None
assert 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 @property
def escaped_name(self): def escaped_name(self):
"""My name, escaped for use in URLs, cookies, etc.""" """My name, escaped for use in URLs, cookies, etc."""
return quote(self.name, safe='@') return quote(self.name, safe='@~')
@property @property
def proxy_spec(self): def proxy_spec(self):
@@ -295,15 +295,17 @@ class User:
@property @property
def domain(self): def domain(self):
"""Get the domain for my server.""" """Get the domain for my server."""
# FIXME: escaped_name probably isn't escaped enough in general for a domain fragment # use underscore as escape char for domains
return self.escaped_name + '.' + self.settings['domain'] return quote(self.name).replace('%', '_').lower() + '.' + self.settings['domain']
@property @property
def host(self): def host(self):
"""Get the *host* for my server (proto://domain[:port])""" """Get the *host* for my server (proto://domain[:port])"""
# FIXME: escaped_name probably isn't escaped enough in general for a domain fragment # FIXME: escaped_name probably isn't escaped enough in general for a domain fragment
parsed = urlparse(self.settings['subdomain_host']) 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 return h
@property @property

View File

@@ -19,7 +19,7 @@ import threading
import uuid import uuid
import warnings 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 import gen, ioloop, web
from tornado.platform.asyncio import to_asyncio_future from tornado.platform.asyncio import to_asyncio_future
from tornado.httpclient import AsyncHTTPClient, HTTPError from tornado.httpclient import AsyncHTTPClient, HTTPError
@@ -452,6 +452,21 @@ def maybe_future(obj):
return to_asyncio_future(gen.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_generator
async def iterate_until(deadline_future, generator): async def iterate_until(deadline_future, generator):
"""An async generator that yields items from a generator """An async generator that yields items from a generator

View File

@@ -83,6 +83,10 @@ for d, _, _ in os.walk('jupyterhub'):
if os.path.exists(pjoin(d, '__init__.py')): if os.path.exists(pjoin(d, '__init__.py')):
packages.append(d.replace(os.path.sep, '.')) packages.append(d.replace(os.path.sep, '.'))
with open('README.md', encoding="utf8") as f:
readme = f.read()
setup_args = dict( setup_args = dict(
name = 'jupyterhub', name = 'jupyterhub',
scripts = glob(pjoin('scripts', '*')), scripts = glob(pjoin('scripts', '*')),
@@ -93,10 +97,11 @@ setup_args = dict(
package_data = get_package_data(), package_data = get_package_data(),
version = ns['__version__'], version = ns['__version__'],
description = "JupyterHub: A multi-user server for Jupyter notebooks", description = "JupyterHub: A multi-user server for Jupyter notebooks",
long_description = "See https://jupyterhub.readthedocs.io for more info.", long_description = readme,
long_description_content_type = 'text/markdown',
author = "Jupyter Development Team", author = "Jupyter Development Team",
author_email = "jupyter@googlegroups.com", author_email = "jupyter@googlegroups.com",
url = "http://jupyter.org", url = "https://jupyter.org",
license = "BSD", license = "BSD",
platforms = "Linux, Mac OS X", platforms = "Linux, Mac OS X",
keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'], keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
@@ -109,6 +114,12 @@ setup_args = dict(
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
], ],
project_urls = {
'Documentation': 'https://jupyterhub.readthedocs.io',
'Funding': 'https://jupyter.org/about',
'Source': 'https://github.com/jupyterhub/jupyterhub/',
'Tracker': 'https://github.com/jupyterhub/jupyterhub/issues',
},
) )
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------

View File

@@ -1,19 +1,29 @@
// Copyright (c) Jupyter Development Team. // Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License. // Distributed under the terms of the Modified BSD License.
require(["jquery", "jhapi"], function ($, JHAPI) { require(["jquery", "jhapi"], function($, JHAPI) {
"use strict"; "use strict";
var base_url = window.jhdata.base_url; var base_url = window.jhdata.base_url;
var user = window.jhdata.user; var user = window.jhdata.user;
var api = new JHAPI(base_url); var api = new JHAPI(base_url);
$("#stop").click(function () { $("#stop").click(function() {
api.stop_server(user, { $("#start")
success: function () { .attr("disabled", true)
$("#stop").hide(); .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 // convert ISO datestamps to nice momentjs ones
el = $(el); el = $(el);
let m = moment(new Date(el.text().trim())); 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() { $("#request-token-form").submit(function() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,7 @@
{% extends "page.html" %} {% extends "page.html" %}
{% if announcement_spawn %}
{% set announcement = announcement_spawn %}
{% endif %}
{% block main %} {% 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 -%} {%- endif -%}
</td> </td>
<td class="time-col col-sm-3"> <td class="time-col col-sm-3">
{%- if token.created -%}
{{ token.created.isoformat() + 'Z' }} {{ token.created.isoformat() + 'Z' }}
{%- else -%}
N/A
{%- endif -%}
</td> </td>
<td class="col-sm-1 text-center"> <td class="col-sm-1 text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button> <button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
@@ -118,7 +122,11 @@
{%- endif -%} {%- endif -%}
</td> </td>
<td class="time-col col-sm-3"> <td class="time-col col-sm-3">
{%- if client['created'] -%}
{{ client['created'].isoformat() + 'Z' }} {{ client['created'].isoformat() + 'Z' }}
{%- else -%}
N/A
{%- endif -%}
</td> </td>
<td class="col-sm-1 text-center"> <td class="col-sm-1 text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</a> <button class="revoke-token-btn btn btn-xs btn-danger">revoke</a>