Compare commits

...

103 Commits

Author SHA1 Message Date
Min RK
935baa8bc6 Merge pull request #2080 from minrk/rel-0.9.2
prepare to release 0.9.2
2018-08-11 14:50:15 +02:00
Min RK
9b77732319 Merge pull request #2078 from minrk/fix-pin-attrs
move attrs pinning to dev-requirements
2018-08-10 13:59:09 +02:00
Min RK
85aac0fa2d prepare to release 0.9.2 2018-08-10 13:56:58 +02:00
Min RK
abd6f35638 Merge pull request #2067 from NERSC/announcement-service-example
Add an example simple announcement service
2018-08-10 12:25:24 +02:00
Min RK
ba4700b3f3 move attrs pinning to dev-requirements
it shouldn’t be in the package’s own requirements, which are propagated to users
2018-08-10 11:51:24 +02:00
Min RK
05b11bd47a Merge pull request #2072 from gesiscss/master
fix links in services doc
2018-08-10 11:40:58 +02:00
Kenan Erdogan
71cb628563 fix links in services doc 2018-08-06 11:11:14 +02:00
Rollin Thomas
0d664355f0 Some explanatory comments 2018-08-03 12:15:50 -07:00
R. C. Thomas
dd6261d031 Merge pull request #1 from NERSC/test-announcement-service-example
Use `hub_users=[]` and `allow_admin=True`
2018-08-02 09:55:23 -07:00
Rollin Thomas
f3f5b69e49 Try hub_users=[] and allow_admin=True 2018-08-02 09:00:46 -07:00
Tim Head
9ea4ca3646 Merge pull request #2065 from minrk/cull-named-servers
cull-idle: fix deletion of named servers
2018-08-02 07:55:27 +01:00
Rollin Thomas
8ee9869ca0 Add an example simple announcement service 2018-08-01 16:11:30 -07:00
Min RK
6cedd73d2a Merge pull request #2062 from chaoleili/master
Ensure request uri with trailing slash
2018-08-01 10:17:58 +02:00
Min RK
59145ca0f7 fix deletion of named servers
first submitted to zero-to-jupyterhub
2018-08-01 10:07:02 +02:00
Chaolei Li
ab02f9c568 Ensure request uri with trailing slash
When request uri matching with base_url in PrefixRedirectHandler,
it's better to ensure uri with tariling slash. That's will avoid
redirecting /foobar to /foobar/hub/foobar.
2018-07-27 17:17:26 +08:00
Min RK
a2f003ed31 Merge pull request #2060 from betatim/docs-env-update
Update dependencies used by ReadTheDocs
2018-07-26 15:35:59 +02:00
Tim Head
7b6dd9f5cf Update dependencies used by ReadTheDocs 2018-07-26 12:53:19 +02:00
Min RK
0fa5c20f89 Merge pull request #2042 from minrk/abort-failures
add Spawner.consecutive_failure_limit
2018-07-26 10:33:36 +02:00
Min RK
204399ee2c Merge pull request #2040 from minrk/sigterm-fix
fix SIGTERM handling
2018-07-26 10:32:25 +02:00
Min RK
5e68dce02f Merge pull request #2057 from adelcast/dev/adelcast/fix_pid_removal
proxy: make process existance check Windows friendly
2018-07-26 10:32:00 +02:00
Alejandro del Castillo
952bbea039 proxy: make process existance check Windows friendly
Currently, to check if the proxy is running, os.kill(pid,0) is used,
which doesn't work on Windows. Wrapped call into a new function that
adds a Windows case.

Signed-off-by: Alejandro del Castillo <alejandro.delcastillo@ni.com>
2018-07-24 15:47:40 -05:00
Tim Head
630e85bfec Merge pull request #2050 from Carreau/https
Switch protocols to https in docs links
2018-07-24 06:09:26 +01:00
Matthias Bussonnier
26f7bb51bd Pin attrs to version greater than 17.4 or jsonschema 3.0.0a fails.
This is strange as JsonSchema already pin to higher than that.
2018-07-23 14:57:45 -07:00
Matthias Bussonnier
a1c2a50810 Switch protocols to https in docs links
Chrome will start to show insecure website for http next week
2018-07-22 18:58:22 -07:00
Min RK
906abcc2f3 add Spawner.consecutive_failure_limit
The Hub will exit if consecutive failure count reaches this threshold

Any successful spawn will reset the count to 0

useful for auto-restarting / self-healing deployments such as kubernetes/systemd/docker where restarting the Hub

default is disabled, since it would bring down the Hub if it’s not an auto-restarting deployment
2018-07-16 12:07:26 -07:00
Min RK
5269370e4a fix SIGTERM handling
raise SystemExit on sigterm instead of calling atexit directly

- ensure fresh asyncio eventloop is created (not just IOLoop)
- makes cleanup more likely to run (one source of orphaned proxies)
2018-07-16 11:49:40 -07:00
Min RK
727356870a Merge pull request #2027 from adelcast/dev/adelcast/fix_services_windows
_ServiceSpawner: add 'SYSTEMROOT' to environment if Windows
2018-07-13 13:24:49 -05:00
Alejandro del Castillo
39aed3a5a0 _ServiceSpawner: add 'SYSTEMROOT' to environment if Windows
Python 3 cannot be started without SYSTEMROOT environment variable.
Otherwise, CryptAcquireContext() is unable to find a dll.

https://bugs.python.org/issue20614

Signed-off-by: Alejandro del Castillo <alejandro.delcastillo@ni.com>
2018-07-06 14:47:19 -05:00
Min RK
ed26578717 back to dev 2018-07-04 11:59:43 +02:00
Min RK
22863f765f 0.9.1 2018-07-04 11:55:42 +02:00
Min RK
b500bd002b Merge pull request #2014 from willingc/bump-testing
add python 3.7 to travis
2018-07-04 11:02:55 +02:00
Carol Willing
aca40b24c3 remove env 2018-07-03 16:32:05 -07:00
Carol Willing
b5fe5a80c6 remove 3.7 from python list but leave in matrix 2018-07-03 14:57:58 -07:00
Carol Willing
ad073dd5dd add 3.7 to travis matrix 2018-07-03 14:44:09 -07:00
Carol Willing
7b815558c6 Merge pull request #2021 from minrk/091-changes
Prepare changelog for 0.9.1
2018-07-03 14:27:01 -07:00
Min RK
55f58b3ba7 review, note proxy prefix fix 2018-07-03 15:12:30 +02:00
Min RK
e1f93a4721 Merge pull request #2009 from BerserkerTroll/patch-2
proxy.py: Respect base_url in add_hub_route
2018-07-03 13:36:48 +02:00
Min RK
2e95f3c039 Merge branch 'master' into patch-2 2018-07-03 13:29:54 +02:00
Min RK
b0ba51f209 host-based routing doesn't support wildcards 2018-07-03 12:27:24 +02:00
Min RK
89e6c2110e add hub.routespec
this is the routespec for sending requests to the hub

