Compare commits

...

169 Commits

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

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

Any successful spawn will reset the count to 0

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

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

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

https://bugs.python.org/issue20614

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

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

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

This ensures that managed service processes talk to the hub over localhost in this case, rather than via the hostname.
2018-06-22 13:48:34 +02:00
Min RK
8e561f1c12 avoid triggering a spawn from API requests to a not-running server
this avoids left-open notebook tabs from respawning a culled server indefinitely
2018-06-20 14:57:41 +02:00
Kenan Erdogan
24d87c882f fix wrong/missing closing tags in templates 2018-06-19 09:15:18 +02:00
Min RK
1e333e2f29 Merge pull request #1992 from willingc/doc-toc
add templates and user env docs to home page index
2018-06-18 15:45:52 +02:00
Carol Willing
a507fa1c8a add templates and user env docs to home page index 2018-06-16 10:21:33 -07:00
Min RK
90cc03b3ec back to dev 2018-06-15 15:39:02 +02:00
Min RK
6f15113e2a link and date for 0.9.0 2018-06-15 15:36:48 +02:00
Min RK
f3f08c9caa 0.9.0 2018-06-15 15:23:25 +02:00
Min RK
c495c4731a Merge pull request #1983 from willingc/test-tilde
add test case for user with tilde
2018-06-15 14:48:49 +02:00
Min RK
e08a50ef66 Merge pull request #1988 from gesiscss/redirects
fix AddSlashHandler for hub_prefix without trailing /
2018-06-15 14:48:14 +02:00
Min RK
fbcd792062 Merge pull request #1984 from chicocvenancio/tilde_safe_in_proxy
mark tilde as safe in proxy routespec quoting FIX:#1982
2018-06-15 14:38:38 +02:00
Min RK
bb81ce0160 also test @ handling in proxy.check_routes
@ and ~ should be the same
2018-06-15 14:33:31 +02:00
Kenan Erdogan
315087d67c fix AddSlashHandler for hub_prefix without trailing / 2018-06-15 13:36:05 +02:00
Chico Venancio
31e6a15a85 mark tilde as safe in proxy routespec quoting FIX:#1982 2018-06-14 18:18:52 -03:00
Carol Willing
aed99d8d19 add test case for user with tilde 2018-06-14 13:24:05 -07:00
Min RK
ec83708892 use pid file to check for previous proxy instances
avoids failure to start when the previous proxy wasn't cleaned up properly
2018-06-14 17:50:33 +02:00
Min RK
bedac5f148 Merge pull request #1980 from willingc/pypi-meta
Add info to display at pypi site
2018-06-14 11:51:29 +02:00
Carol Willing
376aa13981 correct link 2018-06-13 14:37:27 -07:00
Carol Willing
4bc8b48763 add info to display at pypi site 2018-06-13 14:35:23 -07:00
Carol Willing
21496890f6 Remove stray bullet that I missed in review 2018-06-13 11:10:41 -07:00
Carol Willing
70dcd50e44 Merge pull request #1976 from minrk/changelog-more
last few things in changelog
2018-06-13 11:09:30 -07:00
Min RK
24094567e5 Merge pull request #1977 from kpfleming/patch-1
Correct 'conda' installation instructions
2018-06-13 15:44:27 +02:00
Kevin P. Fleming
6bd0febbe1 Correct 'conda' installation instructions
JupyterHub packages are in the 'conda-forge' channel of Anaconda packages; if the Anaconda installation doesn't already have 'conda-forge' enabled, `conda install jupyterhub` fails.

