Compare commits

...

74 Commits

Author SHA1 Message Date
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
35 changed files with 511 additions and 97 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
env:
- JUPYTERHUB_TEST_DB_URL=postgresql://postgres@127.0.0.1/jupyterhub
- python: 3.7
dist: xenial
allow_failures:
- 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).

1
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1 @@

View File

@@ -81,7 +81,7 @@ for administration of the Hub and its users.
To install JupyterHub along with its dependencies including nodejs/npm:
```bash
conda install jupyterhub
conda install -c conda-forge jupyterhub
```
If you plan to run notebook servers locally, install the Jupyter notebook

View File

@@ -252,6 +252,17 @@ paths:
$ref: '#/definitions/Token'
post:
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:
'201':
description: The newly created token
@@ -716,6 +727,10 @@ definitions:
type: string
format: date-time
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:
type: string
format: date-time

View File

@@ -9,7 +9,20 @@ command line for details.
## 0.9
### 0.9.0
### [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.
There are several changes to the database schema,
@@ -93,6 +106,12 @@ and tornado < 5.0.
- 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.
- 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
@@ -113,6 +132,11 @@ and tornado < 5.0.
- Various fixes in race conditions and performance improvements with the default proxy.
- Fixes for CORS headers
- 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
@@ -368,7 +392,9 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
First preview release
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.8.1...HEAD
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.9.1...HEAD
[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.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

View File

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

View File

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

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
upgrading
templates
config-user-env
config-examples
config-ghoauth
config-proxy

View File

@@ -2,7 +2,7 @@
JupyterHub 0.8 introduced the ability to write a custom implementation of the
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
typically is in production deployments).

View File

@@ -15,7 +15,7 @@ This section provides the following information about Services:
## Definition of a Service
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:
- shutting down individuals' single user notebook servers that have been idle

View File

@@ -46,7 +46,16 @@ Most `Spawner.start` functions will look similar to this example:
def start(self):
self.ip = '127.0.0.1'
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)
```

View File

@@ -9,6 +9,7 @@ problem and how to resolve it.
- sudospawner fails to run
- What is the default behavior when none of the lists (admin, whitelist,
group whitelist) are set?
- JupyterHub Docker container not accessible at localhost
[*Errors*](#errors)
- 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
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

View File

@@ -6,9 +6,9 @@
version_info = (
0,
9,
0,
"rc1", # release (b1, rc1)
# "dev", # dev
1,
"", # release (b1, rc1, or "" for final)
# "dev", # dev or nothing
)
# 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)
# some extra handlers, outside hub_prefix
self.handlers.extend([
# add trailing / to `/hub`
(self.hub_prefix.rstrip('/'), handlers.AddSlashHandler),
# add trailing / to ``/user|services/:name`
(r"%s(user|services)/([^/]+)" % self.base_url, handlers.AddSlashHandler),
(r"(?!%s).*" % self.hub_prefix, handlers.PrefixRedirectHandler),
@@ -1110,7 +1108,18 @@ class JupyterHub(Application):
else:
hub_args['ip'] = self.hub_ip
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:
self.hub.connect_ip = self.hub_connect_ip

View File