It is [host]/prefix/ (not /hub/) so it receives all
requests, not just those destined for the hub
2018-07-03 12:05:21 +02:00
Min RK
7dfdc23b4e Prepare changelog for 0.9.1 2018-07-03 11:44:37 +02:00
Min RK
4c7df53a8a Merge pull request #2020 from weatherforce/master
Fix a couple of typos in the technical reference documentation
2018-07-03 11:23:36 +02:00
Alex Marandon
678afd3783 Fix a couple of typos 2018-07-03 11:16:55 +02:00
Carol Willing
0185a08f32 Merge pull request #2015 from minrk/allow_remote
disable host checking in upcoming notebook app
2018-07-02 08:45:41 -07:00
Tim Head
f3787dd2c8 Merge pull request #2016 from minrk/spawner-docs
mention get_env and get_args in spawner reference
2018-06-30 09:59:38 +02:00
Min RK
30f19cfc8c mention get_env and get_args in spawner reference
these are important and usually required (especially get_env) for custom Spawner implementations
2018-06-29 14:46:08 +02:00
Min RK
a84fa38c6b ensure prefix is on next_url in test_pages 2018-06-29 14:21:32 +02:00
Min RK
867ce4c213 use app.base_url in Proxy.check_routes
rather than assuming '/'
2018-06-29 14:19:20 +02:00
Min RK
005118e09d disable upcoming host checking in single-user notebook application 2018-06-29 11:55:47 +02:00
Carol Willing
04ce67ee71 add python 3.7 to travis 2018-06-28 08:47:04 -07:00
Min RK
31807929cb update test expectations for proxy state
expect app.base_url instead of unconditional ‘/‘
2018-06-27 12:46:13 +02:00
Min RK
cb4105b53e Merge pull request #2012 from josemonsalve2/master
c.LocalProcessSpawner.shell_cmd configuration option does not work
2018-06-27 12:39:19 +02:00
Carol Willing
151887dd56 Merge pull request #2008 from minrk/services-localhost
managed services always talk to hub on localhost
2018-06-26 12:07:29 -07:00
Carol Willing
5f97487184 Merge pull request #2001 from minrk/auto-spawn-api
avoid triggering a spawn from API requests to a not-running server
2018-06-26 12:04:10 -07:00
Carol Willing
4d2d677777 Merge pull request #1996 from minrk/proxy-cleanup
use pid file to check for previous proxy instances
2018-06-26 12:02:54 -07:00
Jose M Monsalve Diaz
6a3b3807c9 fixing shell_cmd attribute of the LocalProcessSpawner that was not tag for configuration 2018-06-25 17:07:39 -04:00
Min RK
02a52a0289 Merge pull request #1997 from gesiscss/master
fix wrong/missing closing tags in templates
2018-06-25 12:54:36 +02:00
BerserkerTroll
7bd1e387df proxy.py: Respect base_url in add_hub_route 2018-06-24 17:22:43 +03:00
Min RK
edc0d7901f services always talk to hub on localhost
When the Hub listens on all ips by default, the connection ip is the hostname.

in some cases (e.g. certain kubernetes deployments) the hub’s container’s hostname is not connectable from itself, preventing managed services from connecting to the hub.

This ensures that managed service processes talk to the hub over localhost in this case, rather than via the hostname.
2018-06-22 13:48:34 +02:00
Min RK
8e561f1c12 avoid triggering a spawn from API requests to a not-running server
this avoids left-open notebook tabs from respawning a culled server indefinitely
2018-06-20 14:57:41 +02:00
Kenan Erdogan
24d87c882f fix wrong/missing closing tags in templates 2018-06-19 09:15:18 +02:00
Min RK
1e333e2f29 Merge pull request #1992 from willingc/doc-toc
add templates and user env docs to home page index
2018-06-18 15:45:52 +02:00
Carol Willing
a507fa1c8a add templates and user env docs to home page index 2018-06-16 10:21:33 -07:00
Min RK
90cc03b3ec back to dev 2018-06-15 15:39:02 +02:00
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
ec83708892 use pid file to check for previous proxy instances
avoids failure to start when the previous proxy wasn't cleaned up properly
2018-06-14 17:50:33 +02: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
48 changed files with 786 additions and 119 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

@@ -62,5 +62,7 @@ matrix:
- python: 3.6 - python: 3.6
env: env:
- JUPYTERHUB_TEST_DB_URL=postgresql://postgres@127.0.0.1/jupyterhub - JUPYTERHUB_TEST_DB_URL=postgresql://postgres@127.0.0.1/jupyterhub
- python: 3.7
dist: xenial
allow_failures: allow_failures:
- python: nightly - python: nightly

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

@@ -95,4 +95,4 @@ make html
```bash ```bash
open build/html/index.html open build/html/index.html
``` ```

1
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1 @@

View File