Rather than adding instructions to enable 'conda-forge' in Anaconda, this patch modifies the installation command to specify that channel.
2018-06-13 09:42:05 -04:00
Min RK
57075aba52 Add last few entries in changelog for 0.9 2018-06-13 15:15:18 +02:00
Min RK
f0260aae52 add missing expiry fields in rest-api doc 2018-06-13 15:15:09 +02:00
Min RK
edd8e21f71 Merge pull request #1969 from willingc/edit-userenv
Edit and reflow user environment reference
2018-06-13 09:49:23 +02:00
Min RK
681d3ce2d8 Merge pull request #1971 from willingc/contributor-list
Update contributor list for 0.9 release
2018-06-13 09:45:37 +02:00
Carol Willing
97e792ccde Update issue templates 2018-06-12 15:47:05 -07:00
Carol Willing
a5a0543b2a Delete old issue template 2018-06-12 15:42:46 -07:00
Carol Willing
5a810ccba3 Update issue templates 2018-06-12 15:41:30 -07:00
Carol Willing
0a6b2cdadc Merge pull request #1973 from jupyterhub/willingc-patch-1
Create CODE_OF_CONDUCT.md
2018-06-12 15:33:37 -07:00
Carol Willing
08903e7af8 Create PULL_REQUEST_TEMPLATE.md 2018-06-12 15:29:54 -07:00
Carol Willing
78439329c0 Merge pull request #1972 from willingc/insights
Move issue template one level down in .github directory
2018-06-12 15:28:34 -07:00
Carol Willing
4dfd6bc4b9 Create CODE_OF_CONDUCT.md 2018-06-12 15:25:27 -07:00
Carol Willing
574cc39b5f set up pull request template directory 2018-06-12 15:16:02 -07:00
Carol Willing
6fb43a8241 update issue templaate location to current github recommendation 2018-06-12 15:13:39 -07:00
Carol Willing
84c82fe382 update the contributor list for 0.9 2018-06-12 14:51:51 -07:00
Carol Willing
5e45e76f5b update contributors for 0.9 2018-06-12 14:36:00 -07:00
Carol Willing
92fd819cd6 Merge pull request #1970 from JasonJWilliamsNY/hub-not-found-at-localhost
Hub not found at localhost
2018-06-12 14:04:09 -07:00
Jason Williams
cb5ef0c302 Update troubleshooting.md 2018-06-12 17:01:37 -04:00
Jason Williams
34fab033fe Jupyterhub on Docker add workaround for unable to connect to localhost
Added a command that worked for me to fix the situation that localhost:8000 is unable to reach the hub even though the published command for Docker exposes the correct port.
2018-06-12 16:59:17 -04:00
Carol Willing
37f4c4429e edit and reflow user environment reference 2018-06-12 08:47:22 -07:00
Carol Willing
293410ec94 Merge pull request #1967 from minrk/config-docs
docs: configuring user environments
2018-06-12 07:55:53 -07:00
Min RK
ed6ee27dcd docs: configuring user environments
covers system-wide installation, kernelspec registration, and the differences between containers and host systems
2018-06-12 14:34:26 +02:00
Min RK
ca16ddb7ad back to dev 2018-06-12 14:21:16 +02:00
Min RK
2102c1fd1c 0.9.0rc1 2018-06-12 14:19:59 +02:00
Min RK
aa9676ec5e Merge pull request #1913 from rkdarst/announcement_text
Add customizable announcement text on home,login,logout,spawn
2018-06-12 14:14:21 +02:00
Min RK
5e93c7de4c announcement doc language
per willingc review
2018-06-12 13:48:42 +02:00
Min RK
d22626906b multiline conditionals setting announcement variable in templates
for readability per review by willingc
2018-06-12 13:48:24 +02:00
Min RK
5f91ed044e parametrize test_announcements 2018-06-12 13:47:55 +02:00
Min RK
5c3c7493c1 Merge pull request #1963 from willingc/hooks-doc
add a small section for pre/post spawn hooks
2018-06-11 15:27:39 +02:00
Carol Willing
1b7965092e remove backticks and long for rst format 2018-06-08 14:21:31 -07:00
Carol Willing
ef60be5a99 put backticks outside of link 2018-06-08 14:19:43 -07:00
Carol Willing
f78d652cd6 fix missing brackets 2018-06-08 14:18:14 -07:00
Carol Willing
3650575797 add a small section for pre/post spawn hooks 2018-06-08 14:13:45 -07:00
Tim Head
0f000f6d41 Merge pull request #1961 from willingc/doc-shib
Add link to authenticators used with Shibboleth
2018-06-08 18:17:08 +02:00
Carol Willing
643729ac0c Merge pull request #1962 from chicocvenancio/docs_mysql_dynamic
database docs utfmb4 collation and some versions of mysql/mariadb
2018-06-08 09:14:04 -07:00
Chico Venancio
91a67bf580 database docs: fix formatting 2018-06-08 13:09:09 -03:00
Chico Venancio
c75eddb730 database docs utfmb4 collation and some versions of mysql/mariadb 2018-06-08 12:55:02 -03:00
Carol Willing
0f5888ad6c Add link to authenticators used with Shibboleth 2018-06-08 08:22:11 -07:00
Carol Willing
8c48f3b856 Merge pull request #1960 from willingc/db-doc
add database doc section and edits to upgrading db
2018-06-08 08:08:51 -07:00
Carol Willing
6e7e18bc3c add @minrk review comments 2018-06-08 07:34:09 -07:00
Tim Head
3dfd7e5a84 Merge pull request #1958 from willingc/proxy-error
Add error message text
2018-06-08 15:19:27 +02:00
Carol Willing
19ecbf3734 add note about why no sqlite and nfs 2018-06-08 06:06:15 -07:00
Carol Willing
eac3e8ba90 add database doc section and edits to upgrading db 2018-06-08 05:51:00 -07:00
Carol Willing
a7a6829b69 add additional reference per @betatim review 2018-06-08 05:01:32 -07:00
Carol Willing
61299113c8 add error message text 2018-06-07 21:44:18 -07:00
Tim Head
21a57dfa0b Merge pull request #1949 from willingc/npm-doc
clarify that conda installs npm and proxy
2018-06-07 19:52:00 +02:00
Carol Willing
a7226a8231 changes per @minrk review 2018-06-07 09:10:04 -07:00
Min RK
6e3dd21f60 Merge pull request #1952 from willingc/docker-conda
bump miniconda to 4.5.1 in Dockerfile
2018-06-07 10:24:33 +02:00
Min RK
cf049730d4 Merge pull request #1954 from willingc/black-test
Blacken python doc build files
2018-06-07 10:24:14 +02:00
Min RK
cb9ce4d3af Merge pull request #1955 from dtaniwaki/handle-fatal-error
only relay headers from HTTPErrors
2018-06-07 10:22:38 +02:00
Daisuke Taniwaki
925ee1dfb2 Do not refer spawner on fatal errors 2018-06-07 14:53:46 +09:00
Daisuke Taniwaki
5d9122b26c Avoid setting unexpected headers 2018-06-07 14:53:34 +09:00
Carol Willing
6821ad0c59 blacken autodoc sphinx extension 2018-06-06 12:57:14 -07:00
Carol Willing
ff7851ee2e blacken conf.py 2018-06-06 12:52:30 -07:00
Carol Willing
6940ed85b1 bump miniconda to 4.5.1 2018-06-06 08:25:28 -07:00
Carol Willing
3d497a7f43 clarify that conda installs npm and proxy 2018-06-06 06:56:22 -07:00
Carol Willing
cc6968e225 Merge pull request #1942 from minrk/nginx-file
note where nginx config files are typically created.
2018-06-06 06:02:30 -07:00
Carol Willing
a6c517c344 Merge pull request #1947 from minrk/progress-stopping
Avoid showing spawn-pending page when user is stopping
2018-06-06 06:00:58 -07:00
Carol Willing
a3e08b7f52 Merge pull request #1948 from minrk/aclosing
Python 3.5.1 cannot close async iterators
2018-06-06 05:56:00 -07:00
Min RK
14c8d7dc46 Merge pull request #1946 from dtaniwaki/configure-max-inactive-duration
Configure max inactive duration
2018-06-06 12:54:55 +02:00
Daisuke Taniwaki
ac2590c679 Add active_user_window configuration 2018-06-06 19:00:34 +09:00
Min RK
ead13c6a11 further clarify that we are creating a new file, not editing nginx.confg 2018-06-06 12:00:21 +02:00
Min RK
5002ab2990 Python 3.5.1 cannot close async iterators
so provide a null aclosing async context manager that does nothing
2018-06-06 11:43:33 +02:00
Min RK
ab3e7293a4 disable my server link while stop is pending
makes it a little harder to request a spawn while stop is pending
2018-06-06 10:53:50 +02:00
Min RK
062af5e5cb Avoid showing spawn_pending page when pending action is stop
Separate stop_pending page when this occurs,
similar to the old spawn pending spinner without progress events
2018-06-06 10:53:05 +02:00
Carol Willing
92088570ea Merge pull request #1943 from minrk/getuser-delayed
delay call to getuser in token app
2018-06-05 10:18:08 -07:00
Min RK
604ccf515d delay call to getuser in token app
avoids issues with getuser preventing launch, e.g. in weird containers where the current user doesn’t exist
2018-06-05 17:52:00 +02:00
Min RK
ec9b244990 note where nginx config files are typically created. 2018-06-04 11:10:21 +02:00
Min RK
09acdc23b5 Merge pull request #1940 from dtaniwaki/fix-created-columne-error
Handle NULL created column of tokens table
2018-06-04 10:55:20 +02:00
Richard Darst
e7808b50af Add tests of page announcements
- Adds test_pages.py:test_page_contents, which currently tests just
  the page annoucement variables.
