mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 18:14:10 +00:00
Compare commits
103 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
935baa8bc6 | ||
![]() |
9b77732319 | ||
![]() |
85aac0fa2d | ||
![]() |
abd6f35638 | ||
![]() |
ba4700b3f3 | ||
![]() |
05b11bd47a | ||
![]() |
71cb628563 | ||
![]() |
0d664355f0 | ||
![]() |
dd6261d031 | ||
![]() |
f3f5b69e49 | ||
![]() |
9ea4ca3646 | ||
![]() |
8ee9869ca0 | ||
![]() |
6cedd73d2a | ||
![]() |
59145ca0f7 | ||
![]() |
ab02f9c568 | ||
![]() |
a2f003ed31 | ||
![]() |
7b6dd9f5cf | ||
![]() |
0fa5c20f89 | ||
![]() |
204399ee2c | ||
![]() |
5e68dce02f | ||
![]() |
952bbea039 | ||
![]() |
630e85bfec | ||
![]() |
26f7bb51bd | ||
![]() |
a1c2a50810 | ||
![]() |
906abcc2f3 | ||
![]() |
5269370e4a | ||
![]() |
727356870a | ||
![]() |
39aed3a5a0 | ||
![]() |
ed26578717 | ||
![]() |
22863f765f | ||
![]() |
b500bd002b | ||
![]() |
aca40b24c3 | ||
![]() |
b5fe5a80c6 | ||
![]() |
ad073dd5dd | ||
![]() |
7b815558c6 | ||
![]() |
55f58b3ba7 | ||
![]() |
e1f93a4721 | ||
![]() |
2e95f3c039 | ||
![]() |
b0ba51f209 | ||
![]() |
89e6c2110e | ||
![]() |
7dfdc23b4e | ||
![]() |
4c7df53a8a | ||
![]() |
678afd3783 | ||
![]() |
0185a08f32 | ||
![]() |
f3787dd2c8 | ||
![]() |
30f19cfc8c | ||
![]() |
a84fa38c6b | ||
![]() |
867ce4c213 | ||
![]() |
005118e09d | ||
![]() |
04ce67ee71 | ||
![]() |
31807929cb | ||
![]() |
cb4105b53e | ||
![]() |
151887dd56 | ||
![]() |
5f97487184 | ||
![]() |
4d2d677777 | ||
![]() |
6a3b3807c9 | ||
![]() |
02a52a0289 | ||
![]() |
7bd1e387df | ||
![]() |
edc0d7901f | ||
![]() |
8e561f1c12 | ||
![]() |
24d87c882f | ||
![]() |
1e333e2f29 | ||
![]() |
a507fa1c8a | ||
![]() |
90cc03b3ec | ||
![]() |
6f15113e2a | ||
![]() |
f3f08c9caa | ||
![]() |
c495c4731a | ||
![]() |
e08a50ef66 | ||
![]() |
fbcd792062 | ||
![]() |
bb81ce0160 | ||
![]() |
315087d67c | ||
![]() |
31e6a15a85 | ||
![]() |
aed99d8d19 | ||
![]() |
ec83708892 | ||
![]() |
bedac5f148 | ||
![]() |
376aa13981 | ||
![]() |
4bc8b48763 | ||
![]() |
21496890f6 | ||
![]() |
70dcd50e44 | ||
![]() |
24094567e5 | ||
![]() |
6bd0febbe1 | ||
![]() |
57075aba52 | ||
![]() |
f0260aae52 | ||
![]() |
edd8e21f71 | ||
![]() |
681d3ce2d8 | ||
![]() |
97e792ccde | ||
![]() |
a5a0543b2a | ||
![]() |
5a810ccba3 | ||
![]() |
0a6b2cdadc | ||
![]() |
08903e7af8 | ||
![]() |
78439329c0 | ||
![]() |
4dfd6bc4b9 | ||
![]() |
574cc39b5f | ||
![]() |
6fb43a8241 | ||
![]() |
84c82fe382 | ||
![]() |
5e45e76f5b | ||
![]() |
92fd819cd6 | ||
![]() |
cb5ef0c302 | ||
![]() |
34fab033fe | ||
![]() |
37f4c4429e | ||
![]() |
293410ec94 | ||
![]() |
ed6ee27dcd | ||
![]() |
ca16ddb7ad |
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
7
.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md
vendored
Normal 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
0
.github/PULL_REQUEST_TEMPLATE/.keep
vendored
Normal file
29
.github/issue_template.md
vendored
29
.github/issue_template.md
vendored
@@ -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
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
@@ -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
1
CODE_OF_CONDUCT.md
Normal 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).
|
@@ -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
1
PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
10
README.md
10
README.md
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
|
|
||||||
[](https://pypi.python.org/pypi/jupyterhub)
|
[](https://pypi.python.org/pypi/jupyterhub)
|
||||||
[](http://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
[](https://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
||||||
[](http://jupyterhub.readthedocs.io/en/0.7.2/?badge=0.7.2)
|
[](https://jupyterhub.readthedocs.io/en/0.7.2/?badge=0.7.2)
|
||||||
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
||||||
[](https://circleci.com/gh/jupyterhub/jupyterhub)
|
[](https://circleci.com/gh/jupyterhub/jupyterhub)
|
||||||
[](https://codecov.io/github/jupyterhub/jupyterhub?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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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`
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
```
|
```
|
||||||
|
147
docs/source/reference/config-user-env.md
Normal file
147
docs/source/reference/config-user-env.md
Normal 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.
|
@@ -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
|
||||||
|
@@ -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).
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -130,4 +130,4 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
```
|
```
|
||||||
|
@@ -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:
|
||||||
|
60
examples/service-announcement/README.md
Normal file
60
examples/service-announcement/README.md
Normal 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.
|
73
examples/service-announcement/announcement.py
Normal file
73
examples/service-announcement/announcement.py
Normal 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()
|
15
examples/service-announcement/jupyterhub_config.py
Normal file
15
examples/service-announcement/jupyterhub_config.py
Normal 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"]
|
14
examples/service-announcement/templates/page.html
Normal file
14
examples/service-announcement/templates/page.html
Normal 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 %}
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
|
@@ -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),
|
||||||
|
@@ -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),
|
||||||
|
@@ -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):
|
||||||
|
@@ -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('/')
|
||||||
|
@@ -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,
|
||||||
|
@@ -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 ''
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
@@ -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"}
|
||||||
|
@@ -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
|
||||||
|
15
setup.py
15
setup.py
@@ -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',
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
|
@@ -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 %}
|
||||||
|
@@ -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 %}
|
||||||
|
@@ -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);
|
||||||
|
@@ -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 %}
|
||||||
|
Reference in New Issue
Block a user