@@ -6,6 +6,7 @@
import copy
from datetime import datetime, timedelta
from http.client import responses
import json
import math
import random
import re
@@ -884,6 +885,13 @@ class UserSpawnHandler(BaseHandler):
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):
if not user_path:
user_path = '/'
@@ -910,6 +918,11 @@ class UserSpawnHandler(BaseHandler):
# otherwise redirect users to their own server
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 spawning fails for any reason, point users to /hub/home to retry
@@ -1119,6 +1132,7 @@ class AddSlashHandler(BaseHandler):
self.redirect(urlunparse(dest))
default_handlers = [
(r'', AddSlashHandler), # add trailing / to `/hub`
(r'/user/([^/]+)(/.*)?', UserSpawnHandler),
(r'/user-redirect/(.*)?', UserRedirectHandler),
(r'/security/csp-report', CSPReportHandler),

View File

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

View File

@@ -63,6 +63,9 @@ class Server(HasTraits):
@validate('connect_url')
def _connect_url_add_prefix(self, proposal):
"""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)
if not urlinfo.path.startswith(self.base_url):
urlinfo = urlinfo._replace(path=self.base_url)
@@ -185,6 +188,7 @@ class Hub(Server):
)
return self
public_host = Unicode()
routespec = Unicode()
@property
def api_url(self):

View File

@@ -22,8 +22,10 @@ import asyncio
from functools import wraps
import json
import os
import signal
from subprocess import Popen
from urllib.parse import quote
import time
from urllib.parse import quote, urlparse
from tornado import gen
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']}
futures = []
good_routes = {'/'}
good_routes = {self.app.hub.routespec}
hub = self.app.hub
if '/' not in routes:
hub = self.hub
if self.app.hub.routespec not in routes:
futures.append(self.add_hub_route(hub))
else:
route = routes['/']
route = routes[self.app.hub.routespec]
if route['target'] != hub.host:
self.log.warning("Updating default route %s%s", route['target'], hub.host)
futures.append(self.add_hub_route(hub))
@@ -365,8 +367,8 @@ class Proxy(LoggingConfigurable):
def add_hub_route(self, hub):
"""Add the default route for the Hub"""
self.log.info("Adding default route for Hub: / => %s", hub.host)
return self.add_route('/', self.hub.host, {'hub': True})
self.log.info("Adding default route for Hub: %s => %s", hub.routespec, hub.host)
return self.add_route(hub.routespec, self.hub.host, {'hub': True})
async def restore_routes(self):
self.log.info("Setting up routes on new proxy")
@@ -437,6 +439,12 @@ class ConfigurableHTTPProxy(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")
def __init__(self, **kwargs):
@@ -448,9 +456,72 @@ class ConfigurableHTTPProxy(Proxy):
" 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:
os.kill(pid, 0)
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:
os.kill(pid, 0)
except ProcessLookupError:
break
try:
os.kill(pid, 0)
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):
"""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)
api_server = Server.from_url(self.api_url)
env = os.environ.copy()
@@ -496,6 +567,8 @@ class ConfigurableHTTPProxy(Proxy):
)
raise
self._write_pid_file()
def _check_process():
status = self.proxy_process.poll()
if status is not None:
@@ -519,13 +592,20 @@ class ConfigurableHTTPProxy(Proxy):
self._check_running_callback = pc
pc.start()
def _kill_proc_tree(self, pid):
import psutil
parent = psutil.Process(pid)
children = parent.children(recursive=True)
for child in children:
child.kill()
psutil.wait_procs(children, timeout=5)
def _terminate(self):
"""Terminate our process"""
if os.name == 'nt':
# On Windows we spawned a shell on Popen, so we need to
# terminate all child processes as well
import psutil
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):
self.log.info("Cleaning up proxy[%i]...", self.proxy_process.pid)
@@ -533,14 +613,10 @@ class ConfigurableHTTPProxy(Proxy):
self._check_running_callback.stop()
if self.proxy_process.poll() is None:
try:
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()
self._terminate()
except Exception as e:
self.log.error("Failed to terminate proxy process: %s", e)
self._remove_pid_file()
async def check_running(self):
"""Check if the proxy is still running"""
@@ -549,6 +625,7 @@ class ConfigurableHTTPProxy(Proxy):
self.log.error("Proxy stopped with exit code %r",
'unknown' if self.proxy_process is None else self.proxy_process.poll()
)
self._remove_pid_file()
await self.start()
await self.restore_routes()
@@ -574,7 +651,7 @@ class ConfigurableHTTPProxy(Proxy):
"""
# chp stores routes in unescaped form.
# restore escaped-form we created it with.
routespec = quote(chp_path, safe='@/')
routespec = quote(chp_path, safe='@/~')
if self.host_routing:
# host routes don't start with /
routespec = routespec.lstrip('/')

View File

@@ -39,6 +39,7 @@ A hub-managed service with no URL::
"""
import copy
import pipes
import shutil
from subprocess import Popen
@@ -304,6 +305,15 @@ class Service(LoggingConfigurable):
env['JUPYTERHUB_SERVICE_URL'] = self.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(
cmd=self.command,
environment=env,

View File

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

View File

@@ -1002,7 +1002,7 @@ class LocalProcessSpawner(Spawner):
which could change what the jupyterhub-singleuser launch command does.
Only use this for trusted users.
"""
)
).tag(config=True)
proc = Instance(Popen,
allow_none=True,

View File

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

View File

@@ -216,11 +216,12 @@ def test_spawn_form(app):
orm_u = orm.User.find(app.db, 'jones')
u = app.users[orm_u]
yield u.stop()
r = yield async_requests.post(ujoin(base_url, 'spawn?next=/user/jones/tree'), cookies=cookies, data={
'bounds': ['-1', '1'],
'energy': '511keV',
})
next_url = ujoin(app.base_url, 'user/jones/tree')
r = yield async_requests.post(
url_concat(ujoin(base_url, 'spawn'), {'next': next_url}),
cookies=cookies,
data={'bounds': ['-1', '1'], 'energy': '511keV'},
)
r.raise_for_status()
assert r.history
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)
cookies = yield app.login_user('admin')
u = add_user(app.db, app=app, name='martha')
next_url = ujoin(app.base_url, 'user', u.name, 'tree')
r = yield async_requests.post(
ujoin(base_url, 'spawn/{0}?next=/user/{0}/tree'.format(u.name)),
cookies=cookies, data={
'bounds': ['-3', '3'],
'energy': '938MeV',
})
url_concat(ujoin(base_url, 'spawn', u.name), {'next': next_url}),
cookies=cookies,
data={'bounds': ['-3', '3'], 'energy': '938MeV'},
)
r.raise_for_status()
assert r.history
assert r.url.startswith(public_url(app, u))
@@ -570,3 +571,12 @@ def test_announcements(app, announcements):
app.authenticator.auto_login = auto_login
r.raise_for_status()
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 '/'
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
name = 'river'
@@ -95,7 +95,7 @@ def test_external_proxy(request):
if app.subdomain_host:
host = '%s.%s' % (name, urlparse(app.subdomain_host).hostname)
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
proxy.terminate()
@@ -113,7 +113,7 @@ def test_external_proxy(request):
# check that the routes are correct
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
proxy.terminate()
@@ -146,7 +146,7 @@ def test_external_proxy(request):
# check that the routes are correct
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
@@ -154,6 +154,8 @@ def test_external_proxy(request):
'zoe',
'50fia',
'秀樹',
'~TestJH',
'has@',
])
def test_check_routes(app, username, disable_check_routes):
proxy = app.proxy

View File

@@ -83,6 +83,10 @@ for d, _, _ in os.walk('jupyterhub'):
if os.path.exists(pjoin(d, '__init__.py')):
packages.append(d.replace(os.path.sep, '.'))
with open('README.md', encoding="utf8") as f:
readme = f.read()
setup_args = dict(
name = 'jupyterhub',
scripts = glob(pjoin('scripts', '*')),
@@ -93,10 +97,11 @@ setup_args = dict(
package_data = get_package_data(),
version = ns['__version__'],
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_email = "jupyter@googlegroups.com",
url = "http://jupyter.org",
url = "https://jupyter.org",
license = "BSD",
platforms = "Linux, Mac OS X",
keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
@@ -109,6 +114,12 @@ setup_args = dict(
'Programming Language :: Python',
'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 -%}
</td>
<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="start-server btn btn-xs btn-primary {% if u.running %}hidden{% endif %}">start 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</a>
</td>
<td class="server-col col-sm-1 text-center">
{% 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 %}
</td>
<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 class="edit-col col-sm-1 text-center">
{% 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 %}
</td>
{% endblock user_row %}

View File

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

View File

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

View File

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