2018-06-03 01:18:48 +03:00
Richard Darst
9c27095744 Add customizable announcement text on home,login,logout,spawn
- Using the new template_vars setting (#1872), allow the variable
  `announcement` to create a header message on all the pages in the
  title, or the variables `announcement_{home,login,logout,spawn}` to
  set variables on these single pages.
- This is not the most powerful method of putting an announcement into
  the templates, because it requires a server restart to change.  But
  the invasiveness is very low, and allows minimal message
  without having to touch the templates themselves.
- Closes: #1836
2018-06-03 01:18:48 +03:00
Daisuke Taniwaki
690b07982e Handle NULL created column of api_tokens table 2018-06-02 23:55:21 +09:00
Min RK
784e5aa4ee Merge pull request #1926 from minrk/tilde-safe
tilde is a safe character in user URLs
2018-05-30 14:48:35 +02:00
Min RK
29187cab3a Merge pull request #1929 from minrk/pgbin
install psycopg2 from binary
2018-05-29 11:03:41 +02:00
Min RK
43a72807c6 install psycopg2 from binary
it has a new package name for the binary wheel
2018-05-29 10:41:53 +02:00
Min RK
1d1f6f1870 Merge pull request #1923 from nxg/doc-changes-1747
Documentation clarifications (adding explicitness).
2018-05-29 10:21:42 +02:00
Min RK
505a6eb4e3 ensure user subdomains are valid
escape with `_` instead of `%`.

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

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

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

1
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1 @@

View File

@@ -11,8 +11,8 @@
[![PyPI](https://img.shields.io/pypi/v/jupyterhub.svg)](https://pypi.python.org/pypi/jupyterhub)
[![Documentation Status](https://readthedocs.org/projects/jupyterhub/badge/?version=latest)](http://jupyterhub.readthedocs.org/en/latest/?badge=latest)
[![Documentation Status](http://readthedocs.org/projects/jupyterhub/badge/?version=0.7.2)](http://jupyterhub.readthedocs.io/en/0.7.2/?badge=0.7.2)
[![Documentation Status](https://readthedocs.org/projects/jupyterhub/badge/?version=latest)](https://jupyterhub.readthedocs.org/en/latest/?badge=latest)
[![Documentation Status](http://readthedocs.org/projects/jupyterhub/badge/?version=0.7.2)](https://jupyterhub.readthedocs.io/en/0.7.2/?badge=0.7.2)
[![Build Status](https://travis-ci.org/jupyterhub/jupyterhub.svg?branch=master)](https://travis-ci.org/jupyterhub/jupyterhub)
[![Circle CI](https://circleci.com/gh/jupyterhub/jupyterhub.svg?style=shield&circle-token=b5b65862eb2617b9a8d39e79340b0a6b816da8cc)](https://circleci.com/gh/jupyterhub/jupyterhub)
[![codecov.io](https://codecov.io/github/jupyterhub/jupyterhub/coverage.svg?branch=master)](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
@@ -50,37 +50,62 @@ for administration of the Hub and its users.
## Installation
### Check prerequisites
A Linux/Unix based system with the following:
- A Linux/Unix based system
- [Python](https://www.python.org/downloads/) 3.5 or greater
- [nodejs/npm](https://www.npmjs.com/)
- [Python](https://www.python.org/downloads/) 3.4 or greater
- [nodejs/npm](https://www.npmjs.com/) Install a recent version of
[nodejs/npm](https://docs.npmjs.com/getting-started/installing-node)
For example, install it on Linux (Debian/Ubuntu) using:
* If you are using **`conda`**, the nodejs and npm dependencies will be installed for
you by conda.
sudo apt-get install npm nodejs-legacy
* If you are using **`pip`**, install a recent version of
[nodejs/npm](https://docs.npmjs.com/getting-started/installing-node).
For example, install it on Linux (Debian/Ubuntu) using:
The `nodejs-legacy` package installs the `node` executable and is currently
required for npm to work on Debian/Ubuntu.
```
sudo apt-get install npm nodejs-legacy
```
The `nodejs-legacy` package installs the `node` executable and is currently
required for npm to work on Debian/Ubuntu.
- TLS certificate and key for HTTPS communication
- Domain name
### Install packages
#### Using `conda`
To install JupyterHub along with its dependencies including nodejs/npm:
```bash
conda install -c conda-forge jupyterhub
```
If you plan to run notebook servers locally, install the Jupyter notebook
or JupyterLab:
```bash
conda install notebook
conda install jupyterlab
```
#### Using `pip`
JupyterHub can be installed with `pip`, and the proxy with `npm`:
```bash
npm install -g configurable-http-proxy
pip3 install jupyterhub
python3 -m pip install jupyterhub
```
If you plan to run notebook servers locally, you will need to install the
[Jupyter notebook](https://jupyter.readthedocs.io/en/latest/install.html)
package:
pip3 install --upgrade notebook
python3 -m pip install --upgrade notebook
### Run the Hub server
@@ -99,7 +124,7 @@ more configuration of the system.
## 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.
The [**JupyterHub tutorial**](https://github.com/jupyterhub/jupyterhub-tutorial)
@@ -208,7 +233,7 @@ our JupyterHub [Gitter](https://gitter.im/jupyterhub/jupyterhub) channel.
- [Reporting Issues](https://github.com/jupyterhub/jupyterhub/issues)
- [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 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)

View File

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

View File

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

View File

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

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
@@ -689,6 +700,11 @@ definitions:
description: The command used to start the service (if managed)
items:
type: string
info:
type: object
description: |
Additional information a deployment can attach to a service.
JupyterHub does not use this field.
Token:
type: object
properties:
@@ -711,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,30 @@ command line for details.
## 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.
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.
- 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 +142,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 +402,10 @@ 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.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.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

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

@@ -38,6 +38,8 @@ with any provider, is also available.
- ldapauthenticator for LDAP
- tmpauthenticator for temporary accounts
- For Shibboleth, [jhub_shibboleth_auth](https://github.com/gesiscss/jhub_shibboleth_auth)
and [jhub_remote_user_authenticator](https://github.com/cwaldbieser/jhub_remote_user_authenticator)
## Technical Overview of Authentication
@@ -206,7 +208,13 @@ class MyAuthenticator(Authenticator):
spawner.environment['UPSTREAM_TOKEN'] = auth_state['upstream_token']
```
## pre_spawn_start and post_spawn_stop hooks
Authenticators uses two hooks, [pre_spawn_start(user, spawner)][] and
[post_spawn_stop(user, spawner)][] to add pass additional state information
between the authenticator and a spawner. These hooks are typically used auth-related
startup, i.e. opening a PAM session, and auth-related cleanup, i.e. closing a
PAM session.
## JupyterHub as an OAuth provider
@@ -218,5 +226,5 @@ Beginning with version 0.8, JupyterHub is an OAuth provider.
[OAuth]: https://en.wikipedia.org/wiki/OAuth
[GitHub OAuth]: https://developer.github.com/v3/oauth/
[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
[post_spawn_stop(user, spawner)]: http://jupyterhub.readthedocs.io/en/latest/api/auth.html#jupyterhub.auth.Authenticator.post_spawn_stop
[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)]: https://jupyterhub.readthedocs.io/en/latest/api/auth.html#jupyterhub.auth.Authenticator.post_spawn_stop

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,8 +11,10 @@ Technical Reference
services
proxy
rest
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
@@ -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,
or via the `JUPYTERHUB_API_TOKEN` environment variable.
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)
and in the
[`HubAuth.user_for_token`](services.auth.html#jupyterhub.services.auth.HubAuth.user_for_token)
Most of the logic for authentication implementation is found in the
[`HubAuth.user_for_cookie`][HubAuth.user_for_cookie]
and in the
[`HubAuth.user_for_token`][HubAuth.user_for_token]
methods, which makes a request of the Hub, and returns:
- 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
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).
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.
[requests]: http://docs.python-requests.org/en/master/
[services_auth]: ../api/services.auth.html
[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
[nbviewer example]: https://github.com/jupyter/nbviewer#securing-the-notebook-viewer

View File

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

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

View File

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

View File

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

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

View File

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

View File

@@ -186,10 +186,16 @@ def cull_idle(url, api_token, inactive_limit, cull_users=False, max_age=0, concu
log_name, format_td(age), format_td(inactive))
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(
url=url + '/users/%s/server' % quote(user['name']),
method='DELETE',
headers=auth_header,
url=delete_url, method='DELETE', headers=auth_header,
)
resp = yield fetch(req)
if resp.code == 202:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -140,7 +140,10 @@ class NewToken(Application):
ab01cd23ef45
"""
name = Unicode(getuser())
name = Unicode()
@default('name')
def _default_name(self):
return getuser()
aliases = token_aliases
classes = []
@@ -274,6 +277,9 @@ class JupyterHub(Application):
service_check_interval = Integer(60,
help="Interval (in seconds) at which to check connectivity of services with web endpoints."
).tag(config=True)
active_user_window = Integer(30 * 60,
help="Duration (in seconds) to determine the number of active users."
).tag(config=True)
data_files_path = Unicode(DATA_FILES_PATH,
help="The location of jupyterhub data files (e.g. /usr/local/share/jupyterhub)"
@@ -970,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),
@@ -1104,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
@@ -1786,8 +1801,7 @@ class JupyterHub(Application):
spawner.last_activity = max(spawner.last_activity, dt)
else:
spawner.last_activity = dt
# FIXME: Make this configurable duration. 30 minutes for now!
if (now - user.last_activity).total_seconds() < 30 * 60:
if (now - user.last_activity).total_seconds() < self.active_user_window:
active_users_count += 1
self.statsd.gauge('users.running', users_count)
self.statsd.gauge('users.active', active_users_count)
@@ -1907,8 +1921,7 @@ class JupyterHub(Application):
def sigterm(self, signum, frame):
self.log.critical("Received SIGTERM, shutting down")
self.io_loop.stop()
self.atexit()
raise SystemExit(128 + signum)
_atexit_ran = False
@@ -1918,6 +1931,7 @@ class JupyterHub(Application):
return
self._atexit_ran = True
# 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()
loop = IOLoop()
loop.make_current()

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
@@ -40,7 +41,8 @@ reasons = {
'timeout': "Failed to reach your server."
" Please try again later."
" Contact admin if the issue persists.",
'error': "Failed to start your server. Please contact admin.",
'error': "Failed to start your server on the last attempt. "
" Please contact admin if the issue persists.",
}
# constant, not configurable
@@ -655,6 +657,7 @@ class BaseHandler(RequestHandler):
# hook up spawner._spawn_future so that other requests can await
# this result
finish_spawn_future = spawner._spawn_future = maybe_future(finish_user_spawn())
def _clear_spawn_future(f):
# clear spawner._spawn_future when it's done
# keep an exception around, though, to prevent repeated implicit spawns
@@ -663,10 +666,44 @@ class BaseHandler(RequestHandler):
spawner._spawn_future = None
# Now we're all done. clear _spawn_pending flag
spawner._spawn_pending = False
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:
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:
# waiting_for_response indicates server process has started,
# but is yet to become responsive.
@@ -826,19 +863,27 @@ class BaseHandler(RequestHandler):
)
self.set_header('Content-Type', 'text/html')
# allow setting headers from exceptions
# since exception handler clears headers
headers = getattr(exception, 'headers', None)
if headers:
for key, value in headers.items():
self.set_header(key, value)
if isinstance(exception, web.HTTPError):
# allow setting headers from exceptions
# since exception handler clears headers
headers = getattr(exception, 'headers', None)
if headers:
for key, value in headers.items():
self.set_header(key, value)
# Content-Length must be recalculated.
self.clear_header('Content-Length')
# render the template
try:
html = self.render_template('%s.html' % status_code, **ns)
except TemplateNotFound:
self.log.debug("No template for %d", status_code)
html = self.render_template('error.html', **ns)
try:
html = self.render_template('error.html', **ns)
except:
# In this case, any side effect must be avoided.
ns['no_spawner_check'] = True
html = self.render_template('error.html', **ns)
self.write(html)
@@ -856,6 +901,11 @@ class PrefixRedirectHandler(BaseHandler):
"""
def get(self):
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):
path = self.request.uri[len(self.base_url):]
else:
@@ -875,6 +925,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 = '/'
@@ -901,6 +958,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
@@ -941,7 +1003,7 @@ class UserSpawnHandler(BaseHandler):
raise copy.copy(exc).with_traceback(exc.__traceback__)
# check for pending spawn
if spawner.pending and spawner._spawn_future:
if spawner.pending == 'spawn' and spawner._spawn_future:
# wait on the pending spawn
self.log.debug("Waiting for %s pending %s", spawner._log_name, spawner.pending)
try:
@@ -951,14 +1013,20 @@ class UserSpawnHandler(BaseHandler):
pass
# we may have waited above, check pending again:
# page could be pending spawn *or* stop
if spawner.pending:
self.log.info("%s is pending %s", spawner._log_name, spawner.pending)
# spawn has started, but not finished
self.statsd.incr('redirects.user_spawn_pending', 1)
url_parts = []
if spawner.pending == "stop":
page = "stop_pending.html"
else:
page = "spawn_pending.html"
html = self.render_template(
"spawn_pending.html",
page,
user=user,
spawner=spawner,
progress_url=spawner._progress_url,
)
self.finish(html)
@@ -1104,6 +1172,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,8 +439,22 @@ 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 _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):
super().__init__(**kwargs)
# check for required token if proxy is external
@@ -448,9 +464,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:
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):
"""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()
@@ -488,11 +567,16 @@ class ConfigurableHTTPProxy(Proxy):
except FileNotFoundError as e:
self.log.error(
"Failed to find proxy %r\n"
"The proxy can be installed with `npm install -g configurable-http-proxy`"
"The proxy can be installed with `npm install -g configurable-http-proxy`."
"To install `npm`, install nodejs which includes `npm`."
"If you see an `EACCES` error or permissions error, refer to the `npm` "
"documentation on How To Prevent Permissions Errors."
% self.command
)
raise
self._write_pid_file()
def _check_process():
status = self.proxy_process.poll()
if status is not None:
@@ -516,15 +600,31 @@ class ConfigurableHTTPProxy(Proxy):
self._check_running_callback = pc
pc.start()
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)
if self._check_running_callback is not None:
self._check_running_callback.stop()
if self.proxy_process.poll() is None:
try:
self.proxy_process.terminate()
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"""
@@ -533,6 +633,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()
@@ -558,7 +659,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,8 +39,10 @@ A hub-managed service with no URL::
"""
import copy
import pipes
import shutil
import os
from subprocess import Popen
from traitlets import (
@@ -105,6 +107,8 @@ class _ServiceSpawner(LocalProcessSpawner):
def start(self):
"""Start the process"""
env = self.get_env()
if os.name == 'nt':
env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
cmd = self.cmd
self.log.info("Spawning %s", ' '.join(pipes.quote(s) for s in cmd))
@@ -175,6 +179,13 @@ class Service(LoggingConfigurable):
If unspecified, an API token will be generated for managed services.
"""
).tag(input=True)
info = Dict(
help="""Provide a place to include miscellaneous information about the service,
provided through the configuration
"""
).tag(input=True)
# Managed service API:
spawner = Any()
@@ -297,6 +308,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

@@ -195,6 +195,19 @@ class Spawner(LoggingConfigurable):
"""
).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,
help="""
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.
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
@@ -1525,6 +1525,7 @@ def test_get_services(app, mockservice_url):
'pid': mockservice.proc.pid,
'prefix': mockservice.server.base_url,
'url': mockservice.url,
'info': {},
}
}
@@ -1551,6 +1552,7 @@ def test_get_service(app, mockservice_url):
'pid': mockservice.proc.pid,
'prefix': mockservice.server.base_url,
'url': mockservice.url,
'info': {},
}
r = yield api_request(app, 'services/%s' % mockservice.name,

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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">
@@ -73,7 +71,11 @@
{%- endif -%}
</td>
<td class="time-col col-sm-3">
{%- if token.created -%}
{{ token.created.isoformat() + 'Z' }}
{%- else -%}
N/A
{%- endif -%}
</td>
<td class="col-sm-1 text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
@@ -107,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">
@@ -118,11 +120,14 @@
{%- endif -%}
</td>
<td class="time-col col-sm-3">
{%- if client['created'] -%}
{{ client['created'].isoformat() + 'Z' }}
{%- else -%}
N/A
{%- endif -%}
</td>
<td class="col-sm-1 text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</a>
</button>
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
{% endblock client_row %}
</tr>
{% endfor %}
@@ -131,11 +136,11 @@
</div>
{% endif %}
</div>
{% endblock %}
{% endblock main %}
{% block script %}
{{ super() }}
<script type="text/javascript">
require(["token"]);
</script>
{% endblock %}
{% endblock script %}