@@ -11,8 +11,8 @@
[![PyPI](https://img.shields.io/pypi/v/jupyterhub.svg)](https://pypi.python.org/pypi/jupyterhub) [![PyPI](https://img.shields.io/pypi/v/jupyterhub.svg)](https://pypi.python.org/pypi/jupyterhub)
[![Documentation Status](https://readthedocs.org/projects/jupyterhub/badge/?version=latest)](http://jupyterhub.readthedocs.org/en/latest/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/jupyterhub/badge/?version=latest)](https://jupyterhub.readthedocs.org/en/latest/?badge=latest)
[![Documentation Status](http://readthedocs.org/projects/jupyterhub/badge/?version=0.7.2)](http://jupyterhub.readthedocs.io/en/0.7.2/?badge=0.7.2) [![Documentation Status](http://readthedocs.org/projects/jupyterhub/badge/?version=0.7.2)](https://jupyterhub.readthedocs.io/en/0.7.2/?badge=0.7.2)
[![Build Status](https://travis-ci.org/jupyterhub/jupyterhub.svg?branch=master)](https://travis-ci.org/jupyterhub/jupyterhub) [![Build Status](https://travis-ci.org/jupyterhub/jupyterhub.svg?branch=master)](https://travis-ci.org/jupyterhub/jupyterhub)
[![Circle CI](https://circleci.com/gh/jupyterhub/jupyterhub.svg?style=shield&circle-token=b5b65862eb2617b9a8d39e79340b0a6b816da8cc)](https://circleci.com/gh/jupyterhub/jupyterhub) [![Circle CI](https://circleci.com/gh/jupyterhub/jupyterhub.svg?style=shield&circle-token=b5b65862eb2617b9a8d39e79340b0a6b816da8cc)](https://circleci.com/gh/jupyterhub/jupyterhub)
[![codecov.io](https://codecov.io/github/jupyterhub/jupyterhub/coverage.svg?branch=master)](https://codecov.io/github/jupyterhub/jupyterhub?branch=master) [![codecov.io](https://codecov.io/github/jupyterhub/jupyterhub/coverage.svg?branch=master)](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
@@ -81,7 +81,7 @@ for administration of the Hub and its users.
To install JupyterHub along with its dependencies including nodejs/npm: To install JupyterHub along with its dependencies including nodejs/npm:
```bash ```bash
conda install jupyterhub conda install -c conda-forge jupyterhub
``` ```
If you plan to run notebook servers locally, install the Jupyter notebook If you plan to run notebook servers locally, install the Jupyter notebook
@@ -124,7 +124,7 @@ more configuration of the system.
## Configuration ## Configuration
The [Getting Started](http://jupyterhub.readthedocs.io/en/latest/getting-started/index.html) section of the The [Getting Started](https://jupyterhub.readthedocs.io/en/latest/getting-started/index.html) section of the
documentation explains the common steps in setting up JupyterHub. documentation explains the common steps in setting up JupyterHub.
The [**JupyterHub tutorial**](https://github.com/jupyterhub/jupyterhub-tutorial) The [**JupyterHub tutorial**](https://github.com/jupyterhub/jupyterhub-tutorial)
@@ -233,7 +233,7 @@ our JupyterHub [Gitter](https://gitter.im/jupyterhub/jupyterhub) channel.
- [Reporting Issues](https://github.com/jupyterhub/jupyterhub/issues) - [Reporting Issues](https://github.com/jupyterhub/jupyterhub/issues)
- [JupyterHub tutorial](https://github.com/jupyterhub/jupyterhub-tutorial) - [JupyterHub tutorial](https://github.com/jupyterhub/jupyterhub-tutorial)
- [Documentation for JupyterHub](http://jupyterhub.readthedocs.io/en/latest/) | [PDF (latest)](https://media.readthedocs.org/pdf/jupyterhub/latest/jupyterhub.pdf) | [PDF (stable)](https://media.readthedocs.org/pdf/jupyterhub/stable/jupyterhub.pdf) - [Documentation for JupyterHub](https://jupyterhub.readthedocs.io/en/latest/) | [PDF (latest)](https://media.readthedocs.org/pdf/jupyterhub/latest/jupyterhub.pdf) | [PDF (stable)](https://media.readthedocs.org/pdf/jupyterhub/stable/jupyterhub.pdf)
- [Documentation for JupyterHub's REST API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/master/docs/rest-api.yml#/default) - [Documentation for JupyterHub's REST API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/master/docs/rest-api.yml#/default)
- [Documentation for Project Jupyter](http://jupyter.readthedocs.io/en/latest/index.html) | [PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf) - [Documentation for Project Jupyter](http://jupyter.readthedocs.io/en/latest/index.html) | [PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)
- [Project Jupyter website](https://jupyter.org) - [Project Jupyter website](https://jupyter.org)

View File

@@ -8,3 +8,6 @@ pytest>=3.3
notebook notebook
requests-mock requests-mock
virtualenv virtualenv
# temporary pin of attrs for jsonschema 0.3.0a1
# seems to be a pip bug
attrs>=17.4.0

View File

@@ -1,3 +1,5 @@
# ReadTheDocs uses the `environment.yaml` so make sure to update that as well
# if you change the dependencies of JupyterHub in the various `requirements.txt`
name: jhub_docs name: jhub_docs
channels: channels:
- conda-forge - conda-forge
@@ -17,3 +19,4 @@ dependencies:
- recommonmark==0.4.0 - recommonmark==0.4.0
- async_generator - async_generator
- prometheus_client - prometheus_client
- attrs>=17.4.0

View File

@@ -1,3 +1,5 @@
# ReadTheDocs uses the `environment.yaml` so make sure to update that as well
# if you change this file
-r ../requirements.txt -r ../requirements.txt
sphinx>=1.7 sphinx>=1.7
recommonmark==0.4.0 recommonmark==0.4.0

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
@@ -716,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,30 @@ command line for details.
## 0.9 ## 0.9
### 0.9.0 ### [0.9.2] 2018-08-10
JupyterHub 0.9.2 contains small bugfixes and improvements.
- Documentation and example improvements
- Add `Spawner.consecutive_failure_limit` config for aborting the Hub if too many spawns fail in a row.
- Fix for handling SIGTERM when run with asyncio (tornado 5)
- Windows compatibility fixes
### [0.9.1] 2018-07-04
JupyterHub 0.9.1 contains a number of small bugfixes on top of 0.9.
- Use a PID file for the proxy to decrease the likelihood that a leftover proxy process will prevent JupyterHub from restarting
- `c.LocalProcessSpawner.shell_cmd` is now configurable
- API requests to stopped servers (requests to the hub for `/user/:name/api/...`) fail with 404 rather than triggering a restart of the server
- Compatibility fix for notebook 5.6.0 which will introduce further
security checks for local connections
- Managed services always use localhost to talk to the Hub if the Hub listening on all interfaces
- When using a URL prefix, the Hub route will be `JupyterHub.base_url` instead of unconditionally `/`
- additional fixes and improvements
### [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 +116,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 +142,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 +402,10 @@ 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.2...HEAD
[0.9.2]: https://github.com/jupyterhub/jupyterhub/compare/0.9.1...0.9.2
[0.9.1]: https://github.com/jupyterhub/jupyterhub/compare/0.9.0...0.9.1
[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

@@ -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

@@ -96,4 +96,4 @@ A generic implementation, which you can use for OAuth authentication
with any provider, is also available. with any provider, is also available.
[PAM]: https://en.wikipedia.org/wiki/Pluggable_authentication_module [PAM]: https://en.wikipedia.org/wiki/Pluggable_authentication_module
[OAuthenticator]: https://github.com/jupyterhub/oauthenticator [OAuthenticator]: https://github.com/jupyterhub/oauthenticator

View File

@@ -58,6 +58,8 @@ Contents
* :doc:`reference/services` * :doc:`reference/services`
* :doc:`reference/rest` * :doc:`reference/rest`
* :doc:`reference/upgrading` * :doc:`reference/upgrading`
* :doc:`reference/templates`
* :doc:`reference/config-user-env`
* :doc:`reference/config-examples` * :doc:`reference/config-examples`
* :doc:`reference/config-ghoauth` * :doc:`reference/config-ghoauth`
* :doc:`reference/config-proxy` * :doc:`reference/config-proxy`

View File

@@ -226,5 +226,5 @@ Beginning with version 0.8, JupyterHub is an OAuth provider.
[OAuth]: https://en.wikipedia.org/wiki/OAuth [OAuth]: https://en.wikipedia.org/wiki/OAuth
[GitHub OAuth]: https://developer.github.com/v3/oauth/ [GitHub OAuth]: https://developer.github.com/v3/oauth/
[OAuthenticator]: https://github.com/jupyterhub/oauthenticator [OAuthenticator]: https://github.com/jupyterhub/oauthenticator
[pre_spawn_start(user, spawner)]: http://jupyterhub.readthedocs.io/en/latest/api/auth.html#jupyterhub.auth.Authenticator.pre_spawn_start [pre_spawn_start(user, spawner)]: https://jupyterhub.readthedocs.io/en/latest/api/auth.html#jupyterhub.auth.Authenticator.pre_spawn_start
[post_spawn_stop(user, spawner)]: http://jupyterhub.readthedocs.io/en/latest/api/auth.html#jupyterhub.auth.Authenticator.post_spawn_stop [post_spawn_stop(user, spawner)]: https://jupyterhub.readthedocs.io/en/latest/api/auth.html#jupyterhub.auth.Authenticator.post_spawn_stop

View File

@@ -79,4 +79,4 @@ export OAUTH_CALLBACK_URL=https://example.com/hub/oauth_callback
export CONFIGPROXY_AUTH_TOKEN=super-secret export CONFIGPROXY_AUTH_TOKEN=super-secret
# append log output to log file /var/log/jupyterhub.log # append log output to log file /var/log/jupyterhub.log
jupyterhub -f /etc/jupyterhub/jupyterhub_config.py &>> /var/log/jupyterhub.log jupyterhub -f /etc/jupyterhub/jupyterhub_config.py &>> /var/log/jupyterhub.log
``` ```

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

@@ -14,6 +14,7 @@ Technical Reference
database database
upgrading upgrading
templates templates
config-user-env
config-examples config-examples
config-ghoauth config-ghoauth
config-proxy config-proxy

View File

@@ -2,7 +2,7 @@
JupyterHub 0.8 introduced the ability to write a custom implementation of the JupyterHub 0.8 introduced the ability to write a custom implementation of the
proxy. This enables deployments with different needs than the default proxy, proxy. This enables deployments with different needs than the default proxy,
configurable-http-proxy (CHP). CHP is a single-process nodejs proxy that they configurable-http-proxy (CHP). CHP is a single-process nodejs proxy that the
Hub manages by default as a subprocess (it can be run externally, as well, and Hub manages by default as a subprocess (it can be run externally, as well, and
typically is in production deployments). typically is in production deployments).

View File

@@ -15,7 +15,7 @@ This section provides the following information about Services:
## Definition of a Service ## Definition of a Service
When working with JupyterHub, a **Service** is defined as a process that interacts When working with JupyterHub, a **Service** is defined as a process that interacts
with the Hub's REST API. A Service may perform a specific or with the Hub's REST API. A Service may perform a specific
action or task. For example, the following tasks can each be a unique Service: action or task. For example, the following tasks can each be a unique Service:
- shutting down individuals' single user notebook servers that have been idle - shutting down individuals' single user notebook servers that have been idle
@@ -204,10 +204,10 @@ which implements the requests to the Hub.
To use HubAuth, you must set the `.api_token`, either programmatically when constructing the class, To use HubAuth, you must set the `.api_token`, either programmatically when constructing the class,
or via the `JUPYTERHUB_API_TOKEN` environment variable. or via the `JUPYTERHUB_API_TOKEN` environment variable.
Most of the logic for authentication implementation is found in the Most of the logic for authentication implementation is found in the
[`HubAuth.user_for_cookie`](services.auth.html#jupyterhub.services.auth.HubAuth.user_for_cookie) [`HubAuth.user_for_cookie`][HubAuth.user_for_cookie]
and in the and in the
[`HubAuth.user_for_token`](services.auth.html#jupyterhub.services.auth.HubAuth.user_for_token) [`HubAuth.user_for_token`][HubAuth.user_for_token]
methods, which makes a request of the Hub, and returns: methods, which makes a request of the Hub, and returns:
- None, if no user could be identified, or - None, if no user could be identified, or
@@ -359,14 +359,16 @@ and taking note of the following process:
``` ```
An example of using an Externally-Managed Service and authentication is An example of using an Externally-Managed Service and authentication is
in [nbviewer README]_ section on securing the notebook viewer, in [nbviewer README][nbviewer example] section on securing the notebook viewer,
and an example of its configuration is found [here](https://github.com/jupyter/nbviewer/blob/master/nbviewer/providers/base.py#L94). and an example of its configuration is found [here](https://github.com/jupyter/nbviewer/blob/master/nbviewer/providers/base.py#L94).
nbviewer can also be run as a Hub-Managed Service as described [nbviewer README]_ nbviewer can also be run as a Hub-Managed Service as described [nbviewer README][nbviewer example]
section on securing the notebook viewer. section on securing the notebook viewer.
[requests]: http://docs.python-requests.org/en/master/ [requests]: http://docs.python-requests.org/en/master/
[services_auth]: ../api/services.auth.html [services_auth]: ../api/services.auth.html
[HubAuth]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth [HubAuth]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth
[HubAuth.user_for_cookie]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth.user_for_cookie
[HubAuth.user_for_token]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth.user_for_token
[HubAuthenticated]: ../api/services.auth.html#jupyterhub.services.auth.HubAuthenticated [HubAuthenticated]: ../api/services.auth.html#jupyterhub.services.auth.HubAuthenticated
[nbviewer example]: https://github.com/jupyter/nbviewer#securing-the-notebook-viewer [nbviewer example]: https://github.com/jupyter/nbviewer#securing-the-notebook-viewer

View File

@@ -46,7 +46,16 @@ Most `Spawner.start` functions will look similar to this example:
def start(self): def start(self):
self.ip = '127.0.0.1' self.ip = '127.0.0.1'
self.port = random_port() self.port = random_port()
yield self._actually_start_server_somehow() # get environment variables,
# several of which are required for configuring the single-user server
env = self.get_env()
cmd = []
# get jupyterhub command to run,
# typically ['jupyterhub-singleuser']
cmd.extend(self.cmd)
cmd.extend(self.get_args())
yield self._actually_start_server_somehow(cmd, env)
return (self.ip, self.port) return (self.ip, self.port)
``` ```

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

@@ -130,4 +130,4 @@ else
fi fi
exit 0 exit 0
``` ```

View File

@@ -186,10 +186,16 @@ def cull_idle(url, api_token, inactive_limit, cull_users=False, max_age=0, concu
log_name, format_td(age), format_td(inactive)) log_name, format_td(age), format_td(inactive))
return False return False
if server_name:
# culling a named server
delete_url = url + "/users/%s/servers/%s" % (
quote(user['name']), quote(server['name'])
)
else:
delete_url = url + '/users/%s/server' % quote(user['name'])
req = HTTPRequest( req = HTTPRequest(
url=url + '/users/%s/server' % quote(user['name']), url=delete_url, method='DELETE', headers=auth_header,
method='DELETE',
headers=auth_header,
) )
resp = yield fetch(req) resp = yield fetch(req)
if resp.code == 202: if resp.code == 202:

View File

@@ -0,0 +1,60 @@
# Simple Announcement Service Example
This is a simple service that allows administrators to manage announcements
that appear when JupyterHub renders pages.
To run the service as a hub-managed service simply include in your JupyterHub
configuration file something like:
c.JupyterHub.services = [
{
'name': 'announcement',
'url': 'http://127.0.0.1:8888',
'command': ["python", "-m", "announcement"],
}
]
This starts the announcements service up at `/services/announcement` when
JupyterHub launches. By default the announcement text is empty.
The `announcement` module has a configurable port (default 8888) and an API
prefix setting. By default the API prefix is `JUPYTERHUB_SERVICE_PREFIX` if
that environment variable is set or `/` if it is not.
## Managing the Announcement
Admin users can set the announcement text with an API token:
$ curl -X POST -H "Authorization: token <token>" \
-d "{'announcement':'JupyterHub will be upgraded on August 14!'}" \
https://.../services/announcement
Anyone can read the announcement:
$ curl https://.../services/announcement | python -m json.tool
{
announcement: "JupyterHub will be upgraded on August 14!",
timestamp: "...",
user: "..."
}
The time the announcement was posted is recorded in the `timestamp` field and
the user who posted the announcement is recorded in the `user` field.
To clear the announcement text, just DELETE. Only admin users can do this.
$ curl -X POST -H "Authorization: token <token>" \
https://.../services/announcement
## Seeing the Announcement in JupyterHub
To be able to render the announcement, include the provide `page.html` template
that extends the base `page.html` template. Set `c.JupyterHub.template_paths`
in JupyterHub's configuration to include the path to the extending template.
The template changes the `announcement` element and does a JQuery `$.get()` call
to retrieve the announcement text.
JupyterHub's configurable announcement template variables can be set for various
pages like login, logout, spawn, and home. Including the template provided in
this example overrides all of those.

View File

@@ -0,0 +1,73 @@
import argparse
import datetime
import json
import os
from jupyterhub.services.auth import HubAuthenticated
from tornado import escape, gen, ioloop, web
class AnnouncementRequestHandler(HubAuthenticated, web.RequestHandler):
"""Dynamically manage page announcements"""
hub_users = []
allow_admin = True
def initialize(self, storage):
"""Create storage for announcement text"""
self.storage = storage
@web.authenticated
def post(self):
"""Update announcement"""
doc = escape.json_decode(self.request.body)
self.storage["announcement"] = doc["announcement"]
self.storage["timestamp"] = datetime.datetime.now().isoformat()
self.storage["user"] = user["name"]
self.write_to_json(self.storage)
def get(self):
"""Retrieve announcement"""
self.write_to_json(self.storage)
@web.authenticated
def delete(self):
"""Clear announcement"""
self.storage["announcement"] = ""
self.write_to_json(self.storage)
def write_to_json(self, doc):
"""Write dictionary document as JSON"""
self.set_header("Content-Type", "application/json; charset=UTF-8")
self.write(escape.utf8(json.dumps(doc)))
def main():
args = parse_arguments()
application = create_application(**vars(args))
application.listen(args.port)
ioloop.IOLoop.current().start()
def parse_arguments():
parser = argparse.ArgumentParser()
parser.add_argument("--api-prefix", "-a",
default=os.environ.get("JUPYTERHUB_SERVICE_PREFIX", "/"),
help="application API prefix")
parser.add_argument("--port", "-p",
default=8888,
help="port for API to listen on",
type=int)
return parser.parse_args()
def create_application(api_prefix="/",
handler=AnnouncementRequestHandler,
**kwargs):
storage = dict(announcement="", timestamp="", user="")
return web.Application([(api_prefix, handler, dict(storage=storage))])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,15 @@
# To run the announcement service managed by the hub, add this.
c.JupyterHub.services = [
{
'name': 'announcement',
'url': 'http://127.0.0.1:8888',
'command': ["python", "-m", "announcement"],
}
]
# The announcements need to get on the templates somehow, see page.html
# for an example of how to do this.
c.JupyterHub.template_paths = ["templates"]

View File

@@ -0,0 +1,14 @@
{% extends "templates/page.html" %}
{% block announcement %}
<div class="container text-center announcement">
</div>
{% endblock %}
{% block script %}
{{ super() }}
<script>
$.get("/services/announcement/", function(data) {
$(".announcement").html(data["announcement"]);
});
</script>
{% endblock %}

View File

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

View File

@@ -976,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),
@@ -1110,7 +1108,18 @@ class JupyterHub(Application):
else: else:
hub_args['ip'] = self.hub_ip hub_args['ip'] = self.hub_ip
hub_args['port'] = self.hub_port hub_args['port'] = self.hub_port
self.hub = Hub(**hub_args)
# routespec for the Hub is the *app* base url
# not the hub URL, so it receives requests for non-running servers
# use `/` with host-based routing so the Hub
# gets requests for all hosts
host = ''
if self.subdomain_host:
routespec = '/'
else:
routespec = self.base_url
self.hub = Hub(routespec=routespec, **hub_args)
if self.hub_connect_ip: if self.hub_connect_ip:
self.hub.connect_ip = self.hub_connect_ip self.hub.connect_ip = self.hub_connect_ip
@@ -1912,8 +1921,7 @@ class JupyterHub(Application):
def sigterm(self, signum, frame): def sigterm(self, signum, frame):
self.log.critical("Received SIGTERM, shutting down") self.log.critical("Received SIGTERM, shutting down")
self.io_loop.stop() raise SystemExit(128 + signum)
self.atexit()
_atexit_ran = False _atexit_ran = False
@@ -1923,6 +1931,7 @@ class JupyterHub(Application):
return return
self._atexit_ran = True self._atexit_ran = True
# run the cleanup step (in a new loop, because the interrupted one is unclean) # run the cleanup step (in a new loop, because the interrupted one is unclean)
asyncio.set_event_loop(asyncio.new_event_loop())
IOLoop.clear_current() IOLoop.clear_current()
loop = IOLoop() loop = IOLoop()
loop.make_current() loop.make_current()

View File

@@ -6,6 +6,7 @@
import copy import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from http.client import responses from http.client import responses
import json
import math import math
import random import random
import re import re
@@ -656,6 +657,7 @@ class BaseHandler(RequestHandler):
# hook up spawner._spawn_future so that other requests can await # hook up spawner._spawn_future so that other requests can await
# this result # this result
finish_spawn_future = spawner._spawn_future = maybe_future(finish_user_spawn()) finish_spawn_future = spawner._spawn_future = maybe_future(finish_user_spawn())
def _clear_spawn_future(f): def _clear_spawn_future(f):
# clear spawner._spawn_future when it's done # clear spawner._spawn_future when it's done
# keep an exception around, though, to prevent repeated implicit spawns # keep an exception around, though, to prevent repeated implicit spawns
@@ -664,10 +666,44 @@ class BaseHandler(RequestHandler):
spawner._spawn_future = None spawner._spawn_future = None
# Now we're all done. clear _spawn_pending flag # Now we're all done. clear _spawn_pending flag
spawner._spawn_pending = False spawner._spawn_pending = False
finish_spawn_future.add_done_callback(_clear_spawn_future) finish_spawn_future.add_done_callback(_clear_spawn_future)
# when spawn finishes (success or failure)
# update failure count and abort if consecutive failure limit
# is reached
def _track_failure_count(f):
if f.exception() is None:
# spawn succeeded, reset failure count
self.settings['failure_count'] = 0
return
# spawn failed, increment count and abort if limit reached
self.settings.setdefault('failure_count', 0)
self.settings['failure_count'] += 1
failure_count = self.settings['failure_count']
failure_limit = spawner.consecutive_failure_limit
if failure_limit and 1 < failure_count < failure_limit:
self.log.warning(
"%i consecutive spawns failed. "
"Hub will exit if failure count reaches %i before succeeding",
failure_count, failure_limit,
)
if failure_limit and failure_count >= failure_limit:
self.log.critical(
"Aborting due to %i consecutive spawn failures", failure_count
)
# abort in 2 seconds to allow pending handlers to resolve
# mostly propagating errors for the current failures
def abort():
raise SystemExit(1)
IOLoop.current().call_later(2, abort)
finish_spawn_future.add_done_callback(_track_failure_count)
try: try:
await gen.with_timeout(timedelta(seconds=self.slow_spawn_timeout), finish_spawn_future) await gen.with_timeout(
timedelta(seconds=self.slow_spawn_timeout), finish_spawn_future
)
except gen.TimeoutError: except gen.TimeoutError:
# waiting_for_response indicates server process has started, # waiting_for_response indicates server process has started,
# but is yet to become responsive. # but is yet to become responsive.
@@ -865,6 +901,11 @@ class PrefixRedirectHandler(BaseHandler):
""" """
def get(self): def get(self):
uri = self.request.uri uri = self.request.uri
# Since self.base_url will end with trailing slash.
# Ensure uri will end with trailing slash when matching
# with self.base_url.
if not uri.endswith('/'):
uri += '/'
if uri.startswith(self.base_url): if uri.startswith(self.base_url):
path = self.request.uri[len(self.base_url):] path = self.request.uri[len(self.base_url):]
else: else:
@@ -884,6 +925,13 @@ class UserSpawnHandler(BaseHandler):
which will in turn send her to /user/alice/notebooks/mynotebook.ipynb. which will in turn send her to /user/alice/notebooks/mynotebook.ipynb.
""" """
def _fail_api_request(self, user):
"""Fail an API request to a not-running server"""
self.set_status(404)
self.set_header("Content-Type", "application/json")
self.write(json.dumps({"message": "%s is not running" % user.name}))
self.finish()
async def get(self, name, user_path): async def get(self, name, user_path):
if not user_path: if not user_path:
user_path = '/' user_path = '/'
@@ -910,6 +958,11 @@ class UserSpawnHandler(BaseHandler):
# otherwise redirect users to their own server # otherwise redirect users to their own server
should_spawn = (current_user and current_user.name == name) should_spawn = (current_user and current_user.name == name)
if "api" in user_path.split("/") and not user.active:
# API request for not-running server (e.g. notebook UI left open)
# Avoid triggering a spawn.
self._fail_api_request(user)
return
if should_spawn: if should_spawn:
# if spawning fails for any reason, point users to /hub/home to retry # if spawning fails for any reason, point users to /hub/home to retry
@@ -1119,6 +1172,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

@@ -63,6 +63,9 @@ class Server(HasTraits):
@validate('connect_url') @validate('connect_url')
def _connect_url_add_prefix(self, proposal): def _connect_url_add_prefix(self, proposal):
"""Ensure connect_url includes base_url""" """Ensure connect_url includes base_url"""
if not proposal.value:
# Don't add the prefix if the setting is being cleared
return proposal.value
urlinfo = urlparse(proposal.value) urlinfo = urlparse(proposal.value)
if not urlinfo.path.startswith(self.base_url): if not urlinfo.path.startswith(self.base_url):
urlinfo = urlinfo._replace(path=self.base_url) urlinfo = urlinfo._replace(path=self.base_url)
@@ -185,6 +188,7 @@ class Hub(Server):
) )
return self return self
public_host = Unicode() public_host = Unicode()
routespec = Unicode()
@property @property
def api_url(self): def api_url(self):

View File

@@ -22,8 +22,10 @@ import asyncio
from functools import wraps from functools import wraps
import json import json
import os import os
import signal
from subprocess import Popen from subprocess import Popen
from urllib.parse import quote import time
from urllib.parse import quote, urlparse
from tornado import gen from tornado import gen
from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPError from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPError
@@ -301,13 +303,13 @@ class Proxy(LoggingConfigurable):
user_routes = {path for path, r in routes.items() if 'user' in r['data']} user_routes = {path for path, r in routes.items() if 'user' in r['data']}
futures = [] futures = []
good_routes = {'/'} good_routes = {self.app.hub.routespec}
hub = self.app.hub hub = self.hub
if '/' not in routes: if self.app.hub.routespec not in routes:
futures.append(self.add_hub_route(hub)) futures.append(self.add_hub_route(hub))
else: else:
route = routes['/'] route = routes[self.app.hub.routespec]
if route['target'] != hub.host: if route['target'] != hub.host:
self.log.warning("Updating default route %s%s", route['target'], hub.host) self.log.warning("Updating default route %s%s", route['target'], hub.host)
futures.append(self.add_hub_route(hub)) futures.append(self.add_hub_route(hub))
@@ -365,8 +367,8 @@ class Proxy(LoggingConfigurable):
def add_hub_route(self, hub): def add_hub_route(self, hub):
"""Add the default route for the Hub""" """Add the default route for the Hub"""
self.log.info("Adding default route for Hub: / => %s", hub.host) self.log.info("Adding default route for Hub: %s => %s", hub.routespec, hub.host)
return self.add_route('/', self.hub.host, {'hub': True}) return self.add_route(hub.routespec, self.hub.host, {'hub': True})
async def restore_routes(self): async def restore_routes(self):
self.log.info("Setting up routes on new proxy") self.log.info("Setting up routes on new proxy")
@@ -437,8 +439,22 @@ class ConfigurableHTTPProxy(Proxy):
help="""The command to start the proxy""" help="""The command to start the proxy"""
) )
pid_file = Unicode(
"jupyterhub-proxy.pid",
config=True,
help="File in which to write the PID of the proxy process.",
)
_check_running_callback = Any(help="PeriodicCallback to check if the proxy is running") _check_running_callback = Any(help="PeriodicCallback to check if the proxy is running")
def _check_pid(self, pid):
if os.name == 'nt':
import psutil
if not psutil.pid_exists(pid):
raise ProcessLookupError
else:
os.kill(pid, 0)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# check for required token if proxy is external # check for required token if proxy is external
@@ -448,9 +464,72 @@ class ConfigurableHTTPProxy(Proxy):
" if Proxy.should_start is False" % self.__class__.__name__ " if Proxy.should_start is False" % self.__class__.__name__
) )
def _check_previous_process(self):
"""Check if there's a process leftover and shut it down if so"""
if not self.pid_file or not os.path.exists(self.pid_file):
return
pid_file = os.path.abspath(self.pid_file)
self.log.warning("Found proxy pid file: %s", pid_file)
try:
with open(pid_file, "r") as f:
pid = int(f.read().strip())
except ValueError:
self.log.warning("%s did not appear to contain a pid", pid_file)
self._remove_pid_file()
return
try:
self._check_pid(pid)
except ProcessLookupError:
self.log.warning("Proxy no longer running at pid=%s", pid)
self._remove_pid_file()
return
# if we got here, CHP is still running
self.log.warning("Proxy still running at pid=%s", pid)
for i, sig in enumerate([signal.SIGTERM] * 2 + [signal.SIGKILL]):
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
break
time.sleep(1)
try:
self._check_pid(pid)
except ProcessLookupError:
break
try:
self._check_pid(pid)
except ProcessLookupError:
self.log.warning("Stopped proxy at pid=%s", pid)
self._remove_pid_file()
return
else:
raise RuntimeError("Failed to stop proxy at pid=%s", pid)
def _write_pid_file(self):
"""write pid for proxy to a file"""
self.log.debug("Writing proxy pid file: %s", self.pid_file)
with open(self.pid_file, "w") as f:
f.write(str(self.proxy_process.pid))
def _remove_pid_file(self):
"""Cleanup pid file for proxy after stopping"""
if not self.pid_file:
return
self.log.debug("Removing proxy pid file %s", self.pid_file)
try:
os.remove(self.pid_file)
except FileNotFoundError:
self.log.debug("PID file %s already removed", self.pid_file)
pass
async def start(self): async def start(self):
"""Start the proxy process"""
# check if there is a previous instance still around
self._check_previous_process()
# build the command to launch
public_server = Server.from_url(self.public_url) public_server = Server.from_url(self.public_url)
api_server = Server.from_url(self.api_url) api_server = Server.from_url(self.api_url)
env = os.environ.copy() env = os.environ.copy()
@@ -496,6 +575,8 @@ class ConfigurableHTTPProxy(Proxy):
) )
raise raise
self._write_pid_file()
def _check_process(): def _check_process():
status = self.proxy_process.poll() status = self.proxy_process.poll()
if status is not None: if status is not None:
@@ -519,13 +600,20 @@ class ConfigurableHTTPProxy(Proxy):
self._check_running_callback = pc self._check_running_callback = pc
pc.start() pc.start()
def _kill_proc_tree(self, pid): def _terminate(self):
import psutil """Terminate our process"""
parent = psutil.Process(pid) if os.name == 'nt':
children = parent.children(recursive=True) # On Windows we spawned a shell on Popen, so we need to
for child in children: # terminate all child processes as well
child.kill() import psutil
psutil.wait_procs(children, timeout=5)
parent = psutil.Process(self.proxy_process.pid)
children = parent.children(recursive=True)
for child in children:
child.kill()
psutil.wait_procs(children, timeout=5)
else:
self.proxy_process.terminate()
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)
@@ -533,14 +621,10 @@ class ConfigurableHTTPProxy(Proxy):
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:
if os.name == 'nt': self._terminate()
# 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)
self._remove_pid_file()
async def check_running(self): async def check_running(self):
"""Check if the proxy is still running""" """Check if the proxy is still running"""
@@ -549,6 +633,7 @@ class ConfigurableHTTPProxy(Proxy):
self.log.error("Proxy stopped with exit code %r", self.log.error("Proxy stopped with exit code %r",
'unknown' if self.proxy_process is None else self.proxy_process.poll() 'unknown' if self.proxy_process is None else self.proxy_process.poll()
) )
self._remove_pid_file()
await self.start() await self.start()
await self.restore_routes() await self.restore_routes()
@@ -574,7 +659,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

@@ -39,8 +39,10 @@ A hub-managed service with no URL::
""" """
import copy
import pipes import pipes
import shutil import shutil
import os
from subprocess import Popen from subprocess import Popen
from traitlets import ( from traitlets import (
@@ -105,6 +107,8 @@ class _ServiceSpawner(LocalProcessSpawner):
def start(self): def start(self):
"""Start the process""" """Start the process"""
env = self.get_env() env = self.get_env()
if os.name == 'nt':
env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
cmd = self.cmd cmd = self.cmd
self.log.info("Spawning %s", ' '.join(pipes.quote(s) for s in cmd)) self.log.info("Spawning %s", ' '.join(pipes.quote(s) for s in cmd))
@@ -304,6 +308,15 @@ class Service(LoggingConfigurable):
env['JUPYTERHUB_SERVICE_URL'] = self.url env['JUPYTERHUB_SERVICE_URL'] = self.url
env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url
hub = self.hub
if self.hub.ip in ('0.0.0.0', ''):
# if the Hub is listening on all interfaces,
# tell services to connect via localhost
# since they are always local subprocesses
hub = copy.deepcopy(self.hub)
hub.connect_url = ''
hub.connect_ip = '127.0.0.1'
self.spawner = _ServiceSpawner( self.spawner = _ServiceSpawner(
cmd=self.command, cmd=self.command,
environment=env, environment=env,

View File

@@ -213,7 +213,10 @@ class SingleUserNotebookApp(NotebookApp):
subcommands = {} subcommands = {}
version = __version__ version = __version__
classes = NotebookApp.classes + [HubOAuth] classes = NotebookApp.classes + [HubOAuth]
# disable single-user app's localhost checking
allow_remote_access = True
# don't store cookie secrets # don't store cookie secrets
cookie_secret_file = '' cookie_secret_file = ''
# always generate a new cookie secret on launch # always generate a new cookie secret on launch
@@ -225,7 +228,7 @@ class SingleUserNotebookApp(NotebookApp):
user = CUnicode().tag(config=True) user = CUnicode().tag(config=True)
group = CUnicode().tag(config=True) group = CUnicode().tag(config=True)
@default('user') @default('user')
def _default_user(self): def _default_user(self):
return os.environ.get('JUPYTERHUB_USER') or '' return os.environ.get('JUPYTERHUB_USER') or ''

View File

@@ -195,6 +195,19 @@ class Spawner(LoggingConfigurable):
""" """
).tag(config=True) ).tag(config=True)
consecutive_failure_limit = Integer(
0,
help="""
Maximum number of consecutive failures to allow before
shutting down JupyterHub.
This helps JupyterHub recover from a certain class of problem preventing launch
in contexts where the Hub is automatically restarted (e.g. systemd, docker, kubernetes).
A limit of 0 means no limit and consecutive failures will not be tracked.
""",
).tag(config=True)
start_timeout = Integer(60, start_timeout = Integer(60,
help=""" help="""
Timeout (in seconds) before giving up on starting of single-user server. Timeout (in seconds) before giving up on starting of single-user server.
@@ -1002,7 +1015,7 @@ class LocalProcessSpawner(Spawner):
which could change what the jupyterhub-singleuser launch command does. which could change what the jupyterhub-singleuser launch command does.
Only use this for trusted users. Only use this for trusted users.
""" """
) ).tag(config=True)
proc = Instance(Popen, proc = Instance(Popen,
allow_none=True, allow_none=True,

View File

@@ -1055,7 +1055,7 @@ def test_get_proxy(app):
r = yield api_request(app, 'proxy') r = yield api_request(app, 'proxy')
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert list(reply.keys()) == ['/'] assert list(reply.keys()) == [app.hub.routespec]
@mark.gen_test @mark.gen_test

View File

@@ -216,11 +216,12 @@ def test_spawn_form(app):
orm_u = orm.User.find(app.db, 'jones') orm_u = orm.User.find(app.db, 'jones')
u = app.users[orm_u] u = app.users[orm_u]
yield u.stop() yield u.stop()
next_url = ujoin(app.base_url, 'user/jones/tree')
r = yield async_requests.post(ujoin(base_url, 'spawn?next=/user/jones/tree'), cookies=cookies, data={ r = yield async_requests.post(
'bounds': ['-1', '1'], url_concat(ujoin(base_url, 'spawn'), {'next': next_url}),
'energy': '511keV', cookies=cookies,
}) data={'bounds': ['-1', '1'], 'energy': '511keV'},
)
r.raise_for_status() r.raise_for_status()
assert r.history assert r.history
assert u.spawner.user_options == { assert u.spawner.user_options == {
@@ -236,13 +237,13 @@ def test_spawn_form_admin_access(app, admin_access):
base_url = ujoin(public_host(app), app.hub.base_url) base_url = ujoin(public_host(app), app.hub.base_url)
cookies = yield app.login_user('admin') cookies = yield app.login_user('admin')
u = add_user(app.db, app=app, name='martha') u = add_user(app.db, app=app, name='martha')
next_url = ujoin(app.base_url, 'user', u.name, 'tree')
r = yield async_requests.post( r = yield async_requests.post(
ujoin(base_url, 'spawn/{0}?next=/user/{0}/tree'.format(u.name)), url_concat(ujoin(base_url, 'spawn', u.name), {'next': next_url}),
cookies=cookies, data={ cookies=cookies,
'bounds': ['-3', '3'], data={'bounds': ['-3', '3'], 'energy': '938MeV'},
'energy': '938MeV', )
})
r.raise_for_status() r.raise_for_status()
assert r.history assert r.history
assert r.url.startswith(public_url(app, u)) assert r.url.startswith(public_url(app, u))
@@ -570,3 +571,12 @@ def test_announcements(app, announcements):
app.authenticator.auto_login = auto_login app.authenticator.auto_login = auto_login
r.raise_for_status() r.raise_for_status()
assert_announcement("logout", r.text) assert_announcement("logout", r.text)
@pytest.mark.gen_test
def test_server_not_running_api_request(app):
cookies = yield app.login_user("bees")
r = yield get_page("user/bees/api/status", app, hub=False, cookies=cookies)
assert r.status_code == 404
assert r.headers["content-type"] == "application/json"
assert r.json() == {"message": "bees is not running"}

View File

@@ -79,7 +79,7 @@ def test_external_proxy(request):
# test if api service has a root route '/' # test if api service has a root route '/'
routes = yield app.proxy.get_all_routes() routes = yield app.proxy.get_all_routes()
assert list(routes.keys()) == ['/'] assert list(routes.keys()) == [app.hub.routespec]
# add user to the db and start a single user server # add user to the db and start a single user server
name = 'river' name = 'river'
@@ -95,7 +95,7 @@ def test_external_proxy(request):
if app.subdomain_host: if app.subdomain_host:
host = '%s.%s' % (name, urlparse(app.subdomain_host).hostname) host = '%s.%s' % (name, urlparse(app.subdomain_host).hostname)
user_spec = host + user_path user_spec = host + user_path
assert sorted(routes.keys()) == ['/', user_spec] assert sorted(routes.keys()) == [app.hub.routespec, user_spec]
# teardown the proxy and start a new one in the same place # teardown the proxy and start a new one in the same place
proxy.terminate() proxy.terminate()
@@ -113,7 +113,7 @@ def test_external_proxy(request):
# check that the routes are correct # check that the routes are correct
routes = yield app.proxy.get_all_routes() routes = yield app.proxy.get_all_routes()
assert sorted(routes.keys()) == ['/', user_spec] assert sorted(routes.keys()) == [app.hub.routespec, user_spec]
# teardown the proxy, and start a new one with different auth and port # teardown the proxy, and start a new one with different auth and port
proxy.terminate() proxy.terminate()
@@ -146,7 +146,7 @@ def test_external_proxy(request):
# check that the routes are correct # check that the routes are correct
routes = yield app.proxy.get_all_routes() routes = yield app.proxy.get_all_routes()
assert sorted(routes.keys()) == ['/', user_spec] assert sorted(routes.keys()) == [app.hub.routespec, user_spec]
@pytest.mark.gen_test @pytest.mark.gen_test
@@ -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

@@ -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

@@ -53,20 +53,20 @@
{%- endif -%} {%- endif -%}
</td> </td>
<td class="server-col col-sm-2 text-center"> <td class="server-col col-sm-2 text-center">
<a role="button" class="stop-server btn btn-xs btn-danger {% if not u.running %}hidden{% endif %}">stop server</span> <a role="button" class="stop-server btn btn-xs btn-danger {% if not u.running %}hidden{% endif %}">stop server</a>
<a role="button" class="start-server btn btn-xs btn-primary {% if u.running %}hidden{% endif %}">start server</span> <a role="button" class="start-server btn btn-xs btn-primary {% if u.running %}hidden{% endif %}">start server</a>
</td> </td>
<td class="server-col col-sm-1 text-center"> <td class="server-col col-sm-1 text-center">
{% if admin_access %} {% if admin_access %}
<a role="button" class="access-server btn btn-xs btn-primary {% if not u.running %}hidden{% endif %}">access server</span> <a role="button" class="access-server btn btn-xs btn-primary {% if not u.running %}hidden{% endif %}">access server</a>
{% endif %} {% endif %}
</td> </td>
<td class="edit-col col-sm-1 text-center"> <td class="edit-col col-sm-1 text-center">
<a role="button" class="edit-user btn btn-xs btn-primary">edit</span> <a role="button" class="edit-user btn btn-xs btn-primary">edit</a>
</td> </td>
<td class="edit-col col-sm-1 text-center"> <td class="edit-col col-sm-1 text-center">
{% if u.name != user.name %} {% if u.name != user.name %}
<a role="button" class="delete-user btn btn-xs btn-danger">delete</span> <a role="button" class="delete-user btn btn-xs btn-danger">delete</a>
{% endif %} {% endif %}
</td> </td>
{% endblock user_row %} {% endblock user_row %}

View File

@@ -15,6 +15,7 @@
</div> </div>
</div> </div>
<p id="progress-message"></p> <p id="progress-message"></p>
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-8 col-md-offset-2"> <div class="col-md-8 col-md-offset-2">
@@ -23,6 +24,7 @@
<div id="progress-log"></div> <div id="progress-log"></div>
</details> </details>
</div> </div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -23,7 +23,7 @@
require(["jquery"], function ($) { require(["jquery"], function ($) {
$("#refresh").click(function () { $("#refresh").click(function () {
window.location.reload(); window.location.reload();
}) });
setTimeout(function () { setTimeout(function () {
window.location.reload(); window.location.reload();
}, 5000); }, 5000);

View File

@@ -24,7 +24,6 @@
</div> </div>
<div class="row"> <div class="row">
<p>
<div id="token-area" class="col-md-6 col-md-offset-3" style="display: none;"> <div id="token-area" class="col-md-6 col-md-offset-3" style="display: none;">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
@@ -41,7 +40,6 @@
</div> </div>
</div> </div>
</div> </div>
</p>
</div> </div>
{% if api_tokens %} {% if api_tokens %}
@@ -62,7 +60,7 @@
</thead> </thead>
<tbody> <tbody>
{% for token in api_tokens %} {% for token in api_tokens %}
<tr class="token-row" data-token-id="{{token.api_id}}""> <tr class="token-row" data-token-id="{{token.api_id}}">
{% block token_row scoped %} {% block token_row scoped %}
<td class="note-col col-sm-5">{{token.note}}</td> <td class="note-col col-sm-5">{{token.note}}</td>
<td class="time-col col-sm-3"> <td class="time-col col-sm-3">
@@ -111,7 +109,7 @@
<tbody> <tbody>
{% for client in oauth_clients %} {% for client in oauth_clients %}
<tr class="token-row" <tr class="token-row"
data-token-id="{{ client['token_id'] }}""> data-token-id="{{ client['token_id'] }}">
{% block client_row scoped %} {% block client_row scoped %}
<td class="note-col col-sm-5">{{ client['description'] }}</td> <td class="note-col col-sm-5">{{ client['description'] }}</td>
<td class="time-col col-sm-3"> <td class="time-col col-sm-3">
@@ -129,8 +127,7 @@
{%- endif -%} {%- 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</button>
</button>
{% endblock client_row %} {% endblock client_row %}
</tr> </tr>
{% endfor %} {% endfor %}
@@ -139,11 +136,11 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock main %}
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script type="text/javascript"> <script type="text/javascript">
require(["token"]); require(["token"]);
</script> </script>
{% endblock %} {% endblock script %}