mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 10:34:10 +00:00
Compare commits
1649 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
935baa8bc6 | ||
![]() |
9b77732319 | ||
![]() |
85aac0fa2d | ||
![]() |
abd6f35638 | ||
![]() |
ba4700b3f3 | ||
![]() |
05b11bd47a | ||
![]() |
71cb628563 | ||
![]() |
0d664355f0 | ||
![]() |
dd6261d031 | ||
![]() |
f3f5b69e49 | ||
![]() |
9ea4ca3646 | ||
![]() |
8ee9869ca0 | ||
![]() |
6cedd73d2a | ||
![]() |
59145ca0f7 | ||
![]() |
ab02f9c568 | ||
![]() |
a2f003ed31 | ||
![]() |
7b6dd9f5cf | ||
![]() |
0fa5c20f89 | ||
![]() |
204399ee2c | ||
![]() |
5e68dce02f | ||
![]() |
952bbea039 | ||
![]() |
630e85bfec | ||
![]() |
26f7bb51bd | ||
![]() |
a1c2a50810 | ||
![]() |
906abcc2f3 | ||
![]() |
5269370e4a | ||
![]() |
727356870a | ||
![]() |
39aed3a5a0 | ||
![]() |
ed26578717 | ||
![]() |
22863f765f | ||
![]() |
b500bd002b | ||
![]() |
aca40b24c3 | ||
![]() |
b5fe5a80c6 | ||
![]() |
ad073dd5dd | ||
![]() |
7b815558c6 | ||
![]() |
55f58b3ba7 | ||
![]() |
e1f93a4721 | ||
![]() |
2e95f3c039 | ||
![]() |
b0ba51f209 | ||
![]() |
89e6c2110e | ||
![]() |
7dfdc23b4e | ||
![]() |
4c7df53a8a | ||
![]() |
678afd3783 | ||
![]() |
0185a08f32 | ||
![]() |
f3787dd2c8 | ||
![]() |
30f19cfc8c | ||
![]() |
a84fa38c6b | ||
![]() |
867ce4c213 | ||
![]() |
005118e09d | ||
![]() |
04ce67ee71 | ||
![]() |
31807929cb | ||
![]() |
cb4105b53e | ||
![]() |
151887dd56 | ||
![]() |
5f97487184 | ||
![]() |
4d2d677777 | ||
![]() |
6a3b3807c9 | ||
![]() |
02a52a0289 | ||
![]() |
7bd1e387df | ||
![]() |
edc0d7901f | ||
![]() |
8e561f1c12 | ||
![]() |
24d87c882f | ||
![]() |
1e333e2f29 | ||
![]() |
a507fa1c8a | ||
![]() |
90cc03b3ec | ||
![]() |
6f15113e2a | ||
![]() |
f3f08c9caa | ||
![]() |
c495c4731a | ||
![]() |
e08a50ef66 | ||
![]() |
fbcd792062 | ||
![]() |
bb81ce0160 | ||
![]() |
315087d67c | ||
![]() |
31e6a15a85 | ||
![]() |
aed99d8d19 | ||
![]() |
ec83708892 | ||
![]() |
bedac5f148 | ||
![]() |
376aa13981 | ||
![]() |
4bc8b48763 | ||
![]() |
21496890f6 | ||
![]() |
70dcd50e44 | ||
![]() |
24094567e5 | ||
![]() |
6bd0febbe1 | ||
![]() |
57075aba52 | ||
![]() |
f0260aae52 | ||
![]() |
edd8e21f71 | ||
![]() |
681d3ce2d8 | ||
![]() |
97e792ccde | ||
![]() |
a5a0543b2a | ||
![]() |
5a810ccba3 | ||
![]() |
0a6b2cdadc | ||
![]() |
08903e7af8 | ||
![]() |
78439329c0 | ||
![]() |
4dfd6bc4b9 | ||
![]() |
574cc39b5f | ||
![]() |
6fb43a8241 | ||
![]() |
84c82fe382 | ||
![]() |
5e45e76f5b | ||
![]() |
92fd819cd6 | ||
![]() |
cb5ef0c302 | ||
![]() |
34fab033fe | ||
![]() |
37f4c4429e | ||
![]() |
293410ec94 | ||
![]() |
ed6ee27dcd | ||
![]() |
ca16ddb7ad | ||
![]() |
2102c1fd1c | ||
![]() |
aa9676ec5e | ||
![]() |
5e93c7de4c | ||
![]() |
d22626906b | ||
![]() |
5f91ed044e | ||
![]() |
5c3c7493c1 | ||
![]() |
1b7965092e | ||
![]() |
ef60be5a99 | ||
![]() |
f78d652cd6 | ||
![]() |
3650575797 | ||
![]() |
0f000f6d41 | ||
![]() |
643729ac0c | ||
![]() |
91a67bf580 | ||
![]() |
c75eddb730 | ||
![]() |
0f5888ad6c | ||
![]() |
8c48f3b856 | ||
![]() |
6e7e18bc3c | ||
![]() |
3dfd7e5a84 | ||
![]() |
19ecbf3734 | ||
![]() |
eac3e8ba90 | ||
![]() |
a7a6829b69 | ||
![]() |
61299113c8 | ||
![]() |
21a57dfa0b | ||
![]() |
a7226a8231 | ||
![]() |
6e3dd21f60 | ||
![]() |
cf049730d4 | ||
![]() |
cb9ce4d3af | ||
![]() |
925ee1dfb2 | ||
![]() |
5d9122b26c | ||
![]() |
6821ad0c59 | ||
![]() |
ff7851ee2e | ||
![]() |
6940ed85b1 | ||
![]() |
3d497a7f43 | ||
![]() |
cc6968e225 | ||
![]() |
a6c517c344 | ||
![]() |
a3e08b7f52 | ||
![]() |
14c8d7dc46 | ||
![]() |
ac2590c679 | ||
![]() |
ead13c6a11 | ||
![]() |
5002ab2990 | ||
![]() |
ab3e7293a4 | ||
![]() |
062af5e5cb | ||
![]() |
92088570ea | ||
![]() |
604ccf515d | ||
![]() |
ec9b244990 | ||
![]() |
09acdc23b5 | ||
![]() |
e7808b50af | ||
![]() |
9c27095744 | ||
![]() |
690b07982e | ||
![]() |
784e5aa4ee | ||
![]() |
29187cab3a | ||
![]() |
43a72807c6 | ||
![]() |
1d1f6f1870 | ||
![]() |
505a6eb4e3 | ||
![]() |
cc49df8147 | ||
![]() |
98d60402b5 | ||
![]() |
319e8a1062 | ||
![]() |
0c5d564830 | ||
![]() |
c0404cf9d9 | ||
![]() |
f364661363 | ||
![]() |
f92d77b06d | ||
![]() |
2cf00e6aae | ||
![]() |
dfdb0cff2b | ||
![]() |
d0dad84ffa | ||
![]() |
1745937f1a | ||
![]() |
e7eb674a89 | ||
![]() |
b232633100 | ||
![]() |
6abd19c149 | ||
![]() |
0aa0ff8db7 | ||
![]() |
a907429fd4 | ||
![]() |
598b550a67 | ||
![]() |
92bb442494 | ||
![]() |
2d41f6223e | ||
![]() |
791dd5fb9f | ||
![]() |
9a0ccf4c98 | ||
![]() |
ad2abc5771 | ||
![]() |
2d99b3943f | ||
![]() |
a358132f95 | ||
![]() |
09cd37feee | ||
![]() |
0f3610e81d | ||
![]() |
3f97c438e2 | ||
![]() |
42351201d2 | ||
![]() |
907bbb8e9d | ||
![]() |
63f3d8b621 | ||
![]() |
47d6e841fd | ||
![]() |
e3bb09fabe | ||
![]() |
d4e0c01189 | ||
![]() |
50370d42b0 | ||
![]() |
aa190a80b7 | ||
![]() |
e48bae77aa | ||
![]() |
96cf0f99ed | ||
![]() |
f380968049 | ||
![]() |
02468f4625 | ||
![]() |
24611f94cf | ||
![]() |
dc75a9a4b7 | ||
![]() |
33f459a23a | ||
![]() |
bdcc251002 | ||
![]() |
86052ba7b4 | ||
![]() |
62ebcf55c9 | ||
![]() |
80ac2475a0 | ||
![]() |
5179d922f5 | ||
![]() |
26f085a8ed | ||
![]() |
b7d302cc72 | ||
![]() |
f2941e3631 | ||
![]() |
26a6401af4 | ||
![]() |
5c8ce338a1 | ||
![]() |
5addc7bbaf | ||
![]() |
da095170bf | ||
![]() |
1aab0a69bd | ||
![]() |
fc8e04b62f | ||
![]() |
c6c53b4e10 | ||
![]() |
9b0219a2d8 | ||
![]() |
6e212fa476 | ||
![]() |
58f9237b12 | ||
![]() |
74fd925219 | ||
![]() |
2696bb97d2 | ||
![]() |
9cefb27704 | ||
![]() |
5e75357b06 | ||
![]() |
79bebb4bc9 | ||
![]() |
0ed88f212b | ||
![]() |
a8c1cab5fe | ||
![]() |
e1a6b1a70f | ||
![]() |
c95ed16786 | ||
![]() |
ec784803b4 | ||
![]() |
302d7a22d3 | ||
![]() |
eccd5a460b | ||
![]() |
80437229a1 | ||
![]() |
237ffba641 | ||
![]() |
2695c5e49f | ||
![]() |
b7a608fdfd | ||
![]() |
c3413bad78 | ||
![]() |
dceb244e5b | ||
![]() |
cb31a0b162 | ||
![]() |
7ced657d79 | ||
![]() |
8dd9168077 | ||
![]() |
7c6591aefe | ||
![]() |
58c91e3fd4 | ||
![]() |
db4cf7ae62 | ||
![]() |
a17f5e4f1b | ||
![]() |
6cf7f2b0a7 | ||
![]() |
7e21ea9a48 | ||
![]() |
3f29198bae | ||
![]() |
d4293650ff | ||
![]() |
d65dd16881 | ||
![]() |
f36e163581 | ||
![]() |
f215adcfa2 | ||
![]() |
1549af6f56 | ||
![]() |
c553f82580 | ||
![]() |
196b4ebc9f | ||
![]() |
8710ce1687 | ||
![]() |
f65e8d7369 | ||
![]() |
dc5d9f02c7 | ||
![]() |
2f3f8d7826 | ||
![]() |
297da070fc | ||
![]() |
10ea92dcea | ||
![]() |
2e5f01f232 | ||
![]() |
1a080c4261 | ||
![]() |
0e08963355 | ||
![]() |
cd9e39bf54 | ||
![]() |
580e840165 | ||
![]() |
09a8fd5254 | ||
![]() |
8898faa141 | ||
![]() |
fdbb1dad79 | ||
![]() |
c39244168b | ||
![]() |
9591fd88c5 | ||
![]() |
3558ce958e | ||
![]() |
804a9b7be8 | ||
![]() |
3cae550b13 | ||
![]() |
138bad5913 | ||
![]() |
09011815af | ||
![]() |
7b0c845c3a | ||
![]() |
6a47123ec9 | ||
![]() |
19fab6bbf8 | ||
![]() |
90e6b63e59 | ||
![]() |
bd78217cf3 | ||
![]() |
b0833985e6 | ||
![]() |
a6f73b035f | ||
![]() |
251440ec64 | ||
![]() |
22a1df6fa0 | ||
![]() |
6389751c22 | ||
![]() |
8498691763 | ||
![]() |
1750ff0324 | ||
![]() |
2ce4c46afd | ||
![]() |
a20f5e44d1 | ||
![]() |
cd746d72d4 | ||
![]() |
f7eaff0828 | ||
![]() |
849f119a47 | ||
![]() |
52b68381f6 | ||
![]() |
46d495e1e2 | ||
![]() |
acc6c22355 | ||
![]() |
8143182971 | ||
![]() |
04a22cd482 | ||
![]() |
4376224084 | ||
![]() |
a9fe88c343 | ||
![]() |
6eb95e1c66 | ||
![]() |
a46287c4a6 | ||
![]() |
bc86ee1c31 | ||
![]() |
a73e6f0bf8 | ||
![]() |
10a6c5144d | ||
![]() |
4e5f43aeae | ||
![]() |
ff56db0c8b | ||
![]() |
95a9b97649 | ||
![]() |
a5b5208823 | ||
![]() |
783295fabd | ||
![]() |
1c942ec97c | ||
![]() |
3b6d2655ab | ||
![]() |
8a18d0daab | ||
![]() |
e9f7ccbd25 | ||
![]() |
68d9f35c0b | ||
![]() |
28d78134c1 | ||
![]() |
fd92ac852d | ||
![]() |
8399f5288e | ||
![]() |
f99b7cb7eb | ||
![]() |
bb5166077f | ||
![]() |
b72e4b66ca | ||
![]() |
ed85cd25d6 | ||
![]() |
3f90697e18 | ||
![]() |
73271a3e55 | ||
![]() |
6f9ea712de | ||
![]() |
6ee244e7cb | ||
![]() |
d66a4af79b | ||
![]() |
ea7b1caa4e | ||
![]() |
9cd880fb35 | ||
![]() |
658c152707 | ||
![]() |
6f1ba77608 | ||
![]() |
2344d696ca | ||
![]() |
bd816310cb | ||
![]() |
2bcf759a9f | ||
![]() |
82a04f7032 | ||
![]() |
4281babee4 | ||
![]() |
d89f2965cf | ||
![]() |
e2a2a9903a | ||
![]() |
4401cdc16a | ||
![]() |
e8d3fb2920 | ||
![]() |
f7ccc137ea | ||
![]() |
07bbb4ea02 | ||
![]() |
b189e70c9b | ||
![]() |
de4c9c1463 | ||
![]() |
8bdb73ced4 | ||
![]() |
dee9050939 | ||
![]() |
ae3c214708 | ||
![]() |
d6e81867bf | ||
![]() |
d30a5ee0a5 | ||
![]() |
88bb80be0f | ||
![]() |
bba1ba1678 | ||
![]() |
b50daf20d0 | ||
![]() |
5c6c7cdff5 | ||
![]() |
3f9b2a0c28 | ||
![]() |
453e119808 | ||
![]() |
a021f910c8 | ||
![]() |
e6c2afc4db | ||
![]() |
e6c7b28057 | ||
![]() |
b1840e8be7 | ||
![]() |
15e4b1ad8b | ||
![]() |
2517afcee0 | ||
![]() |
15c7ba3078 | ||
![]() |
f2cb24781a | ||
![]() |
e1d346b8c3 | ||
![]() |
97bdf4811c | ||
![]() |
45c871d779 | ||
![]() |
976fa9c907 | ||
![]() |
771c60ca37 | ||
![]() |
e15eeccd35 | ||
![]() |
ce535b55bc | ||
![]() |
33cb62c2ee | ||
![]() |
32fe3cf61d | ||
![]() |
73a05498ce | ||
![]() |
034147f604 | ||
![]() |
b629e520a9 | ||
![]() |
30280cc6a4 | ||
![]() |
f7f0b72776 | ||
![]() |
251289fc05 | ||
![]() |
6437093a67 | ||
![]() |
be5a878da5 | ||
![]() |
8dc73a852d | ||
![]() |
e37d82951e | ||
![]() |
acc311830e | ||
![]() |
6b1046697a | ||
![]() |
c5befc5b2a | ||
![]() |
e743a5733b | ||
![]() |
5f98801c99 | ||
![]() |
9858a3db9d | ||
![]() |
65c1a525b9 | ||
![]() |
8bd055d4bd | ||
![]() |
5ee14db1f9 | ||
![]() |
58069d015b | ||
![]() |
f2684b59ec | ||
![]() |
e0c0d03c5f | ||
![]() |
1ac47d2bb0 | ||
![]() |
bc75c71ca3 | ||
![]() |
c49fc14528 | ||
![]() |
078bd8c627 | ||
![]() |
33ba9fb5cf | ||
![]() |
4e7e586cb9 | ||
![]() |
62fa795052 | ||
![]() |
b6d9f89518 | ||
![]() |
afbf867169 | ||
![]() |
dace6ac156 | ||
![]() |
cbf2b8cb78 | ||
![]() |
96c5de63d8 | ||
![]() |
b8b57843a6 | ||
![]() |
e3fd4ad77d | ||
![]() |
c08148266a | ||
![]() |
a6a2d04c46 | ||
![]() |
8f7061fb9b | ||
![]() |
7b5235138f | ||
![]() |
7e3fa8c38d | ||
![]() |
151acd5bec | ||
![]() |
23ca2039f6 | ||
![]() |
b291103592 | ||
![]() |
e962c9993b | ||
![]() |
955b769d3f | ||
![]() |
9b914e8f01 | ||
![]() |
307ad636dc | ||
![]() |
2952f62726 | ||
![]() |
6d6e48f434 | ||
![]() |
a189196855 | ||
![]() |
d30e62a205 | ||
![]() |
e56d416210 | ||
![]() |
c0f37c48a1 | ||
![]() |
a3ed387455 | ||
![]() |
beedc94179 | ||
![]() |
5229604782 | ||
![]() |
cf665517dd | ||
![]() |
4663edd8a7 | ||
![]() |
312e7974d9 | ||
![]() |
ca8aa53b32 | ||
![]() |
7122ca1c24 | ||
![]() |
97cdb1a5d8 | ||
![]() |
31d3f7a20b | ||
![]() |
6f8a34127b | ||
![]() |
ee1a86d192 | ||
![]() |
707b300bd6 | ||
![]() |
c9e12182a2 | ||
![]() |
9b7186e9b8 | ||
![]() |
4eb07f9d48 | ||
![]() |
4f78cbbd1b | ||
![]() |
d962e8bcbc | ||
![]() |
ba695a0230 | ||
![]() |
dfed2437a8 | ||
![]() |
ecfcb4ec64 | ||
![]() |
b9335311de | ||
![]() |
354468db0a | ||
![]() |
340a736722 | ||
![]() |
7bf93cb7e6 | ||
![]() |
4fa9535fd4 | ||
![]() |
1abd3217aa | ||
![]() |
d0360d5c98 | ||
![]() |
74365ad05e | ||
![]() |
9dc24c0995 | ||
![]() |
fd40e27be4 | ||
![]() |
05b2bf4c96 | ||
![]() |
a0fcbcbc7d | ||
![]() |
3117ea9d34 | ||
![]() |
8973dea33e | ||
![]() |
3e7d0dbd23 | ||
![]() |
b26b1bc038 | ||
![]() |
74b1102dea | ||
![]() |
a89226279f | ||
![]() |
8b490c8ef0 | ||
![]() |
77a98e7875 | ||
![]() |
c02592d5ba | ||
![]() |
52d7dacbaa | ||
![]() |
9a8457deff | ||
![]() |
5039b3ac6f | ||
![]() |
00705223b6 | ||
![]() |
9f6ab4c419 | ||
![]() |
9012c7310d | ||
![]() |
a3edebcad9 | ||
![]() |
f2abb6a73f | ||
![]() |
e96e5b740a | ||
![]() |
ee067ad97a | ||
![]() |
d01b3a88b6 | ||
![]() |
5a22c978cf | ||
![]() |
f8a0e7d1be | ||
![]() |
25a65564b1 | ||
![]() |
c858023c88 | ||
![]() |
c3e470db26 | ||
![]() |
5908c4da7a | ||
![]() |
b08dbbd106 | ||
![]() |
3b320c75e9 | ||
![]() |
1aa6dc6686 | ||
![]() |
fdc4385e62 | ||
![]() |
5094448762 | ||
![]() |
98c7fa919f | ||
![]() |
5b9f51417f | ||
![]() |
7a91f89474 | ||
![]() |
bf7afa16e5 | ||
![]() |
0d57baae82 | ||
![]() |
446d197cf7 | ||
![]() |
2582f0bbe6 | ||
![]() |
1ee993c664 | ||
![]() |
542c20065f | ||
![]() |
39f663d03c | ||
![]() |
6474a55302 | ||
![]() |
8566d4c5ab | ||
![]() |
e374e93cfb | ||
![]() |
7bd4f6490c | ||
![]() |
25373f510d | ||
![]() |
82cab39e1c | ||
![]() |
22507cc1cd | ||
![]() |
2bded65c7e | ||
![]() |
a3a0c60804 | ||
![]() |
704b172887 | ||
![]() |
135717f8cb | ||
![]() |
1d87ba8534 | ||
![]() |
97cd27775b | ||
![]() |
fe2e9c282e | ||
![]() |
fab125975b | ||
![]() |
cefd7e3b1b | ||
![]() |
344a3e7b24 | ||
![]() |
a0ee237ada | ||
![]() |
e81eb9a5f8 | ||
![]() |
98d3b538af | ||
![]() |
3614a0e368 | ||
![]() |
0421497b1e | ||
![]() |
8b3c2fa12f | ||
![]() |
a58bea6d93 | ||
![]() |
c7c41cd761 | ||
![]() |
b282ec73c7 | ||
![]() |
dad26be2c6 | ||
![]() |
58d602e549 | ||
![]() |
5e14904205 | ||
![]() |
97293ab7ce | ||
![]() |
b6f634368c | ||
![]() |
7b4de150cc | ||
![]() |
7a268c94b0 | ||
![]() |
7a1fa78632 | ||
![]() |
19f02da64d | ||
![]() |
5bf1aac9cb | ||
![]() |
0ae034083c | ||
![]() |
5010af941b | ||
![]() |
015df7e060 | ||
![]() |
e025d58f6e | ||
![]() |
b151d333d3 | ||
![]() |
304c005a85 | ||
![]() |
e2591e8e36 | ||
![]() |
f3c22cb6d0 | ||
![]() |
b2527984bc | ||
![]() |
b8d2271191 | ||
![]() |
b8978b0235 | ||
![]() |
63ef6419cd | ||
![]() |
25dc429455 | ||
![]() |
7550e63fd0 | ||
![]() |
0561968fac | ||
![]() |
7811bf518b | ||
![]() |
bc7116ad94 | ||
![]() |
70eec33d06 | ||
![]() |
773973825f | ||
![]() |
a184d372f4 | ||
![]() |
ca1606a021 | ||
![]() |
5c6d7eb309 | ||
![]() |
4de6b39788 | ||
![]() |
f0494cc7d6 | ||
![]() |
9d98d1ee63 | ||
![]() |
f1238e17b1 | ||
![]() |
4201c8a6f3 | ||
![]() |
53396ed454 | ||
![]() |
8695823165 | ||
![]() |
ec8d008678 | ||
![]() |
a949ad14f8 | ||
![]() |
48e7bd4f10 | ||
![]() |
4b11f8f26b | ||
![]() |
b056444863 | ||
![]() |
872f021ddc | ||
![]() |
079b0c1b91 | ||
![]() |
2664b50a18 | ||
![]() |
6970df4dda | ||
![]() |
22c3064ec4 | ||
![]() |
d6ab65a2e7 | ||
![]() |
aa23b01a57 | ||
![]() |
d82de98001 | ||
![]() |
7df8597484 | ||
![]() |
1b99b1275c | ||
![]() |
d16461052b | ||
![]() |
9640364713 | ||
![]() |
18e0600727 | ||
![]() |
17fffda74e | ||
![]() |
3ac4f48f82 | ||
![]() |
6f8ae98ed0 | ||
![]() |
47b2ce6180 | ||
![]() |
d18d84e187 | ||
![]() |
c1dcdf49e5 | ||
![]() |
079005eab1 | ||
![]() |
dc8cea3a3e | ||
![]() |
efca88cf8b | ||
![]() |
c05a6b96b7 | ||
![]() |
a831ff3b61 | ||
![]() |
b814a09fe6 | ||
![]() |
fb48c8626a | ||
![]() |
fbdeb4c386 | ||
![]() |
4cf9ecc819 | ||
![]() |
e9573b6e24 | ||
![]() |
d5f0137052 | ||
![]() |
d9f5adb1fb | ||
![]() |
0c6aa064ac | ||
![]() |
646c853cf4 | ||
![]() |
fb3bc95623 | ||
![]() |
c8b4cab022 | ||
![]() |
06fb94b4ea | ||
![]() |
9f6cef4fb4 | ||
![]() |
0315dd5612 | ||
![]() |
e4e5bebc1a | ||
![]() |
c688e9ebad | ||
![]() |
6d6041a3c1 | ||
![]() |
dde7b5ea68 | ||
![]() |
9bf533b340 | ||
![]() |
f1a105abec | ||
![]() |
e6587b5dc8 | ||
![]() |
b2ad045a2d | ||
![]() |
89734d8c5f | ||
![]() |
53736099ba | ||
![]() |
2fcfa136c1 | ||
![]() |
9f85209a1b | ||
![]() |
cea1b2fd4d | ||
![]() |
312252b670 | ||
![]() |
4d6b30c17b | ||
![]() |
0beb9c2670 | ||
![]() |
a0289af59f | ||
![]() |
40363834c8 | ||
![]() |
0c9e5fd10b | ||
![]() |
3d90e5cdf6 | ||
![]() |
8e3f1f0955 | ||
![]() |
7c64415096 | ||
![]() |
e3fd1dba0e | ||
![]() |
9866a0fadc | ||
![]() |
f87f24d9e5 | ||
![]() |
4729ae4769 | ||
![]() |
691c4c158f | ||
![]() |
3c597339ba | ||
![]() |
e5fe174e03 | ||
![]() |
1c25a9d026 | ||
![]() |
2db378e9c1 | ||
![]() |
a4067ee681 | ||
![]() |
edb0831028 | ||
![]() |
dac3b0a6f5 | ||
![]() |
9a180cc8ad | ||
![]() |
e81764610e | ||
![]() |
e4e2b627fe | ||
![]() |
ec55f56725 | ||
![]() |
1e4f871bcc | ||
![]() |
69f72919bd | ||
![]() |
dc0336fa45 | ||
![]() |
8c341d262e | ||
![]() |
2b15464e12 | ||
![]() |
a686235ffb | ||
![]() |
29171a4d05 | ||
![]() |
e9123f55e0 | ||
![]() |
ee004486bd | ||
![]() |
498e234c37 | ||
![]() |
b29f19e206 | ||
![]() |
1e00343262 | ||
![]() |
3cd526c019 | ||
![]() |
ea99c58da5 | ||
![]() |
c64f23a64a | ||
![]() |
2099cd37fa | ||
![]() |
2559632079 | ||
![]() |
352df39454 | ||
![]() |
ce3a940b11 | ||
![]() |
6594e88390 | ||
![]() |
339758ec42 | ||
![]() |
0b4c7defd4 | ||
![]() |
6d71e9065b | ||
![]() |
631ab4d4eb | ||
![]() |
589ff47ae6 | ||
![]() |
877034d012 | ||
![]() |
3d440bf8f5 | ||
![]() |
138b2be010 | ||
![]() |
b729944480 | ||
![]() |
870afd9fac | ||
![]() |
e808814725 | ||
![]() |
122cf2250d | ||
![]() |
fa1d962507 | ||
![]() |
6504692c5c | ||
![]() |
bd36962643 | ||
![]() |
f5ccfc3f8a | ||
![]() |
c1a7e0513b | ||
![]() |
af71e79371 | ||
![]() |
bf911cf3a5 | ||
![]() |
6059a1c444 | ||
![]() |
c4966a4bf2 | ||
![]() |
cb9f356a69 | ||
![]() |
9d02f6a408 | ||
![]() |
ee76772e1b | ||
![]() |
f0a030a86d | ||
![]() |
1a31e56f33 | ||
![]() |
04e9e0e687 | ||
![]() |
cec917c2a2 | ||
![]() |
08989a8797 | ||
![]() |
b734c331e4 | ||
![]() |
fe477a6809 | ||
![]() |
6391a4a7f7 | ||
![]() |
e68220d4b3 | ||
![]() |
b873149f9b | ||
![]() |
86aebbcaea | ||
![]() |
fd260cf32f | ||
![]() |
69101a5b14 | ||
![]() |
151d6cbc48 | ||
![]() |
04675e5fcb | ||
![]() |
b38c6fe06a | ||
![]() |
089a12bdc9 | ||
![]() |
d9a0a2003f | ||
![]() |
ad704d9925 | ||
![]() |
0cca79eeee | ||
![]() |
457bea7c34 | ||
![]() |
2479679eeb | ||
![]() |
937405d2d8 | ||
![]() |
d1bed1b9cc | ||
![]() |
acc60bce57 | ||
![]() |
43807ff06b | ||
![]() |
b8a63bcc0c | ||
![]() |
66c1815a78 | ||
![]() |
4e5cfa2077 | ||
![]() |
ebaf5d31b7 | ||
![]() |
760a640c6a | ||
![]() |
4fc06e9504 | ||
![]() |
c283ccb122 | ||
![]() |
80df842b2b | ||
![]() |
f1a8a72a9f | ||
![]() |
0296e16232 | ||
![]() |
f6f7081483 | ||
![]() |
7f7cd0a314 | ||
![]() |
5ffb5763a5 | ||
![]() |
4382037110 | ||
![]() |
963cd88440 | ||
![]() |
885f99ac08 | ||
![]() |
7c3919980a | ||
![]() |
d8860d6f24 | ||
![]() |
6b992e37e3 | ||
![]() |
a3424355fa | ||
![]() |
569a91296d | ||
![]() |
8b583cb445 | ||
![]() |
038a85af43 | ||
![]() |
9165beb41c | ||
![]() |
b285de4412 | ||
![]() |
5826035fe9 | ||
![]() |
b953ac295b | ||
![]() |
8a95066b2e | ||
![]() |
00a4aef607 | ||
![]() |
9e2663491e | ||
![]() |
e01ce7b665 | ||
![]() |
a57df48f28 | ||
![]() |
5d7e008055 | ||
![]() |
ba31b3ecb7 | ||
![]() |
3c5eb934bf | ||
![]() |
82e15df6e9 | ||
![]() |
e3c83c0c29 | ||
![]() |
94542334c4 | ||
![]() |
95494b3ace | ||
![]() |
a131cfb79e | ||
![]() |
f002c67343 | ||
![]() |
b9caf95c72 | ||
![]() |
5356954240 | ||
![]() |
126c73002e | ||
![]() |
65b4502a78 | ||
![]() |
3406161d75 | ||
![]() |
e45f00f0f7 | ||
![]() |
71f4a30562 | ||
![]() |
20ba414b41 | ||
![]() |
f5250f04c5 | ||
![]() |
c2ea20a87a | ||
![]() |
b14989d4a5 | ||
![]() |
04578e329c | ||
![]() |
be05e438ca | ||
![]() |
24d9215029 | ||
![]() |
8892270c24 | ||
![]() |
b928df6cba | ||
![]() |
3fc74bd79e | ||
![]() |
b34be77fec | ||
![]() |
54dcca7ba9 | ||
![]() |
d991c06098 | ||
![]() |
01a67ba156 | ||
![]() |
8831573b6c | ||
![]() |
c5bc5411fb | ||
![]() |
a13ccd7530 | ||
![]() |
e9a744e8b7 | ||
![]() |
582d43c153 | ||
![]() |
7b5550928f | ||
![]() |
83920a3258 | ||
![]() |
d1670aa443 | ||
![]() |
c6f589124e | ||
![]() |
35991e5194 | ||
![]() |
b956190393 | ||
![]() |
122c989b7a | ||
![]() |
5602575099 | ||
![]() |
4534499aad | ||
![]() |
f733a91d7c | ||
![]() |
bf3fa30a01 | ||
![]() |
2625229847 | ||
![]() |
2c3eb6d0d6 | ||
![]() |
5ff98fd1a5 | ||
![]() |
056a7351a3 | ||
![]() |
f79b71727b | ||
![]() |
d3a3b8ca19 | ||
![]() |
df9e002b9a | ||
![]() |
a4a2c9d068 | ||
![]() |
c453e5ad20 | ||
![]() |
617b879c2a | ||
![]() |
a0042e9302 | ||
![]() |
6bbfcdfe4f | ||
![]() |
25662285af | ||
![]() |
84d12e8d72 | ||
![]() |
c317cbce36 | ||
![]() |
d279604fac | ||
![]() |
70fc4ef886 | ||
![]() |
24ff91eef5 | ||
![]() |
afc6789c74 | ||
![]() |
819e5e222a | ||
![]() |
e1a4f37bbc | ||
![]() |
a73477feed | ||
![]() |
89722ee2f3 | ||
![]() |
30d4b2cef4 | ||
![]() |
ca4fce7ffb | ||
![]() |
018b2daace | ||
![]() |
fd01165cf6 | ||
![]() |
34e4719893 | ||
![]() |
c6ac9e1d15 | ||
![]() |
70b8876239 | ||
![]() |
5e34f4481a | ||
![]() |
eae5594698 | ||
![]() |
f02022a00c | ||
![]() |
f964013516 | ||
![]() |
5f7ffaf1f6 | ||
![]() |
0e7ccb7520 | ||
![]() |
c9db504a49 | ||
![]() |
716677393e | ||
![]() |
ba8484f161 | ||
![]() |
ceec84dbb4 | ||
![]() |
f2a83ec846 | ||
![]() |
7deea6083a | ||
![]() |
a169ff3548 | ||
![]() |
f84a88da21 | ||
![]() |
eecec7183e | ||
![]() |
f11705ee26 | ||
![]() |
78ac5abf23 | ||
![]() |
2beeaa0932 | ||
![]() |
90cb8423bc | ||
![]() |
3b07bd286b | ||
![]() |
73564b97ea | ||
![]() |
65cad5efad | ||
![]() |
52eb627cd6 | ||
![]() |
506e568a9a | ||
![]() |
6c89de082f | ||
![]() |
6fb31cc613 | ||
![]() |
cfb22baf05 | ||
![]() |
2d0c1ff0a8 | ||
![]() |
7789e13879 | ||
![]() |
f7b90e2c09 | ||
![]() |
ccb29167dd | ||
![]() |
4ef1eca3c9 | ||
![]() |
c26ede30b9 | ||
![]() |
64c69a3164 | ||
![]() |
ad7867ff11 | ||
![]() |
14fc1588f8 | ||
![]() |
7e5a925f4f | ||
![]() |
3c61e422da | ||
![]() |
0e2cf37981 | ||
![]() |
503d5e389f | ||
![]() |
7b1e61ab2c | ||
![]() |
4692d6638d | ||
![]() |
7829070e1c | ||
![]() |
5e4b935322 | ||
![]() |
4c445c7a88 | ||
![]() |
8e2965df6a | ||
![]() |
7a41d24606 | ||
![]() |
5f84a006dc | ||
![]() |
e19296a230 | ||
![]() |
89ba97f413 | ||
![]() |
fe2157130b | ||
![]() |
e3b17e8176 | ||
![]() |
027f2f95c6 | ||
![]() |
210975324a | ||
![]() |
f9a90d2494 | ||
![]() |
932689f2f8 | ||
![]() |
f91e911d1a | ||
![]() |
b75cce857e | ||
![]() |
62f00690f7 | ||
![]() |
f700ba4154 | ||
![]() |
8b91842eae | ||
![]() |
80a9eb93f4 | ||
![]() |
e1deecbbfb | ||
![]() |
d3142704b7 | ||
![]() |
447edd081a | ||
![]() |
e1531ec277 | ||
![]() |
d12ac4b1f6 | ||
![]() |
17851b7586 | ||
![]() |
118e2fa610 | ||
![]() |
8e3553462c | ||
![]() |
37da47d811 | ||
![]() |
a640a468fb | ||
![]() |
92f034766e | ||
![]() |
f7ea451df8 | ||
![]() |
1b7f54b462 | ||
![]() |
b14b12231a | ||
![]() |
2866be9462 | ||
![]() |
f8648644bf | ||
![]() |
69d4d48db0 | ||
![]() |
df309749f2 | ||
![]() |
58751067db | ||
![]() |
4fd70cf79b | ||
![]() |
ff15bad375 | ||
![]() |
90ac4ab6fe | ||
![]() |
cba5bb1676 | ||
![]() |
4b5fa404fc | ||
![]() |
c4ac1240ac | ||
![]() |
d384ad2700 | ||
![]() |
c3da0b8073 | ||
![]() |
9919cba375 | ||
![]() |
1e6b94de92 | ||
![]() |
8451a4cd08 | ||
![]() |
48f1da1b8d | ||
![]() |
e20050b719 | ||
![]() |
a9c0a46a06 | ||
![]() |
03bb094b90 | ||
![]() |
5d0d552c26 | ||
![]() |
2d50cef098 | ||
![]() |
d6d0b83b4e | ||
![]() |
f1dbeda451 | ||
![]() |
512bbae5cb | ||
![]() |
8c575d40af | ||
![]() |
d6b9909bc6 | ||
![]() |
ef7d6dc091 | ||
![]() |
57f707bbfd | ||
![]() |
0ae7213366 | ||
![]() |
22ff7aa672 | ||
![]() |
ca579fbf4a | ||
![]() |
f2eb30d090 | ||
![]() |
63a4b4744b | ||
![]() |
e03b5b3992 | ||
![]() |
fa4a27fb1a | ||
![]() |
d3a6aa2471 | ||
![]() |
8bd64cff59 | ||
![]() |
760db17640 | ||
![]() |
a9cb25f3a2 | ||
![]() |
d9d5ddb77e | ||
![]() |
9b8e5b03b4 | ||
![]() |
02f0c4a5b8 | ||
![]() |
b254716cee | ||
![]() |
4c52ad6f7c | ||
![]() |
0c09bfcafa | ||
![]() |
0b67546481 | ||
![]() |
2698b00fb9 | ||
![]() |
f7ce705999 | ||
![]() |
ee14131827 | ||
![]() |
828c499ac7 | ||
![]() |
a43d594452 | ||
![]() |
406d572a7b | ||
![]() |
71c38fd515 | ||
![]() |
68e02dd62a | ||
![]() |
dd1902b1d9 | ||
![]() |
39041ee08c | ||
![]() |
eb6a2f9e89 | ||
![]() |
4f826d8245 | ||
![]() |
a434a6f144 | ||
![]() |
0fe1020022 | ||
![]() |
8aca08f508 | ||
![]() |
fb0331aa4c | ||
![]() |
184a9bceb9 | ||
![]() |
dfef7c2b52 | ||
![]() |
6b16b51064 | ||
![]() |
85a75b637a | ||
![]() |
fae2d9414a | ||
![]() |
61e263b160 | ||
![]() |
ac13140083 | ||
![]() |
024fd07ec8 | ||
![]() |
95175155d4 | ||
![]() |
e5c088f8d6 | ||
![]() |
42a103c76f | ||
![]() |
b70f2fa20a | ||
![]() |
8e69b158eb | ||
![]() |
6e2c544a19 | ||
![]() |
c62d080e9c | ||
![]() |
bd0e00ed86 | ||
![]() |
264a78e2cc | ||
![]() |
4f95ef437f | ||
![]() |
f0556954ed | ||
![]() |
44bc569868 | ||
![]() |
1e9bbb1d14 | ||
![]() |
f2953f6b09 | ||
![]() |
fa4c5ec9d4 | ||
![]() |
546268809f | ||
![]() |
6af4c0f9e0 | ||
![]() |
7d0fd85d65 | ||
![]() |
15b78307fb | ||
![]() |
6ba3090cd5 | ||
![]() |
74c4c58e37 | ||
![]() |
31f63264b0 | ||
![]() |
9e7dbbbbff | ||
![]() |
c1d120c9bb | ||
![]() |
3955a8c1d0 | ||
![]() |
12f8073e5d | ||
![]() |
ec2b1dd39b | ||
![]() |
e9d603abf1 | ||
![]() |
ac33ba6ff4 | ||
![]() |
3b4888b8ba | ||
![]() |
5c64c88d5a | ||
![]() |
924d095c68 | ||
![]() |
700ccb17cb | ||
![]() |
1d156f8183 | ||
![]() |
c0e2c5cb71 | ||
![]() |
25d19732e0 | ||
![]() |
f0b8d56e9f | ||
![]() |
718a3fe7ef | ||
![]() |
ca6e0ec9b9 | ||
![]() |
a27765f7d5 | ||
![]() |
bf1dd03df3 | ||
![]() |
2726648982 | ||
![]() |
275a4ce18d | ||
![]() |
0b34e13dd4 | ||
![]() |
e666261434 | ||
![]() |
57c8ad6b92 | ||
![]() |
3f032abc25 | ||
![]() |
f86202c07d | ||
![]() |
1b0ff0a5f6 | ||
![]() |
cebb962645 | ||
![]() |
55000f98bc | ||
![]() |
449aff1b1d | ||
![]() |
3c591f744b | ||
![]() |
329781023f | ||
![]() |
8d9731e241 | ||
![]() |
bde37ba9c2 | ||
![]() |
088fdc8f42 | ||
![]() |
886005be2a | ||
![]() |
684afed3f1 | ||
![]() |
210d7e59fd | ||
![]() |
a19a94b2c2 | ||
![]() |
9bf70208c8 | ||
![]() |
fada0d99f0 | ||
![]() |
e6ce468301 | ||
![]() |
875e5d59fe | ||
![]() |
6556135a69 | ||
![]() |
8636b4ebca | ||
![]() |
4a5f914a62 | ||
![]() |
47b6014d13 | ||
![]() |
1995d825df | ||
![]() |
f49606dff6 | ||
![]() |
7520d4b81e | ||
![]() |
083408a685 | ||
![]() |
9c4972239d | ||
![]() |
4458f2e6d4 | ||
![]() |
a24027f188 | ||
![]() |
c749fc05f4 | ||
![]() |
5ad77df04f | ||
![]() |
4b51d67d35 | ||
![]() |
88268bd76f | ||
![]() |
744d96330e | ||
![]() |
55c3164a7d | ||
![]() |
c78e31b136 | ||
![]() |
ecfd0a6796 | ||
![]() |
162ce2a9c5 | ||
![]() |
1f2125a097 | ||
![]() |
feae3eacb1 | ||
![]() |
a1a706cb31 | ||
![]() |
8a1da297d9 | ||
![]() |
1987221026 | ||
![]() |
4b7b34064b | ||
![]() |
5abb4618bd | ||
![]() |
75c1d36237 | ||
![]() |
90e8e1a8aa | ||
![]() |
32a9b38d26 | ||
![]() |
5714f56083 | ||
![]() |
3d635816c9 | ||
![]() |
1aa5ce2f35 | ||
![]() |
f765fde6c1 | ||
![]() |
523cbf641c | ||
![]() |
112834bbaa | ||
![]() |
f0ab1ae907 | ||
![]() |
d6827a2794 | ||
![]() |
a1591185c1 | ||
![]() |
b77c8a8717 | ||
![]() |
831b7d2a86 | ||
![]() |
057a52dd32 | ||
![]() |
8f88fae530 | ||
![]() |
85cc8eb6f3 | ||
![]() |
349f1b115e | ||
![]() |
27de44b0ec | ||
![]() |
9847408d77 | ||
![]() |
cc24f36e80 | ||
![]() |
e7fe6d25b6 | ||
![]() |
afc968146d | ||
![]() |
471decdbb6 | ||
![]() |
638f980281 | ||
![]() |
8f1115a257 | ||
![]() |
9e8b6503a0 | ||
![]() |
91d042f6f3 | ||
![]() |
d559cad042 | ||
![]() |
f05aecf5f9 | ||
![]() |
58f072e5af | ||
![]() |
afc3bcbc75 | ||
![]() |
8ee2fd2cf8 | ||
![]() |
be7faacd07 | ||
![]() |
dc97433d9b | ||
![]() |
da10a8e7dd | ||
![]() |
847ae21ccb | ||
![]() |
128cf115a7 | ||
![]() |
1b9cff6d5f | ||
![]() |
110a8e22ae | ||
![]() |
7f058c0c77 | ||
![]() |
1e3512ac84 | ||
![]() |
8662a4a807 | ||
![]() |
63d1c918e5 | ||
![]() |
0a89090dc2 | ||
![]() |
645575239f | ||
![]() |
8de38b1708 | ||
![]() |
6db987972a | ||
![]() |
0ddf6bf579 | ||
![]() |
9f8033a147 | ||
![]() |
d007b40e15 | ||
![]() |
bbfd36fc92 | ||
![]() |
3faa02b00d | ||
![]() |
eb1895e980 | ||
![]() |
7ee8e96ece | ||
![]() |
3e796b579d | ||
![]() |
74d9e2f421 | ||
![]() |
2603cbb102 | ||
![]() |
194d6c9d4c | ||
![]() |
f364f8e832 | ||
![]() |
ba6af85e9d | ||
![]() |
d2e411dba0 | ||
![]() |
e15a6bb758 | ||
![]() |
96c04f3c60 | ||
![]() |
9c9bc68092 | ||
![]() |
46f003fe14 | ||
![]() |
1404965b07 | ||
![]() |
9fbb1417f2 | ||
![]() |
158a7090a3 | ||
![]() |
9fa9859495 | ||
![]() |
de85fefa7d | ||
![]() |
dee55df94a | ||
![]() |
62b9450ce1 | ||
![]() |
bcdcf4351d | ||
![]() |
0d941e9c96 | ||
![]() |
9d837b2e4b | ||
![]() |
8544010eb6 | ||
![]() |
f37243169a | ||
![]() |
7caa1e1f0e | ||
![]() |
e019a394b0 | ||
![]() |
c0b482e68c | ||
![]() |
2da115f5c4 | ||
![]() |
639ccf5582 | ||
![]() |
2654794968 | ||
![]() |
2cec124b4f | ||
![]() |
e21737399b | ||
![]() |
9a555d8a6e | ||
![]() |
f7bf2b0ba6 | ||
![]() |
710ed0a5c8 | ||
![]() |
7539523ef2 | ||
![]() |
c97444e438 | ||
![]() |
4c86d10037 | ||
![]() |
69a6c79558 | ||
![]() |
a0466dc322 | ||
![]() |
546e35e9a3 | ||
![]() |
ce53b11cf7 | ||
![]() |
1229fd100f | ||
![]() |
e4541591ea | ||
![]() |
be62b1b9df | ||
![]() |
9c21cf4c62 | ||
![]() |
51af6a98cc | ||
![]() |
520d6160f0 | ||
![]() |
e8ebedb2da | ||
![]() |
fd7700d577 | ||
![]() |
6ee88a5424 | ||
![]() |
c89711d0d5 | ||
![]() |
daee0f8df8 | ||
![]() |
e1444f4aca | ||
![]() |
97b9c4899a | ||
![]() |
b8aa6ecd70 | ||
![]() |
e28f3947bd | ||
![]() |
bc9cc98789 | ||
![]() |
72132e7946 | ||
![]() |
b0307dd98e | ||
![]() |
fd1ac55a70 | ||
![]() |
39d8800389 | ||
![]() |
382a7121e1 | ||
![]() |
4c0ac6d502 | ||
![]() |
40dcbedc2a | ||
![]() |
9eda66b3ae | ||
![]() |
d4c48db248 | ||
![]() |
7bd4861689 | ||
![]() |
72550725da | ||
![]() |
5a8011ea66 | ||
![]() |
0fd1a95405 | ||
![]() |
8d0cfa8e7c | ||
![]() |
3d1187283c | ||
![]() |
7416a55083 | ||
![]() |
e8a3c4dac6 | ||
![]() |
33f2026dac | ||
![]() |
d34f6e779d | ||
![]() |
738976a956 | ||
![]() |
fd8cc1df15 | ||
![]() |
61053b063e | ||
![]() |
a27e1e9d40 | ||
![]() |
a7889eb536 | ||
![]() |
0f17709d4e | ||
![]() |
3eca010f66 | ||
![]() |
041ffd6db2 | ||
![]() |
4b5aad41b1 | ||
![]() |
d6565076f5 | ||
![]() |
c943162649 | ||
![]() |
a2e94b8493 | ||
![]() |
94b2bc1261 | ||
![]() |
7d34f83b18 | ||
![]() |
4f27a18616 | ||
![]() |
5a5aa1c2aa | ||
![]() |
1bafdf9130 | ||
![]() |
9eef5d7b1e | ||
![]() |
aee3c74681 | ||
![]() |
653a39c05e | ||
![]() |
efa6a33b0a | ||
![]() |
0c5a9e8347 | ||
![]() |
657f77b7c6 | ||
![]() |
b528572960 | ||
![]() |
e75d24aca2 | ||
![]() |
7607f8d639 | ||
![]() |
9a59c02077 | ||
![]() |
8e3c4b1925 | ||
![]() |
057bf03d3a | ||
![]() |
224faff879 | ||
![]() |
a6c2939bb4 | ||
![]() |
c78d88707c | ||
![]() |
a79071bb33 | ||
![]() |
dca530d2c0 | ||
![]() |
c5b1542af2 | ||
![]() |
a13e7766fc | ||
![]() |
765e391810 | ||
![]() |
6a12e78cee | ||
![]() |
e0effa567a | ||
![]() |
0322ca6d05 | ||
![]() |
13eda34676 | ||
![]() |
874ed0c450 | ||
![]() |
f25ec3c3f0 | ||
![]() |
8373c4619e | ||
![]() |
549dfd99e5 | ||
![]() |
eed88f6366 | ||
![]() |
fcf745b2f4 | ||
![]() |
69a27b7843 | ||
![]() |
a51141810d | ||
![]() |
396f454998 | ||
![]() |
5f21909138 | ||
![]() |
ebb7b4b4ae | ||
![]() |
e691231f64 | ||
![]() |
471110c0f2 | ||
![]() |
73948c016b | ||
![]() |
864e7ac4ee | ||
![]() |
2207220592 | ||
![]() |
a4a5781f7f | ||
![]() |
194d2b9639 | ||
![]() |
530f499ce1 | ||
![]() |
d167e275d1 | ||
![]() |
cdcc7fc3c1 | ||
![]() |
0a30e0ade5 | ||
![]() |
47dc66db5a | ||
![]() |
c192391551 | ||
![]() |
b0c44aa67a | ||
![]() |
29890dcfa9 | ||
![]() |
1742065f77 | ||
![]() |
28480d0359 | ||
![]() |
2f57cfc812 | ||
![]() |
b12a52e266 | ||
![]() |
5d45a44247 | ||
![]() |
8ee520d99b | ||
![]() |
4c0d4ffc47 | ||
![]() |
44c00a2581 | ||
![]() |
1015f3bf53 | ||
![]() |
71378d23d5 | ||
![]() |
f5d0855c2b | ||
![]() |
88040362b0 | ||
![]() |
8f49412438 | ||
![]() |
bb417b98b8 | ||
![]() |
afed81d173 | ||
![]() |
def99c1795 | ||
![]() |
fcdea007ac | ||
![]() |
383b56276e | ||
![]() |
11e6c38702 | ||
![]() |
a2686ac27b | ||
![]() |
49bf4747fd | ||
![]() |
cf257c48b4 | ||
![]() |
05d939beac | ||
![]() |
fa7fed8ea3 | ||
![]() |
fbf5816952 | ||
![]() |
31fc89c944 | ||
![]() |
f7a05713a1 | ||
![]() |
9f532d6b2d | ||
![]() |
5263e4ceae | ||
![]() |
3145011004 | ||
![]() |
5da4348c2d | ||
![]() |
e33e34748f | ||
![]() |
d2e62a90d7 | ||
![]() |
593a3c8ebb | ||
![]() |
6713277f33 | ||
![]() |
178f1ed5e0 | ||
![]() |
f5c703a04f | ||
![]() |
27e83a3260 | ||
![]() |
e7cd5ec019 | ||
![]() |
8704deeb31 | ||
![]() |
9c6056518f | ||
![]() |
5f813a4206 | ||
![]() |
5cb40531d0 | ||
![]() |
fe85a79ae3 | ||
![]() |
97ec0b803d | ||
![]() |
a5fbc0351f | ||
![]() |
38e772dfec | ||
![]() |
dda3762b48 | ||
![]() |
1ddbf97c11 | ||
![]() |
ca4952a85d | ||
![]() |
d76632de91 | ||
![]() |
b96f3485fd | ||
![]() |
a6f1f6ea09 | ||
![]() |
d2533688b6 | ||
![]() |
6810aba5e9 | ||
![]() |
aca5b1ccd4 | ||
![]() |
888aa99ea6 | ||
![]() |
b112b88587 | ||
![]() |
86276541be | ||
![]() |
bdfd81fe83 | ||
![]() |
c24a0a4995 | ||
![]() |
524b9104d0 | ||
![]() |
19e896c38d | ||
![]() |
62517d0c89 | ||
![]() |
49a0f154d0 | ||
![]() |
39248a532d | ||
![]() |
465c81f281 | ||
![]() |
2d8facd022 | ||
![]() |
d548aa1e72 | ||
![]() |
7968912a7c | ||
![]() |
79bd1a50ad | ||
![]() |
7b96950a9c | ||
![]() |
89331d15cc | ||
![]() |
25910b732a | ||
![]() |
bdcb9e7540 | ||
![]() |
130bec4a2f | ||
![]() |
db2d685c40 | ||
![]() |
f9e0f90e08 | ||
![]() |
4f85644c34 | ||
![]() |
73d77ee56b | ||
![]() |
33a37ffa25 | ||
![]() |
2716ba4dc6 | ||
![]() |
65afc65f51 | ||
![]() |
034432bfba | ||
![]() |
f815fe8b59 | ||
![]() |
cc7605d6a9 | ||
![]() |
d809b8717c | ||
![]() |
f878ad54a8 | ||
![]() |
22bc9b0dbf | ||
![]() |
86428aa0f6 | ||
![]() |
8c8b532ffd | ||
![]() |
475c0a3144 | ||
![]() |
3c6e20585c | ||
![]() |
98c2bd9a6a | ||
![]() |
226b6c40a5 | ||
![]() |
4cb18c931d | ||
![]() |
96b75c18b7 | ||
![]() |
87d9d14e5d | ||
![]() |
1069799ea7 | ||
![]() |
5e55753baa | ||
![]() |
be8f847309 | ||
![]() |
acc31b8441 | ||
![]() |
a98bab8b5e | ||
![]() |
7b944a3a3f | ||
![]() |
a0d32c5b33 | ||
![]() |
89f1254396 | ||
![]() |
41c136392f | ||
![]() |
b4d1ee353d | ||
![]() |
a24d7406fc | ||
![]() |
574d3ba1f4 | ||
![]() |
6eb61e2923 | ||
![]() |
9e679e8024 | ||
![]() |
006488fc74 | ||
![]() |
8e66c383e8 | ||
![]() |
e7a0556118 | ||
![]() |
6117c0b573 | ||
![]() |
c3a90e0804 | ||
![]() |
66cb630b86 | ||
![]() |
2b2eefdd1f | ||
![]() |
db77932a95 | ||
![]() |
2aaf82412d | ||
![]() |
4df93cab04 | ||
![]() |
b778232cac | ||
![]() |
f58015dc57 | ||
![]() |
57d3cbccc4 | ||
![]() |
52fdd0bd8c | ||
![]() |
ced8e9f874 | ||
![]() |
76b589bc90 | ||
![]() |
4c79a8cb2d | ||
![]() |
64f7244808 | ||
![]() |
ebaf36d503 | ||
![]() |
e58c1a5f5a | ||
![]() |
c1eb7618d6 | ||
![]() |
0ce0dfbc35 | ||
![]() |
a555af428d | ||
![]() |
b5666a45f6 | ||
![]() |
170f0f918f | ||
![]() |
a59b0af2b4 | ||
![]() |
3cac9a2203 | ||
![]() |
6b22f80ead | ||
![]() |
5f498ffaf3 | ||
![]() |
258fe7b277 | ||
![]() |
703ed7d21e | ||
![]() |
9a1f84329f | ||
![]() |
a20c7eb4de | ||
![]() |
e866651f96 | ||
![]() |
2b5f42a546 | ||
![]() |
b811c63ac5 | ||
![]() |
c7ea106675 | ||
![]() |
1f2218c875 | ||
![]() |
99369aa5a1 | ||
![]() |
ffd3c171fe | ||
![]() |
ce4b9e8e9f | ||
![]() |
ef51eb21e0 | ||
![]() |
b1efe3a5c1 | ||
![]() |
6d647b5387 | ||
![]() |
d11c7ba4db | ||
![]() |
6b33358c56 | ||
![]() |
9030302ff7 | ||
![]() |
1631a6eab0 | ||
![]() |
c6fe145030 | ||
![]() |
5b1435081a | ||
![]() |
39fce0304d | ||
![]() |
5a5fdc2565 | ||
![]() |
bef121dbe3 | ||
![]() |
0b7a43f6fa | ||
![]() |
2d1a45f019 | ||
![]() |
5494172706 | ||
![]() |
198bb875df | ||
![]() |
d1822ee939 | ||
![]() |
5e1516189b | ||
![]() |
5819b442aa | ||
![]() |
4bb8e47f3b | ||
![]() |
ff6a68112e | ||
![]() |
52b9060415 | ||
![]() |
74728e5f42 | ||
![]() |
3e482d08d7 | ||
![]() |
7e55220c3f | ||
![]() |
453d1daf8b | ||
![]() |
d0eb4e0946 | ||
![]() |
9a40196678 | ||
![]() |
4f7552ea1d | ||
![]() |
7412e357cf | ||
![]() |
bac96c679f | ||
![]() |
4f1d201286 | ||
![]() |
bcf6559514 | ||
![]() |
0af9f2b875 | ||
![]() |
d9393c6663 | ||
![]() |
00274c991f | ||
![]() |
e6848b68aa | ||
![]() |
853a460bd7 | ||
![]() |
ff5b708707 | ||
![]() |
0d62ba2f80 | ||
![]() |
f257716d1b | ||
![]() |
43a6cd0bf9 | ||
![]() |
af8965664e | ||
![]() |
168ad315c7 | ||
![]() |
66510de4e9 | ||
![]() |
942e05888b | ||
![]() |
1970273c58 | ||
![]() |
06d081a73b | ||
![]() |
352efa6d47 | ||
![]() |
586dc3868d | ||
![]() |
0fe149dd57 | ||
![]() |
688845b907 | ||
![]() |
21af37a7a3 | ||
![]() |
6078b8d9e5 | ||
![]() |
bfe1457897 | ||
![]() |
f873b77a5f | ||
![]() |
be1af58147 | ||
![]() |
2b8268f1d4 | ||
![]() |
6cc3cd325c | ||
![]() |
efab0dbc47 | ||
![]() |
a5b4ed83f7 | ||
![]() |
8eed5c7709 | ||
![]() |
883d3ad29b | ||
![]() |
044d5d2a84 | ||
![]() |
68f23b2cdf | ||
![]() |
91553ebe34 | ||
![]() |
a45bc9b31e | ||
![]() |
849f52de67 | ||
![]() |
46f9841dce | ||
![]() |
d8213b5fa5 | ||
![]() |
ee276adcf8 | ||
![]() |
07ae847d08 | ||
![]() |
95dc4713f4 | ||
![]() |
2294dc0ad9 | ||
![]() |
87c0d7e54f | ||
![]() |
18238241ef | ||
![]() |
192cb193a1 | ||
![]() |
2fb503df17 | ||
![]() |
810566729d | ||
![]() |
746912cece | ||
![]() |
7a38a57397 | ||
![]() |
4fdf405d77 | ||
![]() |
488706293f | ||
![]() |
9373325f1b | ||
![]() |
e151248ac2 | ||
![]() |
b09ccc4373 | ||
![]() |
f4a7e28aa5 | ||
![]() |
5b85d1e248 | ||
![]() |
a85bc5cad4 | ||
![]() |
d682edd44f | ||
![]() |
3524399984 | ||
![]() |
b127788100 | ||
![]() |
a823a6b371 | ||
![]() |
b47f76c037 | ||
![]() |
1d19684b2c | ||
![]() |
08e8c93b16 | ||
![]() |
a0103ebd6c | ||
![]() |
b5a600d488 | ||
![]() |
27410a6c51 | ||
![]() |
67d6de9f68 | ||
![]() |
3996fa00ef | ||
![]() |
42f8509287 | ||
![]() |
11b738b837 | ||
![]() |
576858b6ca | ||
![]() |
645c2bdd4a | ||
![]() |
9ae708b367 | ||
![]() |
abf554f9cf | ||
![]() |
9df6e76cc3 | ||
![]() |
7afbe952e6 | ||
![]() |
00aa92f7b6 | ||
![]() |
4ae264de5e | ||
![]() |
4b987dd334 | ||
![]() |
f75c4c0ba3 | ||
![]() |
8b4d089376 | ||
![]() |
bd2e758b04 | ||
![]() |
54e5910e45 | ||
![]() |
5460d5748f | ||
![]() |
25d5d95a5b | ||
![]() |
8db26af57a | ||
![]() |
4f29cbe81f | ||
![]() |
0dced91495 | ||
![]() |
c02a463348 | ||
![]() |
a2f717fba2 | ||
![]() |
8973571dc0 | ||
![]() |
0fe3aacb4d | ||
![]() |
7313b4fd26 | ||
![]() |
5c0b3f8b34 | ||
![]() |
a4eb795d32 | ||
![]() |
8e1efc2851 | ||
![]() |
8c999907c2 | ||
![]() |
cd7a31dd3c | ||
![]() |
b21b0427d1 | ||
![]() |
3a2299f7f2 | ||
![]() |
7d5287000f | ||
![]() |
bc37c56742 | ||
![]() |
97b04d8b43 | ||
![]() |
5de1c078d2 | ||
![]() |
021ef6e6c4 | ||
![]() |
69d20eb297 | ||
![]() |
4688348020 | ||
![]() |
cf02f3133a | ||
![]() |
e0748540d7 | ||
![]() |
ab3c28e46a | ||
![]() |
13ae12b57d | ||
![]() |
222cdc7f79 | ||
![]() |
e8a1d2f1bd | ||
![]() |
5245670af1 | ||
![]() |
6b83d516a7 | ||
![]() |
b72562e805 | ||
![]() |
0b964c8358 | ||
![]() |
d61f9547fe | ||
![]() |
78360608b1 | ||
![]() |
2a25e3cb89 | ||
![]() |
f3b7fda4a8 | ||
![]() |
6c6d070b16 | ||
![]() |
eec0a11ef0 | ||
![]() |
2b262f453d | ||
![]() |
c2b494f702 | ||
![]() |
958ee00efd | ||
![]() |
363354d941 | ||
![]() |
074ea61514 | ||
![]() |
abc59d3d30 | ||
![]() |
fea683f992 | ||
![]() |
3402f4f514 | ||
![]() |
3bb82ea330 | ||
![]() |
bced09e5b3 | ||
![]() |
9e84402f42 | ||
![]() |
18c65453fd | ||
![]() |
57ed99020f | ||
![]() |
caa3b0c438 | ||
![]() |
5133cf0275 | ||
![]() |
7f6c080b46 | ||
![]() |
142907395f | ||
![]() |
43d069438e | ||
![]() |
e7b73c4f53 | ||
![]() |
f2ca0a2372 | ||
![]() |
021cfe446f | ||
![]() |
1a71c906d5 | ||
![]() |
10d2eb6449 | ||
![]() |
0f283e088e | ||
![]() |
025977f19a | ||
![]() |
2a9ba788d0 | ||
![]() |
aa65266726 | ||
![]() |
4b6c58292b | ||
![]() |
d0813cc736 | ||
![]() |
f1d7e5e779 | ||
![]() |
66f01fc880 | ||
![]() |
d93384536f | ||
![]() |
69250db70e | ||
![]() |
ad52398087 | ||
![]() |
4f1eec31a1 | ||
![]() |
43c02740ab | ||
![]() |
4605f74cf9 | ||
![]() |
9ab4b35f22 | ||
![]() |
e9784f0e69 | ||
![]() |
3e37d0a39b | ||
![]() |
44ae162f09 | ||
![]() |
2821b9a832 | ||
![]() |
cf97247f75 | ||
![]() |
1bb40e2be1 | ||
![]() |
869db9e31c | ||
![]() |
39ee52ad3c | ||
![]() |
7e699af2b5 | ||
![]() |
2b344cc717 | ||
![]() |
246f0bc442 | ||
![]() |
4afb659f44 | ||
![]() |
a43069fc35 | ||
![]() |
5b43266278 | ||
![]() |
5df16371e1 | ||
![]() |
c086f05c7c | ||
![]() |
7a38857bcd | ||
![]() |
e860925f57 | ||
![]() |
3808067dd7 | ||
![]() |
c7d7dec40d | ||
![]() |
e96e0acc9f | ||
![]() |
3efd2398ca | ||
![]() |
7284ef6e06 | ||
![]() |
f5dc3ad753 | ||
![]() |
fc0d0031bf | ||
![]() |
d44ee4b8fa | ||
![]() |
eb5e755aa6 | ||
![]() |
6fc9e90f28 | ||
![]() |
2effd3da16 | ||
![]() |
69230b1147 | ||
![]() |
5435bf3ec4 | ||
![]() |
b0b13bfcb9 | ||
![]() |
c3b0b2ecf0 | ||
![]() |
a276421d25 | ||
![]() |
dbb6303bdc | ||
![]() |
d28036e173 | ||
![]() |
bc3f1cae16 | ||
![]() |
5e84d0c2b3 | ||
![]() |
086f88852d | ||
![]() |
11196443ac | ||
![]() |
6694cb42c8 | ||
![]() |
b6e293c38e | ||
![]() |
02090c953b | ||
![]() |
dbe8bf5428 |
21
.circleci/config.yml
Normal file
21
.circleci/config.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Python CircleCI 2.0 configuration file
|
||||||
|
# Updating CircleCI configuration from v1 to v2
|
||||||
|
# Check https://circleci.com/docs/2.0/language-python/ for more details
|
||||||
|
#
|
||||||
|
version: 2
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
name: build images
|
||||||
|
command: |
|
||||||
|
docker build -t jupyterhub/jupyterhub .
|
||||||
|
docker build -t jupyterhub/jupyterhub-onbuild onbuild
|
||||||
|
docker build -t jupyterhub/jupyterhub:alpine -f dockerfiles/Dockerfile.alpine .
|
||||||
|
docker build -t jupyterhub/singleuser singleuser
|
||||||
|
- run:
|
||||||
|
name: smoke test jupyterhub
|
||||||
|
command: |
|
||||||
|
docker run --rm -it jupyterhub/jupyterhub jupyterhub --help
|
13
.coveragerc
13
.coveragerc
@@ -1,4 +1,17 @@
|
|||||||
[run]
|
[run]
|
||||||
|
branch = False
|
||||||
omit =
|
omit =
|
||||||
jupyterhub/tests/*
|
jupyterhub/tests/*
|
||||||
jupyterhub/alembic/*
|
jupyterhub/alembic/*
|
||||||
|
|
||||||
|
[report]
|
||||||
|
exclude_lines =
|
||||||
|
if self.debug:
|
||||||
|
pragma: no cover
|
||||||
|
raise NotImplementedError
|
||||||
|
if __name__ == .__main__.:
|
||||||
|
ignore_errors = True
|
||||||
|
omit =
|
||||||
|
jupyterhub/tests/*
|
||||||
|
jupyterhub/alembic/*
|
||||||
|
*/site-packages/*
|
||||||
|
@@ -4,3 +4,7 @@ jupyterhub_cookie_secret
|
|||||||
jupyterhub.sqlite
|
jupyterhub.sqlite
|
||||||
jupyterhub_config.py
|
jupyterhub_config.py
|
||||||
node_modules
|
node_modules
|
||||||
|
docs
|
||||||
|
.git
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
25
.flake8
Normal file
25
.flake8
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[flake8]
|
||||||
|
# Ignore style and complexity
|
||||||
|
# E: style errors
|
||||||
|
# W: style warnings
|
||||||
|
# C: complexity
|
||||||
|
# F401: module imported but unused
|
||||||
|
# F403: import *
|
||||||
|
# F811: redefinition of unused `name` from line `N`
|
||||||
|
# F841: local variable assigned but never used
|
||||||
|
# E402: module level import not at top of file
|
||||||
|
# I100: Import statements are in the wrong order
|
||||||
|
# I101: Imported names are in the wrong order. Should be
|
||||||
|
ignore = E, C, W, F401, F403, F811, F841, E402, I100, I101
|
||||||
|
|
||||||
|
exclude =
|
||||||
|
.cache,
|
||||||
|
.github,
|
||||||
|
docs,
|
||||||
|
examples,
|
||||||
|
jupyterhub/alembic*,
|
||||||
|
onbuild,
|
||||||
|
scripts,
|
||||||
|
share,
|
||||||
|
tools,
|
||||||
|
setup.py
|
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Hi! Thanks for using JupyterHub.
|
||||||
|
|
||||||
|
If you are reporting an issue with JupyterHub, please use the [GitHub issue](https://github.com/jupyterhub/jupyterhub/issues) search feature to check if your issue has been asked already. If it has, please add your comments to the existing issue.
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
|
|
||||||
|
- Running `jupyter troubleshoot` from the command line, if possible, and posting
|
||||||
|
its output would also be helpful.
|
||||||
|
- Running in `--debug` mode can also be helpful for troubleshooting.
|
7
.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/installation-and-configuration-issues.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
name: Installation and configuration issues
|
||||||
|
about: Installation and configuration assistance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If you are having issues with installation or configuration, you may ask for help on the JupyterHub gitter channel or file an issue here.
|
0
.github/PULL_REQUEST_TEMPLATE/.keep
vendored
Normal file
0
.github/PULL_REQUEST_TEMPLATE/.keep
vendored
Normal file
29
.github/issue_template.md
vendored
29
.github/issue_template.md
vendored
@@ -1,29 +0,0 @@
|
|||||||
Hi! Thanks for using JupyterHub.
|
|
||||||
|
|
||||||
If you are reporting an issue with JupyterHub:
|
|
||||||
|
|
||||||
- Please use the [GitHub issue](https://github.com/jupyterhub/jupyterhub/issues)
|
|
||||||
search feature to check if your issue has been asked already. If it has,
|
|
||||||
please add your comments to the existing issue.
|
|
||||||
|
|
||||||
- Where applicable, please fill out the details below to help us troubleshoot
|
|
||||||
the issue that you are facing. Please be as thorough as you are able to
|
|
||||||
provide details on the issue.
|
|
||||||
|
|
||||||
**How to reproduce the issue**
|
|
||||||
|
|
||||||
**What you expected to happen**
|
|
||||||
|
|
||||||
**What actually happens**
|
|
||||||
|
|
||||||
**Share what version of JupyterHub you are using**
|
|
||||||
|
|
||||||
Running `jupyter troubleshoot` from the command line, if possible, and posting
|
|
||||||
its output would also be helpful.
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Insert jupyter troubleshoot output here
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
13
.gitignore
vendored
13
.gitignore
vendored
@@ -3,9 +3,10 @@ node_modules
|
|||||||
*~
|
*~
|
||||||
.cache
|
.cache
|
||||||
.DS_Store
|
.DS_Store
|
||||||
build
|
/build
|
||||||
dist
|
dist
|
||||||
docs/_build
|
docs/_build
|
||||||
|
docs/build
|
||||||
docs/source/_static/rest-api
|
docs/source/_static/rest-api
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
# ignore config file at the top-level of the repo
|
# ignore config file at the top-level of the repo
|
||||||
@@ -13,11 +14,13 @@ docs/source/_static/rest-api
|
|||||||
/jupyterhub_config.py
|
/jupyterhub_config.py
|
||||||
jupyterhub_cookie_secret
|
jupyterhub_cookie_secret
|
||||||
jupyterhub.sqlite
|
jupyterhub.sqlite
|
||||||
share/jupyter/hub/static/components
|
package-lock.json
|
||||||
share/jupyter/hub/static/css/style.min.css
|
share/jupyterhub/static/components
|
||||||
share/jupyter/hub/static/css/style.min.css.map
|
share/jupyterhub/static/css/style.min.css
|
||||||
|
share/jupyterhub/static/css/style.min.css.map
|
||||||
*.egg-info
|
*.egg-info
|
||||||
MANIFEST
|
MANIFEST
|
||||||
.coverage
|
.coverage
|
||||||
htmlcov
|
htmlcov
|
||||||
|
.idea/
|
||||||
|
.pytest_cache
|
||||||
|
64
.travis.yml
64
.travis.yml
@@ -1,22 +1,68 @@
|
|||||||
# http://travis-ci.org/#!/jupyter/jupyterhub
|
|
||||||
language: python
|
language: python
|
||||||
sudo: false
|
sudo: false
|
||||||
|
cache:
|
||||||
|
- pip
|
||||||
python:
|
python:
|
||||||
- 3.6-dev
|
- 3.6
|
||||||
- 3.5
|
- 3.5
|
||||||
- 3.4
|
- nightly
|
||||||
- 3.3
|
env:
|
||||||
|
global:
|
||||||
|
- ASYNC_TEST_TIMEOUT=15
|
||||||
|
- MYSQL_HOST=127.0.0.1
|
||||||
|
- MYSQL_TCP_PORT=13306
|
||||||
|
services:
|
||||||
|
- postgres
|
||||||
|
- docker
|
||||||
|
|
||||||
|
# installing dependencies
|
||||||
before_install:
|
before_install:
|
||||||
|
- nvm install 6; nvm use 6
|
||||||
- npm install
|
- npm install
|
||||||
- npm install -g configurable-http-proxy
|
- npm install -g configurable-http-proxy
|
||||||
- git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels
|
- |
|
||||||
|
# setup database
|
||||||
|
if [[ $JUPYTERHUB_TEST_DB_URL == mysql* ]]; then
|
||||||
|
unset MYSQL_UNIX_PORT
|
||||||
|
DB=mysql bash ci/docker-db.sh
|
||||||
|
DB=mysql bash ci/init-db.sh
|
||||||
|
pip install 'mysql-connector<2.2'
|
||||||
|
elif [[ $JUPYTERHUB_TEST_DB_URL == postgresql* ]]; then
|
||||||
|
DB=postgres bash ci/init-db.sh
|
||||||
|
pip install psycopg2-binary
|
||||||
|
fi
|
||||||
install:
|
install:
|
||||||
- pip install --pre -f travis-wheels/wheelhouse -r dev-requirements.txt .
|
- pip install --upgrade pip
|
||||||
|
- pip install --pre -r dev-requirements.txt .
|
||||||
|
- pip freeze
|
||||||
|
|
||||||
|
# running tests
|
||||||
script:
|
script:
|
||||||
- travis_retry py.test --cov jupyterhub jupyterhub/tests -v
|
- |
|
||||||
|
# run tests
|
||||||
|
set -e
|
||||||
|
pytest -v --maxfail=2 --cov=jupyterhub jupyterhub/tests
|
||||||
|
- |
|
||||||
|
# build docs
|
||||||
|
pushd docs
|
||||||
|
pip install -r requirements.txt
|
||||||
|
make html
|
||||||
|
popd
|
||||||
after_success:
|
after_success:
|
||||||
- codecov
|
- codecov
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
|
fast_finish: true
|
||||||
include:
|
include:
|
||||||
- python: 3.5
|
- python: 3.6
|
||||||
env: JUPYTERHUB_TEST_SUBDOMAIN_HOST=http://127.0.0.1.xip.io:8000
|
env: JUPYTERHUB_TEST_SUBDOMAIN_HOST=http://localhost.jovyan.org:8000
|
||||||
|
- python: 3.6
|
||||||
|
env:
|
||||||
|
- JUPYTERHUB_TEST_DB_URL=mysql+mysqlconnector://root@127.0.0.1:$MYSQL_TCP_PORT/jupyterhub
|
||||||
|
- 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
1
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Please refer to [Project Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md).
|
@@ -1,3 +1,98 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
We mainly follow the [IPython Contributing Guide](https://github.com/ipython/ipython/blob/master/CONTRIBUTING.md).
|
Welcome! As a [Jupyter](https://jupyter.org) project, we follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html).
|
||||||
|
|
||||||
|
|
||||||
|
## Set up your development system
|
||||||
|
|
||||||
|
For a development install, clone the [repository](https://github.com/jupyterhub/jupyterhub)
|
||||||
|
and then install from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/jupyterhub/jupyterhub
|
||||||
|
cd jupyterhub
|
||||||
|
npm install -g configurable-http-proxy
|
||||||
|
pip3 install -r dev-requirements.txt -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting a development install
|
||||||
|
|
||||||
|
If the `pip3 install` command fails and complains about `lessc` being
|
||||||
|
unavailable, you may need to explicitly install some additional JavaScript
|
||||||
|
dependencies:
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
||||||
|
This will fetch client-side JavaScript dependencies necessary to compile CSS.
|
||||||
|
|
||||||
|
You may also need to manually update JavaScript and CSS after some development
|
||||||
|
updates, with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 setup.py js # fetch updated client-side js
|
||||||
|
python3 setup.py css # recompile CSS from LESS sources
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the test suite
|
||||||
|
|
||||||
|
We use [pytest](http://doc.pytest.org/en/latest/) for running tests.
|
||||||
|
|
||||||
|
1. Set up a development install as described above.
|
||||||
|
|
||||||
|
2. Set environment variable for `ASYNC_TEST_TIMEOUT` to 15 seconds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ASYNC_TEST_TIMEOUT=15
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run tests.
|
||||||
|
|
||||||
|
To run all the tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest -v jupyterhub/tests
|
||||||
|
```
|
||||||
|
|
||||||
|
To run an individual test file (i.e. `test_api.py`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest -v jupyterhub/tests/test_api.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting tests
|
||||||
|
|
||||||
|
If you see test failures because of timeouts, you may wish to increase the
|
||||||
|
`ASYNC_TEST_TIMEOUT` used by the
|
||||||
|
[pytest-tornado-plugin](https://github.com/eugeniy/pytest-tornado/blob/c79f68de2222eb7cf84edcfe28650ebf309a4d0c/README.rst#markers)
|
||||||
|
from the default of 5 seconds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ASYNC_TEST_TIMEOUT=15
|
||||||
|
```
|
||||||
|
|
||||||
|
If you see many test errors and failures, double check that you have installed
|
||||||
|
`configurable-http-proxy`.
|
||||||
|
|
||||||
|
## Building the Docs locally
|
||||||
|
|
||||||
|
1. Install the development system as described above.
|
||||||
|
|
||||||
|
2. Install the dependencies for documentation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pip install -r docs/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build the docs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docs
|
||||||
|
make clean
|
||||||
|
make html
|
||||||
|
```
|
||||||
|
|
||||||
|
4. View the docs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open build/html/index.html
|
||||||
|
```
|
||||||
|
24
Dockerfile
24
Dockerfile
@@ -21,28 +21,26 @@
|
|||||||
# your jupyterhub_config.py will be added automatically
|
# your jupyterhub_config.py will be added automatically
|
||||||
# from your docker directory.
|
# from your docker directory.
|
||||||
|
|
||||||
FROM debian:jessie
|
FROM ubuntu:18.04
|
||||||
MAINTAINER Jupyter Project <jupyter@googlegroups.com>
|
LABEL maintainer="Jupyter Project <jupyter@googlegroups.com>"
|
||||||
|
|
||||||
# install nodejs, utf8 locale, set CDN because default httpredir is unreliable
|
# install nodejs, utf8 locale, set CDN because default httpredir is unreliable
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
RUN REPO=http://cdn-fastly.deb.debian.org && \
|
RUN apt-get -y update && \
|
||||||
echo "deb $REPO/debian jessie main\ndeb $REPO/debian-security jessie/updates main" > /etc/apt/sources.list && \
|
|
||||||
apt-get -y update && \
|
|
||||||
apt-get -y upgrade && \
|
apt-get -y upgrade && \
|
||||||
apt-get -y install wget locales git bzip2 &&\
|
apt-get -y install wget git bzip2 && \
|
||||||
/usr/sbin/update-locale LANG=C.UTF-8 && \
|
apt-get purge && \
|
||||||
locale-gen C.UTF-8 && \
|
|
||||||
apt-get remove -y locales && \
|
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|
||||||
# install Python + NodeJS with conda
|
# install Python + NodeJS with conda
|
||||||
RUN wget -q https://repo.continuum.io/miniconda/Miniconda3-4.2.12-Linux-x86_64.sh -O /tmp/miniconda.sh && \
|
RUN wget -q https://repo.continuum.io/miniconda/Miniconda3-4.5.1-Linux-x86_64.sh -O /tmp/miniconda.sh && \
|
||||||
echo 'd0c7c71cc5659e54ab51f2005a8d96f3 */tmp/miniconda.sh' | md5sum -c - && \
|
echo '0c28787e3126238df24c5d4858bd0744 */tmp/miniconda.sh' | md5sum -c - && \
|
||||||
bash /tmp/miniconda.sh -f -b -p /opt/conda && \
|
bash /tmp/miniconda.sh -f -b -p /opt/conda && \
|
||||||
/opt/conda/bin/conda install --yes -c conda-forge python=3.5 sqlalchemy tornado jinja2 traitlets requests pip nodejs configurable-http-proxy && \
|
/opt/conda/bin/conda install --yes -c conda-forge \
|
||||||
|
python=3.6 sqlalchemy tornado jinja2 traitlets requests pip pycurl \
|
||||||
|
nodejs configurable-http-proxy && \
|
||||||
/opt/conda/bin/pip install --upgrade pip && \
|
/opt/conda/bin/pip install --upgrade pip && \
|
||||||
rm /tmp/miniconda.sh
|
rm /tmp/miniconda.sh
|
||||||
ENV PATH=/opt/conda/bin:$PATH
|
ENV PATH=/opt/conda/bin:$PATH
|
||||||
@@ -50,7 +48,7 @@ ENV PATH=/opt/conda/bin:$PATH
|
|||||||
ADD . /src/jupyterhub
|
ADD . /src/jupyterhub
|
||||||
WORKDIR /src/jupyterhub
|
WORKDIR /src/jupyterhub
|
||||||
|
|
||||||
RUN python setup.py js && pip install . && \
|
RUN pip install . && \
|
||||||
rm -rf $PWD ~/.cache ~/.npm
|
rm -rf $PWD ~/.cache ~/.npm
|
||||||
|
|
||||||
RUN mkdir -p /srv/jupyterhub/
|
RUN mkdir -p /srv/jupyterhub/
|
||||||
|
22
MANIFEST.in
22
MANIFEST.in
@@ -1,8 +1,9 @@
|
|||||||
include README.md
|
include README.md
|
||||||
include COPYING.md
|
include COPYING.md
|
||||||
include setupegg.py
|
include setupegg.py
|
||||||
include bower.json
|
include bower-lite
|
||||||
include package.json
|
include package.json
|
||||||
|
include package-lock.json
|
||||||
include *requirements.txt
|
include *requirements.txt
|
||||||
include Dockerfile
|
include Dockerfile
|
||||||
|
|
||||||
@@ -10,20 +11,23 @@ graft onbuild
|
|||||||
graft jupyterhub
|
graft jupyterhub
|
||||||
graft scripts
|
graft scripts
|
||||||
graft share
|
graft share
|
||||||
|
graft singleuser
|
||||||
|
graft ci
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
graft docs
|
graft docs
|
||||||
prune docs/node_modules
|
prune docs/node_modules
|
||||||
|
|
||||||
# prune some large unused files from components
|
# prune some large unused files from components
|
||||||
prune share/jupyter/hub/static/components/bootstrap/css
|
prune share/jupyterhub/static/components/bootstrap/dist/css
|
||||||
exclude share/jupyter/hub/static/components/components/fonts/*.svg
|
exclude share/jupyterhub/static/components/bootstrap/dist/fonts/*.svg
|
||||||
exclude share/jupyter/hub/static/components/bootstrap/less/*.js
|
prune share/jupyterhub/static/components/font-awesome/css
|
||||||
exclude share/jupyter/hub/static/components/font-awesome/css
|
prune share/jupyterhub/static/components/font-awesome/scss
|
||||||
exclude share/jupyter/hub/static/components/font-awesome/fonts/*.svg
|
exclude share/jupyterhub/static/components/font-awesome/fonts/*.svg
|
||||||
exclude share/jupyter/hub/static/components/jquery/*migrate*.js
|
prune share/jupyterhub/static/components/jquery/external
|
||||||
prune share/jupyter/hub/static/components/moment/lang
|
prune share/jupyterhub/static/components/jquery/src
|
||||||
prune share/jupyter/hub/static/components/moment/min
|
prune share/jupyterhub/static/components/moment/lang
|
||||||
|
prune share/jupyterhub/static/components/moment/min
|
||||||
|
|
||||||
# Patterns to exclude from any directory
|
# Patterns to exclude from any directory
|
||||||
global-exclude *~
|
global-exclude *~
|
||||||
|
1
PULL_REQUEST_TEMPLATE.md
Normal file
1
PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
281
README.md
281
README.md
@@ -1,210 +1,249 @@
|
|||||||
**[Technical overview](#technical-overview)** |
|
**[Technical Overview](#technical-overview)** |
|
||||||
**[Prerequisites](#prerequisites)** |
|
|
||||||
**[Installation](#installation)** |
|
**[Installation](#installation)** |
|
||||||
**[Running the Hub Server](#running-the-hub-server)** |
|
|
||||||
**[Configuration](#configuration)** |
|
**[Configuration](#configuration)** |
|
||||||
**[Docker](#docker)** |
|
**[Docker](#docker)** |
|
||||||
**[Contributing](#contributing)** |
|
**[Contributing](#contributing)** |
|
||||||
**[License](#license)** |
|
**[License](#license)** |
|
||||||
**[Getting help](#getting-help)**
|
**[Help and Resources](#help-and-resources)**
|
||||||
|
|
||||||
|
|
||||||
# [JupyterHub](https://github.com/jupyterhub/jupyterhub)
|
# [JupyterHub](https://github.com/jupyterhub/jupyterhub)
|
||||||
|
|
||||||
|
|
||||||
|
[](https://pypi.python.org/pypi/jupyterhub)
|
||||||
|
[](https://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
||||||
|
[](https://jupyterhub.readthedocs.io/en/0.7.2/?badge=0.7.2)
|
||||||
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
||||||
[](https://circleci.com/gh/jupyterhub/jupyterhub)
|
[](https://circleci.com/gh/jupyterhub/jupyterhub)
|
||||||
[](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
|
[](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
|
||||||
"
|
[](https://groups.google.com/forum/#!forum/jupyter)
|
||||||
[](http://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
|
||||||
"
|
|
||||||
[](https://groups.google.com/forum/#!forum/jupyter)
|
|
||||||
|
|
||||||
With [JupyterHub](https://jupyterhub.readthedocs.io) you can create a
|
With [JupyterHub](https://jupyterhub.readthedocs.io) you can create a
|
||||||
**multi-user Hub** which spawns, manages, and proxies multiple instances of the
|
**multi-user Hub** which spawns, manages, and proxies multiple instances of the
|
||||||
single-user [Jupyter notebook *(IPython notebook)* ](https://jupyter-notebook.readthedocs.io) server.
|
single-user [Jupyter notebook](https://jupyter-notebook.readthedocs.io)
|
||||||
|
server.
|
||||||
|
|
||||||
JupyterHub provides **single-user notebook servers to many users**. For example,
|
[Project Jupyter](https://jupyter.org) created JupyterHub to support many
|
||||||
JupyterHub could serve notebooks to a class of students, a corporate
|
users. The Hub can offer notebook servers to a class of students, a corporate
|
||||||
workgroup, or a science research group.
|
data science workgroup, a scientific research project, or a high performance
|
||||||
|
computing group.
|
||||||
by [Project Jupyter](https://jupyter.org)
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
## Technical overview
|
## Technical overview
|
||||||
|
|
||||||
Three main actors make up JupyterHub:
|
Three main actors make up JupyterHub:
|
||||||
|
|
||||||
- multi-user **Hub** (tornado process)
|
- multi-user **Hub** (tornado process)
|
||||||
- configurable http **proxy** (node-http-proxy)
|
- configurable http **proxy** (node-http-proxy)
|
||||||
- multiple **single-user Jupyter notebook servers** (Python/IPython/tornado)
|
- multiple **single-user Jupyter notebook servers** (Python/Jupyter/tornado)
|
||||||
|
|
||||||
JupyterHub's basic principles for operation are:
|
Basic principles for operation are:
|
||||||
|
|
||||||
- Hub spawns a proxy
|
- Hub launches a proxy.
|
||||||
- Proxy forwards all requests to Hub by default
|
- Proxy forwards all requests to Hub by default.
|
||||||
- Hub handles login, and spawns single-user servers on demand
|
- Hub handles login, and spawns single-user servers on demand.
|
||||||
- Hub configures proxy to forward url prefixes to the single-user servers
|
- Hub configures proxy to forward url prefixes to the single-user notebook
|
||||||
|
servers.
|
||||||
|
|
||||||
JupyterHub also provides a
|
JupyterHub also provides a
|
||||||
[REST API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/master/docs/rest-api.yml#/default)
|
[REST API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/master/docs/rest-api.yml#/default)
|
||||||
for administration of the Hub and users.
|
for administration of the Hub and its users.
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
Before installing JupyterHub, you need:
|
|
||||||
|
|
||||||
- [Python](https://www.python.org/downloads/) 3.3 or greater
|
|
||||||
|
|
||||||
An understanding of using [`pip`](https://pip.pypa.io/en/stable/) for installing
|
|
||||||
Python packages is recommended.
|
|
||||||
|
|
||||||
- [nodejs/npm](https://www.npmjs.com/)
|
|
||||||
|
|
||||||
[Install nodejs/npm](https://docs.npmjs.com/getting-started/installing-node), which is available from your
|
|
||||||
package manager. For example, install 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.)
|
|
||||||
|
|
||||||
- TLS certificate and key for HTTPS communication
|
|
||||||
|
|
||||||
- Domain name
|
|
||||||
|
|
||||||
Before running the single-user notebook servers (which may be on the same system as the Hub or not):
|
|
||||||
|
|
||||||
- [Jupyter Notebook](https://jupyter.readthedocs.io/en/latest/install.html) version 4 or greater
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
||||||
|
### Check prerequisites
|
||||||
|
|
||||||
|
- A Linux/Unix based system
|
||||||
|
- [Python](https://www.python.org/downloads/) 3.5 or greater
|
||||||
|
- [nodejs/npm](https://www.npmjs.com/)
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
- 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`:
|
JupyterHub can be installed with `pip`, and the proxy with `npm`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g configurable-http-proxy
|
npm install -g configurable-http-proxy
|
||||||
pip3 install jupyterhub
|
python3 -m pip install jupyterhub
|
||||||
```
|
```
|
||||||
|
|
||||||
If you plan to run notebook servers locally, you will need to install the
|
If you plan to run notebook servers locally, you will need to install the
|
||||||
Jupyter notebook:
|
[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
|
||||||
|
|
||||||
## Running the Hub server
|
|
||||||
To start the Hub server, run the command:
|
To start the Hub server, run the command:
|
||||||
|
|
||||||
jupyterhub
|
jupyterhub
|
||||||
|
|
||||||
Visit `https://localhost:8000` in your browser, and sign in with your unix credentials.
|
Visit `https://localhost:8000` in your browser, and sign in with your unix
|
||||||
|
PAM credentials.
|
||||||
|
|
||||||
To allow multiple users to sign into the server, you will need to
|
*Note*: To allow multiple users to sign into the server, you will need to
|
||||||
run the `jupyterhub` command as a *privileged user*, such as root.
|
run the `jupyterhub` command as a *privileged user*, such as root.
|
||||||
The [wiki](https://github.com/jupyterhub/jupyterhub/wiki/Using-sudo-to-run-JupyterHub-without-root-privileges)
|
The [wiki](https://github.com/jupyterhub/jupyterhub/wiki/Using-sudo-to-run-JupyterHub-without-root-privileges)
|
||||||
describes how to run the server as a *less privileged user*, which requires more
|
describes how to run the server as a *less privileged user*, which requires
|
||||||
configuration of the system.
|
more configuration of the system.
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
The [getting started document](docs/source/getting-started.md) contains the
|
|
||||||
basics of configuring a JupyterHub deployment.
|
|
||||||
|
|
||||||
The JupyterHub **tutorial** provides a video and documentation that explains and illustrates the fundamental steps for installation and configuration. [Repo](https://github.com/jupyterhub/jupyterhub-tutorial)
|
The [Getting Started](https://jupyterhub.readthedocs.io/en/latest/getting-started/index.html) section of the
|
||||||
| [Tutorial documentation](http://jupyterhub-tutorial.readthedocs.io/en/latest/)
|
documentation explains the common steps in setting up JupyterHub.
|
||||||
|
|
||||||
#### Generate a default configuration file
|
The [**JupyterHub tutorial**](https://github.com/jupyterhub/jupyterhub-tutorial)
|
||||||
Generate a default config file:
|
provides an in-depth video and sample configurations of JupyterHub.
|
||||||
|
|
||||||
|
### Create a configuration file
|
||||||
|
|
||||||
|
To generate a default config file with settings and descriptions:
|
||||||
|
|
||||||
jupyterhub --generate-config
|
jupyterhub --generate-config
|
||||||
|
|
||||||
#### Customize the configuration, authentication, and process spawning
|
### Start the Hub
|
||||||
Spawn the server on ``10.0.1.2:443`` with **https**:
|
|
||||||
|
To start the Hub on a specific url and port ``10.0.1.2:443`` with **https**:
|
||||||
|
|
||||||
jupyterhub --ip 10.0.1.2 --port 443 --ssl-key my_ssl.key --ssl-cert my_ssl.cert
|
jupyterhub --ip 10.0.1.2 --port 443 --ssl-key my_ssl.key --ssl-cert my_ssl.cert
|
||||||
|
|
||||||
The authentication and process spawning mechanisms can be replaced,
|
### Authenticators
|
||||||
which should allow plugging into a variety of authentication or process control environments.
|
|
||||||
Some examples, meant as illustration and testing of this concept:
|
|
||||||
|
|
||||||
- Using GitHub OAuth instead of PAM with [OAuthenticator](https://github.com/jupyterhub/oauthenticator)
|
| Authenticator | Description |
|
||||||
- Spawning single-user servers with Docker, using the [DockerSpawner](https://github.com/jupyterhub/dockerspawner)
|
| --------------------------------------------------------------------------- | ------------------------------------------------- |
|
||||||
|
| PAMAuthenticator | Default, built-in authenticator |
|
||||||
|
| [OAuthenticator](https://github.com/jupyterhub/oauthenticator) | OAuth + JupyterHub Authenticator = OAuthenticator |
|
||||||
|
| [ldapauthenticator](https://github.com/jupyterhub/ldapauthenticator) | Simple LDAP Authenticator Plugin for JupyterHub |
|
||||||
|
| [kdcAuthenticator](https://github.com/bloomberg/jupyterhub-kdcauthenticator)| Kerberos Authenticator Plugin for JupyterHub |
|
||||||
|
|
||||||
----
|
### Spawners
|
||||||
|
|
||||||
|
| Spawner | Description |
|
||||||
|
| -------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||||
|
| LocalProcessSpawner | Default, built-in spawner starts single-user servers as local processes |
|
||||||
|
| [dockerspawner](https://github.com/jupyterhub/dockerspawner) | Spawn single-user servers in Docker containers |
|
||||||
|
| [kubespawner](https://github.com/jupyterhub/kubespawner) | Kubernetes spawner for JupyterHub |
|
||||||
|
| [sudospawner](https://github.com/jupyterhub/sudospawner) | Spawn single-user servers without being root |
|
||||||
|
| [systemdspawner](https://github.com/jupyterhub/systemdspawner) | Spawn single-user notebook servers using systemd |
|
||||||
|
| [batchspawner](https://github.com/jupyterhub/batchspawner) | Designed for clusters using batch scheduling software |
|
||||||
|
| [wrapspawner](https://github.com/jupyterhub/wrapspawner) | WrapSpawner and ProfilesSpawner enabling runtime configuration of spawners |
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
A starter [docker image for JupyterHub](https://hub.docker.com/r/jupyterhub/jupyterhub/) gives a baseline deployment of JupyterHub.
|
|
||||||
|
|
||||||
**Important:** This `jupyterhub/jupyterhub` image contains only the Hub itself, with no configuration. In general, one needs
|
A starter [**docker image for JupyterHub**](https://hub.docker.com/r/jupyterhub/jupyterhub/)
|
||||||
to make a derivative image, with at least a `jupyterhub_config.py` setting up an Authenticator and/or a Spawner. To run the
|
gives a baseline deployment of JupyterHub using Docker.
|
||||||
single-user servers, which may be on the same system as the Hub or not, Jupyter Notebook version 4 or greater must be installed.
|
|
||||||
|
**Important:** This `jupyterhub/jupyterhub` image contains only the Hub itself,
|
||||||
|
with no configuration. In general, one needs to make a derivative image, with
|
||||||
|
at least a `jupyterhub_config.py` setting up an Authenticator and/or a Spawner.
|
||||||
|
To run the single-user servers, which may be on the same system as the Hub or
|
||||||
|
not, Jupyter Notebook version 4 or greater must be installed.
|
||||||
|
|
||||||
#### Starting JupyterHub with docker
|
|
||||||
The JupyterHub docker image can be started with the following command:
|
The JupyterHub docker image can be started with the following command:
|
||||||
|
|
||||||
docker run -d --name jupyterhub jupyterhub/jupyterhub jupyterhub
|
docker run -p 8000:8000 -d --name jupyterhub jupyterhub/jupyterhub jupyterhub
|
||||||
|
|
||||||
This command will create a container named `jupyterhub` that you can **stop and resume** with `docker stop/start`.
|
This command will create a container named `jupyterhub` that you can
|
||||||
|
**stop and resume** with `docker stop/start`.
|
||||||
|
|
||||||
The Hub service will be listening on all interfaces at port 8000, which makes this a good choice for **testing JupyterHub on your desktop or laptop**.
|
The Hub service will be listening on all interfaces at port 8000, which makes
|
||||||
|
this a good choice for **testing JupyterHub on your desktop or laptop**.
|
||||||
|
|
||||||
If you want to run docker on a computer that has a public IP then you should (as in MUST) **secure it with ssl** by
|
If you want to run docker on a computer that has a public IP then you should
|
||||||
adding ssl options to your docker configuration or using a ssl enabled proxy.
|
(as in MUST) **secure it with ssl** by adding ssl options to your docker
|
||||||
|
configuration or by using a ssl enabled proxy.
|
||||||
|
|
||||||
[Mounting volumes](https://docs.docker.com/engine/userguide/containers/dockervolumes/) will
|
[Mounting volumes](https://docs.docker.com/engine/admin/volumes/volumes/) will
|
||||||
allow you to **store data outside the docker image (host system) so it will be persistent**, even when you start
|
allow you to **store data outside the docker image (host system) so it will be persistent**, even when you start
|
||||||
a new image.
|
a new image.
|
||||||
|
|
||||||
The command `docker exec -it jupyterhub bash` will spawn a root shell in your docker
|
The command `docker exec -it jupyterhub bash` will spawn a root shell in your docker
|
||||||
container. You can **use the root shell to create system users in the container**. These accounts will be used for authentication
|
container. You can **use the root shell to create system users in the container**.
|
||||||
in JupyterHub's default configuration.
|
These accounts will be used for authentication in JupyterHub's default configuration.
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
If you would like to contribute to the project, please read our [contributor documentation](http://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html) and the [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
||||||
|
|
||||||
For a **development install**, clone the [repository](https://github.com/jupyterhub/jupyterhub) and then install from source:
|
If you would like to contribute to the project, please read our
|
||||||
|
[contributor documentation](http://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html)
|
||||||
|
and the [`CONTRIBUTING.md`](CONTRIBUTING.md). The `CONTRIBUTING.md` file
|
||||||
|
explains how to set up a development installation, how to run the test suite,
|
||||||
|
and how to contribute to documentation.
|
||||||
|
|
||||||
```bash
|
### A note about platform support
|
||||||
git clone https://github.com/jupyterhub/jupyterhub
|
|
||||||
cd jupyterhub
|
|
||||||
pip3 install -r dev-requirements.txt -e .
|
|
||||||
```
|
|
||||||
|
|
||||||
If the `pip3 install` command fails and complains about `lessc` being unavailable, you may need to explicitly install some additional JavaScript dependencies:
|
JupyterHub is supported on Linux/Unix based systems.
|
||||||
|
|
||||||
npm install
|
JupyterHub officially **does not** support Windows. You may be able to use
|
||||||
|
JupyterHub on Windows if you use a Spawner and Authenticator that work on
|
||||||
|
Windows, but the JupyterHub defaults will not. Bugs reported on Windows will not
|
||||||
|
be accepted, and the test suite will not run on Windows. Small patches that fix
|
||||||
|
minor Windows compatibility issues (such as basic installation) **may** be accepted,
|
||||||
|
however. For Windows-based systems, we would recommend running JupyterHub in a
|
||||||
|
docker container or Linux VM.
|
||||||
|
|
||||||
This will fetch client-side JavaScript dependencies necessary to compile CSS.
|
[Additional Reference:](http://www.tornadoweb.org/en/stable/#installation) Tornado's documentation on Windows platform support
|
||||||
|
|
||||||
You may also need to manually update JavaScript and CSS after some development updates, with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 setup.py js # fetch updated client-side js
|
|
||||||
python3 setup.py css # recompile CSS from LESS sources
|
|
||||||
```
|
|
||||||
|
|
||||||
We use [pytest](http://doc.pytest.org/en/latest/) for testing. To run tests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pytest jupyterhub/tests
|
|
||||||
```
|
|
||||||
|
|
||||||
----
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
We use a shared copyright model that enables all contributors to maintain the
|
We use a shared copyright model that enables all contributors to maintain the
|
||||||
copyright on their contributions.
|
copyright on their contributions.
|
||||||
|
|
||||||
All code is licensed under the terms of the revised BSD license.
|
All code is licensed under the terms of the revised BSD license.
|
||||||
|
|
||||||
## Getting help
|
## Help and resources
|
||||||
We encourage you to ask questions on the [mailing list](https://groups.google.com/forum/#!forum/jupyter),
|
|
||||||
and you may participate in development discussions or get live help on [Gitter](https://gitter.im/jupyterhub/jupyterhub).
|
We encourage you to ask questions on the [Jupyter mailing list](https://groups.google.com/forum/#!forum/jupyter).
|
||||||
|
To participate in development discussions or get help, talk with us on
|
||||||
|
our JupyterHub [Gitter](https://gitter.im/jupyterhub/jupyterhub) channel.
|
||||||
|
|
||||||
## Resources
|
|
||||||
- [Reporting Issues](https://github.com/jupyterhub/jupyterhub/issues)
|
- [Reporting Issues](https://github.com/jupyterhub/jupyterhub/issues)
|
||||||
- JupyterHub tutorial | [Repo](https://github.com/jupyterhub/jupyterhub-tutorial)
|
- [JupyterHub tutorial](https://github.com/jupyterhub/jupyterhub-tutorial)
|
||||||
| [Tutorial documentation](http://jupyterhub-tutorial.readthedocs.io/en/latest/)
|
- [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](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's REST API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/master/docs/rest-api.yml#/default)
|
- [Documentation for JupyterHub's REST API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/master/docs/rest-api.yml#/default)
|
||||||
- [Documentation for Project Jupyter](http://jupyter.readthedocs.io/en/latest/index.html) | [PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)
|
- [Documentation for Project Jupyter](http://jupyter.readthedocs.io/en/latest/index.html) | [PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)
|
||||||
- [Project Jupyter website](https://jupyter.org)
|
- [Project Jupyter website](https://jupyter.org)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**[Technical Overview](#technical-overview)** |
|
||||||
|
**[Installation](#installation)** |
|
||||||
|
**[Configuration](#configuration)** |
|
||||||
|
**[Docker](#docker)** |
|
||||||
|
**[Contributing](#contributing)** |
|
||||||
|
**[License](#license)** |
|
||||||
|
**[Help and Resources](#help-and-resources)**
|
||||||
|
36
bower-lite
Executable file
36
bower-lite
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
bower-lite
|
||||||
|
|
||||||
|
Since Bower's on its way out,
|
||||||
|
stage frontend dependencies from node_modules into components
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from os.path import join
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
components = join(HERE, "share", "jupyterhub", "static", "components")
|
||||||
|
node_modules = join(HERE, "node_modules")
|
||||||
|
|
||||||
|
if os.path.exists(components):
|
||||||
|
shutil.rmtree(components)
|
||||||
|
os.mkdir(components)
|
||||||
|
|
||||||
|
with open(join(HERE, 'package.json')) as f:
|
||||||
|
package_json = json.load(f)
|
||||||
|
|
||||||
|
dependencies = package_json['dependencies']
|
||||||
|
for dep in dependencies:
|
||||||
|
src = join(node_modules, dep)
|
||||||
|
dest = join(components, dep)
|
||||||
|
print("%s -> %s" % (src, dest))
|
||||||
|
shutil.copytree(src, dest)
|
11
bower.json
11
bower.json
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "jupyterhub-deps",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"bootstrap": "components/bootstrap#~3.1",
|
|
||||||
"font-awesome": "components/font-awesome#~4.1",
|
|
||||||
"jquery": "components/jquery#~2.0",
|
|
||||||
"moment": "~2.7",
|
|
||||||
"requirejs": "~2.1"
|
|
||||||
}
|
|
||||||
}
|
|
50
ci/docker-db.sh
Executable file
50
ci/docker-db.sh
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# source this file to setup postgres and mysql
|
||||||
|
# for local testing (as similar as possible to docker)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
export MYSQL_HOST=127.0.0.1
|
||||||
|
export MYSQL_TCP_PORT=${MYSQL_TCP_PORT:-13306}
|
||||||
|
export PGHOST=127.0.0.1
|
||||||
|
NAME="hub-test-$DB"
|
||||||
|
DOCKER_RUN="docker run -d --name $NAME"
|
||||||
|
|
||||||
|
docker rm -f "$NAME" 2>/dev/null || true
|
||||||
|
|
||||||
|
case "$DB" in
|
||||||
|
"mysql")
|
||||||
|
RUN_ARGS="-e MYSQL_ALLOW_EMPTY_PASSWORD=1 -p $MYSQL_TCP_PORT:3306 mysql:5.7"
|
||||||
|
CHECK="mysql --host $MYSQL_HOST --port $MYSQL_TCP_PORT --user root -e \q"
|
||||||
|
;;
|
||||||
|
"postgres")
|
||||||
|
RUN_ARGS="-p 5432:5432 postgres:9.5"
|
||||||
|
CHECK="psql --user postgres -c \q"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '$DB must be mysql or postgres'
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
|
|
||||||
|
$DOCKER_RUN $RUN_ARGS
|
||||||
|
|
||||||
|
echo -n "waiting for $DB "
|
||||||
|
for i in {1..60}; do
|
||||||
|
if $CHECK; then
|
||||||
|
echo 'done'
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo -n '.'
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
$CHECK
|
||||||
|
|
||||||
|
|
||||||
|
echo -e "
|
||||||
|
Set these environment variables:
|
||||||
|
|
||||||
|
export MYSQL_HOST=127.0.0.1
|
||||||
|
export MYSQL_TCP_PORT=$MYSQL_TCP_PORT
|
||||||
|
export PGHOST=127.0.0.1
|
||||||
|
"
|
27
ci/init-db.sh
Executable file
27
ci/init-db.sh
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# initialize jupyterhub databases for testing
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
MYSQL="mysql --user root --host $MYSQL_HOST --port $MYSQL_TCP_PORT -e "
|
||||||
|
PSQL="psql --user postgres -c "
|
||||||
|
|
||||||
|
case "$DB" in
|
||||||
|
"mysql")
|
||||||
|
EXTRA_CREATE='CHARACTER SET utf8 COLLATE utf8_general_ci'
|
||||||
|
SQL="$MYSQL"
|
||||||
|
;;
|
||||||
|
"postgres")
|
||||||
|
SQL="$PSQL"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '$DB must be mysql or postgres'
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
|
|
||||||
|
set -x
|
||||||
|
|
||||||
|
for SUFFIX in '' _upgrade_072 _upgrade_081; do
|
||||||
|
$SQL "DROP DATABASE jupyterhub${SUFFIX};" 2>/dev/null || true
|
||||||
|
$SQL "CREATE DATABASE jupyterhub${SUFFIX} ${EXTRA_CREATE};"
|
||||||
|
done
|
24
circle.yml
24
circle.yml
@@ -1,24 +0,0 @@
|
|||||||
machine:
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
override:
|
|
||||||
- ls
|
|
||||||
|
|
||||||
test:
|
|
||||||
override:
|
|
||||||
- docker build -t jupyterhub/jupyterhub .
|
|
||||||
- docker build -t jupyterhub/jupyterhub-onbuild:${CIRCLE_TAG:-latest} onbuild
|
|
||||||
|
|
||||||
deployment:
|
|
||||||
hub:
|
|
||||||
branch: master
|
|
||||||
commands:
|
|
||||||
- docker login -u $DOCKER_USER -p $DOCKER_PASS -e unused@example.com
|
|
||||||
- docker push jupyterhub/jupyterhub-onbuild
|
|
||||||
release:
|
|
||||||
tag: /.*/
|
|
||||||
commands:
|
|
||||||
- docker login -u $DOCKER_USER -p $DOCKER_PASS -e unused@example.com
|
|
||||||
- docker push jupyterhub/jupyterhub-onbuild:$CIRCLE_TAG
|
|
@@ -1,7 +1,13 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
mock
|
mock
|
||||||
codecov
|
codecov
|
||||||
|
cryptography
|
||||||
pytest-cov
|
pytest-cov
|
||||||
pytest>=2.8
|
pytest-tornado
|
||||||
|
pytest>=3.3
|
||||||
notebook
|
notebook
|
||||||
requests-mock
|
requests-mock
|
||||||
|
virtualenv
|
||||||
|
# temporary pin of attrs for jsonschema 0.3.0a1
|
||||||
|
# seems to be a pip bug
|
||||||
|
attrs>=17.4.0
|
||||||
|
11
dockerfiles/Dockerfile.alpine
Normal file
11
dockerfiles/Dockerfile.alpine
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.6.3-alpine3.6
|
||||||
|
|
||||||
|
ARG JUPYTERHUB_VERSION=0.8.1
|
||||||
|
|
||||||
|
RUN pip3 install --no-cache jupyterhub==${JUPYTERHUB_VERSION}
|
||||||
|
ENV LANG=en_US.UTF-8
|
||||||
|
|
||||||
|
USER nobody
|
||||||
|
CMD ["jupyterhub"]
|
||||||
|
|
||||||
|
|
21
dockerfiles/README.md
Normal file
21
dockerfiles/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## What is Dockerfile.alpine
|
||||||
|
Dockerfile.alpine contains base image for jupyterhub. It does not work independently, but only as part of a full jupyterhub cluster
|
||||||
|
|
||||||
|
## How to use it?
|
||||||
|
|
||||||
|
1. A running configurable-http-proxy, whose API is accessible.
|
||||||
|
2. A jupyterhub_config file.
|
||||||
|
3. Authentication and other libraries required by the specific jupyterhub_config file.
|
||||||
|
|
||||||
|
|
||||||
|
## Steps to test it outside a cluster
|
||||||
|
|
||||||
|
* start configurable-http-proxy in another container
|
||||||
|
* specify CONFIGPROXY_AUTH_TOKEN env in both containers
|
||||||
|
* put both containers on the same network (e.g. docker create network jupyterhub; docker run ... --net jupyterhub)
|
||||||
|
* tell jupyterhub where CHP is (e.g. c.ConfigurableHTTPProxy.api_url = 'http://chp:8001')
|
||||||
|
* tell jupyterhub not to start the proxy itself (c.ConfigurableHTTPProxy.should_start = False)
|
||||||
|
* Use dummy authenticator for ease of testing. Update following in jupyterhub_config file
|
||||||
|
- c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator'
|
||||||
|
- c.DummyAuthenticator.password = "your strong password"
|
||||||
|
|
@@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
# You can set these variables from the command line.
|
# You can set these variables from the command line.
|
||||||
SPHINXOPTS =
|
SPHINXOPTS = "-W"
|
||||||
SPHINXBUILD = sphinx-build
|
SPHINXBUILD = sphinx-build
|
||||||
PAPER =
|
PAPER =
|
||||||
BUILDDIR = build
|
BUILDDIR = build
|
||||||
|
@@ -1,16 +1,22 @@
|
|||||||
|
# ReadTheDocs uses the `environment.yaml` so make sure to update that as well
|
||||||
|
# if you change the dependencies of JupyterHub in the various `requirements.txt`
|
||||||
name: jhub_docs
|
name: jhub_docs
|
||||||
channels:
|
channels:
|
||||||
- conda-forge
|
- conda-forge
|
||||||
dependencies:
|
dependencies:
|
||||||
- nodejs
|
- nodejs
|
||||||
- python=3
|
- python=3.6
|
||||||
|
- alembic
|
||||||
- jinja2
|
- jinja2
|
||||||
- pamela
|
- pamela
|
||||||
- requests
|
- requests
|
||||||
- sqlalchemy>=1
|
- sqlalchemy>=1
|
||||||
- tornado>=4.1
|
- tornado>=5.0
|
||||||
- traitlets>=4.1
|
- traitlets>=4.1
|
||||||
- sphinx>=1.3.6
|
- sphinx>=1.7
|
||||||
- sphinx_rtd_theme
|
|
||||||
- pip:
|
- pip:
|
||||||
|
- python-oauth2
|
||||||
- recommonmark==0.4.0
|
- recommonmark==0.4.0
|
||||||
|
- async_generator
|
||||||
|
- prometheus_client
|
||||||
|
- attrs>=17.4.0
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jupyterhub-docs-build",
|
"name": "jupyterhub-docs-build",
|
||||||
"version": "0.0.0",
|
"version": "0.8.0",
|
||||||
"description": "build JupyterHub swagger docs",
|
"description": "build JupyterHub swagger docs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"rest-api": "bootprint openapi ./rest-api.yml source/_static/rest-api"
|
"rest-api": "bootprint openapi ./rest-api.yml source/_static/rest-api"
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bootprint": "^0.10.0",
|
"bootprint": "^1.0.0",
|
||||||
"bootprint-openapi": "^0.17.0"
|
"bootprint-openapi": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
# ReadTheDocs uses the `environment.yaml` so make sure to update that as well
|
||||||
|
# if you change this file
|
||||||
-r ../requirements.txt
|
-r ../requirements.txt
|
||||||
sphinx>=1.3.6
|
sphinx>=1.7
|
||||||
recommonmark==0.4.0
|
recommonmark==0.4.0
|
@@ -3,7 +3,7 @@ swagger: '2.0'
|
|||||||
info:
|
info:
|
||||||
title: JupyterHub
|
title: JupyterHub
|
||||||
description: The REST API for JupyterHub
|
description: The REST API for JupyterHub
|
||||||
version: 0.7.0
|
version: 0.9.0dev
|
||||||
license:
|
license:
|
||||||
name: BSD-3-Clause
|
name: BSD-3-Clause
|
||||||
schemes:
|
schemes:
|
||||||
@@ -203,18 +203,91 @@ paths:
|
|||||||
description: The user's notebook server has stopped
|
description: The user's notebook server has stopped
|
||||||
'202':
|
'202':
|
||||||
description: The user's notebook server has not yet stopped as it is taking a while to stop
|
description: The user's notebook server has not yet stopped as it is taking a while to stop
|
||||||
/users/{name}/admin-access:
|
/users/{name}/servers/{server_name}:
|
||||||
post:
|
post:
|
||||||
summary: Grant admin access to this user's notebook server
|
summary: Start a user's single-user named-server notebook server
|
||||||
parameters:
|
parameters:
|
||||||
- name: name
|
- name: name
|
||||||
description: username
|
description: username
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
- name: server_name
|
||||||
|
description: name given to a named-server
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: The user's notebook named-server has started
|
||||||
|
'202':
|
||||||
|
description: The user's notebook named-server has not yet started, but has been requested
|
||||||
|
delete:
|
||||||
|
summary: Stop a user's named-server
|
||||||
|
parameters:
|
||||||
|
- name: name
|
||||||
|
description: username
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- name: server_name
|
||||||
|
description: name given to a named-server
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: The user's notebook named-server has stopped
|
||||||
|
'202':
|
||||||
|
description: The user's notebook named-server has not yet stopped as it is taking a while to stop
|
||||||
|
/users/{name}/tokens:
|
||||||
|
get:
|
||||||
|
summary: List tokens for the user
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Sets a cookie granting the requesting administrator access to the user's notebook server
|
description: The list of tokens
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$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
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Token'
|
||||||
|
/users/{name}/tokens/{token_id}:
|
||||||
|
get:
|
||||||
|
summary: Get the model for a token by id
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The info for the new token
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Token'
|
||||||
|
delete:
|
||||||
|
summary: Delete (revoke) a token by id
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: The token has been deleted
|
||||||
|
/user:
|
||||||
|
summary: Return authenticated user's model
|
||||||
|
description:
|
||||||
|
parameters:
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The authenticated user's model is returned.
|
||||||
/groups:
|
/groups:
|
||||||
get:
|
get:
|
||||||
summary: List groups
|
summary: List groups
|
||||||
@@ -377,9 +450,38 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Success
|
description: Success
|
||||||
|
/authorizations/token:
|
||||||
|
post:
|
||||||
|
summary: Request a new API token
|
||||||
|
description: |
|
||||||
|
Request a new API token to use with the JupyterHub REST API.
|
||||||
|
If not already authenticated, username and password can be sent
|
||||||
|
in the JSON request body.
|
||||||
|
Logging in via this method is only available when the active Authenticator
|
||||||
|
accepts passwords (e.g. not OAuth).
|
||||||
|
parameters:
|
||||||
|
- name: username
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
- name: password
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The new API token
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
description: The new API token.
|
||||||
|
'403':
|
||||||
|
description: The user can not be authenticated.
|
||||||
/authorizations/token/{token}:
|
/authorizations/token/{token}:
|
||||||
get:
|
get:
|
||||||
summary: Identify a user from an API token
|
summary: Identify a user or service from an API token
|
||||||
parameters:
|
parameters:
|
||||||
- name: token
|
- name: token
|
||||||
in: path
|
in: path
|
||||||
@@ -387,9 +489,9 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: The user identified by the API token
|
description: The user or service identified by the API token
|
||||||
schema:
|
'404':
|
||||||
$ref: '#/definitions/User'
|
description: A user or service is not found.
|
||||||
/authorizations/cookie/{cookie_name}/{cookie_value}:
|
/authorizations/cookie/{cookie_name}/{cookie_value}:
|
||||||
get:
|
get:
|
||||||
summary: Identify a user from a cookie
|
summary: Identify a user from a cookie
|
||||||
@@ -408,6 +510,81 @@ paths:
|
|||||||
description: The user identified by the cookie
|
description: The user identified by the cookie
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/User'
|
$ref: '#/definitions/User'
|
||||||
|
'404':
|
||||||
|
description: A user is not found.
|
||||||
|
/oauth2/authorize:
|
||||||
|
get:
|
||||||
|
summary: 'OAuth 2.0 authorize endpoint'
|
||||||
|
description: |
|
||||||
|
Redirect users to this URL to begin the OAuth process.
|
||||||
|
It is not an API endpoint.
|
||||||
|
parameters:
|
||||||
|
- name: client_id
|
||||||
|
description: The client id
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- name: response_type
|
||||||
|
description: The response type (always 'code')
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- name: state
|
||||||
|
description: A state string
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
- name: redirect_uri
|
||||||
|
description: The redirect url
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
/oauth2/token:
|
||||||
|
post:
|
||||||
|
summary: Request an OAuth2 token
|
||||||
|
description: |
|
||||||
|
Request an OAuth2 token from an authorization code.
|
||||||
|
This request completes the OAuth process.
|
||||||
|
consumes:
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
parameters:
|
||||||
|
- name: client_id
|
||||||
|
description: The client id
|
||||||
|
in: form
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- name: client_secret
|
||||||
|
description: The client secret
|
||||||
|
in: form
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- name: grant_type
|
||||||
|
description: The grant type (always 'authorization_code')
|
||||||
|
in: form
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- name: code
|
||||||
|
description: The code provided by the authorization redirect
|
||||||
|
in: form
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- name: redirect_uri
|
||||||
|
description: The redirect url
|
||||||
|
in: form
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: JSON response including the token
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
access_token:
|
||||||
|
type: string
|
||||||
|
description: The new API token for the user
|
||||||
|
token_type:
|
||||||
|
type: string
|
||||||
|
description: Will always be 'Bearer'
|
||||||
/shutdown:
|
/shutdown:
|
||||||
post:
|
post:
|
||||||
summary: Shutdown the Hub
|
summary: Shutdown the Hub
|
||||||
@@ -419,10 +596,7 @@ paths:
|
|||||||
- name: servers
|
- name: servers
|
||||||
in: body
|
in: body
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Whether users's notebook servers should be shutdown as well (default from Hub config)
|
description: Whether users' notebook servers should be shutdown as well (default from Hub config)
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Hub has shutdown
|
|
||||||
definitions:
|
definitions:
|
||||||
User:
|
User:
|
||||||
type: object
|
type: object
|
||||||
@@ -443,12 +617,55 @@ definitions:
|
|||||||
description: The user's notebook server's base URL, if running; null if not.
|
description: The user's notebook server's base URL, if running; null if not.
|
||||||
pending:
|
pending:
|
||||||
type: string
|
type: string
|
||||||
enum: ["spawn", "stop"]
|
enum: ["spawn", "stop", null]
|
||||||
description: The currently pending action, if any
|
description: The currently pending action, if any
|
||||||
last_activity:
|
last_activity:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
description: Timestamp of last-seen activity from the user
|
description: Timestamp of last-seen activity from the user
|
||||||
|
servers:
|
||||||
|
type: object
|
||||||
|
description: The active servers for this user.
|
||||||
|
items:
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Server'
|
||||||
|
Server:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The server's name. The user's default server has an empty name ('')
|
||||||
|
ready:
|
||||||
|
type: boolean
|
||||||
|
description: |
|
||||||
|
Whether the server is ready for traffic.
|
||||||
|
Will always be false when any transition is pending.
|
||||||
|
pending:
|
||||||
|
type: string
|
||||||
|
enum: ["spawn", "stop", null]
|
||||||
|
description: |
|
||||||
|
The currently pending action, if any.
|
||||||
|
A server is not ready if an action is pending.
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
The URL where the server can be accessed
|
||||||
|
(typically /user/:name/:server.name/).
|
||||||
|
progress_url:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
The URL for an event-stream to retrieve events during a spawn.
|
||||||
|
started:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: UTC timestamp when the server was last started.
|
||||||
|
last_activity:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: UTC timestamp last-seen activity on this server.
|
||||||
|
state:
|
||||||
|
type: object
|
||||||
|
description: Arbitrary internal state from this server's spawner. Only available on the hub's users list or get-user-by-name method, and only if a hub admin. None otherwise.
|
||||||
Group:
|
Group:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -483,3 +700,40 @@ definitions:
|
|||||||
description: The command used to start the service (if managed)
|
description: The command used to start the service (if managed)
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
info:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Additional information a deployment can attach to a service.
|
||||||
|
JupyterHub does not use this field.
|
||||||
|
Token:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
description: The token itself. Only present in responses to requests for a new token.
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: The id of the API token. Used for modifying or deleting the token.
|
||||||
|
user:
|
||||||
|
type: string
|
||||||
|
description: The user that owns a token (undefined if owned by a service)
|
||||||
|
service:
|
||||||
|
type: string
|
||||||
|
description: The service that owns the token (undefined of owned by a user)
|
||||||
|
note:
|
||||||
|
type: string
|
||||||
|
description: A note about the token, typically describing what it was created for.
|
||||||
|
created:
|
||||||
|
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
|
||||||
|
description: |
|
||||||
|
Timestamp of last-seen activity using this token.
|
||||||
|
Can be null if token has never been used.
|
||||||
|
106
docs/source/_static/custom.css
Normal file
106
docs/source/_static/custom.css
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
div#helm-chart-schema h2,
|
||||||
|
div#helm-chart-schema h3,
|
||||||
|
div#helm-chart-schema h4,
|
||||||
|
div#helm-chart-schema h5,
|
||||||
|
div#helm-chart-schema h6 {
|
||||||
|
font-family: courier new;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3, h3 ~ * {
|
||||||
|
margin-left: 3% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4, h4 ~ * {
|
||||||
|
margin-left: 6% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5, h5 ~ * {
|
||||||
|
margin-left: 9% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6, h6 ~ * {
|
||||||
|
margin-left: 12% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h7, h7 ~ * {
|
||||||
|
margin-left: 15% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.logo {
|
||||||
|
width:100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-next {
|
||||||
|
float: right;
|
||||||
|
max-width: 45%;
|
||||||
|
overflow: auto;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-next::after{
|
||||||
|
content: ' »';
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-prev {
|
||||||
|
float: left;
|
||||||
|
max-width: 45%;
|
||||||
|
overflow: auto;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-prev::before{
|
||||||
|
content: '« ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-next-bottom {
|
||||||
|
margin-top: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-next-top {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar TOC and headers */
|
||||||
|
|
||||||
|
div.sphinxsidebarwrapper div {
|
||||||
|
margin-bottom: .8em;
|
||||||
|
}
|
||||||
|
div.sphinxsidebar h3 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
padding-top: 0px;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-left: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar p.caption {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
margin-left: 0px !important;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #767676;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar ul {
|
||||||
|
font-size: .8em;
|
||||||
|
margin-top: 0px;
|
||||||
|
padding-left: 3%;
|
||||||
|
margin-left: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.relations ul {
|
||||||
|
font-size: 1em;
|
||||||
|
margin-left: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#searchbox form {
|
||||||
|
margin-left: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* body elements */
|
||||||
|
.toctree-wrapper span.caption-text {
|
||||||
|
color: #767676;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
BIN
docs/source/_static/images/logo/favicon.ico
Normal file
BIN
docs/source/_static/images/logo/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
BIN
docs/source/_static/images/logo/logo.png
Normal file
BIN
docs/source/_static/images/logo/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
16
docs/source/_templates/navigation.html
Normal file
16
docs/source/_templates/navigation.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{# Custom template for navigation.html
|
||||||
|
|
||||||
|
alabaster theme does not provide blocks for titles to
|
||||||
|
be overridden so this custom theme handles title and
|
||||||
|
toctree for sidebar
|
||||||
|
#}
|
||||||
|
<h3>{{ _('Table of Contents') }}</h3>
|
||||||
|
{{ toctree(includehidden=theme_sidebar_includehidden, collapse=theme_sidebar_collapse) }}
|
||||||
|
{% if theme_extra_nav_links %}
|
||||||
|
<hr />
|
||||||
|
<ul>
|
||||||
|
{% for text, uri in theme_extra_nav_links.items() %}
|
||||||
|
<li class="toctree-l1"><a href="{{ uri }}">{{ text }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
30
docs/source/_templates/page.html
Normal file
30
docs/source/_templates/page.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{% extends '!page.html' %}
|
||||||
|
|
||||||
|
{# Custom template for page.html
|
||||||
|
|
||||||
|
Alabaster theme does not provide blocks for prev/next at bottom of each page.
|
||||||
|
This is _in addition_ to the prev/next in the sidebar. The "Prev/Next" text
|
||||||
|
or symbols are handled by CSS classes in _static/custom.css
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% macro prev_next(prev, next, prev_title='', next_title='') %}
|
||||||
|
{%- if prev %}
|
||||||
|
<a class='left-prev' href="{{ prev.link|e }}" title="{{ _('previous chapter')}}">{{ prev_title or prev.title }}</a>
|
||||||
|
{%- endif %}
|
||||||
|
{%- if next %}
|
||||||
|
<a class='right-next' href="{{ next.link|e }}" title="{{ _('next chapter')}}">{{ next_title or next.title }}</a>
|
||||||
|
{%- endif %}
|
||||||
|
<div style='clear:both;'></div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class='prev-next-top'>
|
||||||
|
{{ prev_next(prev, next, 'Previous', 'Next') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{super()}}
|
||||||
|
<div class='prev-next-bottom'>
|
||||||
|
{{ prev_next(prev, next) }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
17
docs/source/_templates/relations.html
Normal file
17
docs/source/_templates/relations.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{# Custom template for relations.html
|
||||||
|
|
||||||
|
alabaster theme does not provide previous/next page by default
|
||||||
|
#}
|
||||||
|
<div class="relations">
|
||||||
|
<h3>Navigation</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{{ pathto(master_doc) }}">Documentation Home</a><ul>
|
||||||
|
{%- if prev %}
|
||||||
|
<li><a href="{{ prev.link|e }}" title="Previous">Previous topic</a></li>
|
||||||
|
{%- endif %}
|
||||||
|
{%- if next %}
|
||||||
|
<li><a href="{{ next.link|e }}" title="Next">Next topic</a></li>
|
||||||
|
{%- endif %}
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
</div>
|
16
docs/source/api/app.rst
Normal file
16
docs/source/api/app.rst
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
=========================
|
||||||
|
Application configuration
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Module: :mod:`jupyterhub.app`
|
||||||
|
=============================
|
||||||
|
|
||||||
|
.. automodule:: jupyterhub.app
|
||||||
|
|
||||||
|
.. currentmodule:: jupyterhub.app
|
||||||
|
|
||||||
|
:class:`JupyterHub`
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. autoconfigurable:: JupyterHub
|
||||||
|
|
@@ -9,13 +9,20 @@ Module: :mod:`jupyterhub.auth`
|
|||||||
|
|
||||||
.. currentmodule:: jupyterhub.auth
|
.. currentmodule:: jupyterhub.auth
|
||||||
|
|
||||||
|
:class:`Authenticator`
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. autoconfigurable:: Authenticator
|
||||||
.. autoclass:: Authenticator
|
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: LocalAuthenticator
|
:class:`LocalAuthenticator`
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. autoconfigurable:: LocalAuthenticator
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: PAMAuthenticator
|
:class:`PAMAuthenticator`
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. autoconfigurable:: PAMAuthenticator
|
||||||
|
|
||||||
|
@@ -1,19 +1,21 @@
|
|||||||
.. _api-index:
|
.. _api-index:
|
||||||
|
|
||||||
####################
|
##################
|
||||||
The JupyterHub API
|
The JupyterHub API
|
||||||
####################
|
##################
|
||||||
|
|
||||||
:Release: |release|
|
:Release: |release|
|
||||||
:Date: |today|
|
:Date: |today|
|
||||||
|
|
||||||
JupyterHub also provides a REST API for administration of the Hub and users.
|
JupyterHub also provides a REST API for administration of the Hub and users.
|
||||||
The documentation on `Using JupyterHub's REST API <../rest.html>`_ provides
|
The documentation on `Using JupyterHub's REST API <../reference/rest.html>`_ provides
|
||||||
information on:
|
information on:
|
||||||
|
|
||||||
- Creating an API token
|
- what you can do with the API
|
||||||
- Adding tokens to the configuration file (optional)
|
- creating an API token
|
||||||
- Making an API request
|
- adding API tokens to the config files
|
||||||
|
- making an API request programmatically using the requests library
|
||||||
|
- learning more about JupyterHub's API
|
||||||
|
|
||||||
The same JupyterHub API spec, as found here, is available in an interactive form
|
The same JupyterHub API spec, as found here, is available in an interactive form
|
||||||
`here (on swagger's petstore) <http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default>`__.
|
`here (on swagger's petstore) <http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default>`__.
|
||||||
@@ -24,9 +26,12 @@ JupyterHub API Reference:
|
|||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|
||||||
|
app
|
||||||
auth
|
auth
|
||||||
spawner
|
spawner
|
||||||
|
proxy
|
||||||
user
|
user
|
||||||
|
service
|
||||||
services.auth
|
services.auth
|
||||||
|
|
||||||
|
|
||||||
|
23
docs/source/api/proxy.rst
Normal file
23
docs/source/api/proxy.rst
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
=======
|
||||||
|
Proxies
|
||||||
|
=======
|
||||||
|
|
||||||
|
Module: :mod:`jupyterhub.proxy`
|
||||||
|
===============================
|
||||||
|
|
||||||
|
.. automodule:: jupyterhub.proxy
|
||||||
|
|
||||||
|
.. currentmodule:: jupyterhub.proxy
|
||||||
|
|
||||||
|
:class:`Proxy`
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. autoconfigurable:: Proxy
|
||||||
|
:members:
|
||||||
|
|
||||||
|
:class:`ConfigurableHTTPProxy`
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. autoconfigurable:: ConfigurableHTTPProxy
|
||||||
|
:members: debug, auth_token, check_running_interval, api_url, command
|
||||||
|
|
17
docs/source/api/service.rst
Normal file
17
docs/source/api/service.rst
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
========
|
||||||
|
Services
|
||||||
|
========
|
||||||
|
|
||||||
|
Module: :mod:`jupyterhub.services.service`
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
.. automodule:: jupyterhub.services.service
|
||||||
|
|
||||||
|
.. currentmodule:: jupyterhub.services.service
|
||||||
|
|
||||||
|
:class:`Service`
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. autoconfigurable:: Service
|
||||||
|
:members: name, admin, url, api_token, managed, kind, command, cwd, environment, user, oauth_client_id, server, prefix, proxy_spec
|
||||||
|
|
@@ -1,5 +1,5 @@
|
|||||||
=======================
|
=======================
|
||||||
Authenticating Services
|
Services Authentication
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
Module: :mod:`jupyterhub.services.auth`
|
Module: :mod:`jupyterhub.services.auth`
|
||||||
@@ -10,9 +10,32 @@ Module: :mod:`jupyterhub.services.auth`
|
|||||||
.. currentmodule:: jupyterhub.services.auth
|
.. currentmodule:: jupyterhub.services.auth
|
||||||
|
|
||||||
|
|
||||||
.. autoclass:: HubAuth
|
:class:`HubAuth`
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. autoconfigurable:: HubAuth
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
:class:`HubOAuth`
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. autoconfigurable:: HubOAuth
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
:class:`HubAuthenticated`
|
||||||
|
-------------------------
|
||||||
|
|
||||||
.. autoclass:: HubAuthenticated
|
.. autoclass:: HubAuthenticated
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
:class:`HubOAuthenticated`
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. autoclass:: HubOAuthenticated
|
||||||
|
|
||||||
|
:class:`HubOAuthCallbackHandler`
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
.. autoclass:: HubOAuthCallbackHandler
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
==============
|
========
|
||||||
Spawners
|
Spawners
|
||||||
==============
|
========
|
||||||
|
|
||||||
Module: :mod:`jupyterhub.spawner`
|
Module: :mod:`jupyterhub.spawner`
|
||||||
=================================
|
=================================
|
||||||
@@ -12,7 +12,11 @@ Module: :mod:`jupyterhub.spawner`
|
|||||||
:class:`Spawner`
|
:class:`Spawner`
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
.. autoclass:: Spawner
|
.. autoconfigurable:: Spawner
|
||||||
:members: options_from_form, poll, start, stop, get_args, get_env, get_state, template_namespace, format_string
|
:members: options_from_form, poll, start, stop, get_args, get_env, get_state, template_namespace, format_string
|
||||||
|
|
||||||
.. autoclass:: LocalProcessSpawner
|
:class:`LocalProcessSpawner`
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. autoconfigurable:: LocalProcessSpawner
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
=============
|
=====
|
||||||
Users
|
Users
|
||||||
=============
|
=====
|
||||||
|
|
||||||
Module: :mod:`jupyterhub.user`
|
Module: :mod:`jupyterhub.user`
|
||||||
==============================
|
==============================
|
||||||
@@ -9,11 +9,16 @@ Module: :mod:`jupyterhub.user`
|
|||||||
|
|
||||||
.. currentmodule:: jupyterhub.user
|
.. currentmodule:: jupyterhub.user
|
||||||
|
|
||||||
|
:class:`UserDict`
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. autoclass:: UserDict
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
:class:`User`
|
:class:`User`
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
.. class:: Server
|
|
||||||
|
|
||||||
.. autoclass:: User
|
.. autoclass:: User
|
||||||
:members: escaped_name
|
:members: escaped_name
|
||||||
|
|
||||||
@@ -29,3 +34,4 @@ Module: :mod:`jupyterhub.user`
|
|||||||
.. attribute:: spawner
|
.. attribute:: spawner
|
||||||
|
|
||||||
The user's :class:`~.Spawner` instance.
|
The user's :class:`~.Spawner` instance.
|
||||||
|
|
||||||
|
@@ -1,113 +0,0 @@
|
|||||||
# Authenticators
|
|
||||||
|
|
||||||
The [Authenticator][] is the mechanism for authorizing users.
|
|
||||||
Basic authenticators use simple username and password authentication.
|
|
||||||
JupyterHub ships only with a [PAM][]-based Authenticator,
|
|
||||||
for logging in with local user accounts.
|
|
||||||
|
|
||||||
You can use custom Authenticator subclasses to enable authentication via other systems.
|
|
||||||
One such example is using [GitHub OAuth][].
|
|
||||||
|
|
||||||
Because the username is passed from the Authenticator to the Spawner,
|
|
||||||
a custom Authenticator and Spawner are often used together.
|
|
||||||
|
|
||||||
See a list of custom Authenticators [on the wiki](https://github.com/jupyterhub/jupyterhub/wiki/Authenticators).
|
|
||||||
|
|
||||||
|
|
||||||
## Basics of Authenticators
|
|
||||||
|
|
||||||
A basic Authenticator has one central method:
|
|
||||||
|
|
||||||
### Authenticator.authenticate
|
|
||||||
|
|
||||||
Authenticator.authenticate(handler, data)
|
|
||||||
|
|
||||||
This method is passed the tornado RequestHandler and the POST data from the login form.
|
|
||||||
Unless the login form has been customized, `data` will have two keys:
|
|
||||||
|
|
||||||
- `username` (self-explanatory)
|
|
||||||
- `password` (also self-explanatory)
|
|
||||||
|
|
||||||
`authenticate`'s job is simple:
|
|
||||||
|
|
||||||
- return a username (non-empty str)
|
|
||||||
of the authenticated user if authentication is successful
|
|
||||||
- return `None` otherwise
|
|
||||||
|
|
||||||
Writing an Authenticator that looks up passwords in a dictionary
|
|
||||||
requires only overriding this one method:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from tornado import gen
|
|
||||||
from IPython.utils.traitlets import Dict
|
|
||||||
from jupyterhub.auth import Authenticator
|
|
||||||
|
|
||||||
class DictionaryAuthenticator(Authenticator):
|
|
||||||
|
|
||||||
passwords = Dict(config=True,
|
|
||||||
help="""dict of username:password for authentication"""
|
|
||||||
)
|
|
||||||
|
|
||||||
@gen.coroutine
|
|
||||||
def authenticate(self, handler, data):
|
|
||||||
if self.passwords.get(data['username']) == data['password']:
|
|
||||||
return data['username']
|
|
||||||
```
|
|
||||||
|
|
||||||
### Authenticator.whitelist
|
|
||||||
|
|
||||||
Authenticators can specify a whitelist of usernames to allow authentication.
|
|
||||||
For local user authentication (e.g. PAM), this lets you limit which users
|
|
||||||
can login.
|
|
||||||
|
|
||||||
|
|
||||||
## Normalizing and validating usernames
|
|
||||||
|
|
||||||
Since the Authenticator and Spawner both use the same username,
|
|
||||||
sometimes you want to transform the name coming from the authentication service
|
|
||||||
(e.g. turning email addresses into local system usernames) before adding them to the Hub service.
|
|
||||||
Authenticators can define `normalize_username`, which takes a username.
|
|
||||||
The default normalization is to cast names to lowercase
|
|
||||||
|
|
||||||
For simple mappings, a configurable dict `Authenticator.username_map` is used to turn one name into another:
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.Authenticator.username_map = {
|
|
||||||
'service-name': 'localname'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validating usernames
|
|
||||||
|
|
||||||
In most cases, there is a very limited set of acceptable usernames.
|
|
||||||
Authenticators can define `validate_username(username)`,
|
|
||||||
which should return True for a valid username and False for an invalid one.
|
|
||||||
The primary effect this has is improving error messages during user creation.
|
|
||||||
|
|
||||||
The default behavior is to use configurable `Authenticator.username_pattern`,
|
|
||||||
which is a regular expression string for validation.
|
|
||||||
|
|
||||||
To only allow usernames that start with 'w':
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.Authenticator.username_pattern = r'w.*'
|
|
||||||
```
|
|
||||||
|
|
||||||
## OAuth and other non-password logins
|
|
||||||
|
|
||||||
Some login mechanisms, such as [OAuth][], don't map onto username+password.
|
|
||||||
For these, you can override the login handlers.
|
|
||||||
|
|
||||||
You can see an example implementation of an Authenticator that uses [GitHub OAuth][]
|
|
||||||
at [OAuthenticator][].
|
|
||||||
|
|
||||||
|
|
||||||
## Writing a custom authenticator
|
|
||||||
|
|
||||||
If you are interested in writing a custom authenticator, you can read [this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/authenticators.html).
|
|
||||||
|
|
||||||
[Authenticator]: https://github.com/jupyterhub/jupyterhub/blob/master/jupyterhub/auth.py
|
|
||||||
[PAM]: https://en.wikipedia.org/wiki/Pluggable_authentication_module
|
|
||||||
[OAuth]: https://en.wikipedia.org/wiki/OAuth
|
|
||||||
[GitHub OAuth]: https://developer.github.com/v3/oauth/
|
|
||||||
[OAuthenticator]: https://github.com/jupyterhub/oauthenticator
|
|
@@ -1,11 +1,268 @@
|
|||||||
# Change log summary
|
# Changelog
|
||||||
|
|
||||||
For detailed changes from the prior release, click on the version number, and
|
For detailed changes from the prior release, click on the version number, and
|
||||||
its link will bring up a GitHub listing of changes. Use `git log` on the
|
its link will bring up a GitHub listing of changes. Use `git log` on the
|
||||||
command line for details.
|
command line for details.
|
||||||
|
|
||||||
|
|
||||||
## [Unreleased] 0.8
|
## [Unreleased]
|
||||||
|
|
||||||
|
## 0.9
|
||||||
|
|
||||||
|
### [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,
|
||||||
|
so make sure to backup your database and run:
|
||||||
|
|
||||||
|
jupyterhub upgrade-db
|
||||||
|
|
||||||
|
after upgrading jupyterhub.
|
||||||
|
|
||||||
|
The biggest change for 0.9 is the switch to asyncio coroutines everywhere
|
||||||
|
instead of tornado coroutines. Custom Spawners and Authenticators are still
|
||||||
|
free to use tornado coroutines for async methods, as they will continue to
|
||||||
|
work. As part of this upgrade, JupyterHub 0.9 drops support for Python < 3.5
|
||||||
|
and tornado < 5.0.
|
||||||
|
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
- Require Python >= 3.5
|
||||||
|
- Require tornado >= 5.0
|
||||||
|
- Use asyncio coroutines throughout
|
||||||
|
- Set status 409 for conflicting actions instead of 400,
|
||||||
|
e.g. creating users or groups that already exist.
|
||||||
|
- timestamps in REST API continue to be UTC, but now include 'Z' suffix
|
||||||
|
to identify them as such.
|
||||||
|
- REST API User model always includes `servers` dict,
|
||||||
|
not just when named servers are enabled.
|
||||||
|
- `server` info is no longer available to oauth identification endpoints,
|
||||||
|
only user info and group membership.
|
||||||
|
- `User.last_activity` may be None if a user has not been seen,
|
||||||
|
rather than starting with the user creation time
|
||||||
|
which is now separately stored as `User.created`.
|
||||||
|
- static resources are now found in `$PREFIX/share/jupyterhub` instead of `share/jupyter/hub` for improved consistency.
|
||||||
|
- Deprecate `.extra_log_file` config. Use pipe redirection instead:
|
||||||
|
|
||||||
|
jupyterhub &>> /var/log/jupyterhub.log
|
||||||
|
|
||||||
|
- Add `JupyterHub.bind_url` config for setting the full bind URL of the proxy.
|
||||||
|
Sets ip, port, base_url all at once.
|
||||||
|
- Add `JupyterHub.hub_bind_url` for setting the full host+port of the Hub.
|
||||||
|
`hub_bind_url` supports unix domain sockets, e.g.
|
||||||
|
`unix+http://%2Fsrv%2Fjupyterhub.sock`
|
||||||
|
- Deprecate `JupyterHub.hub_connect_port` config in favor of `JupyterHub.hub_connect_url`. `hub_connect_ip` is not deprecated
|
||||||
|
and can still be used in the common case where only the ip address of the hub differs from the bind ip.
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- Spawners can define a `.progress` method which should be an async generator.
|
||||||
|
The generator should yield events of the form:
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"message": "some-state-message",
|
||||||
|
"progress": 50,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
These messages will be shown with a progress bar on the spawn-pending page.
|
||||||
|
The `async_generator` package can be used to make async generators
|
||||||
|
compatible with Python 3.5.
|
||||||
|
- track activity of individual API tokens
|
||||||
|
- new REST API for managing API tokens at `/hub/api/user/tokens[/token-id]`
|
||||||
|
- allow viewing/revoking tokens via token page
|
||||||
|
- User creation time is available in the REST API as `User.created`
|
||||||
|
- Server start time is stored as `Server.started`
|
||||||
|
- `Spawner.start` may return a URL for connecting to a notebook instead of `(ip, port)`. This enables Spawners to launch servers that setup their own HTTPS.
|
||||||
|
- Optimize database performance by disabling sqlalchemy expire_on_commit by default.
|
||||||
|
- Add `python -m jupyterhub.dbutil shell` entrypoint for quickly
|
||||||
|
launching an IPython session connected to your JupyterHub database.
|
||||||
|
- Include `User.auth_state` in user model on single-user REST endpoints for admins only.
|
||||||
|
- Include `Server.state` in server model on REST endpoints for admins only.
|
||||||
|
- Add `Authenticator.blacklist` for blacklisting users instead of whitelisting.
|
||||||
|
- Pass `c.JupyterHub.tornado_settings['cookie_options']` down to Spawners
|
||||||
|
so that cookie options (e.g. `expires_days`) can be set globally for the whole application.
|
||||||
|
- SIGINFO (`ctrl-t`) handler showing the current status of all running threads,
|
||||||
|
coroutines, and CPU/memory/FD consumption.
|
||||||
|
- Add async `Spawner.get_options_form` alternative to `.options_form`, so it can be a coroutine.
|
||||||
|
- Add `JupyterHub.redirect_to_server` config to govern whether
|
||||||
|
users should be sent to their server on login or the JuptyerHub home page.
|
||||||
|
- html page templates can be more easily customized and extended.
|
||||||
|
- Allow registering external OAuth clients for using the Hub as an OAuth provider.
|
||||||
|
- Add basic prometheus metrics at `/hub/metrics` endpoint.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- Remove green from theme to improve accessibility
|
||||||
|
- Fix error when proxy deletion fails due to route already being deleted
|
||||||
|
- clear `?redirects` from URL on successful launch
|
||||||
|
- disable send2trash by default, which is rarely desirable for jupyterhub
|
||||||
|
- Put PAM calls in a thread so they don't block the main application
|
||||||
|
in cases where PAM is slow (e.g. LDAP).
|
||||||
|
- Remove implicit spawn from login handler,
|
||||||
|
instead relying on subsequent request for `/user/:name` to trigger spawn.
|
||||||
|
- Fixed several inconsistencies for initial redirects,
|
||||||
|
depending on whether server is running or not and whether the user is logged in or not.
|
||||||
|
- Admin requests for `/user/:name` (when admin-access is enabled) launch the right server if it's not running instead of redirecting to their own.
|
||||||
|
- Major performance improvement starting up JupyterHub with many users,
|
||||||
|
especially when most are inactive.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### [0.8.1] 2017-11-07
|
||||||
|
|
||||||
|
JupyterHub 0.8.1 is a collection of bugfixes and small improvements on 0.8.
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- Run tornado with AsyncIO by default
|
||||||
|
- Add `jupyterhub --upgrade-db` flag for automatically upgrading the database as part of startup.
|
||||||
|
This is useful for cases where manually running `jupyterhub upgrade-db`
|
||||||
|
as a separate step is unwieldy.
|
||||||
|
- Avoid creating backups of the database when no changes are to be made by
|
||||||
|
`jupyterhub upgrade-db`.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Add some further validation to usernames - `/` is not allowed in usernames.
|
||||||
|
- Fix empty logout page when using auto_login
|
||||||
|
- Fix autofill of username field in default login form.
|
||||||
|
- Fix listing of users on the admin page who have not yet started their server.
|
||||||
|
- Fix ever-growing traceback when re-raising Exceptions from spawn failures.
|
||||||
|
- Remove use of deprecated `bower` for javascript client dependencies.
|
||||||
|
|
||||||
|
|
||||||
|
### [0.8.0] 2017-10-03
|
||||||
|
|
||||||
|
JupyterHub 0.8 is a big release!
|
||||||
|
|
||||||
|
Perhaps the biggest change is the use of OAuth to negotiate authentication
|
||||||
|
between the Hub and single-user services.
|
||||||
|
Due to this change, it is important that the single-user server
|
||||||
|
and Hub are both running the same version of JupyterHub.
|
||||||
|
If you are using containers (e.g. via DockerSpawner or KubeSpawner),
|
||||||
|
this means upgrading jupyterhub in your user images at the same time as the Hub.
|
||||||
|
In most cases, a
|
||||||
|
|
||||||
|
pip install jupyterhub==version
|
||||||
|
|
||||||
|
in your Dockerfile is sufficient.
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- JupyterHub now defined a `Proxy` API for custom
|
||||||
|
proxy implementations other than the default.
|
||||||
|
The defaults are unchanged,
|
||||||
|
but configuration of the proxy is now done on the `ConfigurableHTTPProxy` class instead of the top-level JupyterHub.
|
||||||
|
TODO: docs for writing a custom proxy.
|
||||||
|
- Single-user servers and services
|
||||||
|
(anything that uses HubAuth)
|
||||||
|
can now accept token-authenticated requests via the Authentication header.
|
||||||
|
- Authenticators can now store state in the Hub's database.
|
||||||
|
To do so, the `authenticate` method should return a dict of the form
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'username': 'name',
|
||||||
|
'state': {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This data will be encrypted and requires `JUPYTERHUB_CRYPT_KEY` environment variable to be set
|
||||||
|
and the `Authenticator.enable_auth_state` flag to be True.
|
||||||
|
If these are not set, auth_state returned by the Authenticator will not be stored.
|
||||||
|
- There is preliminary support for multiple (named) servers per user in the REST API.
|
||||||
|
Named servers can be created via API requests, but there is currently no UI for managing them.
|
||||||
|
- Add `LocalProcessSpawner.popen_kwargs` and `LocalProcessSpawner.shell_cmd`
|
||||||
|
for customizing how user server processes are launched.
|
||||||
|
- Add `Authenticator.auto_login` flag for skipping the "Login with..." page explicitly.
|
||||||
|
- Add `JupyterHub.hub_connect_ip` configuration
|
||||||
|
for the ip that should be used when connecting to the Hub.
|
||||||
|
This is promoting (and deprecating) `DockerSpawner.hub_ip_connect`
|
||||||
|
for use by all Spawners.
|
||||||
|
- Add `Spawner.pre_spawn_hook(spawner)` hook for customizing
|
||||||
|
pre-spawn events.
|
||||||
|
- Add `JupyterHub.active_server_limit` and `JupyterHub.concurrent_spawn_limit`
|
||||||
|
for limiting the total number of running user servers and the number of pending spawns, respectively.
|
||||||
|
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
- more arguments to spawners are now passed via environment variables (`.get_env()`)
|
||||||
|
rather than CLI arguments (`.get_args()`)
|
||||||
|
- internally generated tokens no longer get extra hash rounds,
|
||||||
|
significantly speeding up authentication.
|
||||||
|
The hash rounds were deemed unnecessary because the tokens were already
|
||||||
|
generated with high entropy.
|
||||||
|
- `JUPYTERHUB_API_TOKEN` env is available at all times,
|
||||||
|
rather than being removed during single-user start.
|
||||||
|
The token is now accessible to kernel processes,
|
||||||
|
enabling user kernels to make authenticated API requests to Hub-authenticated services.
|
||||||
|
- Cookie secrets should be 32B hex instead of large base64 secrets.
|
||||||
|
- pycurl is used by default, if available.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
So many things fixed!
|
||||||
|
|
||||||
|
- Collisions are checked when users are renamed
|
||||||
|
- Fix bug where OAuth authenticators could not logout users
|
||||||
|
due to being redirected right back through the login process.
|
||||||
|
- If there are errors loading your config files,
|
||||||
|
JupyterHub will refuse to start with an informative error.
|
||||||
|
Previously, the bad config would be ignored and JupyterHub would launch with default configuration.
|
||||||
|
- Raise 403 error on unauthorized user rather than redirect to login,
|
||||||
|
which could cause redirect loop.
|
||||||
|
- Set `httponly` on cookies because it's prudent.
|
||||||
|
- Improve support for MySQL as the database backend
|
||||||
|
- Many race conditions and performance problems under heavy load have been fixed.
|
||||||
|
- Fix alembic tagging of database schema versions.
|
||||||
|
|
||||||
|
#### Removed
|
||||||
|
|
||||||
|
- End support for Python 3.3
|
||||||
|
|
||||||
## 0.7
|
## 0.7
|
||||||
|
|
||||||
@@ -145,7 +402,13 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
|
|||||||
First preview release
|
First preview release
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.7.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
|
||||||
[0.7.1]: https://github.com/jupyterhub/jupyterhub/compare/0.7.0...0.7.1
|
[0.7.1]: https://github.com/jupyterhub/jupyterhub/compare/0.7.0...0.7.1
|
||||||
[0.7.0]: https://github.com/jupyterhub/jupyterhub/compare/0.6.1...0.7.0
|
[0.7.0]: https://github.com/jupyterhub/jupyterhub/compare/0.6.1...0.7.0
|
||||||
[0.6.1]: https://github.com/jupyterhub/jupyterhub/compare/0.6.0...0.6.1
|
[0.6.1]: https://github.com/jupyterhub/jupyterhub/compare/0.6.0...0.6.1
|
||||||
|
@@ -8,7 +8,7 @@ import shlex
|
|||||||
import recommonmark.parser
|
import recommonmark.parser
|
||||||
|
|
||||||
# Set paths
|
# Set paths
|
||||||
#sys.path.insert(0, os.path.abspath('.'))
|
sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ extensions = [
|
|||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
'sphinx.ext.intersphinx',
|
'sphinx.ext.intersphinx',
|
||||||
'sphinx.ext.napoleon',
|
'sphinx.ext.napoleon',
|
||||||
|
'autodoc_traits',
|
||||||
]
|
]
|
||||||
|
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
@@ -34,11 +35,14 @@ author = u'Project Jupyter team'
|
|||||||
|
|
||||||
# Autopopulate version
|
# Autopopulate version
|
||||||
from os.path import dirname
|
from os.path import dirname
|
||||||
|
|
||||||
docs = dirname(dirname(__file__))
|
docs = dirname(dirname(__file__))
|
||||||
root = dirname(docs)
|
root = dirname(docs)
|
||||||
sys.path.insert(0, root)
|
sys.path.insert(0, root)
|
||||||
|
sys.path.insert(0, os.path.join(docs, 'sphinxext'))
|
||||||
|
|
||||||
import jupyterhub
|
import jupyterhub
|
||||||
|
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '%i.%i' % jupyterhub.version_info[:2]
|
version = '%i.%i' % jupyterhub.version_info[:2]
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
@@ -49,11 +53,12 @@ exclude_patterns = []
|
|||||||
pygments_style = 'sphinx'
|
pygments_style = 'sphinx'
|
||||||
todo_include_todos = False
|
todo_include_todos = False
|
||||||
|
|
||||||
|
# Set the default role so we can use `foo` instead of ``foo``
|
||||||
|
default_role = 'literal'
|
||||||
|
|
||||||
# -- Source -------------------------------------------------------------
|
# -- Source -------------------------------------------------------------
|
||||||
|
|
||||||
source_parsers = {
|
source_parsers = {'.md': 'recommonmark.parser.CommonMarkParser'}
|
||||||
'.md': 'recommonmark.parser.CommonMarkParser',
|
|
||||||
}
|
|
||||||
|
|
||||||
source_suffix = ['.rst', '.md']
|
source_suffix = ['.rst', '.md']
|
||||||
# source_encoding = 'utf-8-sig'
|
# source_encoding = 'utf-8-sig'
|
||||||
@@ -61,34 +66,39 @@ source_suffix = ['.rst', '.md']
|
|||||||
# -- Options for HTML output ----------------------------------------------
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages.
|
# The theme to use for HTML and HTML Help pages.
|
||||||
html_theme = 'sphinx_rtd_theme'
|
html_theme = 'alabaster'
|
||||||
|
|
||||||
#html_theme_options = {}
|
html_logo = '_static/images/logo/logo.png'
|
||||||
#html_theme_path = []
|
html_favicon = '_static/images/logo/favicon.ico'
|
||||||
#html_title = None
|
|
||||||
#html_short_title = None
|
|
||||||
#html_logo = None
|
|
||||||
#html_favicon = None
|
|
||||||
|
|
||||||
# Paths that contain custom static files (such as style sheets)
|
# Paths that contain custom static files (such as style sheets)
|
||||||
html_static_path = ['_static']
|
html_static_path = ['_static']
|
||||||
|
|
||||||
#html_extra_path = []
|
html_theme_options = {
|
||||||
#html_last_updated_fmt = '%b %d, %Y'
|
'show_related': True,
|
||||||
#html_use_smartypants = True
|
'description': 'Documentation for JupyterHub',
|
||||||
#html_sidebars = {}
|
'github_user': 'jupyterhub',
|
||||||
#html_additional_pages = {}
|
'github_repo': 'jupyterhub',
|
||||||
#html_domain_indices = True
|
'github_banner': False,
|
||||||
#html_use_index = True
|
'github_button': True,
|
||||||
#html_split_index = False
|
'github_type': 'star',
|
||||||
#html_show_sourcelink = True
|
'show_powered_by': False,
|
||||||
#html_show_sphinx = True
|
'extra_nav_links': {
|
||||||
#html_show_copyright = True
|
'GitHub Repo': 'http://github.com/jupyterhub/jupyterhub',
|
||||||
#html_use_opensearch = ''
|
'Issue Tracker': 'http://github.com/jupyterhub/jupyterhub/issues',
|
||||||
#html_file_suffix = None
|
},
|
||||||
#html_search_language = 'en'
|
}
|
||||||
#html_search_options = {'type': 'default'}
|
|
||||||
#html_search_scorer = 'scorer.js'
|
html_sidebars = {
|
||||||
|
'**': [
|
||||||
|
'about.html',
|
||||||
|
'searchbox.html',
|
||||||
|
'navigation.html',
|
||||||
|
'relations.html',
|
||||||
|
'sourcelink.html',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
htmlhelp_basename = 'JupyterHubdoc'
|
htmlhelp_basename = 'JupyterHubdoc'
|
||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
@@ -104,8 +114,13 @@ latex_elements = {
|
|||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
(master_doc, 'JupyterHub.tex', u'JupyterHub Documentation',
|
(
|
||||||
u'Project Jupyter team', 'manual'),
|
master_doc,
|
||||||
|
'JupyterHub.tex',
|
||||||
|
u'JupyterHub Documentation',
|
||||||
|
u'Project Jupyter team',
|
||||||
|
'manual',
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
# latex_logo = None
|
# latex_logo = None
|
||||||
@@ -120,10 +135,7 @@ latex_documents = [
|
|||||||
|
|
||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [(master_doc, 'jupyterhub', u'JupyterHub Documentation', [author], 1)]
|
||||||
(master_doc, 'jupyterhub', u'JupyterHub Documentation',
|
|
||||||
[author], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
# man_show_urls = False
|
# man_show_urls = False
|
||||||
|
|
||||||
@@ -134,9 +146,15 @@ man_pages = [
|
|||||||
# (source start file, target name, title, author,
|
# (source start file, target name, title, author,
|
||||||
# dir menu entry, description, category)
|
# dir menu entry, description, category)
|
||||||
texinfo_documents = [
|
texinfo_documents = [
|
||||||
(master_doc, 'JupyterHub', u'JupyterHub Documentation',
|
(
|
||||||
author, 'JupyterHub', 'One line description of project.',
|
master_doc,
|
||||||
'Miscellaneous'),
|
'JupyterHub',
|
||||||
|
u'JupyterHub Documentation',
|
||||||
|
author,
|
||||||
|
'JupyterHub',
|
||||||
|
'One line description of project.',
|
||||||
|
'Miscellaneous',
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
# texinfo_appendices = []
|
# texinfo_appendices = []
|
||||||
@@ -158,21 +176,18 @@ epub_exclude_files = ['search.html']
|
|||||||
|
|
||||||
# -- Intersphinx ----------------------------------------------------------
|
# -- Intersphinx ----------------------------------------------------------
|
||||||
|
|
||||||
intersphinx_mapping = {'https://docs.python.org/': None}
|
intersphinx_mapping = {'https://docs.python.org/3/': None}
|
||||||
|
|
||||||
# -- Read The Docs --------------------------------------------------------
|
# -- Read The Docs --------------------------------------------------------
|
||||||
|
|
||||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||||
|
|
||||||
if not on_rtd:
|
if not on_rtd:
|
||||||
# only import and set the theme if we're building docs locally
|
html_theme = 'alabaster'
|
||||||
import sphinx_rtd_theme
|
|
||||||
html_theme = 'sphinx_rtd_theme'
|
|
||||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
|
||||||
else:
|
else:
|
||||||
# readthedocs.org uses their theme by default, so no need to specify it
|
# readthedocs.org uses their theme by default, so no need to specify it
|
||||||
# build rest-api, since RTD doesn't run make
|
# build rest-api, since RTD doesn't run make
|
||||||
from subprocess import check_call as sh
|
from subprocess import check_call as sh
|
||||||
|
|
||||||
sh(['make', 'rest-api'], cwd=docs)
|
sh(['make', 'rest-api'], cwd=docs)
|
||||||
|
|
||||||
# -- Spell checking -------------------------------------------------------
|
# -- Spell checking -------------------------------------------------------
|
||||||
|
@@ -1,194 +0,0 @@
|
|||||||
# Configuration examples
|
|
||||||
|
|
||||||
This section provides configuration files and tips for the following
|
|
||||||
configurations:
|
|
||||||
|
|
||||||
- Example with GitHub OAuth
|
|
||||||
- Example with nginx reverse proxy
|
|
||||||
|
|
||||||
|
|
||||||
## Example with GitHub OAuth
|
|
||||||
|
|
||||||
In the following example, we show a configuration files for a fairly standard JupyterHub deployment with the following assumptions:
|
|
||||||
|
|
||||||
* JupyterHub is running on a single cloud server
|
|
||||||
* Using SSL on the standard HTTPS port 443
|
|
||||||
* You want to use GitHub OAuth (using oauthenticator) for login
|
|
||||||
* You need the users to exist locally on the server
|
|
||||||
* You want users' notebooks to be served from `~/assignments` to allow users to browse for notebooks within
|
|
||||||
other users home directories
|
|
||||||
* You want the landing page for each user to be a Welcome.ipynb notebook in their assignments directory.
|
|
||||||
* All runtime files are put into `/srv/jupyterhub` and log files in `/var/log`.
|
|
||||||
|
|
||||||
Let's start out with `jupyterhub_config.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# jupyterhub_config.py
|
|
||||||
c = get_config()
|
|
||||||
|
|
||||||
import os
|
|
||||||
pjoin = os.path.join
|
|
||||||
|
|
||||||
runtime_dir = os.path.join('/srv/jupyterhub')
|
|
||||||
ssl_dir = pjoin(runtime_dir, 'ssl')
|
|
||||||
if not os.path.exists(ssl_dir):
|
|
||||||
os.makedirs(ssl_dir)
|
|
||||||
|
|
||||||
|
|
||||||
# https on :443
|
|
||||||
c.JupyterHub.port = 443
|
|
||||||
c.JupyterHub.ssl_key = pjoin(ssl_dir, 'ssl.key')
|
|
||||||
c.JupyterHub.ssl_cert = pjoin(ssl_dir, 'ssl.cert')
|
|
||||||
|
|
||||||
# put the JupyterHub cookie secret and state db
|
|
||||||
# in /var/run/jupyterhub
|
|
||||||
c.JupyterHub.cookie_secret_file = pjoin(runtime_dir, 'cookie_secret')
|
|
||||||
c.JupyterHub.db_url = pjoin(runtime_dir, 'jupyterhub.sqlite')
|
|
||||||
# or `--db=/path/to/jupyterhub.sqlite` on the command-line
|
|
||||||
|
|
||||||
# put the log file in /var/log
|
|
||||||
c.JupyterHub.extra_log_file = '/var/log/jupyterhub.log'
|
|
||||||
|
|
||||||
# use GitHub OAuthenticator for local users
|
|
||||||
|
|
||||||
c.JupyterHub.authenticator_class = 'oauthenticator.LocalGitHubOAuthenticator'
|
|
||||||
c.GitHubOAuthenticator.oauth_callback_url = os.environ['OAUTH_CALLBACK_URL']
|
|
||||||
# create system users that don't exist yet
|
|
||||||
c.LocalAuthenticator.create_system_users = True
|
|
||||||
|
|
||||||
# specify users and admin
|
|
||||||
c.Authenticator.whitelist = {'rgbkrk', 'minrk', 'jhamrick'}
|
|
||||||
c.Authenticator.admin_users = {'jhamrick', 'rgbkrk'}
|
|
||||||
|
|
||||||
# start single-user notebook servers in ~/assignments,
|
|
||||||
# with ~/assignments/Welcome.ipynb as the default landing page
|
|
||||||
# this config could also be put in
|
|
||||||
# /etc/ipython/ipython_notebook_config.py
|
|
||||||
c.Spawner.notebook_dir = '~/assignments'
|
|
||||||
c.Spawner.args = ['--NotebookApp.default_url=/notebooks/Welcome.ipynb']
|
|
||||||
```
|
|
||||||
|
|
||||||
Using the GitHub Authenticator [requires a few additional env variables][oauth-setup],
|
|
||||||
which we will need to set when we launch the server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export GITHUB_CLIENT_ID=github_id
|
|
||||||
export GITHUB_CLIENT_SECRET=github_secret
|
|
||||||
export OAUTH_CALLBACK_URL=https://example.com/hub/oauth_callback
|
|
||||||
export CONFIGPROXY_AUTH_TOKEN=super-secret
|
|
||||||
jupyterhub -f /path/to/aboveconfig.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example with nginx reverse proxy
|
|
||||||
|
|
||||||
In the following example, we show configuration files for a JupyterHub server running locally on port `8000` but accessible from the outside on the standard SSL port `443`. This could be useful if the JupyterHub server machine is also hosting other domains or content on `443`. The goal here is to have the following be true:
|
|
||||||
|
|
||||||
* JupyterHub is running on a server, accessed *only* via `HUB.DOMAIN.TLD:443`
|
|
||||||
* On the same machine, `NO_HUB.DOMAIN.TLD` strictly serves different content, also on port `443`
|
|
||||||
* `nginx` is used to manage the web servers / reverse proxy (which means that only nginx will be able to bind two servers to `443`)
|
|
||||||
* After testing, the server in question should be able to score an A+ on the Qualys SSL Labs [SSL Server Test](https://www.ssllabs.com/ssltest/)
|
|
||||||
|
|
||||||
Let's start out with `jupyterhub_config.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Force the proxy to only listen to connections to 127.0.0.1
|
|
||||||
c.JupyterHub.ip = '127.0.0.1'
|
|
||||||
```
|
|
||||||
|
|
||||||
The `nginx` server config files are fairly standard fare except for the two `location` blocks within the `HUB.DOMAIN.TLD` config file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# HTTP server to redirect all 80 traffic to SSL/HTTPS
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name HUB.DOMAIN.TLD;
|
|
||||||
|
|
||||||
# Tell all requests to port 80 to be 302 redirected to HTTPS
|
|
||||||
return 302 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTPS server to handle JupyterHub
|
|
||||||
server {
|
|
||||||
listen 443;
|
|
||||||
ssl on;
|
|
||||||
|
|
||||||
server_name HUB.DOMAIN.TLD;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/HUB.DOMAIN.TLD/fullchain.pem
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/HUB.DOMAIN.TLD/privkey.pem
|
|
||||||
|
|
||||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
|
||||||
ssl_prefer_server_ciphers on;
|
|
||||||
ssl_dhparam /etc/ssl/certs/dhparam.pem;
|
|
||||||
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
|
|
||||||
ssl_session_timeout 1d;
|
|
||||||
ssl_session_cache shared:SSL:50m;
|
|
||||||
ssl_stapling on;
|
|
||||||
ssl_stapling_verify on;
|
|
||||||
add_header Strict-Transport-Security max-age=15768000;
|
|
||||||
|
|
||||||
# Managing literal requests to the JupyterHub front end
|
|
||||||
location / {
|
|
||||||
proxy_pass https://127.0.0.1:8000;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Managing WebHook/Socket requests between hub user servers and external proxy
|
|
||||||
location ~* /(api/kernels/[^/]+/(channels|iopub|shell|stdin)|terminals/websocket)/? {
|
|
||||||
proxy_pass https://127.0.0.1:8000;
|
|
||||||
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
# WebSocket support
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
# Managing requests to verify letsencrypt host
|
|
||||||
location ~ /.well-known {
|
|
||||||
allow all;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`nginx` will now be the front facing element of JupyterHub on `443` which means it is also free to bind other servers, like `NO_HUB.DOMAIN.TLD` to the same port on the same machine and network interface. In fact, one can simply use the same server blocks as above for `NO_HUB` and simply add line for the root directory of the site as well as the applicable location call:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name NO_HUB.DOMAIN.TLD;
|
|
||||||
|
|
||||||
# Tell all requests to port 80 to be 302 redirected to HTTPS
|
|
||||||
return 302 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443;
|
|
||||||
ssl on;
|
|
||||||
|
|
||||||
# INSERT OTHER SSL PARAMETERS HERE AS ABOVE
|
|
||||||
|
|
||||||
# Set the appropriate root directory
|
|
||||||
root /var/www/html
|
|
||||||
|
|
||||||
# Set URI handling
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Managing requests to verify letsencrypt host
|
|
||||||
location ~ /.well-known {
|
|
||||||
allow all;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Now just restart `nginx`, restart the JupyterHub, and enjoy accessing https://HUB.DOMAIN.TLD while serving other content securely on https://NO_HUB.DOMAIN.TLD.
|
|
@@ -3,56 +3,120 @@
|
|||||||
Project Jupyter thanks the following people for their help and
|
Project Jupyter thanks the following people for their help and
|
||||||
contribution on JupyterHub:
|
contribution on JupyterHub:
|
||||||
|
|
||||||
|
- adelcast
|
||||||
|
- Analect
|
||||||
- anderbubble
|
- anderbubble
|
||||||
|
- anikitml
|
||||||
|
- ankitksharma
|
||||||
|
- apetresc
|
||||||
|
- athornton
|
||||||
|
- barrachri
|
||||||
|
- BerserkerTroll
|
||||||
- betatim
|
- betatim
|
||||||
- Carreau
|
- Carreau
|
||||||
|
- cfournie
|
||||||
|
- charnpreetsingh
|
||||||
|
- chicovenancio
|
||||||
|
- cikao
|
||||||
- ckald
|
- ckald
|
||||||
|
- cmoscardi
|
||||||
|
- consideRatio
|
||||||
|
- cqzlxl
|
||||||
|
- CRegenschein
|
||||||
- cwaldbieser
|
- cwaldbieser
|
||||||
- danielballen
|
- danielballen
|
||||||
|
- danoventa
|
||||||
- daradib
|
- daradib
|
||||||
|
- darky2004
|
||||||
- datapolitan
|
- datapolitan
|
||||||
- dblockow-d2dcrc
|
- dblockow-d2dcrc
|
||||||
|
- DeepHorizons
|
||||||
|
- DerekHeldtWerle
|
||||||
|
- dhirschfeld
|
||||||
- dietmarw
|
- dietmarw
|
||||||
|
- dingc3
|
||||||
|
- dmartzol
|
||||||
- DominicFollettSmith
|
- DominicFollettSmith
|
||||||
- dsblank
|
- dsblank
|
||||||
|
- dtaniwaki
|
||||||
|
- echarles
|
||||||
- ellisonbg
|
- ellisonbg
|
||||||
|
- emmanuel
|
||||||
- evanlinde
|
- evanlinde
|
||||||
- Fokko
|
- Fokko
|
||||||
|
- fperez
|
||||||
|
- franga2000
|
||||||
|
- GladysNalvarte
|
||||||
|
- glenak1911
|
||||||
|
- gweis
|
||||||
- iamed18
|
- iamed18
|
||||||
|
- jamescurtin
|
||||||
- JamiesHQ
|
- JamiesHQ
|
||||||
|
- JasonJWilliamsNY
|
||||||
|
- jbweston
|
||||||
- jdavidheiser
|
- jdavidheiser
|
||||||
|
- jencabral
|
||||||
- jhamrick
|
- jhamrick
|
||||||
|
- jkinkead
|
||||||
|
- johnkpark
|
||||||
- josephtate
|
- josephtate
|
||||||
|
- jzf2101
|
||||||
|
- karfai
|
||||||
- kinuax
|
- kinuax
|
||||||
- KrishnaPG
|
- KrishnaPG
|
||||||
|
- kroq-gar78
|
||||||
- ksolan
|
- ksolan
|
||||||
- mbmilligan
|
- mbmilligan
|
||||||
|
- mgeplf
|
||||||
- minrk
|
- minrk
|
||||||
- mistercrunch
|
- mistercrunch
|
||||||
- Mistobaan
|
- Mistobaan
|
||||||
|
- mpacer
|
||||||
- mwmarkland
|
- mwmarkland
|
||||||
|
- ndly
|
||||||
- nthiery
|
- nthiery
|
||||||
|
- nxg
|
||||||
- ObiWahn
|
- ObiWahn
|
||||||
- ozancaglayan
|
- ozancaglayan
|
||||||
|
- paccorsi
|
||||||
- parente
|
- parente
|
||||||
- PeterDaveHello
|
- PeterDaveHello
|
||||||
- peterruppel
|
- peterruppel
|
||||||
|
- phill84
|
||||||
|
- pjamason
|
||||||
|
- prasadkatti
|
||||||
- rafael-ladislau
|
- rafael-ladislau
|
||||||
|
- rcthomas
|
||||||
- rgbkrk
|
- rgbkrk
|
||||||
|
- rkdarst
|
||||||
- robnagler
|
- robnagler
|
||||||
|
- rschroll
|
||||||
- ryanlovett
|
- ryanlovett
|
||||||
|
- sangramga
|
||||||
- Scrypy
|
- Scrypy
|
||||||
|
- schon
|
||||||
- shreddd
|
- shreddd
|
||||||
|
- Siecje
|
||||||
|
- smiller5678
|
||||||
- spoorthyv
|
- spoorthyv
|
||||||
- ssanderson
|
- ssanderson
|
||||||
|
- summerswallow
|
||||||
|
- syutbai
|
||||||
- takluyver
|
- takluyver
|
||||||
- temogen
|
- temogen
|
||||||
|
- ThomasMChen
|
||||||
|
- Thoralf Gutierrez
|
||||||
|
- timfreund
|
||||||
- TimShawver
|
- TimShawver
|
||||||
|
- tklever
|
||||||
- Todd-Z-Li
|
- Todd-Z-Li
|
||||||
- toobaz
|
- toobaz
|
||||||
- tsaeger
|
- tsaeger
|
||||||
|
- tschaume
|
||||||
- vilhelmen
|
- vilhelmen
|
||||||
|
- whitead
|
||||||
- willingc
|
- willingc
|
||||||
- YannBrrd
|
- YannBrrd
|
||||||
- yuvipanda
|
- yuvipanda
|
||||||
- zoltan-fedor
|
- zoltan-fedor
|
||||||
|
- zonca
|
||||||
|
169
docs/source/gallery-jhub-deployments.md
Normal file
169
docs/source/gallery-jhub-deployments.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# A Gallery of JupyterHub Deployments
|
||||||
|
|
||||||
|
**A JupyterHub Community Resource**
|
||||||
|
|
||||||
|
We've compiled this list of JupyterHub deployments to help the community
|
||||||
|
see the breadth and growth of JupyterHub's use in education, research, and
|
||||||
|
high performance computing.
|
||||||
|
|
||||||
|
Please submit pull requests to update information or to add new institutions or uses.
|
||||||
|
|
||||||
|
|
||||||
|
## Academic Institutions, Research Labs, and Supercomputer Centers
|
||||||
|
|
||||||
|
### University of California Berkeley
|
||||||
|
|
||||||
|
- [BIDS - Berkeley Institute for Data Science](https://bids.berkeley.edu/)
|
||||||
|
- [Teaching with Jupyter notebooks and JupyterHub](https://bids.berkeley.edu/resources/videos/teaching-ipythonjupyter-notebooks-and-jupyterhub)
|
||||||
|
|
||||||
|
- [Data 8](http://data8.org/)
|
||||||
|
- [GitHub organization](https://github.com/data-8)
|
||||||
|
|
||||||
|
- [NERSC](http://www.nersc.gov/)
|
||||||
|
- [Press release on Jupyter and Cori](http://www.nersc.gov/news-publications/nersc-news/nersc-center-news/2016/jupyter-notebooks-will-open-up-new-possibilities-on-nerscs-cori-supercomputer/)
|
||||||
|
- [Moving and sharing data](https://www.nersc.gov/assets/Uploads/03-MovingAndSharingData-Cholia.pdf)
|
||||||
|
|
||||||
|
- [Research IT](http://research-it.berkeley.edu)
|
||||||
|
- [JupyterHub server supports campus research computation](http://research-it.berkeley.edu/blog/17/01/24/free-fully-loaded-jupyterhub-server-supports-campus-research-computation)
|
||||||
|
|
||||||
|
### University of California Davis
|
||||||
|
|
||||||
|
- [Spinning up multiple Jupyter Notebooks on AWS for a tutorial](https://github.com/mblmicdiv/course2017/blob/master/exercises/sourmash-setup.md)
|
||||||
|
|
||||||
|
Although not technically a JupyterHub deployment, this tutorial setup
|
||||||
|
may be helpful to others in the Jupyter community.
|
||||||
|
|
||||||
|
Thank you C. Titus Brown for sharing this with the Software Carpentry
|
||||||
|
mailing list.
|
||||||
|
|
||||||
|
```
|
||||||
|
* I started a big Amazon machine;
|
||||||
|
* I installed Docker and built a custom image containing my software of
|
||||||
|
interest;
|
||||||
|
* I ran multiple containers, one connected to port 8000, one on 8001,
|
||||||
|
etc. and gave each student a different port;
|
||||||
|
* students could connect in and use the Terminal program in Jupyter to
|
||||||
|
execute commands, and could upload/download files via the Jupyter
|
||||||
|
console interface;
|
||||||
|
* in theory I could have used notebooks too, but for this I didn’t have
|
||||||
|
need.
|
||||||
|
|
||||||
|
I am aware that JupyterHub can probably do all of this including manage
|
||||||
|
the containers, but I’m still a bit shy of diving into that; this was
|
||||||
|
fairly straightforward, gave me disposable containers that were isolated
|
||||||
|
for each individual student, and worked almost flawlessly. Should be
|
||||||
|
easy to do with RStudio too.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cal Poly San Luis Obispo
|
||||||
|
|
||||||
|
- [jupyterhub-deploy-teaching](https://github.com/jupyterhub/jupyterhub-deploy-teaching) based on work by Brian Granger for Cal Poly's Data Science 301 Course
|
||||||
|
|
||||||
|
### Clemson University
|
||||||
|
|
||||||
|
- Advanced Computing
|
||||||
|
- [Palmetto cluster and JupyterHub](http://citi.sites.clemson.edu/2016/08/18/JupyterHub-for-Palmetto-Cluster.html)
|
||||||
|
|
||||||
|
### University of Colorado Boulder
|
||||||
|
|
||||||
|
- (CU Research Computing) CURC
|
||||||
|
- [JupyterHub User Guide](https://www.rc.colorado.edu/support/user-guide/jupyterhub.html)
|
||||||
|
- Slurm job dispatched on Crestone compute cluster
|
||||||
|
- log troubleshooting
|
||||||
|
- Profiles in IPython Clusters tab
|
||||||
|
- [Parallel Processing with JupyterHub tutorial](https://www.rc.colorado.edu/support/examples-and-tutorials/parallel-processing-with-jupyterhub.html)
|
||||||
|
- [Parallel Programming with JupyterHub document](https://www.rc.colorado.edu/book/export/html/833)
|
||||||
|
|
||||||
|
- Earth Lab at CU
|
||||||
|
- [Tutorial on Parallel R on JupyterHub](https://earthdatascience.org/tutorials/parallel-r-on-jupyterhub/)
|
||||||
|
|
||||||
|
### HTCondor
|
||||||
|
|
||||||
|
- [HTCondor Python Bindings Tutorial from HTCondor Week 2017 includes information on their JupyterHub tutorials](https://research.cs.wisc.edu/htcondor/HTCondorWeek2017/presentations/TueBockelman_Python.pdf)
|
||||||
|
|
||||||
|
### University of Illinois
|
||||||
|
|
||||||
|
- https://datascience.business.illinois.edu
|
||||||
|
|
||||||
|
### MIT and Lincoln Labs
|
||||||
|
|
||||||
|
|
||||||
|
### Michigan State University
|
||||||
|
|
||||||
|
- [Setting up JupyterHub](https://mediaspace.msu.edu/media/Setting+Up+Your+JupyterHub+Password/1_hgv13aag/11980471)
|
||||||
|
|
||||||
|
### University of Minnesota
|
||||||
|
|
||||||
|
- [JupyterHub Inside HPC](https://insidehpc.com/tag/jupyterhub/)
|
||||||
|
|
||||||
|
### University of Missouri
|
||||||
|
|
||||||
|
- https://dsa.missouri.edu/faq/
|
||||||
|
|
||||||
|
### University of Rochester CIRC
|
||||||
|
|
||||||
|
- [JupyterHub Userguide](https://info.circ.rochester.edu/Web_Applications/JupyterHub.html) - Slurm, beehive
|
||||||
|
|
||||||
|
### University of California San Diego
|
||||||
|
|
||||||
|
- San Diego Supercomputer Center - Andrea Zonca
|
||||||
|
- [Deploy JupyterHub on a Supercomputer with SSH](https://zonca.github.io/2017/05/jupyterhub-hpc-batchspawner-ssh.html)
|
||||||
|
- [Run Jupyterhub on a Supercomputer](https://zonca.github.io/2015/04/jupyterhub-hpc.html)
|
||||||
|
- [Deploy JupyterHub on a VM for a Workshop](https://zonca.github.io/2016/04/jupyterhub-sdsc-cloud.html)
|
||||||
|
- [Customize your Python environment in Jupyterhub](https://zonca.github.io/2017/02/customize-python-environment-jupyterhub.html)
|
||||||
|
- [Jupyterhub deployment on multiple nodes with Docker Swarm](https://zonca.github.io/2016/05/jupyterhub-docker-swarm.html)
|
||||||
|
- [Sample deployment of Jupyterhub in HPC on SDSC Comet](https://zonca.github.io/2017/02/sample-deployment-jupyterhub-hpc.html)
|
||||||
|
|
||||||
|
- Educational Technology Services - Paul Jamason
|
||||||
|
- [jupyterhub.ucsd.edu](https://jupyterhub.ucsd.edu)
|
||||||
|
|
||||||
|
### TACC University of Texas
|
||||||
|
|
||||||
|
### Texas A&M
|
||||||
|
|
||||||
|
- Kristen Thyng - Oceanography
|
||||||
|
- [Teaching with JupyterHub and nbgrader](http://kristenthyng.com/blog/2016/09/07/jupyterhub+nbgrader/)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Service Providers
|
||||||
|
|
||||||
|
### AWS
|
||||||
|
|
||||||
|
- [running-jupyter-notebook-and-jupyterhub-on-amazon-emr](https://aws.amazon.com/blogs/big-data/running-jupyter-notebook-and-jupyterhub-on-amazon-emr/)
|
||||||
|
|
||||||
|
### Google Cloud Platform
|
||||||
|
|
||||||
|
- [Using Tensorflow and JupyterHub in Classrooms](https://cloud.google.com/solutions/using-tensorflow-jupyterhub-classrooms)
|
||||||
|
- [using-tensorflow-and-jupyterhub blog post](https://opensource.googleblog.com/2016/10/using-tensorflow-and-jupyterhub.html)
|
||||||
|
|
||||||
|
### Everware
|
||||||
|
|
||||||
|
[Everware](https://github.com/everware) Reproducible and reusable science powered by jupyterhub and docker. Like nbviewer, but executable. CERN, Geneva [website](http://everware.xyz/)
|
||||||
|
|
||||||
|
|
||||||
|
### Microsoft Azure
|
||||||
|
|
||||||
|
- https://docs.microsoft.com/en-us/azure/machine-learning/machine-learning-data-science-linux-dsvm-intro
|
||||||
|
|
||||||
|
### Rackspace Carina
|
||||||
|
|
||||||
|
- https://getcarina.com/blog/learning-how-to-whale/
|
||||||
|
- http://carolynvanslyck.com/talk/carina/jupyterhub/#/
|
||||||
|
|
||||||
|
### jcloud.io
|
||||||
|
- Open to public JupyterHub server
|
||||||
|
- https://jcloud.io
|
||||||
|
## Miscellaneous
|
||||||
|
|
||||||
|
- https://medium.com/@ybarraud/setting-up-jupyterhub-with-sudospawner-and-anaconda-844628c0dbee#.rm3yt87e1
|
||||||
|
- https://groups.google.com/forum/#!topic/jupyter/nkPSEeMr8c0 Mailing list UT deployment
|
||||||
|
- JupyterHub setup on Centos https://gist.github.com/johnrc/604971f7d41ebf12370bf5729bf3e0a4
|
||||||
|
- Deploy JupyterHub to Docker Swarm https://jupyterhub.surge.sh/#/welcome
|
||||||
|
- http://www.laketide.com/building-your-lab-part-3/
|
||||||
|
- http://estrellita.hatenablog.com/entry/2015/07/31/083202
|
||||||
|
- http://www.walkingrandomly.com/?p=5734
|
||||||
|
- https://wrdrd.com/docs/consulting/education-technology
|
||||||
|
- https://bitbucket.org/jackhale/fenics-jupyter
|
||||||
|
- [LinuxCluster blog](https://linuxcluster.wordpress.com/category/application/jupyterhub/)
|
||||||
|
- [Network Technology](https://arnesund.com/tag/jupyterhub/) [Spark Cluster on OpenStack with Multi-User Jupyter Notebook](https://arnesund.com/2015/09/21/spark-cluster-on-openstack-with-multi-user-jupyter-notebook/)
|
@@ -1,534 +0,0 @@
|
|||||||
# Getting started with JupyterHub
|
|
||||||
|
|
||||||
This section contains getting started information on the following topics:
|
|
||||||
|
|
||||||
- [Technical Overview](getting-started.html#technical-overview)
|
|
||||||
- [Installation](getting-started.html#installation)
|
|
||||||
- [Configuration](getting-started.html#configuration)
|
|
||||||
- [Networking](getting-started.html#networking)
|
|
||||||
- [Security](getting-started.html#security)
|
|
||||||
- [Authentication and users](getting-started.html#authentication-and-users)
|
|
||||||
- [Spawners and single-user notebook servers](getting-started.html#spawners-and-single-user-notebook-servers)
|
|
||||||
- [External Services](getting-started.html#external-services)
|
|
||||||
|
|
||||||
|
|
||||||
## Technical Overview
|
|
||||||
|
|
||||||
JupyterHub is a set of processes that together provide a single user Jupyter
|
|
||||||
Notebook server for each person in a group.
|
|
||||||
|
|
||||||
### Three subsystems
|
|
||||||
Three major subsystems run by the `jupyterhub` command line program:
|
|
||||||
|
|
||||||
- **Single-User Notebook Server**: a dedicated, single-user, Jupyter Notebook server is
|
|
||||||
started for each user on the system when the user logs in. The object that
|
|
||||||
starts these servers is called a **Spawner**.
|
|
||||||
- **Proxy**: the public facing part of JupyterHub that uses a dynamic proxy
|
|
||||||
to route HTTP requests to the Hub and Single User Notebook Servers.
|
|
||||||
- **Hub**: manages user accounts, authentication, and coordinates Single User
|
|
||||||
Notebook Servers using a Spawner.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
### Deployment server
|
|
||||||
To use JupyterHub, you need a Unix server (typically Linux) running somewhere
|
|
||||||
that is accessible to your team on the network. The JupyterHub server can be
|
|
||||||
on an internal network at your organization, or it can run on the public
|
|
||||||
internet (in which case, take care with the Hub's
|
|
||||||
[security](getting-started.html#security)).
|
|
||||||
|
|
||||||
### Basic operation
|
|
||||||
Users access JupyterHub through a web browser, by going to the IP address or
|
|
||||||
the domain name of the server.
|
|
||||||
|
|
||||||
Basic principles of operation:
|
|
||||||
|
|
||||||
* Hub spawns proxy
|
|
||||||
* Proxy forwards all requests to hub by default
|
|
||||||
* Hub handles login, and spawns single-user servers on demand
|
|
||||||
* Hub configures proxy to forward url prefixes to single-user servers
|
|
||||||
|
|
||||||
Different **[authenticators](authenticators.html)** control access
|
|
||||||
to JupyterHub. The default one (PAM) uses the user accounts on the server where
|
|
||||||
JupyterHub is running. If you use this, you will need to create a user account
|
|
||||||
on the system for each user on your team. Using other authenticators, you can
|
|
||||||
allow users to sign in with e.g. a GitHub account, or with any single-sign-on
|
|
||||||
system your organization has.
|
|
||||||
|
|
||||||
Next, **[spawners](spawners.html)** control how JupyterHub starts
|
|
||||||
the individual notebook server for each user. The default spawner will
|
|
||||||
start a notebook server on the same machine running under their system username.
|
|
||||||
The other main option is to start each server in a separate container, often
|
|
||||||
using Docker.
|
|
||||||
|
|
||||||
### Default behavior
|
|
||||||
|
|
||||||
**IMPORTANT: You should not run JupyterHub without SSL encryption on a public network.**
|
|
||||||
|
|
||||||
See [Security documentation](#security) for how to configure JupyterHub to use SSL,
|
|
||||||
or put it behind SSL termination in another proxy server, such as nginx.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Deprecation note:** Removed `--no-ssl` in version 0.7.
|
|
||||||
|
|
||||||
JupyterHub versions 0.5 and 0.6 require extra confirmation via `--no-ssl` to
|
|
||||||
allow running without SSL using the command `jupyterhub --no-ssl`. The
|
|
||||||
`--no-ssl` command line option is not needed anymore in version 0.7.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
To start JupyterHub in its default configuration, type the following at the command line:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo jupyterhub
|
|
||||||
```
|
|
||||||
|
|
||||||
The default Authenticator that ships with JupyterHub authenticates users
|
|
||||||
with their system name and password (via [PAM][]).
|
|
||||||
Any user on the system with a password will be allowed to start a single-user notebook server.
|
|
||||||
|
|
||||||
The default Spawner starts servers locally as each user, one dedicated server per user.
|
|
||||||
These servers listen on localhost, and start in the given user's home directory.
|
|
||||||
|
|
||||||
By default, the **Proxy** listens on all public interfaces on port 8000.
|
|
||||||
Thus you can reach JupyterHub through either:
|
|
||||||
|
|
||||||
- `http://localhost:8000`
|
|
||||||
- or any other public IP or domain pointing to your system.
|
|
||||||
|
|
||||||
In their default configuration, the other services, the **Hub** and **Single-User Servers**,
|
|
||||||
all communicate with each other on localhost only.
|
|
||||||
|
|
||||||
By default, starting JupyterHub will write two files to disk in the current working directory:
|
|
||||||
|
|
||||||
- `jupyterhub.sqlite` is the sqlite database containing all of the state of the **Hub**.
|
|
||||||
This file allows the **Hub** to remember what users are running and where,
|
|
||||||
as well as other information enabling you to restart parts of JupyterHub separately. It is
|
|
||||||
important to note that this database contains *no* sensitive information other than **Hub**
|
|
||||||
usernames.
|
|
||||||
- `jupyterhub_cookie_secret` is the encryption key used for securing cookies.
|
|
||||||
This file needs to persist in order for restarting the Hub server to avoid invalidating cookies.
|
|
||||||
Conversely, deleting this file and restarting the server effectively invalidates all login cookies.
|
|
||||||
The cookie secret file is discussed in the [Cookie Secret documentation](#cookie-secret).
|
|
||||||
|
|
||||||
The location of these files can be specified via configuration, discussed below.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
See the project's [README](https://github.com/jupyterhub/jupyterhub/blob/master/README.md)
|
|
||||||
for help installing JupyterHub.
|
|
||||||
|
|
||||||
### Planning your installation
|
|
||||||
|
|
||||||
Prior to beginning installation, it's helpful to consider some of the following:
|
|
||||||
- deployment system (bare metal, Docker)
|
|
||||||
- Authentication (PAM, OAuth, etc.)
|
|
||||||
- Spawner of singleuser notebook servers (Docker, Batch, etc.)
|
|
||||||
- Services (nbgrader, etc.)
|
|
||||||
- JupyterHub database (default SQLite; traditional RDBMS such as PostgreSQL,)
|
|
||||||
MySQL, or other databases supported by [SQLAlchemy](http://www.sqlalchemy.org))
|
|
||||||
|
|
||||||
### Folders and File Locations
|
|
||||||
|
|
||||||
It is recommended to put all of the files used by JupyterHub into standard
|
|
||||||
UNIX filesystem locations.
|
|
||||||
|
|
||||||
* `/srv/jupyterhub` for all security and runtime files
|
|
||||||
* `/etc/jupyterhub` for all configuration files
|
|
||||||
* `/var/log` for log files
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
JupyterHub is configured in two ways:
|
|
||||||
|
|
||||||
1. Configuration file
|
|
||||||
2. Command-line arguments
|
|
||||||
|
|
||||||
### Configuration file
|
|
||||||
By default, JupyterHub will look for a configuration file (which may not be created yet)
|
|
||||||
named `jupyterhub_config.py` in the current working directory.
|
|
||||||
You can create an empty configuration file with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
jupyterhub --generate-config
|
|
||||||
```
|
|
||||||
|
|
||||||
This empty configuration file has descriptions of all configuration variables and their default
|
|
||||||
values. You can load a specific config file with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
jupyterhub -f /path/to/jupyterhub_config.py
|
|
||||||
```
|
|
||||||
|
|
||||||
See also: [general docs](http://ipython.org/ipython-doc/dev/development/config.html)
|
|
||||||
on the config system Jupyter uses.
|
|
||||||
|
|
||||||
### Command-line arguments
|
|
||||||
Type the following for brief information about the command-line arguments:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
jupyterhub -h
|
|
||||||
```
|
|
||||||
|
|
||||||
or:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
jupyterhub --help-all
|
|
||||||
```
|
|
||||||
|
|
||||||
for the full command line help.
|
|
||||||
|
|
||||||
All configurable options are technically configurable on the command-line,
|
|
||||||
even if some are really inconvenient to type. Just replace the desired option,
|
|
||||||
`c.Class.trait`, with `--Class.trait`. For example, to configure the
|
|
||||||
`c.Spawner.notebook_dir` trait from the command-line:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
jupyterhub --Spawner.notebook_dir='~/assignments'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Networking
|
|
||||||
|
|
||||||
### Configuring the Proxy's IP address and port
|
|
||||||
The Proxy's main IP address setting determines where JupyterHub is available to users.
|
|
||||||
By default, JupyterHub is configured to be available on all network interfaces
|
|
||||||
(`''`) on port 8000. **Note**: Use of `'*'` is discouraged for IP configuration;
|
|
||||||
instead, use of `'0.0.0.0'` is preferred.
|
|
||||||
|
|
||||||
Changing the IP address and port can be done with the following command line
|
|
||||||
arguments:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
jupyterhub --ip=192.168.1.2 --port=443
|
|
||||||
```
|
|
||||||
|
|
||||||
Or by placing the following lines in a configuration file:
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.JupyterHub.ip = '192.168.1.2'
|
|
||||||
c.JupyterHub.port = 443
|
|
||||||
```
|
|
||||||
|
|
||||||
Port 443 is used as an example since 443 is the default port for SSL/HTTPS.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
### Configuring the Proxy's REST API communication IP address and port (optional)
|
|
||||||
The Hub service talks to the proxy via a REST API on a secondary port,
|
|
||||||
whose network interface and port can be configured separately.
|
|
||||||
By default, this REST API listens on port 8081 of localhost only.
|
|
||||||
|
|
||||||
If running the Proxy separate from the Hub,
|
|
||||||
configure the REST API communication IP address and port with:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# ideally a private network address
|
|
||||||
c.JupyterHub.proxy_api_ip = '10.0.1.4'
|
|
||||||
c.JupyterHub.proxy_api_port = 5432
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuring the Hub if Spawners or Proxy are remote or isolated in containers
|
|
||||||
The Hub service also listens only on localhost (port 8080) by default.
|
|
||||||
The Hub needs needs to be accessible from both the proxy and all Spawners.
|
|
||||||
When spawning local servers, an IP address setting of localhost is fine.
|
|
||||||
If *either* the Proxy *or* (more likely) the Spawners will be remote or
|
|
||||||
isolated in containers, the Hub must listen on an IP that is accessible.
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.JupyterHub.hub_ip = '10.0.1.4'
|
|
||||||
c.JupyterHub.hub_port = 54321
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
**IMPORTANT: You should not run JupyterHub without SSL encryption on a public network.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Deprecation note:** Removed `--no-ssl` in version 0.7.
|
|
||||||
|
|
||||||
JupyterHub versions 0.5 and 0.6 require extra confirmation via `--no-ssl` to
|
|
||||||
allow running without SSL using the command `jupyterhub --no-ssl`. The
|
|
||||||
`--no-ssl` command line option is not needed anymore in version 0.7.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Security is the most important aspect of configuring Jupyter. There are four main aspects of the
|
|
||||||
security configuration:
|
|
||||||
|
|
||||||
1. SSL encryption (to enable HTTPS)
|
|
||||||
2. Cookie secret (a key for encrypting browser cookies)
|
|
||||||
3. Proxy authentication token (used for the Hub and other services to authenticate to the Proxy)
|
|
||||||
4. Periodic security audits
|
|
||||||
|
|
||||||
*Note* that the **Hub** hashes all secrets (e.g., auth tokens) before storing them in its
|
|
||||||
database. A loss of control over read-access to the database should have no security impact
|
|
||||||
on your deployment.
|
|
||||||
|
|
||||||
### SSL encryption
|
|
||||||
|
|
||||||
Since JupyterHub includes authentication and allows arbitrary code execution, you should not run
|
|
||||||
it without SSL (HTTPS). This will require you to obtain an official, trusted SSL certificate or
|
|
||||||
create a self-signed certificate. Once you have obtained and installed a key and certificate you
|
|
||||||
need to specify their locations in the configuration file as follows:
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.JupyterHub.ssl_key = '/path/to/my.key'
|
|
||||||
c.JupyterHub.ssl_cert = '/path/to/my.cert'
|
|
||||||
```
|
|
||||||
|
|
||||||
It is also possible to use letsencrypt (https://letsencrypt.org/) to obtain
|
|
||||||
a free, trusted SSL certificate. If you run letsencrypt using the default
|
|
||||||
options, the needed configuration is (replace `mydomain.tld` by your fully
|
|
||||||
qualified domain name):
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.JupyterHub.ssl_key = '/etc/letsencrypt/live/{mydomain.tld}/privkey.pem'
|
|
||||||
c.JupyterHub.ssl_cert = '/etc/letsencrypt/live/{mydomain.tld}/fullchain.pem'
|
|
||||||
```
|
|
||||||
|
|
||||||
If the fully qualified domain name (FQDN) is `example.com`, the following
|
|
||||||
would be the needed configuration:
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.JupyterHub.ssl_key = '/etc/letsencrypt/live/example.com/privkey.pem'
|
|
||||||
c.JupyterHub.ssl_cert = '/etc/letsencrypt/live/example.com/fullchain.pem'
|
|
||||||
```
|
|
||||||
|
|
||||||
Some cert files also contain the key, in which case only the cert is needed. It is important that
|
|
||||||
these files be put in a secure location on your server, where they are not readable by regular
|
|
||||||
users.
|
|
||||||
|
|
||||||
Note on **chain certificates**: If you are using a chain certificate, see also
|
|
||||||
[chained certificate for SSL](troubleshooting.md#chained-certificates-for-ssl) in the JupyterHub troubleshooting FAQ).
|
|
||||||
|
|
||||||
Note: In certain cases, e.g. **behind SSL termination in nginx**, allowing no SSL
|
|
||||||
running on the hub may be desired.
|
|
||||||
|
|
||||||
### Cookie secret
|
|
||||||
|
|
||||||
The cookie secret is an encryption key, used to encrypt the browser cookies used for
|
|
||||||
authentication. If this value changes for the Hub, all single-user servers must also be restarted.
|
|
||||||
Normally, this value is stored in a file, the location of which can be specified in a config file
|
|
||||||
as follows:
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/cookie_secret'
|
|
||||||
```
|
|
||||||
|
|
||||||
The content of this file should be a long random string encoded in MIME Base64. An example would be to generate this file as:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl rand -base64 2048 > /srv/jupyterhub/cookie_secret
|
|
||||||
```
|
|
||||||
|
|
||||||
In most deployments of JupyterHub, you should point this to a secure location on the file
|
|
||||||
system, such as `/srv/jupyterhub/cookie_secret`. If the cookie secret file doesn't exist when
|
|
||||||
the Hub starts, a new cookie secret is generated and stored in the file. The
|
|
||||||
file must not be readable by group or other or the server won't start.
|
|
||||||
The recommended permissions for the cookie secret file are 600 (owner-only rw).
|
|
||||||
|
|
||||||
|
|
||||||
If you would like to avoid the need for files, the value can be loaded in the Hub process from
|
|
||||||
the `JPY_COOKIE_SECRET` environment variable, which is a hex-encoded string. You
|
|
||||||
can set it this way:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export JPY_COOKIE_SECRET=`openssl rand -hex 1024`
|
|
||||||
```
|
|
||||||
|
|
||||||
For security reasons, this environment variable should only be visible to the Hub.
|
|
||||||
If you set it dynamically as above, all users will be logged out each time the
|
|
||||||
Hub starts.
|
|
||||||
|
|
||||||
You can also set the cookie secret in the configuration file itself,`jupyterhub_config.py`,
|
|
||||||
as a binary string:
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.JupyterHub.cookie_secret = bytes.fromhex('VERY LONG SECRET HEX STRING')
|
|
||||||
```
|
|
||||||
|
|
||||||
### Proxy authentication token
|
|
||||||
|
|
||||||
The Hub authenticates its requests to the Proxy using a secret token that
|
|
||||||
the Hub and Proxy agree upon. The value of this string should be a random
|
|
||||||
string (for example, generated by `openssl rand -hex 32`). You can pass
|
|
||||||
this value to the Hub and Proxy using either the `CONFIGPROXY_AUTH_TOKEN`
|
|
||||||
environment variable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export CONFIGPROXY_AUTH_TOKEN=`openssl rand -hex 32`
|
|
||||||
```
|
|
||||||
|
|
||||||
This environment variable needs to be visible to the Hub and Proxy.
|
|
||||||
|
|
||||||
Or you can set the value in the configuration file, `jupyterhub_config.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.JupyterHub.proxy_auth_token = '0bc02bede919e99a26de1e2a7a5aadfaf6228de836ec39a05a6c6942831d8fe5'
|
|
||||||
```
|
|
||||||
|
|
||||||
If you don't set the Proxy authentication token, the Hub will generate a random key itself, which
|
|
||||||
means that any time you restart the Hub you **must also restart the Proxy**. If the proxy is a
|
|
||||||
subprocess of the Hub, this should happen automatically (this is the default configuration).
|
|
||||||
|
|
||||||
Another time you must set the Proxy authentication token yourself is if
|
|
||||||
you want other services, such as [nbgrader](https://github.com/jupyter/nbgrader)
|
|
||||||
to also be able to connect to the Proxy.
|
|
||||||
|
|
||||||
### Security audits
|
|
||||||
|
|
||||||
We recommend that you do periodic reviews of your deployment's security. It's
|
|
||||||
good practice to keep JupyterHub, configurable-http-proxy, and nodejs
|
|
||||||
versions up to date.
|
|
||||||
|
|
||||||
A handy website for testing your deployment is
|
|
||||||
[Qualsys' SSL analyzer tool](https://www.ssllabs.com/ssltest/analyze.html).
|
|
||||||
|
|
||||||
## Authentication and users
|
|
||||||
|
|
||||||
The default Authenticator uses [PAM][] to authenticate system users with
|
|
||||||
their username and password. The default behavior of this Authenticator
|
|
||||||
is to allow any user with an account and password on the system to login.
|
|
||||||
|
|
||||||
### Creating a whitelist of users
|
|
||||||
|
|
||||||
You can restrict which users are allowed to login with `Authenticator.whitelist`:
|
|
||||||
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.Authenticator.whitelist = {'mal', 'zoe', 'inara', 'kaylee'}
|
|
||||||
```
|
|
||||||
|
|
||||||
Users listed in the whitelist are added to the Hub database when the Hub is
|
|
||||||
started.
|
|
||||||
|
|
||||||
### Managing Hub administrators
|
|
||||||
|
|
||||||
Admin users of JupyterHub have the ability to take actions on users' behalf,
|
|
||||||
such as stopping and restarting their servers,
|
|
||||||
and adding and removing new users from the whitelist.
|
|
||||||
Any users in the admin list are automatically added to the whitelist,
|
|
||||||
if they are not already present.
|
|
||||||
The set of initial Admin users can configured as follows:
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.Authenticator.admin_users = {'mal', 'zoe'}
|
|
||||||
```
|
|
||||||
|
|
||||||
If `JupyterHub.admin_access` is True (not default),
|
|
||||||
then admin users have permission to log in *as other users* on their respective machines, for debugging.
|
|
||||||
**You should make sure your users know if admin_access is enabled.**
|
|
||||||
|
|
||||||
Note: additional configuration examples are provided in this guide's
|
|
||||||
[Configuration Examples section](./config-examples.html).
|
|
||||||
|
|
||||||
### Add or remove users from the Hub
|
|
||||||
|
|
||||||
Users can be added to and removed from the Hub via either the admin panel or
|
|
||||||
REST API.
|
|
||||||
|
|
||||||
If a user is **added**, the user will be automatically added to the whitelist
|
|
||||||
and database. Restarting the Hub will not require manually updating the
|
|
||||||
whitelist in your config file, as the users will be loaded from the database.
|
|
||||||
|
|
||||||
After starting the Hub once, it is not sufficient to **remove** a user from
|
|
||||||
the whitelist in your config file. You must also remove the user from the Hub's
|
|
||||||
database, either by deleting the user from the admin page, or you can clear
|
|
||||||
the `jupyterhub.sqlite` database and start fresh.
|
|
||||||
|
|
||||||
The default `PAMAuthenticator` is one case of a special kind of authenticator, called a
|
|
||||||
`LocalAuthenticator`, indicating that it manages users on the local system. When you add a user to
|
|
||||||
the Hub, a `LocalAuthenticator` checks if that user already exists. Normally, there will be an
|
|
||||||
error telling you that the user doesn't exist. If you set the configuration value
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.LocalAuthenticator.create_system_users = True
|
|
||||||
```
|
|
||||||
|
|
||||||
however, adding a user to the Hub that doesn't already exist on the system will result in the Hub
|
|
||||||
creating that user via the system `adduser` command line tool. This option is typically used on
|
|
||||||
hosted deployments of JupyterHub, to avoid the need to manually create all your users before
|
|
||||||
launching the service. It is not recommended when running JupyterHub in situations where
|
|
||||||
JupyterHub users maps directly onto UNIX users.
|
|
||||||
|
|
||||||
## Spawners and single-user notebook servers
|
|
||||||
|
|
||||||
Since the single-user server is an instance of `jupyter notebook`, an entire separate
|
|
||||||
multi-process application, there are many aspect of that server can configure, and a lot of ways
|
|
||||||
to express that configuration.
|
|
||||||
|
|
||||||
At the JupyterHub level, you can set some values on the Spawner. The simplest of these is
|
|
||||||
`Spawner.notebook_dir`, which lets you set the root directory for a user's server. This root
|
|
||||||
notebook directory is the highest level directory users will be able to access in the notebook
|
|
||||||
dashboard. In this example, the root notebook directory is set to `~/notebooks`, where `~` is
|
|
||||||
expanded to the user's home directory.
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.Spawner.notebook_dir = '~/notebooks'
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also specify extra command-line arguments to the notebook server with:
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.Spawner.args = ['--debug', '--profile=PHYS131']
|
|
||||||
```
|
|
||||||
|
|
||||||
This could be used to set the users default page for the single user server:
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.Spawner.args = ['--NotebookApp.default_url=/notebooks/Welcome.ipynb']
|
|
||||||
```
|
|
||||||
|
|
||||||
Since the single-user server extends the notebook server application,
|
|
||||||
it still loads configuration from the `ipython_notebook_config.py` config file.
|
|
||||||
Each user may have one of these files in `$HOME/.ipython/profile_default/`.
|
|
||||||
IPython also supports loading system-wide config files from `/etc/ipython/`,
|
|
||||||
which is the place to put configuration that you want to affect all of your users.
|
|
||||||
|
|
||||||
## External services
|
|
||||||
|
|
||||||
JupyterHub has a REST API that can be used by external services like the
|
|
||||||
[cull_idle_servers](https://github.com/jupyterhub/jupyterhub/blob/master/examples/cull-idle/cull_idle_servers.py)
|
|
||||||
script which monitors and kills idle single-user servers periodically. In order to run such an
|
|
||||||
external service, you need to provide it an API token. In the case of `cull_idle_servers`, it is passed
|
|
||||||
as the environment variable called `JPY_API_TOKEN`.
|
|
||||||
|
|
||||||
Currently there are two ways of registering that token with JupyterHub. The first one is to use
|
|
||||||
the `jupyterhub` command to generate a token for a specific hub user:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
jupyterhub token <username>
|
|
||||||
```
|
|
||||||
|
|
||||||
As of [version 0.6.0](./changelog.html), the preferred way of doing this is to first generate an API token:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl rand -hex 32
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
and then write it to your JupyterHub configuration file (note that the **key** is the token while the **value** is the username):
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.JupyterHub.api_tokens = {'token' : 'username'}
|
|
||||||
```
|
|
||||||
|
|
||||||
Upon restarting JupyterHub, you should see a message like below in the logs:
|
|
||||||
|
|
||||||
```
|
|
||||||
Adding API token for <username>
|
|
||||||
```
|
|
||||||
|
|
||||||
Now you can run your script, i.e. `cull_idle_servers`, by providing it the API token and it will authenticate through
|
|
||||||
the REST API to interact with it.
|
|
||||||
|
|
||||||
|
|
||||||
[oauth-setup]: https://github.com/jupyterhub/oauthenticator#setup
|
|
||||||
[oauthenticator]: https://github.com/jupyterhub/oauthenticator
|
|
||||||
[PAM]: https://en.wikipedia.org/wiki/Pluggable_authentication_module
|
|
99
docs/source/getting-started/authenticators-users-basics.md
Normal file
99
docs/source/getting-started/authenticators-users-basics.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Authentication and User Basics
|
||||||
|
|
||||||
|
The default Authenticator uses [PAM][] to authenticate system users with
|
||||||
|
their username and password. With the default Authenticator, any user
|
||||||
|
with an account and password on the system will be allowed to login.
|
||||||
|
|
||||||
|
## Create a whitelist of users
|
||||||
|
|
||||||
|
You can restrict which users are allowed to login with a whitelist,
|
||||||
|
`Authenticator.whitelist`:
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.Authenticator.whitelist = {'mal', 'zoe', 'inara', 'kaylee'}
|
||||||
|
```
|
||||||
|
|
||||||
|
Users in the whitelist are added to the Hub database when the Hub is
|
||||||
|
started.
|
||||||
|
|
||||||
|
## Configure admins (`admin_users`)
|
||||||
|
|
||||||
|
Admin users of JupyterHub, `admin_users`, can add and remove users from
|
||||||
|
the user `whitelist`. `admin_users` can take actions on other users'
|
||||||
|
behalf, such as stopping and restarting their servers.
|
||||||
|
|
||||||
|
A set of initial admin users, `admin_users` can configured be as follows:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.Authenticator.admin_users = {'mal', 'zoe'}
|
||||||
|
```
|
||||||
|
Users in the admin list are automatically added to the user `whitelist`,
|
||||||
|
if they are not already present.
|
||||||
|
|
||||||
|
## Give admin access to other users' notebook servers (`admin_access`)
|
||||||
|
|
||||||
|
Since the default `JupyterHub.admin_access` setting is False, the admins
|
||||||
|
do not have permission to log in to the single user notebook servers
|
||||||
|
owned by *other users*. If `JupyterHub.admin_access` is set to True,
|
||||||
|
then admins have permission to log in *as other users* on their
|
||||||
|
respective machines, for debugging. **As a courtesy, you should make
|
||||||
|
sure your users know if admin_access is enabled.**
|
||||||
|
|
||||||
|
## Add or remove users from the Hub
|
||||||
|
|
||||||
|
Users can be added to and removed from the Hub via either the admin
|
||||||
|
panel or the REST API. When a user is **added**, the user will be
|
||||||
|
automatically added to the whitelist and database. Restarting the Hub
|
||||||
|
will not require manually updating the whitelist in your config file,
|
||||||
|
as the users will be loaded from the database.
|
||||||
|
|
||||||
|
After starting the Hub once, it is not sufficient to **remove** a user
|
||||||
|
from the whitelist in your config file. You must also remove the user
|
||||||
|
from the Hub's database, either by deleting the user from JupyterHub's
|
||||||
|
admin page, or you can clear the `jupyterhub.sqlite` database and start
|
||||||
|
fresh.
|
||||||
|
|
||||||
|
## Use LocalAuthenticator to create system users
|
||||||
|
|
||||||
|
The `LocalAuthenticator` is a special kind of authenticator that has
|
||||||
|
the ability to manage users on the local system. When you try to add a
|
||||||
|
new user to the Hub, a `LocalAuthenticator` will check if the user
|
||||||
|
already exists. If you set the configuration value, `create_system_users`,
|
||||||
|
to `True` in the configuration file, the `LocalAuthenticator` has
|
||||||
|
the privileges to add users to the system. The setting in the config
|
||||||
|
file is:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.LocalAuthenticator.create_system_users = True
|
||||||
|
```
|
||||||
|
|
||||||
|
Adding a user to the Hub that doesn't already exist on the system will
|
||||||
|
result in the Hub creating that user via the system `adduser` command
|
||||||
|
line tool. This option is typically used on hosted deployments of
|
||||||
|
JupyterHub, to avoid the need to manually create all your users before
|
||||||
|
launching the service. This approach is not recommended when running
|
||||||
|
JupyterHub in situations where JupyterHub users map directly onto the
|
||||||
|
system's UNIX users.
|
||||||
|
|
||||||
|
## Use OAuthenticator to support OAuth with popular service providers
|
||||||
|
|
||||||
|
JupyterHub's [OAuthenticator][] currently supports the following
|
||||||
|
popular services:
|
||||||
|
|
||||||
|
- Auth0
|
||||||
|
- Bitbucket
|
||||||
|
- CILogon
|
||||||
|
- GitHub
|
||||||
|
- GitLab
|
||||||
|
- Globus
|
||||||
|
- Google
|
||||||
|
- MediaWiki
|
||||||
|
- Okpy
|
||||||
|
- OpenShift
|
||||||
|
|
||||||
|
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
|
87
docs/source/getting-started/config-basics.md
Normal file
87
docs/source/getting-started/config-basics.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Configuration Basics
|
||||||
|
|
||||||
|
The section contains basic information about configuring settings for a JupyterHub
|
||||||
|
deployment. The [Technical Reference](../reference/index.html)
|
||||||
|
documentation provides additional details.
|
||||||
|
|
||||||
|
This section will help you learn how to:
|
||||||
|
|
||||||
|
- generate a default configuration file, `jupyterhub_config.py`
|
||||||
|
- start with a specific configuration file
|
||||||
|
- configure JupyterHub using command line options
|
||||||
|
- find information and examples for some common deployments
|
||||||
|
|
||||||
|
## Generate a default config file
|
||||||
|
|
||||||
|
On startup, JupyterHub will look by default for a configuration file,
|
||||||
|
`jupyterhub_config.py`, in the current working directory.
|
||||||
|
|
||||||
|
To generate a default config file, `jupyterhub_config.py`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jupyterhub --generate-config
|
||||||
|
```
|
||||||
|
|
||||||
|
This default `jupyterhub_config.py` file contains comments and guidance for all
|
||||||
|
configuration variables and their default values. We recommend storing
|
||||||
|
configuration files in the standard UNIX filesystem location, i.e.
|
||||||
|
`/etc/jupyterhub`.
|
||||||
|
|
||||||
|
## Start with a specific config file
|
||||||
|
|
||||||
|
You can load a specific config file and start JupyterHub using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jupyterhub -f /path/to/jupyterhub_config.py
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have stored your configuration file in the recommended UNIX filesystem
|
||||||
|
location, `/etc/jupyterhub`, the following command will start JupyterHub using
|
||||||
|
the configuration file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jupyterhub -f /etc/jupyterhub/jupyterhub_config.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The IPython documentation provides additional information on the
|
||||||
|
[config system](http://ipython.readthedocs.io/en/stable/development/config.html)
|
||||||
|
that Jupyter uses.
|
||||||
|
|
||||||
|
## Configure using command line options
|
||||||
|
|
||||||
|
To display all command line options that are available for configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jupyterhub --help-all
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration using the command line options is done when launching JupyterHub.
|
||||||
|
For example, to start JupyterHub on ``10.0.1.2:443`` with https, you
|
||||||
|
would enter:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jupyterhub --ip 10.0.1.2 --port 443 --ssl-key my_ssl.key --ssl-cert my_ssl.cert
|
||||||
|
```
|
||||||
|
|
||||||
|
All configurable options may technically be set on the command-line,
|
||||||
|
though some are inconvenient to type. To set a particular configuration
|
||||||
|
parameter, `c.Class.trait`, you would use the command line option,
|
||||||
|
`--Class.trait`, when starting JupyterHub. For example, to configure the
|
||||||
|
`c.Spawner.notebook_dir` trait from the command-line, use the
|
||||||
|
`--Spawner.notebook_dir` option:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jupyterhub --Spawner.notebook_dir='~/assignments'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configure for various deployment environments
|
||||||
|
|
||||||
|
The default authentication and process spawning mechanisms can be replaced, and
|
||||||
|
specific [authenticators](./authenticators-users-basics.html) and
|
||||||
|
[spawners](./spawners-basics.html) can be set in the configuration file.
|
||||||
|
This enables JupyterHub to be used with a variety of authentication methods or
|
||||||
|
process control and deployment environments. [Some examples](../reference/config-examples.html),
|
||||||
|
meant as illustration, are:
|
||||||
|
|
||||||
|
- Using GitHub OAuth instead of PAM with [OAuthenticator](https://github.com/jupyterhub/oauthenticator)
|
||||||
|
- Spawning single-user servers with Docker, using the [DockerSpawner](https://github.com/jupyterhub/dockerspawner)
|
12
docs/source/getting-started/index.rst
Normal file
12
docs/source/getting-started/index.rst
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Getting Started
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
config-basics
|
||||||
|
networking-basics
|
||||||
|
security-basics
|
||||||
|
authenticators-users-basics
|
||||||
|
spawners-basics
|
||||||
|
services-basics
|
101
docs/source/getting-started/networking-basics.md
Normal file
101
docs/source/getting-started/networking-basics.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Networking basics
|
||||||
|
|
||||||
|
This section will help you with basic proxy and network configuration to:
|
||||||
|
|
||||||
|
- set the proxy's IP address and port
|
||||||
|
- set the proxy's REST API URL
|
||||||
|
- configure the Hub if the Proxy or Spawners are remote or isolated
|
||||||
|
- set the `hub_connect_ip` which services will use to communicate with the hub
|
||||||
|
|
||||||
|
## Set the Proxy's IP address and port
|
||||||
|
|
||||||
|
The Proxy's main IP address setting determines where JupyterHub is available to users.
|
||||||
|
By default, JupyterHub is configured to be available on all network interfaces
|
||||||
|
(`''`) on port 8000. *Note*: Use of `'*'` is discouraged for IP configuration;
|
||||||
|
instead, use of `'0.0.0.0'` is preferred.
|
||||||
|
|
||||||
|
Changing the Proxy's main IP address and port can be done with the following
|
||||||
|
JupyterHub **command line options**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jupyterhub --ip=192.168.1.2 --port=443
|
||||||
|
```
|
||||||
|
|
||||||
|
Or by placing the following lines in a **configuration file**,
|
||||||
|
`jupyterhub_config.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.ip = '192.168.1.2'
|
||||||
|
c.JupyterHub.port = 443
|
||||||
|
```
|
||||||
|
|
||||||
|
Port 443 is used in the examples since 443 is the default port for SSL/HTTPS.
|
||||||
|
|
||||||
|
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.
|
||||||
|
The Hub service talks to the proxy via a REST API on a secondary port. The
|
||||||
|
API URL can be configured separately and override the default settings.
|
||||||
|
|
||||||
|
### Set api_url
|
||||||
|
|
||||||
|
The URL to access the API, `c.configurableHTTPProxy.api_url`, is configurable.
|
||||||
|
An example entry to set the proxy's API URL in `jupyterhub_config.py` is:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.ConfigurableHTTPProxy.api_url = 'http://10.0.1.4:5432'
|
||||||
|
```
|
||||||
|
|
||||||
|
### proxy_api_ip and proxy_api_port (Deprecated in 0.8)
|
||||||
|
|
||||||
|
If running the Proxy separate from the Hub, configure the REST API communication
|
||||||
|
IP address and port by adding this to the `jupyterhub_config.py` file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ideally a private network address
|
||||||
|
c.JupyterHub.proxy_api_ip = '10.0.1.4'
|
||||||
|
c.JupyterHub.proxy_api_port = 5432
|
||||||
|
```
|
||||||
|
|
||||||
|
We recommend using the proxy's `api_url` setting instead of the deprecated
|
||||||
|
settings, `proxy_api_ip` and `proxy_api_port`.
|
||||||
|
|
||||||
|
## Configure the Hub if the Proxy or Spawners are remote or isolated
|
||||||
|
|
||||||
|
The Hub service listens only on `localhost` (port 8081) by default.
|
||||||
|
The Hub needs to be accessible from both the proxy and all Spawners.
|
||||||
|
When spawning local servers, an IP address setting of `localhost` is fine.
|
||||||
|
|
||||||
|
If *either* the Proxy *or* (more likely) the Spawners will be remote or
|
||||||
|
isolated in containers, the Hub must listen on an IP that is accessible.
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.hub_ip = '10.0.1.4'
|
||||||
|
c.JupyterHub.hub_port = 54321
|
||||||
|
```
|
||||||
|
|
||||||
|
**Added in 0.8:** The `c.JupyterHub.hub_connect_ip` setting is the ip address or
|
||||||
|
hostname that other services should use to connect to the Hub. A common
|
||||||
|
configuration for, e.g. docker, is:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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.
|
186
docs/source/getting-started/security-basics.rst
Normal file
186
docs/source/getting-started/security-basics.rst
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
Security settings
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
You should not run JupyterHub without SSL encryption on a public network.
|
||||||
|
|
||||||
|
Security is the most important aspect of configuring Jupyter. Three
|
||||||
|
configuration settings are the main aspects of security configuration:
|
||||||
|
|
||||||
|
1. :ref:`SSL encryption <ssl-encryption>` (to enable HTTPS)
|
||||||
|
2. :ref:`Cookie secret <cookie-secret>` (a key for encrypting browser cookies)
|
||||||
|
3. Proxy :ref:`authentication token <authentication-token>` (used for the Hub and
|
||||||
|
other services to authenticate to the Proxy)
|
||||||
|
|
||||||
|
The Hub hashes all secrets (e.g., auth tokens) before storing them in its
|
||||||
|
database. A loss of control over read-access to the database should have
|
||||||
|
minimal impact on your deployment; if your database has been compromised, it
|
||||||
|
is still a good idea to revoke existing tokens.
|
||||||
|
|
||||||
|
.. _ssl-encryption:
|
||||||
|
|
||||||
|
Enabling SSL encryption
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Since JupyterHub includes authentication and allows arbitrary code execution,
|
||||||
|
you should not run it without SSL (HTTPS).
|
||||||
|
|
||||||
|
Using an SSL certificate
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This will require you to obtain an official, trusted SSL certificate or create a
|
||||||
|
self-signed certificate. Once you have obtained and installed a key and
|
||||||
|
certificate you need to specify their locations in the ``jupyterhub_config.py``
|
||||||
|
configuration file as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
c.JupyterHub.ssl_key = '/path/to/my.key'
|
||||||
|
c.JupyterHub.ssl_cert = '/path/to/my.cert'
|
||||||
|
|
||||||
|
|
||||||
|
Some cert files also contain the key, in which case only the cert is needed. It
|
||||||
|
is important that these files be put in a secure location on your server, where
|
||||||
|
they are not readable by regular users.
|
||||||
|
|
||||||
|
If you are using a **chain certificate**, see also chained certificate for SSL
|
||||||
|
in the JupyterHub `troubleshooting FAQ <troubleshooting>`_.
|
||||||
|
|
||||||
|
Using letsencrypt
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
It is also possible to use `letsencrypt <https://letsencrypt.org/>`_ to obtain
|
||||||
|
a free, trusted SSL certificate. If you run letsencrypt using the default
|
||||||
|
options, the needed configuration is (replace ``mydomain.tld`` by your fully
|
||||||
|
qualified domain name):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
c.JupyterHub.ssl_key = '/etc/letsencrypt/live/{mydomain.tld}/privkey.pem'
|
||||||
|
c.JupyterHub.ssl_cert = '/etc/letsencrypt/live/{mydomain.tld}/fullchain.pem'
|
||||||
|
|
||||||
|
If the fully qualified domain name (FQDN) is ``example.com``, the following
|
||||||
|
would be the needed configuration:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
c.JupyterHub.ssl_key = '/etc/letsencrypt/live/example.com/privkey.pem'
|
||||||
|
c.JupyterHub.ssl_cert = '/etc/letsencrypt/live/example.com/fullchain.pem'
|
||||||
|
|
||||||
|
|
||||||
|
If SSL termination happens outside of the Hub
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
Cookie secret
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The cookie secret is an encryption key, used to encrypt the browser cookies
|
||||||
|
which are used for authentication. Three common methods are described for
|
||||||
|
generating and configuring the cookie secret.
|
||||||
|
|
||||||
|
Generating and storing as a cookie secret file
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The cookie secret should be 32 random bytes, encoded as hex, and is typically
|
||||||
|
stored in a ``jupyterhub_cookie_secret`` file. An example command to generate the
|
||||||
|
``jupyterhub_cookie_secret`` file is:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
openssl rand -hex 32 > /srv/jupyterhub/jupyterhub_cookie_secret
|
||||||
|
|
||||||
|
In most deployments of JupyterHub, you should point this to a secure location on
|
||||||
|
the file system, such as ``/srv/jupyterhub/jupyterhub_cookie_secret``.
|
||||||
|
|
||||||
|
The location of the ``jupyterhub_cookie_secret`` file can be specified in the
|
||||||
|
``jupyterhub_config.py`` file as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/jupyterhub_cookie_secret'
|
||||||
|
|
||||||
|
If the cookie secret file doesn't exist when the Hub starts, a new cookie
|
||||||
|
secret is generated and stored in the file. The file must not be readable by
|
||||||
|
``group`` or ``other`` or the server won't start. The recommended permissions
|
||||||
|
for the cookie secret file are ``600`` (owner-only rw).
|
||||||
|
|
||||||
|
Generating and storing as an environment variable
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If you would like to avoid the need for files, the value can be loaded in the
|
||||||
|
Hub process from the ``JPY_COOKIE_SECRET`` environment variable, which is a
|
||||||
|
hex-encoded string. You can set it this way:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
export JPY_COOKIE_SECRET=`openssl rand -hex 32`
|
||||||
|
|
||||||
|
For security reasons, this environment variable should only be visible to the
|
||||||
|
Hub. If you set it dynamically as above, all users will be logged out each time
|
||||||
|
the Hub starts.
|
||||||
|
|
||||||
|
Generating and storing as a binary string
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can also set the cookie secret in the configuration file
|
||||||
|
itself, ``jupyterhub_config.py``, as a binary string:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
c.JupyterHub.cookie_secret = bytes.fromhex('64 CHAR HEX STRING')
|
||||||
|
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
If the cookie secret value changes for the Hub, all single-user notebook
|
||||||
|
servers must also be restarted.
|
||||||
|
|
||||||
|
|
||||||
|
.. _authentication-token:
|
||||||
|
|
||||||
|
Proxy authentication token
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
The Hub authenticates its requests to the Proxy using a secret token that
|
||||||
|
the Hub and Proxy agree upon. The value of this string should be a random
|
||||||
|
string (for example, generated by ``openssl rand -hex 32``).
|
||||||
|
|
||||||
|
Generating and storing token in the configuration file
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Or you can set the value in the configuration file, ``jupyterhub_config.py``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
c.JupyterHub.proxy_auth_token = '0bc02bede919e99a26de1e2a7a5aadfaf6228de836ec39a05a6c6942831d8fe5'
|
||||||
|
|
||||||
|
Generating and storing as an environment variable
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can pass this value of the proxy authentication token to the Hub and Proxy
|
||||||
|
using the ``CONFIGPROXY_AUTH_TOKEN`` environment variable:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
export CONFIGPROXY_AUTH_TOKEN='openssl rand -hex 32'
|
||||||
|
|
||||||
|
This environment variable needs to be visible to the Hub and Proxy.
|
||||||
|
|
||||||
|
Default if token is not set
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If you don't set the Proxy authentication token, the Hub will generate a random
|
||||||
|
key itself, which means that any time you restart the Hub you **must also
|
||||||
|
restart the Proxy**. If the proxy is a subprocess of the Hub, this should happen
|
||||||
|
automatically (this is the default configuration).
|
121
docs/source/getting-started/services-basics.md
Normal file
121
docs/source/getting-started/services-basics.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# External services
|
||||||
|
|
||||||
|
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 action or task. For example, shutting down individuals' single user
|
||||||
|
notebook servers that have been is a good example of a task that could
|
||||||
|
be automated by a Service. Let's look at how the [cull_idle_servers][]
|
||||||
|
script can be used as a Service.
|
||||||
|
|
||||||
|
## Real-world example to cull idle servers
|
||||||
|
|
||||||
|
JupyterHub has a REST API that can be used by external services. This
|
||||||
|
document will:
|
||||||
|
|
||||||
|
- explain some basic information about API tokens
|
||||||
|
- clarify that API tokens can be used to authenticate to
|
||||||
|
single-user servers as of [version 0.8.0](../changelog.html)
|
||||||
|
- show how the [cull_idle_servers][] script can be:
|
||||||
|
- used in a Hub-managed service
|
||||||
|
- run as a standalone script
|
||||||
|
|
||||||
|
Both examples for `cull_idle_servers` will communicate tasks to the
|
||||||
|
Hub via the REST API.
|
||||||
|
|
||||||
|
## API Token basics
|
||||||
|
|
||||||
|
### Create an API token
|
||||||
|
|
||||||
|
To run such an external service, an API token must be created and
|
||||||
|
provided to the service.
|
||||||
|
|
||||||
|
As of [version 0.6.0](../changelog.html), the preferred way of doing
|
||||||
|
this is to first generate an API token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
In [version 0.8.0](../changelog.html), a TOKEN request page for
|
||||||
|
generating an API token is available from the JupyterHub user interface:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Pass environment variable with token to the Hub
|
||||||
|
|
||||||
|
In the case of `cull_idle_servers`, it is passed as the environment
|
||||||
|
variable called `JUPYTERHUB_API_TOKEN`.
|
||||||
|
|
||||||
|
### Use API tokens for services and tasks that require external access
|
||||||
|
|
||||||
|
While API tokens are often associated with a specific user, API tokens
|
||||||
|
can be used by services that require external access for activities
|
||||||
|
that may not correspond to a specific human, e.g. adding users during
|
||||||
|
setup for a tutorial or workshop. Add a service and its API token to the
|
||||||
|
JupyterHub configuration file, `jupyterhub_config.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.services = [
|
||||||
|
{'name': 'adding-users', 'api_token': 'super-secret-token'},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart JupyterHub
|
||||||
|
|
||||||
|
Upon restarting JupyterHub, you should see a message like below in the
|
||||||
|
logs:
|
||||||
|
|
||||||
|
```
|
||||||
|
Adding API token for <username>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authenticating to single-user servers using API token
|
||||||
|
|
||||||
|
In JupyterHub 0.7, there is no mechanism for token authentication to
|
||||||
|
single-user servers, and only cookies can be used for authentication.
|
||||||
|
0.8 supports using JupyterHub API tokens to authenticate to single-user
|
||||||
|
servers.
|
||||||
|
|
||||||
|
## Configure `cull-idle` to run as a Hub-Managed Service
|
||||||
|
|
||||||
|
In `jupyterhub_config.py`, add the following dictionary for the
|
||||||
|
`cull-idle` Service to the `c.JupyterHub.services` list:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.services = [
|
||||||
|
{
|
||||||
|
'name': 'cull-idle',
|
||||||
|
'admin': True,
|
||||||
|
'command': 'python3 cull_idle_servers.py --timeout=3600'.split(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
where:
|
||||||
|
|
||||||
|
- `'admin': True` indicates that the Service has 'admin' permissions, and
|
||||||
|
- `'command'` indicates that the Service will be launched as a
|
||||||
|
subprocess, managed by the Hub.
|
||||||
|
|
||||||
|
## Run `cull-idle` manually as a standalone script
|
||||||
|
|
||||||
|
Now you can run your script, i.e. `cull_idle_servers`, by providing it
|
||||||
|
the API token and it will authenticate through the REST API to
|
||||||
|
interact with it.
|
||||||
|
|
||||||
|
This will run `cull-idle` manually. `cull-idle` can be run as a standalone
|
||||||
|
script anywhere with access to the Hub, and will periodically check for idle
|
||||||
|
servers and shut them down via the Hub's REST API. In order to shutdown the
|
||||||
|
servers, the token given to cull-idle must have admin privileges.
|
||||||
|
|
||||||
|
Generate an API token and store it in the `JUPYTERHUB_API_TOKEN` environment
|
||||||
|
variable. Run `cull_idle_servers.py` manually.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export JUPYTERHUB_API_TOKEN='token'
|
||||||
|
python3 cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api]
|
||||||
|
```
|
||||||
|
|
||||||
|
[cull_idle_servers]: https://github.com/jupyterhub/jupyterhub/blob/master/examples/cull-idle/cull_idle_servers.py
|
33
docs/source/getting-started/spawners-basics.md
Normal file
33
docs/source/getting-started/spawners-basics.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Spawners and single-user notebook servers
|
||||||
|
|
||||||
|
Since the single-user server is an instance of `jupyter notebook`, an entire separate
|
||||||
|
multi-process application, there are many aspect of that server can configure, and a lot of ways
|
||||||
|
to express that configuration.
|
||||||
|
|
||||||
|
At the JupyterHub level, you can set some values on the Spawner. The simplest of these is
|
||||||
|
`Spawner.notebook_dir`, which lets you set the root directory for a user's server. This root
|
||||||
|
notebook directory is the highest level directory users will be able to access in the notebook
|
||||||
|
dashboard. In this example, the root notebook directory is set to `~/notebooks`, where `~` is
|
||||||
|
expanded to the user's home directory.
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.Spawner.notebook_dir = '~/notebooks'
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also specify extra command-line arguments to the notebook server with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.Spawner.args = ['--debug', '--profile=PHYS131']
|
||||||
|
```
|
||||||
|
|
||||||
|
This could be used to set the users default page for the single user server:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.Spawner.args = ['--NotebookApp.default_url=/notebooks/Welcome.ipynb']
|
||||||
|
```
|
||||||
|
|
||||||
|
Since the single-user server extends the notebook server application,
|
||||||
|
it still loads configuration from the `jupyter_notebook_config.py` config file.
|
||||||
|
Each user may have one of these files in `$HOME/.jupyter/`.
|
||||||
|
Jupyter also supports loading system-wide config files from `/etc/jupyter/`,
|
||||||
|
which is the place to put configuration that you want to affect all of your users.
|
@@ -1,77 +0,0 @@
|
|||||||
# How JupyterHub works
|
|
||||||
|
|
||||||
JupyterHub is a multi-user server that manages and proxies multiple instances of the single-user Jupyter notebook server.
|
|
||||||
|
|
||||||
There are three basic processes involved:
|
|
||||||
|
|
||||||
- multi-user Hub (Python/Tornado)
|
|
||||||
- [configurable http proxy](https://github.com/jupyterhub/configurable-http-proxy) (node-http-proxy)
|
|
||||||
- multiple single-user IPython notebook servers (Python/IPython/Tornado)
|
|
||||||
|
|
||||||
The proxy is the only process that listens on a public interface.
|
|
||||||
The Hub sits behind the proxy at `/hub`.
|
|
||||||
Single-user servers sit behind the proxy at `/user/[username]`.
|
|
||||||
|
|
||||||
|
|
||||||
## Logging in
|
|
||||||
|
|
||||||
When a new browser logs in to JupyterHub, the following events take place:
|
|
||||||
|
|
||||||
- Login data is handed to the [Authenticator](#authentication) instance for validation
|
|
||||||
- The Authenticator returns the username, if login information is valid
|
|
||||||
- A single-user server instance is [Spawned](#spawning) for the logged-in user
|
|
||||||
- When the server starts, the proxy is notified to forward `/user/[username]/*` to the single-user server
|
|
||||||
- Two cookies are set, one for `/hub/` and another for `/user/[username]`,
|
|
||||||
containing an encrypted token.
|
|
||||||
- The browser is redirected to `/user/[username]`, which is handled by the single-user server
|
|
||||||
|
|
||||||
Logging into a single-user server is authenticated via the Hub:
|
|
||||||
|
|
||||||
- On request, the single-user server forwards the encrypted cookie to the Hub for verification
|
|
||||||
- The Hub replies with the username if it is a valid cookie
|
|
||||||
- If the user is the owner of the server, access is allowed
|
|
||||||
- If it is the wrong user or an invalid cookie, the browser is redirected to `/hub/login`
|
|
||||||
|
|
||||||
|
|
||||||
## Customizing JupyterHub
|
|
||||||
|
|
||||||
There are two basic extension points for JupyterHub: How users are authenticated,
|
|
||||||
and how their server processes are started.
|
|
||||||
Each is governed by a customizable class,
|
|
||||||
and JupyterHub ships with just the most basic version of each.
|
|
||||||
|
|
||||||
To enable custom authentication and/or spawning,
|
|
||||||
subclass Authenticator or Spawner,
|
|
||||||
and override the relevant methods.
|
|
||||||
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
Authentication is customizable via the Authenticator class.
|
|
||||||
Authentication can be replaced by any mechanism,
|
|
||||||
such as OAuth, Kerberos, etc.
|
|
||||||
|
|
||||||
JupyterHub only ships with [PAM](https://en.wikipedia.org/wiki/Pluggable_authentication_module) authentication,
|
|
||||||
which requires the server to be run as root,
|
|
||||||
or at least with access to the PAM service,
|
|
||||||
which regular users typically do not have
|
|
||||||
(on Ubuntu, this requires being added to the `shadow` group).
|
|
||||||
|
|
||||||
[More info on custom Authenticators](authenticators.html).
|
|
||||||
|
|
||||||
See a list of custom Authenticators [on the wiki](https://github.com/jupyterhub/jupyterhub/wiki/Authenticators).
|
|
||||||
|
|
||||||
|
|
||||||
### Spawning
|
|
||||||
|
|
||||||
Each single-user server is started by a Spawner.
|
|
||||||
The Spawner represents an abstract interface to a process,
|
|
||||||
and needs to be able to take three actions:
|
|
||||||
|
|
||||||
1. start the process
|
|
||||||
2. poll whether the process is still running
|
|
||||||
3. stop the process
|
|
||||||
|
|
||||||
[More info on custom Spawners](spawners.html).
|
|
||||||
|
|
||||||
See a list of custom Spawners [on the wiki](https://github.com/jupyterhub/jupyterhub/wiki/Spawners).
|
|
BIN
docs/source/images/instance.png
Normal file
BIN
docs/source/images/instance.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
BIN
docs/source/images/security.png
Normal file
BIN
docs/source/images/security.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
docs/source/images/token-request-success.png
Normal file
BIN
docs/source/images/token-request-success.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
BIN
docs/source/images/token-request.png
Normal file
BIN
docs/source/images/token-request.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
@@ -1,106 +1,92 @@
|
|||||||
JupyterHub
|
JupyterHub
|
||||||
==========
|
==========
|
||||||
|
|
||||||
With JupyterHub you can create a **multi-user Hub** which spawns, manages,
|
`JupyterHub`_, a multi-user **Hub**, spawns, manages, and proxies multiple
|
||||||
and proxies multiple instances of the single-user
|
instances of the single-user `Jupyter notebook`_ server.
|
||||||
`Jupyter notebook <https://jupyter-notebook.readthedocs.io/en/latest/>`_ server.
|
JupyterHub can be used to serve notebooks to a class of students, a corporate
|
||||||
Due to its flexibility and customization options, JupyterHub can be used to
|
data science group, or a scientific research group.
|
||||||
serve notebooks to a class of students, a corporate data science group, or a
|
|
||||||
scientific research group.
|
|
||||||
|
|
||||||
|
|
||||||
.. image:: images/jhub-parts.png
|
.. image:: images/jhub-parts.png
|
||||||
:alt: JupyterHub subsystems
|
:alt: JupyterHub subsystems
|
||||||
:width: 40%
|
:width: 40%
|
||||||
:align: right
|
:align: right
|
||||||
|
|
||||||
|
|
||||||
Three subsystems make up JupyterHub:
|
Three subsystems make up JupyterHub:
|
||||||
|
|
||||||
* a multi-user **Hub** (tornado process)
|
* a multi-user **Hub** (tornado process)
|
||||||
* a **configurable http proxy** (node-http-proxy)
|
* a **configurable http proxy** (node-http-proxy)
|
||||||
* multiple **single-user Jupyter notebook servers** (Python/IPython/tornado)
|
* multiple **single-user Jupyter notebook servers** (Python/IPython/tornado)
|
||||||
|
|
||||||
JupyterHub's basic flow of operations includes:
|
JupyterHub performs the following functions:
|
||||||
|
|
||||||
- The Hub spawns a proxy
|
- The Hub launches a proxy
|
||||||
- The proxy forwards all requests to the Hub by default
|
- The proxy forwards all requests to the Hub by default
|
||||||
- The Hub handles user login and spawns single-user servers on demand
|
- The Hub handles user login and spawns single-user servers on demand
|
||||||
- The Hub configures the proxy to forward URL prefixes to the single-user notebook servers
|
- The Hub configures the proxy to forward URL prefixes to the single-user
|
||||||
|
notebook servers
|
||||||
|
|
||||||
For convenient administration of the Hub, its users, and :doc:`services`
|
For convenient administration of the Hub, its users, and services,
|
||||||
(added in version 7.0), JupyterHub also provides a
|
JupyterHub also provides a `REST API`_.
|
||||||
`REST API <http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default>`__.
|
|
||||||
|
|
||||||
Contents
|
Contents
|
||||||
--------
|
--------
|
||||||
|
|
||||||
**User Guide**
|
**Installation Guide**
|
||||||
|
|
||||||
|
* :doc:`installation-guide`
|
||||||
* :doc:`quickstart`
|
* :doc:`quickstart`
|
||||||
* :doc:`getting-started`
|
* :doc:`quickstart-docker`
|
||||||
* :doc:`howitworks`
|
* :doc:`installation-basics`
|
||||||
* :doc:`websecurity`
|
|
||||||
* :doc:`rest`
|
|
||||||
|
|
||||||
.. toctree::
|
**Getting Started**
|
||||||
:maxdepth: 2
|
|
||||||
:hidden:
|
|
||||||
:caption: User Guide
|
|
||||||
|
|
||||||
quickstart
|
* :doc:`getting-started/index`
|
||||||
getting-started
|
* :doc:`getting-started/config-basics`
|
||||||
howitworks
|
* :doc:`getting-started/networking-basics`
|
||||||
websecurity
|
* :doc:`getting-started/security-basics`
|
||||||
rest
|
* :doc:`getting-started/authenticators-users-basics`
|
||||||
|
* :doc:`getting-started/spawners-basics`
|
||||||
|
* :doc:`getting-started/services-basics`
|
||||||
|
|
||||||
**Configuration Guide**
|
**Technical Reference**
|
||||||
|
|
||||||
* :doc:`authenticators`
|
|
||||||
* :doc:`spawners`
|
|
||||||
* :doc:`services`
|
|
||||||
* :doc:`config-examples`
|
|
||||||
* :doc:`upgrading`
|
|
||||||
* :doc:`troubleshooting`
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:hidden:
|
|
||||||
:caption: Configuration Guide
|
|
||||||
|
|
||||||
authenticators
|
|
||||||
spawners
|
|
||||||
services
|
|
||||||
config-examples
|
|
||||||
upgrading
|
|
||||||
troubleshooting
|
|
||||||
|
|
||||||
|
* :doc:`reference/index`
|
||||||
|
* :doc:`reference/technical-overview`
|
||||||
|
* :doc:`reference/websecurity`
|
||||||
|
* :doc:`reference/authenticators`
|
||||||
|
* :doc:`reference/spawners`
|
||||||
|
* :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`
|
||||||
|
* :doc:`reference/config-sudo`
|
||||||
|
|
||||||
**API Reference**
|
**API Reference**
|
||||||
|
|
||||||
* :doc:`api/index`
|
* :doc:`api/index`
|
||||||
|
|
||||||
.. toctree::
|
**Tutorials**
|
||||||
:maxdepth: 2
|
|
||||||
:hidden:
|
|
||||||
:caption: API Reference
|
|
||||||
|
|
||||||
api/index
|
* :doc:`tutorials/index`
|
||||||
|
* :doc:`tutorials/upgrade-dot-eight`
|
||||||
|
* `Zero to JupyterHub with Kubernetes <https://zero-to-jupyterhub.readthedocs.io/en/latest/>`_
|
||||||
|
|
||||||
|
**Troubleshooting**
|
||||||
|
|
||||||
|
* :doc:`troubleshooting`
|
||||||
|
|
||||||
**About JupyterHub**
|
**About JupyterHub**
|
||||||
|
|
||||||
* :doc:`changelog`
|
|
||||||
* :doc:`contributor-list`
|
* :doc:`contributor-list`
|
||||||
|
* :doc:`gallery-jhub-deployments`
|
||||||
|
|
||||||
.. toctree::
|
**Changelog**
|
||||||
:maxdepth: 2
|
|
||||||
:hidden:
|
|
||||||
:caption: About JupyterHub
|
|
||||||
|
|
||||||
changelog
|
|
||||||
contributor-list
|
|
||||||
|
|
||||||
|
* :doc:`changelog`
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
------------------
|
------------------
|
||||||
@@ -114,3 +100,26 @@ Questions? Suggestions?
|
|||||||
|
|
||||||
- `Jupyter mailing list <https://groups.google.com/forum/#!forum/jupyter>`_
|
- `Jupyter mailing list <https://groups.google.com/forum/#!forum/jupyter>`_
|
||||||
- `Jupyter website <https://jupyter.org>`_
|
- `Jupyter website <https://jupyter.org>`_
|
||||||
|
|
||||||
|
.. _contents:
|
||||||
|
|
||||||
|
Full Table of Contents
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
installation-guide
|
||||||
|
getting-started/index
|
||||||
|
reference/index
|
||||||
|
api/index
|
||||||
|
tutorials/index
|
||||||
|
troubleshooting
|
||||||
|
contributor-list
|
||||||
|
gallery-jhub-deployments
|
||||||
|
changelog
|
||||||
|
|
||||||
|
|
||||||
|
.. _JupyterHub: https://github.com/jupyterhub/jupyterhub
|
||||||
|
.. _Jupyter notebook: https://jupyter-notebook.readthedocs.io/en/latest/
|
||||||
|
.. _REST API: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default
|
||||||
|
40
docs/source/installation-basics.md
Normal file
40
docs/source/installation-basics.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Installation Basics
|
||||||
|
|
||||||
|
## Platform support
|
||||||
|
|
||||||
|
JupyterHub is supported on Linux/Unix based systems. To use JupyterHub, you need
|
||||||
|
a Unix server (typically Linux) running somewhere that is accessible to your
|
||||||
|
team on the network. The JupyterHub server can be on an internal network at your
|
||||||
|
organization, or it can run on the public internet (in which case, take care
|
||||||
|
with the Hub's [security](./security-basics.html)).
|
||||||
|
|
||||||
|
JupyterHub officially **does not** support Windows. You may be able to use
|
||||||
|
JupyterHub on Windows if you use a Spawner and Authenticator that work on
|
||||||
|
Windows, but the JupyterHub defaults will not. Bugs reported on Windows will not
|
||||||
|
be accepted, and the test suite will not run on Windows. Small patches that fix
|
||||||
|
minor Windows compatibility issues (such as basic installation) **may** be accepted,
|
||||||
|
however. For Windows-based systems, we would recommend running JupyterHub in a
|
||||||
|
docker container or Linux VM.
|
||||||
|
|
||||||
|
[Additional Reference:](http://www.tornadoweb.org/en/stable/#installation)
|
||||||
|
Tornado's documentation on Windows platform support
|
||||||
|
|
||||||
|
## Planning your installation
|
||||||
|
|
||||||
|
Prior to beginning installation, it's helpful to consider some of the following:
|
||||||
|
|
||||||
|
- deployment system (bare metal, Docker)
|
||||||
|
- Authentication (PAM, OAuth, etc.)
|
||||||
|
- Spawner of singleuser notebook servers (Docker, Batch, etc.)
|
||||||
|
- Services (nbgrader, etc.)
|
||||||
|
- JupyterHub database (default SQLite; traditional RDBMS such as PostgreSQL,)
|
||||||
|
MySQL, or other databases supported by [SQLAlchemy](http://www.sqlalchemy.org))
|
||||||
|
|
||||||
|
## Folders and File Locations
|
||||||
|
|
||||||
|
It is recommended to put all of the files used by JupyterHub into standard
|
||||||
|
UNIX filesystem locations.
|
||||||
|
|
||||||
|
- `/srv/jupyterhub` for all security and runtime files
|
||||||
|
- `/etc/jupyterhub` for all configuration files
|
||||||
|
- `/var/log` for log files
|
9
docs/source/installation-guide.rst
Normal file
9
docs/source/installation-guide.rst
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Installation Guide
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
quickstart
|
||||||
|
quickstart-docker
|
||||||
|
installation-basics
|
49
docs/source/quickstart-docker.rst
Normal file
49
docs/source/quickstart-docker.rst
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
Using Docker
|
||||||
|
============
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
We highly recommend following the `Zero to JupyterHub`_ tutorial for
|
||||||
|
installing JupyterHub.
|
||||||
|
|
||||||
|
Alternate installation using Docker
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
A ready to go `docker image <https://hub.docker.com/r/jupyterhub/jupyterhub/>`_
|
||||||
|
gives a straightforward deployment of JupyterHub.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This ``jupyterhub/jupyterhub`` docker image is only an image for running
|
||||||
|
the Hub service itself. It does not provide the other Jupyter components,
|
||||||
|
such as Notebook installation, which are needed by the single-user servers.
|
||||||
|
To run the single-user servers, which may be on the same system as the Hub or
|
||||||
|
not, Jupyter Notebook version 4 or greater must be installed.
|
||||||
|
|
||||||
|
Starting JupyterHub with docker
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
The JupyterHub docker image can be started with the following command::
|
||||||
|
|
||||||
|
docker run -d --name jupyterhub jupyterhub/jupyterhub jupyterhub
|
||||||
|
|
||||||
|
This command will create a container named ``jupyterhub`` that you can
|
||||||
|
**stop and resume** with ``docker stop/start``.
|
||||||
|
|
||||||
|
The Hub service will be listening on all interfaces at port 8000, which makes
|
||||||
|
this a good choice for **testing JupyterHub on your desktop or laptop**.
|
||||||
|
|
||||||
|
If you want to run docker on a computer that has a public IP then you should
|
||||||
|
(as in MUST) **secure it with ssl** by adding ssl options to your docker
|
||||||
|
configuration or using a ssl enabled proxy.
|
||||||
|
|
||||||
|
`Mounting volumes <https://docs.docker.com/engine/admin/volumes/volumes/>`_
|
||||||
|
will allow you to store data outside the docker image (host system) so it will
|
||||||
|
be persistent, even when you start a new image.
|
||||||
|
|
||||||
|
The command ``docker exec -it jupyterhub bash`` will spawn a root shell in your
|
||||||
|
docker container. You can use the root shell to **create system users in the container**.
|
||||||
|
These accounts will be used for authentication in JupyterHub's default
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
.. _Zero to JupyterHub: https://zero-to-jupyterhub.readthedocs.io/en/latest/
|
@@ -1,73 +1,67 @@
|
|||||||
# Quickstart - Installation
|
# Quickstart
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
**Before installing JupyterHub**, you will need:
|
Before installing JupyterHub, you will need:
|
||||||
|
|
||||||
- [Python](https://www.python.org/downloads/) 3.3 or greater
|
- a Linux/Unix based system
|
||||||
|
- [Python](https://www.python.org/downloads/) 3.5 or greater. An understanding
|
||||||
An understanding of using [`pip`](https://pip.pypa.io/en/stable/) or
|
of using [`pip`](https://pip.pypa.io/en/stable/) or
|
||||||
[`conda`](http://conda.pydata.org/docs/get-started.html) for
|
[`conda`](https://conda.io/docs/get-started.html) for
|
||||||
installing Python packages is helpful.
|
installing Python packages is helpful.
|
||||||
|
- [nodejs/npm](https://www.npmjs.com/). [Install nodejs/npm](https://docs.npmjs.com/getting-started/installing-node),
|
||||||
|
using your operating system's package manager.
|
||||||
|
|
||||||
- [nodejs/npm](https://www.npmjs.com/)
|
* If you are using **`conda`**, the nodejs and npm dependencies will be installed for
|
||||||
|
you by conda.
|
||||||
|
|
||||||
[Install nodejs/npm](https://docs.npmjs.com/getting-started/installing-node),
|
* If you are using **`pip`**, install a recent version of
|
||||||
using your operating system's package manager. For example, install on Linux
|
[nodejs/npm](https://docs.npmjs.com/getting-started/installing-node).
|
||||||
(Debian/Ubuntu) using:
|
For example, install it on Linux (Debian/Ubuntu) using:
|
||||||
|
|
||||||
```bash
|
```
|
||||||
sudo apt-get install npm nodejs-legacy
|
sudo apt-get install npm nodejs-legacy
|
||||||
```
|
```
|
||||||
|
|
||||||
(The `nodejs-legacy` package installs the `node` executable and is currently
|
The `nodejs-legacy` package installs the `node` executable and is currently
|
||||||
required for npm to work on Debian/Ubuntu.)
|
required for npm to work on Debian/Ubuntu.
|
||||||
|
|
||||||
- TLS certificate and key for HTTPS communication
|
- TLS certificate and key for HTTPS communication
|
||||||
|
|
||||||
- Domain name
|
- Domain name
|
||||||
|
|
||||||
**Before running the single-user notebook servers** (which may be on the same
|
Before running the single-user notebook servers (which may be on the same
|
||||||
system as the Hub or not):
|
system as the Hub or not), you will need:
|
||||||
|
|
||||||
- [Jupyter Notebook](https://jupyter.readthedocs.io/en/latest/install.html)
|
- [Jupyter Notebook](https://jupyter.readthedocs.io/en/latest/install.html)
|
||||||
version 4 or greater
|
version 4 or greater
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
JupyterHub can be installed with `pip` or `conda` and the proxy with `npm`:
|
JupyterHub can be installed with `pip` (and the proxy with `npm`) or `conda`:
|
||||||
|
|
||||||
**pip, npm:**
|
**pip, npm:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m pip install jupyterhub
|
python3 -m pip install jupyterhub
|
||||||
npm install -g configurable-http-proxy
|
npm install -g configurable-http-proxy
|
||||||
|
python3 -m pip install notebook # needed if running the notebook servers locally
|
||||||
```
|
```
|
||||||
|
|
||||||
**conda** (one command installs jupyterhub and proxy):
|
**conda** (one command installs jupyterhub and proxy):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
conda install -c conda-forge jupyterhub
|
conda install -c conda-forge jupyterhub # installs jupyterhub and proxy
|
||||||
|
conda install notebook # needed if running the notebook servers locally
|
||||||
```
|
```
|
||||||
|
|
||||||
To test your installation:
|
Test your installation. If installed, these commands should return the packages'
|
||||||
|
help contents:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
jupyterhub -h
|
jupyterhub -h
|
||||||
configurable-http-proxy -h
|
configurable-http-proxy -h
|
||||||
```
|
```
|
||||||
|
|
||||||
If you plan to run notebook servers locally, you will need also to install
|
|
||||||
Jupyter notebook:
|
|
||||||
|
|
||||||
**pip:**
|
|
||||||
```bash
|
|
||||||
python3 -m pip install notebook
|
|
||||||
```
|
|
||||||
|
|
||||||
**conda:**
|
|
||||||
```bash
|
|
||||||
conda install notebook
|
|
||||||
```
|
|
||||||
|
|
||||||
## Start the Hub server
|
## Start the Hub server
|
||||||
|
|
||||||
To start the Hub server, run the command:
|
To start the Hub server, run the command:
|
||||||
@@ -79,82 +73,13 @@ jupyterhub
|
|||||||
Visit `https://localhost:8000` in your browser, and sign in with your unix
|
Visit `https://localhost:8000` in your browser, and sign in with your unix
|
||||||
credentials.
|
credentials.
|
||||||
|
|
||||||
To allow multiple users to sign into the Hub server, you must start `jupyterhub` as a *privileged user*, such as root:
|
To **allow multiple users to sign in** to the Hub server, you must start
|
||||||
|
`jupyterhub` as a *privileged user*, such as root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo jupyterhub
|
sudo jupyterhub
|
||||||
```
|
```
|
||||||
|
|
||||||
The [wiki](https://github.com/jupyterhub/jupyterhub/wiki/Using-sudo-to-run-JupyterHub-without-root-privileges)
|
The [wiki](https://github.com/jupyterhub/jupyterhub/wiki/Using-sudo-to-run-JupyterHub-without-root-privileges)
|
||||||
describes how to run the server as a *less privileged user*, which requires
|
describes how to run the server as a *less privileged user*. This requires
|
||||||
additional configuration of the system.
|
additional configuration of the system.
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
## Basic Configuration
|
|
||||||
|
|
||||||
The [getting started document](docs/source/getting-started.md) contains
|
|
||||||
detailed information abouts configuring a JupyterHub deployment.
|
|
||||||
|
|
||||||
The JupyterHub **tutorial** provides a video and documentation that explains
|
|
||||||
and illustrates the fundamental steps for installation and configuration.
|
|
||||||
[Repo](https://github.com/jupyterhub/jupyterhub-tutorial)
|
|
||||||
| [Tutorial documentation](http://jupyterhub-tutorial.readthedocs.io/en/latest/)
|
|
||||||
|
|
||||||
#### Generate a default configuration file
|
|
||||||
|
|
||||||
Generate a default config file:
|
|
||||||
|
|
||||||
jupyterhub --generate-config
|
|
||||||
|
|
||||||
#### Customize the configuration, authentication, and process spawning
|
|
||||||
|
|
||||||
Spawn the server on ``10.0.1.2:443`` with **https**:
|
|
||||||
|
|
||||||
jupyterhub --ip 10.0.1.2 --port 443 --ssl-key my_ssl.key --ssl-cert my_ssl.cert
|
|
||||||
|
|
||||||
The authentication and process spawning mechanisms can be replaced,
|
|
||||||
which should allow plugging into a variety of authentication or process
|
|
||||||
control environments. Some examples, meant as illustration and testing of this
|
|
||||||
concept, are:
|
|
||||||
|
|
||||||
- Using GitHub OAuth instead of PAM with [OAuthenticator](https://github.com/jupyterhub/oauthenticator)
|
|
||||||
- Spawning single-user servers with Docker, using the [DockerSpawner](https://github.com/jupyterhub/dockerspawner)
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
## Alternate Installation using Docker
|
|
||||||
|
|
||||||
A ready to go [docker image for JupyterHub](https://hub.docker.com/r/jupyterhub/jupyterhub/)
|
|
||||||
gives a straightforward deployment of JupyterHub.
|
|
||||||
|
|
||||||
*Note: This `jupyterhub/jupyterhub` docker image is only an image for running
|
|
||||||
the Hub service itself. It does not provide the other Jupyter components, such
|
|
||||||
as Notebook installation, which are needed by the single-user servers.
|
|
||||||
To run the single-user servers, which may be on the same system as the Hub or
|
|
||||||
not, Jupyter Notebook version 4 or greater must be installed.*
|
|
||||||
|
|
||||||
#### Starting JupyterHub with docker
|
|
||||||
|
|
||||||
The JupyterHub docker image can be started with the following command:
|
|
||||||
|
|
||||||
docker run -d --name jupyterhub jupyterhub/jupyterhub jupyterhub
|
|
||||||
|
|
||||||
This command will create a container named `jupyterhub` that you can
|
|
||||||
**stop and resume** with `docker stop/start`.
|
|
||||||
|
|
||||||
The Hub service will be listening on all interfaces at port 8000, which makes
|
|
||||||
this a good choice for **testing JupyterHub on your desktop or laptop**.
|
|
||||||
|
|
||||||
If you want to run docker on a computer that has a public IP then you should
|
|
||||||
(as in MUST) **secure it with ssl** by adding ssl options to your docker
|
|
||||||
configuration or using a ssl enabled proxy.
|
|
||||||
|
|
||||||
[Mounting volumes](https://docs.docker.com/engine/userguide/containers/dockervolumes/)
|
|
||||||
will allow you to **store data outside the docker image (host system) so it will be persistent**,
|
|
||||||
even when you start a new image.
|
|
||||||
|
|
||||||
The command `docker exec -it jupyterhub bash` will spawn a root shell in your
|
|
||||||
docker container. You can **use the root shell to create system users in the container**.
|
|
||||||
These accounts will be used for authentication in JupyterHub's default
|
|
||||||
configuration.
|
|
||||||
|
230
docs/source/reference/authenticators.md
Normal file
230
docs/source/reference/authenticators.md
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
# Authenticators
|
||||||
|
|
||||||
|
The [Authenticator][] is the mechanism for authorizing users to use the
|
||||||
|
Hub and single user notebook servers.
|
||||||
|
|
||||||
|
## The default PAM Authenticator
|
||||||
|
|
||||||
|
JupyterHub ships only with the default [PAM][]-based Authenticator,
|
||||||
|
for logging in with local user accounts via a username and password.
|
||||||
|
|
||||||
|
## The OAuthenticator
|
||||||
|
|
||||||
|
Some login mechanisms, such as [OAuth][], don't map onto username and
|
||||||
|
password authentication, and instead use tokens. When using these
|
||||||
|
mechanisms, you can override the login handlers.
|
||||||
|
|
||||||
|
You can see an example implementation of an Authenticator that uses
|
||||||
|
[GitHub OAuth][] at [OAuthenticator][].
|
||||||
|
|
||||||
|
JupyterHub's [OAuthenticator][] currently supports the following
|
||||||
|
popular services:
|
||||||
|
|
||||||
|
- Auth0
|
||||||
|
- Bitbucket
|
||||||
|
- CILogon
|
||||||
|
- GitHub
|
||||||
|
- GitLab
|
||||||
|
- Globus
|
||||||
|
- Google
|
||||||
|
- MediaWiki
|
||||||
|
- Okpy
|
||||||
|
- OpenShift
|
||||||
|
|
||||||
|
A generic implementation, which you can use for OAuth authentication
|
||||||
|
with any provider, is also available.
|
||||||
|
|
||||||
|
## Additional Authenticators
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### How the Base Authenticator works
|
||||||
|
|
||||||
|
The base authenticator uses simple username and password authentication.
|
||||||
|
|
||||||
|
The base Authenticator has one central method:
|
||||||
|
|
||||||
|
#### Authenticator.authenticate method
|
||||||
|
|
||||||
|
Authenticator.authenticate(handler, data)
|
||||||
|
|
||||||
|
This method is passed the Tornado `RequestHandler` and the `POST data`
|
||||||
|
from JupyterHub's login form. Unless the login form has been customized,
|
||||||
|
`data` will have two keys:
|
||||||
|
|
||||||
|
- `username`
|
||||||
|
- `password`
|
||||||
|
|
||||||
|
The `authenticate` method's job is simple:
|
||||||
|
|
||||||
|
- return the username (non-empty str) of the authenticated user if
|
||||||
|
authentication is successful
|
||||||
|
- return `None` otherwise
|
||||||
|
|
||||||
|
Writing an Authenticator that looks up passwords in a dictionary
|
||||||
|
requires only overriding this one method:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from tornado import gen
|
||||||
|
from IPython.utils.traitlets import Dict
|
||||||
|
from jupyterhub.auth import Authenticator
|
||||||
|
|
||||||
|
class DictionaryAuthenticator(Authenticator):
|
||||||
|
|
||||||
|
passwords = Dict(config=True,
|
||||||
|
help="""dict of username:password for authentication"""
|
||||||
|
)
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
|
def authenticate(self, handler, data):
|
||||||
|
if self.passwords.get(data['username']) == data['password']:
|
||||||
|
return data['username']
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Normalize usernames
|
||||||
|
|
||||||
|
Since the Authenticator and Spawner both use the same username,
|
||||||
|
sometimes you want to transform the name coming from the authentication service
|
||||||
|
(e.g. turning email addresses into local system usernames) before adding them to the Hub service.
|
||||||
|
Authenticators can define `normalize_username`, which takes a username.
|
||||||
|
The default normalization is to cast names to lowercase
|
||||||
|
|
||||||
|
For simple mappings, a configurable dict `Authenticator.username_map` is used to turn one name into another:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.Authenticator.username_map = {
|
||||||
|
'service-name': 'localname'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Validate usernames
|
||||||
|
|
||||||
|
In most cases, there is a very limited set of acceptable usernames.
|
||||||
|
Authenticators can define `validate_username(username)`,
|
||||||
|
which should return True for a valid username and False for an invalid one.
|
||||||
|
The primary effect this has is improving error messages during user creation.
|
||||||
|
|
||||||
|
The default behavior is to use configurable `Authenticator.username_pattern`,
|
||||||
|
which is a regular expression string for validation.
|
||||||
|
|
||||||
|
To only allow usernames that start with 'w':
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.Authenticator.username_pattern = r'w.*'
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### How to write a custom authenticator
|
||||||
|
|
||||||
|
You can use custom Authenticator subclasses to enable authentication
|
||||||
|
via other mechanisms. One such example is using [GitHub OAuth][].
|
||||||
|
|
||||||
|
Because the username is passed from the Authenticator to the Spawner,
|
||||||
|
a custom Authenticator and Spawner are often used together.
|
||||||
|
For example, the Authenticator methods, [pre_spawn_start(user, spawner)][]
|
||||||
|
and [post_spawn_stop(user, spawner)][], are hooks that can be used to do
|
||||||
|
auth-related startup (e.g. opening PAM sessions) and cleanup
|
||||||
|
(e.g. closing PAM sessions).
|
||||||
|
|
||||||
|
|
||||||
|
See a list of custom Authenticators [on the wiki](https://github.com/jupyterhub/jupyterhub/wiki/Authenticators).
|
||||||
|
|
||||||
|
If you are interested in writing a custom authenticator, you can read
|
||||||
|
[this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/authenticators.html).
|
||||||
|
|
||||||
|
|
||||||
|
### Authentication state
|
||||||
|
|
||||||
|
JupyterHub 0.8 adds the ability to persist state related to authentication,
|
||||||
|
such as auth-related tokens.
|
||||||
|
If such state should be persisted, `.authenticate()` should return a dictionary of the form:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'name': username,
|
||||||
|
'auth_state': {
|
||||||
|
'key': 'value',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
where `username` is the username that has been authenticated,
|
||||||
|
and `auth_state` is any JSON-serializable dictionary.
|
||||||
|
|
||||||
|
Because `auth_state` may contain sensitive information,
|
||||||
|
it is encrypted before being stored in the database.
|
||||||
|
To store auth_state, two conditions must be met:
|
||||||
|
|
||||||
|
1. persisting auth state must be enabled explicitly via configuration
|
||||||
|
```python
|
||||||
|
c.Authenticator.enable_auth_state = True
|
||||||
|
```
|
||||||
|
2. encryption must be enabled by the presence of `JUPYTERHUB_CRYPT_KEY` environment variable,
|
||||||
|
which should be a hex-encoded 32-byte key.
|
||||||
|
For example:
|
||||||
|
```bash
|
||||||
|
export JUPYTERHUB_CRYPT_KEY=$(openssl rand -hex 32)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
JupyterHub uses [Fernet](https://cryptography.io/en/latest/fernet/) to encrypt auth_state.
|
||||||
|
To facilitate key-rotation, `JUPYTERHUB_CRYPT_KEY` may be a semicolon-separated list of encryption keys.
|
||||||
|
If there are multiple keys present, the **first** key is always used to persist any new auth_state.
|
||||||
|
|
||||||
|
|
||||||
|
#### Using auth_state
|
||||||
|
|
||||||
|
Typically, if `auth_state` is persisted it is desirable to affect the Spawner environment in some way.
|
||||||
|
This may mean defining environment variables, placing certificate in the user's home directory, etc.
|
||||||
|
The `Authenticator.pre_spawn_start` method can be used to pass information from authenticator state
|
||||||
|
to Spawner environment:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyAuthenticator(Authenticator):
|
||||||
|
@gen.coroutine
|
||||||
|
def authenticate(self, handler, data=None):
|
||||||
|
username = yield identify_user(handler, data)
|
||||||
|
upstream_token = yield token_for_user(username)
|
||||||
|
return {
|
||||||
|
'name': username,
|
||||||
|
'auth_state': {
|
||||||
|
'upstream_token': upstream_token,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
|
def pre_spawn_start(self, user, spawner):
|
||||||
|
"""Pass upstream_token to spawner via environment variable"""
|
||||||
|
auth_state = yield user.get_auth_state()
|
||||||
|
if not auth_state:
|
||||||
|
# auth_state not enabled
|
||||||
|
return
|
||||||
|
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
|
||||||
|
|
||||||
|
Beginning with version 0.8, JupyterHub is an OAuth provider.
|
||||||
|
|
||||||
|
|
||||||
|
[Authenticator]: https://github.com/jupyterhub/jupyterhub/blob/master/jupyterhub/auth.py
|
||||||
|
[PAM]: https://en.wikipedia.org/wiki/Pluggable_authentication_module
|
||||||
|
[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)]: 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
|
8
docs/source/reference/config-examples.md
Normal file
8
docs/source/reference/config-examples.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Configuration examples
|
||||||
|
|
||||||
|
The following sections provide examples, including configuration files and tips, for the
|
||||||
|
following:
|
||||||
|
|
||||||
|
- Configuring GitHub OAuth
|
||||||
|
- Using reverse proxy (nginx and Apache)
|
||||||
|
- Run JupyterHub without root privileges using `sudo`
|
82
docs/source/reference/config-ghoauth.md
Normal file
82
docs/source/reference/config-ghoauth.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Configure GitHub OAuth
|
||||||
|
|
||||||
|
In this example, we show a configuration file for a fairly standard JupyterHub
|
||||||
|
deployment with the following assumptions:
|
||||||
|
|
||||||
|
* Running JupyterHub on a single cloud server
|
||||||
|
* Using SSL on the standard HTTPS port 443
|
||||||
|
* Using GitHub OAuth (using oauthenticator) for login
|
||||||
|
* Using the default spawner (to configure other spawners, uncomment and edit
|
||||||
|
`spawner_class` as well as follow the instructions for your desired spawner)
|
||||||
|
* Users exist locally on the server
|
||||||
|
* Users' notebooks to be served from `~/assignments` to allow users to browse
|
||||||
|
for notebooks within other users' home directories
|
||||||
|
* You want the landing page for each user to be a `Welcome.ipynb` notebook in
|
||||||
|
their assignments directory.
|
||||||
|
* All runtime files are put into `/srv/jupyterhub` and log files in `/var/log`.
|
||||||
|
|
||||||
|
|
||||||
|
The `jupyterhub_config.py` file would have these settings:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# jupyterhub_config.py file
|
||||||
|
c = get_config()
|
||||||
|
|
||||||
|
import os
|
||||||
|
pjoin = os.path.join
|
||||||
|
|
||||||
|
runtime_dir = os.path.join('/srv/jupyterhub')
|
||||||
|
ssl_dir = pjoin(runtime_dir, 'ssl')
|
||||||
|
if not os.path.exists(ssl_dir):
|
||||||
|
os.makedirs(ssl_dir)
|
||||||
|
|
||||||
|
# Allows multiple single-server per user
|
||||||
|
c.JupyterHub.allow_named_servers = True
|
||||||
|
|
||||||
|
# https on :443
|
||||||
|
c.JupyterHub.port = 443
|
||||||
|
c.JupyterHub.ssl_key = pjoin(ssl_dir, 'ssl.key')
|
||||||
|
c.JupyterHub.ssl_cert = pjoin(ssl_dir, 'ssl.cert')
|
||||||
|
|
||||||
|
# put the JupyterHub cookie secret and state db
|
||||||
|
# in /var/run/jupyterhub
|
||||||
|
c.JupyterHub.cookie_secret_file = pjoin(runtime_dir, 'cookie_secret')
|
||||||
|
c.JupyterHub.db_url = pjoin(runtime_dir, 'jupyterhub.sqlite')
|
||||||
|
# or `--db=/path/to/jupyterhub.sqlite` on the command-line
|
||||||
|
|
||||||
|
# use GitHub OAuthenticator for local users
|
||||||
|
c.JupyterHub.authenticator_class = 'oauthenticator.LocalGitHubOAuthenticator'
|
||||||
|
c.GitHubOAuthenticator.oauth_callback_url = os.environ['OAUTH_CALLBACK_URL']
|
||||||
|
|
||||||
|
# create system users that don't exist yet
|
||||||
|
c.LocalAuthenticator.create_system_users = True
|
||||||
|
|
||||||
|
# specify users and admin
|
||||||
|
c.Authenticator.whitelist = {'rgbkrk', 'minrk', 'jhamrick'}
|
||||||
|
c.Authenticator.admin_users = {'jhamrick', 'rgbkrk'}
|
||||||
|
|
||||||
|
# uses the default spawner
|
||||||
|
# To use a different spawner, uncomment `spawner_class` and set to desired
|
||||||
|
# spawner (e.g. SudoSpawner). Follow instructions for desired spawner
|
||||||
|
# configuration.
|
||||||
|
# c.JupyterHub.spawner_class = 'sudospawner.SudoSpawner'
|
||||||
|
|
||||||
|
# start single-user notebook servers in ~/assignments,
|
||||||
|
# with ~/assignments/Welcome.ipynb as the default landing page
|
||||||
|
# this config could also be put in
|
||||||
|
# /etc/jupyter/jupyter_notebook_config.py
|
||||||
|
c.Spawner.notebook_dir = '~/assignments'
|
||||||
|
c.Spawner.args = ['--NotebookApp.default_url=/notebooks/Welcome.ipynb']
|
||||||
|
```
|
||||||
|
|
||||||
|
Using the GitHub Authenticator requires a few additional
|
||||||
|
environment variable to be set prior to launching JupyterHub:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GITHUB_CLIENT_ID=github_id
|
||||||
|
export GITHUB_CLIENT_SECRET=github_secret
|
||||||
|
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
|
||||||
|
```
|
192
docs/source/reference/config-proxy.md
Normal file
192
docs/source/reference/config-proxy.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# Using a reverse proxy
|
||||||
|
|
||||||
|
In the following example, we show configuration files for a JupyterHub server
|
||||||
|
running locally on port `8000` but accessible from the outside on the standard
|
||||||
|
SSL port `443`. This could be useful if the JupyterHub server machine is also
|
||||||
|
hosting other domains or content on `443`. The goal in this example is to
|
||||||
|
satisfy the following:
|
||||||
|
|
||||||
|
* JupyterHub is running on a server, accessed *only* via `HUB.DOMAIN.TLD:443`
|
||||||
|
* On the same machine, `NO_HUB.DOMAIN.TLD` strictly serves different content,
|
||||||
|
also on port `443`
|
||||||
|
* `nginx` or `apache` is used as the public access point (which means that
|
||||||
|
only nginx/apache will bind to `443`)
|
||||||
|
* After testing, the server in question should be able to score at least an A on the
|
||||||
|
Qualys SSL Labs [SSL Server Test](https://www.ssllabs.com/ssltest/)
|
||||||
|
|
||||||
|
Let's start out with needed JupyterHub configuration in `jupyterhub_config.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Force the proxy to only listen to connections to 127.0.0.1
|
||||||
|
c.JupyterHub.ip = '127.0.0.1'
|
||||||
|
```
|
||||||
|
|
||||||
|
For high-quality SSL configuration, we also generate Diffie-Helman parameters.
|
||||||
|
This can take a few minutes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
|
||||||
|
```
|
||||||
|
|
||||||
|
## nginx
|
||||||
|
|
||||||
|
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
|
||||||
|
# If Upgrade is defined, Connection = upgrade
|
||||||
|
# If Upgrade is empty, Connection = close
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP server to redirect all 80 traffic to SSL/HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name HUB.DOMAIN.TLD;
|
||||||
|
|
||||||
|
# Tell all requests to port 80 to be 302 redirected to HTTPS
|
||||||
|
return 302 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS server to handle JupyterHub
|
||||||
|
server {
|
||||||
|
listen 443;
|
||||||
|
ssl on;
|
||||||
|
|
||||||
|
server_name HUB.DOMAIN.TLD;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/HUB.DOMAIN.TLD/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/HUB.DOMAIN.TLD/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_dhparam /etc/ssl/certs/dhparam.pem;
|
||||||
|
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_cache shared:SSL:50m;
|
||||||
|
ssl_stapling on;
|
||||||
|
ssl_stapling_verify on;
|
||||||
|
add_header Strict-Transport-Security max-age=15768000;
|
||||||
|
|
||||||
|
# Managing literal requests to the JupyterHub front end
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8000;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
# websocket headers
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Managing requests to verify letsencrypt host
|
||||||
|
location ~ /.well-known {
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `nginx` is not running on port 443, substitute `$http_host` for `$host` on
|
||||||
|
the lines setting the `Host` header.
|
||||||
|
|
||||||
|
`nginx` will now be the front facing element of JupyterHub on `443` which means
|
||||||
|
it is also free to bind other servers, like `NO_HUB.DOMAIN.TLD` to the same port
|
||||||
|
on the same machine and network interface. In fact, one can simply use the same
|
||||||
|
server blocks as above for `NO_HUB` and simply add line for the root directory
|
||||||
|
of the site as well as the applicable location call:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name NO_HUB.DOMAIN.TLD;
|
||||||
|
|
||||||
|
# Tell all requests to port 80 to be 302 redirected to HTTPS
|
||||||
|
return 302 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443;
|
||||||
|
ssl on;
|
||||||
|
|
||||||
|
# INSERT OTHER SSL PARAMETERS HERE AS ABOVE
|
||||||
|
# SSL cert may differ
|
||||||
|
|
||||||
|
# Set the appropriate root directory
|
||||||
|
root /var/www/html
|
||||||
|
|
||||||
|
# Set URI handling
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Managing requests to verify letsencrypt host
|
||||||
|
location ~ /.well-known {
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now restart `nginx`, restart the JupyterHub, and enjoy accessing
|
||||||
|
`https://HUB.DOMAIN.TLD` while serving other content securely on
|
||||||
|
`https://NO_HUB.DOMAIN.TLD`.
|
||||||
|
|
||||||
|
|
||||||
|
## Apache
|
||||||
|
|
||||||
|
As with nginx above, you can use [Apache](https://httpd.apache.org) as the reverse proxy.
|
||||||
|
First, we will need to enable the apache modules that we are going to need:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
a2enmod ssl rewrite proxy proxy_http proxy_wstunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
Our Apache configuration is equivalent to the nginx configuration above:
|
||||||
|
|
||||||
|
- Redirect HTTP to HTTPS
|
||||||
|
- Good SSL Configuration
|
||||||
|
- Support for websockets on any proxied URL
|
||||||
|
- JupyterHub is running locally at http://127.0.0.1:8000
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# redirect HTTP to HTTPS
|
||||||
|
Listen 80
|
||||||
|
<VirtualHost HUB.DOMAIN.TLD:80>
|
||||||
|
ServerName HUB.DOMAIN.TLD
|
||||||
|
Redirect / https://HUB.DOMAIN.TLD/
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
Listen 443
|
||||||
|
<VirtualHost HUB.DOMAIN.TLD:443>
|
||||||
|
|
||||||
|
ServerName HUB.DOMAIN.TLD
|
||||||
|
|
||||||
|
# configure SSL
|
||||||
|
SSLEngine on
|
||||||
|
SSLCertificateFile /etc/letsencrypt/live/HUB.DOMAIN.TLD/fullchain.pem
|
||||||
|
SSLCertificateKeyFile /etc/letsencrypt/live/HUB.DOMAIN.TLD/privkey.pem
|
||||||
|
SSLProtocol All -SSLv2 -SSLv3
|
||||||
|
SSLOpenSSLConfCmd DHParameters /etc/ssl/certs/dhparam.pem
|
||||||
|
SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH
|
||||||
|
|
||||||
|
# Use RewriteEngine to handle websocket connection upgrades
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{HTTP:Connection} Upgrade [NC]
|
||||||
|
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||||
|
RewriteRule /(.*) ws://127.0.0.1:8000/$1 [P,L]
|
||||||
|
|
||||||
|
<Location "/">
|
||||||
|
# preserve Host header to avoid cross-origin problems
|
||||||
|
ProxyPreserveHost on
|
||||||
|
# proxy to JupyterHub
|
||||||
|
ProxyPass http://127.0.0.1:8000/
|
||||||
|
ProxyPassReverse http://127.0.0.1:8000/
|
||||||
|
</Location>
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
254
docs/source/reference/config-sudo.md
Normal file
254
docs/source/reference/config-sudo.md
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# Run JupyterHub without root privileges using `sudo`
|
||||||
|
|
||||||
|
**Note:** Setting up `sudo` permissions involves many pieces of system
|
||||||
|
configuration. It is quite easy to get wrong and very difficult to debug.
|
||||||
|
Only do this if you are very sure you must.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
There are many Authenticators and Spawners available for JupyterHub. Some, such
|
||||||
|
as DockerSpawner or OAuthenticator, do not need any elevated permissions. This
|
||||||
|
document describes how to get the full default behavior of JupyterHub while
|
||||||
|
running notebook servers as real system users on a shared system without
|
||||||
|
running the Hub itself as root.
|
||||||
|
|
||||||
|
Since JupyterHub needs to spawn processes as other users, the simplest way
|
||||||
|
is to run it as root, spawning user servers with [setuid](http://linux.die.net/man/2/setuid).
|
||||||
|
But this isn't especially safe, because you have a process running on the
|
||||||
|
public web as root.
|
||||||
|
|
||||||
|
A **more prudent way** to run the server while preserving functionality is to
|
||||||
|
create a dedicated user with `sudo` access restricted to launching and
|
||||||
|
monitoring single-user servers.
|
||||||
|
|
||||||
|
## Create a user
|
||||||
|
|
||||||
|
To do this, first create a user that will run the Hub:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo useradd rhea
|
||||||
|
```
|
||||||
|
|
||||||
|
This user shouldn't have a login shell or password (possible with -r).
|
||||||
|
|
||||||
|
## Set up sudospawner
|
||||||
|
|
||||||
|
Next, you will need [sudospawner](https://github.com/jupyter/sudospawner)
|
||||||
|
to enable monitoring the single-user servers with sudo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pip install sudospawner
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we have to configure sudo to allow the Hub user (`rhea`) to launch
|
||||||
|
the sudospawner script on behalf of our hub users (here `zoe` and `wash`).
|
||||||
|
We want to confine these permissions to only what we really need.
|
||||||
|
|
||||||
|
## Edit `/etc/sudoers`
|
||||||
|
|
||||||
|
To do this we add to `/etc/sudoers` (use `visudo` for safe editing of sudoers):
|
||||||
|
|
||||||
|
- specify the list of users `JUPYTER_USERS` for whom `rhea` can spawn servers
|
||||||
|
- set the command `JUPYTER_CMD` that `rhea` can execute on behalf of users
|
||||||
|
- give `rhea` permission to run `JUPYTER_CMD` on behalf of `JUPYTER_USERS`
|
||||||
|
without entering a password
|
||||||
|
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# comma-separated whitelist of users that can spawn single-user servers
|
||||||
|
# this should include all of your Hub users
|
||||||
|
Runas_Alias JUPYTER_USERS = rhea, zoe, wash
|
||||||
|
|
||||||
|
# the command(s) the Hub can run on behalf of the above users without needing a password
|
||||||
|
# the exact path may differ, depending on how sudospawner was installed
|
||||||
|
Cmnd_Alias JUPYTER_CMD = /usr/local/bin/sudospawner
|
||||||
|
|
||||||
|
# actually give the Hub user permission to run the above command on behalf
|
||||||
|
# of the above users without prompting for a password
|
||||||
|
rhea ALL=(JUPYTER_USERS) NOPASSWD:JUPYTER_CMD
|
||||||
|
```
|
||||||
|
|
||||||
|
It might be useful to modifiy `secure_path` to add commands in path.
|
||||||
|
|
||||||
|
As an alternative to adding every user to the `/etc/sudoers` file, you can
|
||||||
|
use a group in the last line above, instead of `JUPYTER_USERS`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rhea ALL=(%jupyterhub) NOPASSWD:JUPYTER_CMD
|
||||||
|
```
|
||||||
|
|
||||||
|
If the `jupyterhub` group exists, there will be no need to edit `/etc/sudoers`
|
||||||
|
again. A new user will gain access to the application when added to the group:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ adduser -G jupyterhub newuser
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test `sudo` setup
|
||||||
|
|
||||||
|
Test that the new user doesn't need to enter a password to run the sudospawner
|
||||||
|
command.
|
||||||
|
|
||||||
|
This should prompt for your password to switch to rhea, but *not* prompt for
|
||||||
|
any password for the second switch. It should show some help output about
|
||||||
|
logging options:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo -u rhea sudo -n -u $USER /usr/local/bin/sudospawner --help
|
||||||
|
Usage: /usr/local/bin/sudospawner [OPTIONS]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
--help show this help information
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
And this should fail:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo -u rhea sudo -n -u $USER echo 'fail'
|
||||||
|
sudo: a password is required
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enable PAM for non-root
|
||||||
|
|
||||||
|
By default, [PAM authentication](http://en.wikipedia.org/wiki/Pluggable_authentication_module)
|
||||||
|
is used by JupyterHub. To use PAM, the process may need to be able to read
|
||||||
|
the shadow password database.
|
||||||
|
|
||||||
|
### Shadow group (Linux)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ls -l /etc/shadow
|
||||||
|
-rw-r----- 1 root shadow 2197 Jul 21 13:41 shadow
|
||||||
|
```
|
||||||
|
|
||||||
|
If there's already a shadow group, you are set. If its permissions are more like:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ls -l /etc/shadow
|
||||||
|
-rw------- 1 root wheel 2197 Jul 21 13:41 shadow
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you may want to add a shadow group, and make the shadow file group-readable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo groupadd shadow
|
||||||
|
$ sudo chgrp shadow /etc/shadow
|
||||||
|
$ sudo chmod g+r /etc/shadow
|
||||||
|
```
|
||||||
|
|
||||||
|
We want our new user to be able to read the shadow passwords, so add it to the shadow group:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo usermod -a -G shadow rhea
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want jupyterhub to serve pages on a restricted port (such as port 80 for http),
|
||||||
|
then you will need to give `node` permission to do so:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo setcap 'cap_net_bind_service=+ep' /usr/bin/node
|
||||||
|
```
|
||||||
|
However, you may want to further understand the consequences of this.
|
||||||
|
|
||||||
|
You may also be interested in limiting the amount of CPU any process can use
|
||||||
|
on your server. `cpulimit` is a useful tool that is available for many Linux
|
||||||
|
distributions' packaging system. This can be used to keep any user's process
|
||||||
|
from using too much CPU cycles. You can configure it accoring to [these
|
||||||
|
instructions](http://ubuntuforums.org/showthread.php?t=992706).
|
||||||
|
|
||||||
|
|
||||||
|
### Shadow group (FreeBSD)
|
||||||
|
|
||||||
|
**NOTE:** This has not been tested and may not work as expected.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ls -l /etc/spwd.db /etc/master.passwd
|
||||||
|
-rw------- 1 root wheel 2516 Aug 22 13:35 /etc/master.passwd
|
||||||
|
-rw------- 1 root wheel 40960 Aug 22 13:35 /etc/spwd.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a shadow group if there isn't one, and make the shadow file group-readable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo pw group add shadow
|
||||||
|
$ sudo chgrp shadow /etc/spwd.db
|
||||||
|
$ sudo chmod g+r /etc/spwd.db
|
||||||
|
$ sudo chgrp shadow /etc/master.passwd
|
||||||
|
$ sudo chmod g+r /etc/master.passwd
|
||||||
|
```
|
||||||
|
|
||||||
|
We want our new user to be able to read the shadow passwords, so add it to the
|
||||||
|
shadow group:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo pw user mod rhea -G shadow
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test that PAM works
|
||||||
|
|
||||||
|
We can verify that PAM is working, with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo -u rhea python3 -c "import pamela, getpass; print(pamela.authenticate('$USER', getpass.getpass()))"
|
||||||
|
Password: [enter your unix password]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Make a directory for JupyterHub
|
||||||
|
|
||||||
|
JupyterHub stores its state in a database, so it needs write access to a directory.
|
||||||
|
The simplest way to deal with this is to make a directory owned by your Hub user,
|
||||||
|
and use that as the CWD when launching the server.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo mkdir /etc/jupyterhub
|
||||||
|
$ sudo chown rhea /etc/jupyterhub
|
||||||
|
```
|
||||||
|
|
||||||
|
## Start jupyterhub
|
||||||
|
|
||||||
|
Finally, start the server as our newly configured user, `rhea`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cd /etc/jupyterhub
|
||||||
|
$ sudo -u rhea jupyterhub --JupyterHub.spawner_class=sudospawner.SudoSpawner
|
||||||
|
```
|
||||||
|
|
||||||
|
And try logging in.
|
||||||
|
|
||||||
|
### Troubleshooting: SELinux
|
||||||
|
|
||||||
|
If you still get a generic `Permission denied` `PermissionError`, it's possible SELinux is blocking you.
|
||||||
|
Here's how you can make a module to allow this.
|
||||||
|
First, put this in a file sudo_exec_selinux.te:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
module sudo_exec 1.1;
|
||||||
|
|
||||||
|
require {
|
||||||
|
type unconfined_t;
|
||||||
|
type sudo_exec_t;
|
||||||
|
class file { read entrypoint };
|
||||||
|
}
|
||||||
|
|
||||||
|
#============= unconfined_t ==============
|
||||||
|
allow unconfined_t sudo_exec_t:file entrypoint;
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run all of these commands as root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ checkmodule -M -m -o sudo_exec_selinux.mod sudo_exec_selinux.te
|
||||||
|
$ semodule_package -o sudo_exec_selinux.pp -m sudo_exec_selinux.mod
|
||||||
|
$ semodule -i sudo_exec_selinux.pp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting: PAM session errors
|
||||||
|
|
||||||
|
If the PAM authentication doesn't work and you see errors for
|
||||||
|
`login:session-auth`, or similar, considering updating to `master`
|
||||||
|
and/or incorporating this commit https://github.com/jupyter/jupyterhub/commit/40368b8f555f04ffdd662ffe99d32392a088b1d2
|
||||||
|
and configuration option, `c.PAMAuthenticator.open_sessions = False`.
|
147
docs/source/reference/config-user-env.md
Normal file
147
docs/source/reference/config-user-env.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Configuring user environments
|
||||||
|
|
||||||
|
Deploying JupyterHub means you are providing Jupyter notebook environments for
|
||||||
|
multiple users. Often, this includes a desire to configure the user
|
||||||
|
environment in some way.
|
||||||
|
|
||||||
|
Since the `jupyterhub-singleuser` server extends the standard Jupyter notebook
|
||||||
|
server, most configuration and documentation that applies to Jupyter Notebook
|
||||||
|
applies to the single-user environments. Configuration of user environments
|
||||||
|
typically does not occur through JupyterHub itself, but rather through system-
|
||||||
|
wide configuration of Jupyter, which is inherited by `jupyterhub-singleuser`.
|
||||||
|
|
||||||
|
**Tip:** When searching for configuration tips for JupyterHub user
|
||||||
|
environments, try removing JupyterHub from your search because there are a lot
|
||||||
|
more people out there configuring Jupyter than JupyterHub and the
|
||||||
|
configuration is the same.
|
||||||
|
|
||||||
|
This section will focus on user environments, including:
|
||||||
|
|
||||||
|
- Installing packages
|
||||||
|
- Configuring Jupyter and IPython
|
||||||
|
- Installing kernelspecs
|
||||||
|
- Using containers vs. multi-user hosts
|
||||||
|
|
||||||
|
|
||||||
|
## Installing packages
|
||||||
|
|
||||||
|
To make packages available to users, you generally will install packages
|
||||||
|
system-wide or in a shared environment.
|
||||||
|
|
||||||
|
This installation location should always be in the same environment that
|
||||||
|
`jupyterhub-singleuser` itself is installed in, and must be *readable and
|
||||||
|
executable* by your users. If you want users to be able to install additional
|
||||||
|
packages, it must also be *writable* by your users.
|
||||||
|
|
||||||
|
If you are using a standard system Python install, you would use:
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo python3 -m pip install numpy
|
||||||
|
```
|
||||||
|
|
||||||
|
to install the numpy package in the default system Python 3 environment
|
||||||
|
(typically `/usr/local`).
|
||||||
|
|
||||||
|
You may also use conda to install packages. If you do, you should make sure
|
||||||
|
that the conda environment has appropriate permissions for users to be able to
|
||||||
|
run Python code in the env.
|
||||||
|
|
||||||
|
|
||||||
|
## Configuring Jupyter and IPython
|
||||||
|
|
||||||
|
[Jupyter](https://jupyter-notebook.readthedocs.io/en/stable/config_overview.html)
|
||||||
|
and [IPython](https://ipython.readthedocs.io/en/stable/development/config.html)
|
||||||
|
have their own configuration systems.
|
||||||
|
|
||||||
|
As a JupyterHub administrator, you will typically want to install and configure
|
||||||
|
environments for all JupyterHub users. For example, you wish for each student in
|
||||||
|
a class to have the same user environment configuration.
|
||||||
|
|
||||||
|
Jupyter and IPython support **"system-wide"** locations for configuration, which
|
||||||
|
is the logical place to put global configuration that you want to affect all
|
||||||
|
users. It's generally more efficient to configure user environments "system-wide",
|
||||||
|
and it's a good idea to avoid creating files in users' home directories.
|
||||||
|
|
||||||
|
The typical locations for these config files are:
|
||||||
|
- **system-wide** in `/etc/{jupyter|ipython}`
|
||||||
|
- **env-wide** (environment wide) in `{sys.prefix}/etc/{jupyter|ipython}`.
|
||||||
|
|
||||||
|
### Example: Enable an extension system-wide
|
||||||
|
|
||||||
|
For example, to enable the `cython` IPython extension for all of your users,
|
||||||
|
create the file `/etc/ipython/ipython_config.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.InteractiveShellApp.extensions.append("cython")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Enable a Jupyter notebook configuration setting for all users
|
||||||
|
|
||||||
|
To enable Jupyter notebook's internal idle-shutdown behavior (requires
|
||||||
|
notebook ≥ 5.4), set the following in the `/etc/jupyter/jupyter_notebook_config.py`
|
||||||
|
file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# shutdown the server after no activity for an hour
|
||||||
|
c.NotebookApp.shutdown_no_activity_timeout = 60 * 60
|
||||||
|
# shutdown kernels after no activity for 20 minutes
|
||||||
|
c.MappingKernelManager.cull_idle_timeout = 20 * 60
|
||||||
|
# check for idle kernels every two minutes
|
||||||
|
c.MappingKernelManager.cull_interval = 2 * 60
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Installing kernelspecs
|
||||||
|
|
||||||
|
You may have multiple Jupyter kernels installed and want to make sure that
|
||||||
|
they are available to all of your users. This means installing kernelspecs
|
||||||
|
either system-wide (e.g. in /usr/local/) or in the `sys.prefix` of JupyterHub
|
||||||
|
itself.
|
||||||
|
|
||||||
|
Jupyter kernelspec installation is system wide by default, but some kernels
|
||||||
|
may default to installing kernelspecs in your home directory. These will need
|
||||||
|
to be moved system-wide to ensure that they are accessible.
|
||||||
|
|
||||||
|
You can see where your kernelspecs are with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jupyter kernelspec list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Installing kernels system-wide
|
||||||
|
|
||||||
|
Assuming I have a Python 2 and Python 3 environment that I want to make
|
||||||
|
sure are available, I can install their specs system-wide (in /usr/local) with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/path/to/python3 -m IPython kernel install --prefix=/usr/local
|
||||||
|
/path/to/python2 -m IPython kernel install --prefix=/usr/local
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Multi-user hosts vs. Containers
|
||||||
|
|
||||||
|
There are two broad categories of user environments that depend on what
|
||||||
|
Spawner you choose:
|
||||||
|
|
||||||
|
- Multi-user hosts (shared sytem)
|
||||||
|
- Container-based
|
||||||
|
|
||||||
|
How you configure user environments for each category can differ a bit
|
||||||
|
depending on what Spawner you are using.
|
||||||
|
|
||||||
|
The first category is a **shared system (multi-user host)** where
|
||||||
|
each user has a JupyterHub account and a home directory as well as being
|
||||||
|
a real system user. In this example, shared configuration and installation
|
||||||
|
must be in a 'system-wide' location, such as `/etc/` or `/usr/local`
|
||||||
|
or a custom prefix such as `/opt/conda`.
|
||||||
|
|
||||||
|
When JupyterHub uses **container-based** Spawners (e.g. KubeSpawner or
|
||||||
|
DockerSpawner), the 'system-wide' environment is really the container image
|
||||||
|
which you are using for users.
|
||||||
|
|
||||||
|
In both cases, you want to *avoid putting configuration in user home
|
||||||
|
directories* because users can change those configuration settings. Also,
|
||||||
|
home directories typically persist once they are created, so they are
|
||||||
|
difficult for admins to update later.
|
62
docs/source/reference/database.md
Normal file
62
docs/source/reference/database.md
Normal 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.
|
21
docs/source/reference/index.rst
Normal file
21
docs/source/reference/index.rst
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Technical Reference
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
technical-overview
|
||||||
|
websecurity
|
||||||
|
authenticators
|
||||||
|
spawners
|
||||||
|
services
|
||||||
|
proxy
|
||||||
|
rest
|
||||||
|
database
|
||||||
|
upgrading
|
||||||
|
templates
|
||||||
|
config-user-env
|
||||||
|
config-examples
|
||||||
|
config-ghoauth
|
||||||
|
config-proxy
|
||||||
|
config-sudo
|
181
docs/source/reference/proxy.md
Normal file
181
docs/source/reference/proxy.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# Writing a custom Proxy implementation
|
||||||
|
|
||||||
|
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 the
|
||||||
|
Hub manages by default as a subprocess (it can be run externally, as well, and
|
||||||
|
typically is in production deployments).
|
||||||
|
|
||||||
|
The upside to CHP, and why we use it by default, is that it's easy to install
|
||||||
|
and run (if you have nodejs, you are set!). The downsides are that it's a
|
||||||
|
single process and does not support any persistence of the routing table. So
|
||||||
|
if the proxy process dies, your whole JupyterHub instance is inaccessible
|
||||||
|
until the Hub notices, restarts the proxy, and restores the routing table. For
|
||||||
|
deployments that want to avoid such a single point of failure, or leverage
|
||||||
|
existing proxy infrastructure in their chosen deployment (such as Kubernetes
|
||||||
|
ingress objects), the Proxy API provides a way to do that.
|
||||||
|
|
||||||
|
In general, for a proxy to be usable by JupyterHub, it must:
|
||||||
|
|
||||||
|
1. support websockets without prior knowledge of the URL where websockets may
|
||||||
|
occur
|
||||||
|
2. support trie-based routing (i.e. allow different routes on `/foo` and
|
||||||
|
`/foo/bar` and route based on specificity)
|
||||||
|
3. adding or removing a route should not cause existing connections to drop
|
||||||
|
|
||||||
|
Optionally, if the JupyterHub deployment is to use host-based routing,
|
||||||
|
the Proxy must additionally support routing based on the Host of the request.
|
||||||
|
|
||||||
|
## Subclassing Proxy
|
||||||
|
|
||||||
|
To start, any Proxy implementation should subclass the base Proxy class,
|
||||||
|
as is done with custom Spawners and Authenticators.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from jupyterhub.proxy import Proxy
|
||||||
|
|
||||||
|
class MyProxy(Proxy):
|
||||||
|
"""My Proxy implementation"""
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Starting and stopping the proxy
|
||||||
|
|
||||||
|
If your proxy should be launched when the Hub starts, you must define how
|
||||||
|
to start and stop your proxy:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from tornado import gen
|
||||||
|
class MyProxy(Proxy):
|
||||||
|
...
|
||||||
|
@gen.coroutine
|
||||||
|
def start(self):
|
||||||
|
"""Start the proxy"""
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the proxy"""
|
||||||
|
```
|
||||||
|
|
||||||
|
These methods **may** be coroutines.
|
||||||
|
|
||||||
|
`c.Proxy.should_start` is a configurable flag that determines whether the
|
||||||
|
Hub should call these methods when the Hub itself starts and stops.
|
||||||
|
|
||||||
|
### Purely external proxies
|
||||||
|
|
||||||
|
Probably most custom proxies will be externally managed,
|
||||||
|
such as Kubernetes ingress-based implementations.
|
||||||
|
In this case, you do not need to define `start` and `stop`.
|
||||||
|
To disable the methods, you can define `should_start = False` at the class level:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyProxy(Proxy):
|
||||||
|
should_start = False
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
At its most basic, a Proxy implementation defines a mechanism to add, remove,
|
||||||
|
and retrieve routes. A proxy that implements these three methods is complete.
|
||||||
|
Each of these methods **may** be a coroutine.
|
||||||
|
|
||||||
|
**Definition:** routespec
|
||||||
|
|
||||||
|
A routespec, which will appear in these methods, is a string describing a
|
||||||
|
route to be proxied, such as `/user/name/`. A routespec will:
|
||||||
|
|
||||||
|
1. always end with `/`
|
||||||
|
2. always start with `/` if it is a path-based route `/proxy/path/`
|
||||||
|
3. precede the leading `/` with a host for host-based routing, e.g.
|
||||||
|
`host.tld/proxy/path/`
|
||||||
|
|
||||||
|
### Adding a route
|
||||||
|
|
||||||
|
When adding a route, JupyterHub may pass a JSON-serializable dict as a `data`
|
||||||
|
argument that should be attacked to the proxy route. When that route is
|
||||||
|
retrieved, the `data` argument should be returned as well. If your proxy
|
||||||
|
implementation doesn't support storing data attached to routes, then your
|
||||||
|
Python wrapper may have to handle storing the `data` piece itself, e.g in a
|
||||||
|
simple file or database.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@gen.coroutine
|
||||||
|
def add_route(self, routespec, target, data):
|
||||||
|
"""Proxy `routespec` to `target`.
|
||||||
|
|
||||||
|
Store `data` associated with the routespec
|
||||||
|
for retrieval later.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Adding a route for a user looks like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
proxy.add_route('/user/pgeorgiou/', 'http://127.0.0.1:1227',
|
||||||
|
{'user': 'pgeorgiou'})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removing routes
|
||||||
|
|
||||||
|
`delete_route()` is given a routespec to delete. If there is no such route,
|
||||||
|
`delete_route` should still succeed, but a warning may be issued.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@gen.coroutine
|
||||||
|
def delete_route(self, routespec):
|
||||||
|
"""Delete the route"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retrieving routes
|
||||||
|
|
||||||
|
For retrieval, you only *need* to implement a single method that retrieves all
|
||||||
|
routes. The return value for this function should be a dictionary, keyed by
|
||||||
|
`routespect`, of dicts whose keys are the same three arguments passed to
|
||||||
|
`add_route` (`routespec`, `target`, `data`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@gen.coroutine
|
||||||
|
def get_all_routes(self):
|
||||||
|
"""Return all routes, keyed by routespec"""
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'/proxy/path/': {
|
||||||
|
'routespec': '/proxy/path/',
|
||||||
|
'target': 'http://...',
|
||||||
|
'data': {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Note on activity tracking
|
||||||
|
|
||||||
|
JupyterHub can track activity of users, for use in services such as culling
|
||||||
|
idle servers. As of JupyterHub 0.8, this activity tracking is the
|
||||||
|
responsibility of the proxy. If your proxy implementation can track activity
|
||||||
|
to endpoints, it may add a `last_activity` key to the `data` of routes
|
||||||
|
retrieved in `.get_all_routes()`. If present, the value of `last_activity`
|
||||||
|
should be an [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) UTC date
|
||||||
|
string:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'/user/pgeorgiou/': {
|
||||||
|
'routespec': '/user/pgeorgiou/',
|
||||||
|
'target': 'http://127.0.0.1:1227',
|
||||||
|
'data': {
|
||||||
|
'user': 'pgeourgiou',
|
||||||
|
'last_activity': '2017-10-03T10:33:49.570Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the proxy does not track activity, then only activity to the Hub itself is
|
||||||
|
tracked, and services such as cull-idle will not work.
|
||||||
|
|
||||||
|
Now that `notebook-5.0` tracks activity internally, we can retrieve activity
|
||||||
|
information from the single-user servers instead, removing the need to track
|
||||||
|
activity in the proxy. But this is not yet implemented in JupyterHub 0.8.0.
|
182
docs/source/reference/rest.md
Normal file
182
docs/source/reference/rest.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# Using JupyterHub's REST API
|
||||||
|
|
||||||
|
This section will give you information on:
|
||||||
|
|
||||||
|
- what you can do with the API
|
||||||
|
- create an API token
|
||||||
|
- add API tokens to the config files
|
||||||
|
- make an API request programmatically using the requests library
|
||||||
|
- learn more about JupyterHub's API
|
||||||
|
|
||||||
|
## What you can do with the API
|
||||||
|
|
||||||
|
Using the [JupyterHub REST API][], you can perform actions on the Hub,
|
||||||
|
such as:
|
||||||
|
|
||||||
|
- checking which users are active
|
||||||
|
- adding or removing users
|
||||||
|
- stopping or starting single user notebook servers
|
||||||
|
- authenticating services
|
||||||
|
|
||||||
|
A [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)
|
||||||
|
API provides a standard way for users to get and send information to the
|
||||||
|
Hub.
|
||||||
|
|
||||||
|
## Create an API token
|
||||||
|
|
||||||
|
To send requests using JupyterHub API, you must pass an API token with
|
||||||
|
the request.
|
||||||
|
|
||||||
|
As of [version 0.6.0](../changelog.html), the preferred way of
|
||||||
|
generating an API token is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
This `openssl` command generates a potential token that can then be
|
||||||
|
added to JupyterHub using `.api_tokens` configuration setting in
|
||||||
|
`jupyterhub_config.py`.
|
||||||
|
|
||||||
|
Alternatively, use the `jupyterhub token` command to generate a token
|
||||||
|
for a specific hub user by passing the 'username':
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jupyterhub token <username>
|
||||||
|
```
|
||||||
|
|
||||||
|
This command generates a random string to use as a token and registers
|
||||||
|
it for the given user with the Hub's database.
|
||||||
|
|
||||||
|
In [version 0.8.0](../changelog.html), a TOKEN request page for
|
||||||
|
generating an API token is available from the JupyterHub user interface:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Add API tokens to the config file
|
||||||
|
|
||||||
|
You may also add a dictionary of API tokens and usernames to the hub's
|
||||||
|
configuration file, `jupyterhub_config.py` (note that
|
||||||
|
the **key** is the 'secret-token' while the **value** is the 'username'):
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.api_tokens = {
|
||||||
|
'secret-token': 'username',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Make an API request
|
||||||
|
|
||||||
|
To authenticate your requests, pass the API token in the request's
|
||||||
|
Authorization header.
|
||||||
|
|
||||||
|
### Use requests
|
||||||
|
|
||||||
|
Using the popular Python [requests](http://docs.python-requests.org/en/master/)
|
||||||
|
library, here's example code to make an API request for the users of a JupyterHub
|
||||||
|
deployment. An API GET request is made, and the request sends an API token for
|
||||||
|
authorization. The response contains information about the users:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
api_url = 'http://127.0.0.1:8081/hub/api'
|
||||||
|
|
||||||
|
r = requests.get(api_url + '/users',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token %s' % token,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
users = r.json()
|
||||||
|
```
|
||||||
|
|
||||||
|
This example provides a slightly more complicated request, yet the
|
||||||
|
process is very similar:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
api_url = 'http://127.0.0.1:8081/hub/api'
|
||||||
|
|
||||||
|
data = {'name': 'mygroup', 'users': ['user1', 'user2']}
|
||||||
|
|
||||||
|
r = requests.post(api_url + '/groups/formgrade-data301/users',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token %s' % token,
|
||||||
|
},
|
||||||
|
json=data
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
r.json()
|
||||||
|
```
|
||||||
|
|
||||||
|
The same API token can also authorize access to the [Jupyter Notebook REST API][]
|
||||||
|
provided by notebook servers managed by JupyterHub if one of the following is true:
|
||||||
|
|
||||||
|
1. The token is for the same user as the owner of the notebook
|
||||||
|
2. The token is tied to an admin user or service **and** `c.JupyterHub.admin_access` is set to `True`
|
||||||
|
|
||||||
|
## Enabling users to spawn multiple named-servers via the API
|
||||||
|
|
||||||
|
With JupyterHub version 0.8, support for multiple servers per user has landed.
|
||||||
|
Prior to that, each user could only launch a single default server via the API
|
||||||
|
like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "Authorization: token <token>" "http://127.0.0.1:8081/hub/api/users/<user>/server"
|
||||||
|
```
|
||||||
|
|
||||||
|
With the named-server functionality, it's now possible to launch more than one
|
||||||
|
specifically named servers against a given user. This could be used, for instance,
|
||||||
|
to launch each server based on a different image.
|
||||||
|
|
||||||
|
First you must enable named-servers by including the following setting in the `jupyterhub_config.py` file.
|
||||||
|
|
||||||
|
`c.JupyterHub.allow_named_servers = True`
|
||||||
|
|
||||||
|
If using the [zero-to-jupyterhub-k8s](https://github.com/jupyterhub/zero-to-jupyterhub-k8s) set-up to run JupyterHub,
|
||||||
|
then instead of editing the `jupyterhub_config.py` file directly, you could pass
|
||||||
|
the following as part of the `config.yaml` file, as per the [tutorial](https://zero-to-jupyterhub.readthedocs.io/en/latest/):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hub:
|
||||||
|
extraConfig: |
|
||||||
|
c.JupyterHub.allow_named_servers = True
|
||||||
|
```
|
||||||
|
|
||||||
|
With that setting in place, a new named-server is activated like this:
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "Authorization: token <token>" "http://127.0.0.1:8081/hub/api/users/<user>/servers/<serverA>"
|
||||||
|
curl -X POST -H "Authorization: token <token>" "http://127.0.0.1:8081/hub/api/users/<user>/servers/<serverB>"
|
||||||
|
```
|
||||||
|
|
||||||
|
The same servers can be stopped by substituting `DELETE` for `POST` above.
|
||||||
|
|
||||||
|
### Some caveats for using named-servers
|
||||||
|
|
||||||
|
The named-server capabilities are not fully implemented for JupyterHub as yet.
|
||||||
|
While it's possible to start/stop a server via the API, the UI on the
|
||||||
|
JupyterHub control-panel has not been implemented, and so it may not be obvious
|
||||||
|
to those viewing the panel that a named-server may be running for a given user.
|
||||||
|
|
||||||
|
For named-servers via the API to work, the spawner used to spawn these servers
|
||||||
|
will need to be able to handle the case of multiple servers per user and ensure
|
||||||
|
uniqueness of names, particularly if servers are spawned via docker containers
|
||||||
|
or kubernetes pods.
|
||||||
|
|
||||||
|
|
||||||
|
## Learn more about the API
|
||||||
|
|
||||||
|
You can see the full [JupyterHub REST API][] for details. This REST API Spec can
|
||||||
|
be viewed in a more [interactive style on swagger's petstore][].
|
||||||
|
Both resources contain the same information and differ only in its display.
|
||||||
|
Note: The Swagger specification is being renamed the [OpenAPI Initiative][].
|
||||||
|
|
||||||
|
[interactive style on swagger's petstore]: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default
|
||||||
|
[OpenAPI Initiative]: https://www.openapis.org/
|
||||||
|
[JupyterHub REST API]: ../_static/rest-api/index.html
|
||||||
|
[Jupyter Notebook REST API]: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml
|
@@ -4,18 +4,18 @@ With version 0.7, JupyterHub adds support for **Services**.
|
|||||||
|
|
||||||
This section provides the following information about Services:
|
This section provides the following information about Services:
|
||||||
|
|
||||||
- [Definition of a Service](services.html#definition-of-a-service)
|
- [Definition of a Service](#definition-of-a-service)
|
||||||
- [Properties of a Service](services.html#properties-of-a-service)
|
- [Properties of a Service](#properties-of-a-service)
|
||||||
- [Hub-Managed Services](services.html#hub-managed-services)
|
- [Hub-Managed Services](#hub-managed-services)
|
||||||
- [Launching a Hub-Managed Service](services.html#launching-a-hub-managed-service)
|
- [Launching a Hub-Managed Service](#launching-a-hub-managed-service)
|
||||||
- [Externally-Managed Services](services.html#externally-managed-services)
|
- [Externally-Managed Services](#externally-managed-services)
|
||||||
- [Writing your own Services](services.html#writing-your-own-services)
|
- [Writing your own Services](#writing-your-own-services)
|
||||||
- [Hub Authentication and Services](services.html#hub-authentication-and-services)
|
- [Hub Authentication and Services](#hub-authentication-and-services)
|
||||||
|
|
||||||
## Definition of a Service
|
## Definition of a Service
|
||||||
|
|
||||||
When working with JupyterHub, a **Service** is defined as a process that interacts
|
When working with JupyterHub, a **Service** is defined as a process that interacts
|
||||||
with the Hub's REST API. A Service may perform a specific or
|
with the Hub's REST API. A Service may perform a specific
|
||||||
action or task. For example, the following tasks can each be a unique Service:
|
action or task. For example, the following tasks can each be a unique Service:
|
||||||
|
|
||||||
- shutting down individuals' single user notebook servers that have been idle
|
- shutting down individuals' single user notebook servers that have been idle
|
||||||
@@ -45,6 +45,8 @@ A Service may have the following properties:
|
|||||||
- `url: str (default - None)` - The URL where the service is/should be. If a
|
- `url: str (default - None)` - The URL where the service is/should be. If a
|
||||||
url is specified for where the Service runs its own web server,
|
url is specified for where the Service runs its own web server,
|
||||||
the service will be added to the proxy at `/services/:name`
|
the service will be added to the proxy at `/services/:name`
|
||||||
|
- `api_token: str (default - None)` - For Externally-Managed Services you need to specify
|
||||||
|
an API token to perform API requests to the Hub
|
||||||
|
|
||||||
If a service is also to be managed by the Hub, it has a few extra options:
|
If a service is also to be managed by the Hub, it has a few extra options:
|
||||||
|
|
||||||
@@ -176,7 +178,13 @@ When you run a service that has a url, it will be accessible under a
|
|||||||
your service to route proxied requests properly, it must take
|
your service to route proxied requests properly, it must take
|
||||||
`JUPYTERHUB_SERVICE_PREFIX` into account when routing requests. For example, a
|
`JUPYTERHUB_SERVICE_PREFIX` into account when routing requests. For example, a
|
||||||
web service would normally service its root handler at `'/'`, but the proxied
|
web service would normally service its root handler at `'/'`, but the proxied
|
||||||
service would need to serve `JUPYTERHUB_SERVICE_PREFIX + '/'`.
|
service would need to serve `JUPYTERHUB_SERVICE_PREFIX`.
|
||||||
|
|
||||||
|
Note that `JUPYTERHUB_SERVICE_PREFIX` will contain a trailing slash. This must
|
||||||
|
be taken into consideration when creating the service routes. If you include an
|
||||||
|
extra slash you might get unexpected behavior. For example if your service has a
|
||||||
|
`/foo` endpoint, the route would be `JUPYTERHUB_SERVICE_PREFIX + foo`, and
|
||||||
|
`/foo/bar` would be `JUPYTERHUB_SERVICE_PREFIX + foo/bar`.
|
||||||
|
|
||||||
## Hub Authentication and Services
|
## Hub Authentication and Services
|
||||||
|
|
||||||
@@ -197,8 +205,10 @@ To use HubAuth, you must set the `.api_token`, either programmatically when cons
|
|||||||
or via the `JUPYTERHUB_API_TOKEN` environment variable.
|
or via the `JUPYTERHUB_API_TOKEN` environment variable.
|
||||||
|
|
||||||
Most of the logic for authentication implementation is found in the
|
Most of the logic for authentication implementation is found in the
|
||||||
[`HubAuth.user_for_cookie`](services.auth.html#jupyterhub.services.auth.HubAuth.user_for_cookie)
|
[`HubAuth.user_for_cookie`][HubAuth.user_for_cookie]
|
||||||
method, which makes a request of the Hub, and returns:
|
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
|
- None, if no user could be identified, or
|
||||||
- a dict of the following form:
|
- a dict of the following form:
|
||||||
@@ -250,8 +260,11 @@ def authenticated(f):
|
|||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
cookie = request.cookies.get(auth.cookie_name)
|
cookie = request.cookies.get(auth.cookie_name)
|
||||||
|
token = request.headers.get(auth.auth_header_name)
|
||||||
if cookie:
|
if cookie:
|
||||||
user = auth.user_for_cookie(cookie)
|
user = auth.user_for_cookie(cookie)
|
||||||
|
elif token:
|
||||||
|
user = auth.user_for_token(token)
|
||||||
else:
|
else:
|
||||||
user = None
|
user = None
|
||||||
if user:
|
if user:
|
||||||
@@ -262,7 +275,7 @@ def authenticated(f):
|
|||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
@app.route(prefix + '/')
|
@app.route(prefix)
|
||||||
@authenticated
|
@authenticated
|
||||||
def whoami(user):
|
def whoami(user):
|
||||||
return Response(
|
return Response(
|
||||||
@@ -346,12 +359,16 @@ and taking note of the following process:
|
|||||||
```
|
```
|
||||||
|
|
||||||
An example of using an Externally-Managed Service and authentication is
|
An example of using an Externally-Managed Service and authentication is
|
||||||
[nbviewer](https://github.com/jupyter/nbviewer#securing-the-notebook-viewer),
|
in [nbviewer README][nbviewer example] section on securing the notebook viewer,
|
||||||
and an example of its configuration is found [here](https://github.com/jupyter/nbviewer/blob/master/nbviewer/providers/base.py#L94).
|
and an example of its configuration is found [here](https://github.com/jupyter/nbviewer/blob/master/nbviewer/providers/base.py#L94).
|
||||||
nbviewer can also be run as a Hub-Managed Service as described [here](https://github.com/jupyter/nbviewer#securing-the-notebook-viewer).
|
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/
|
[requests]: http://docs.python-requests.org/en/master/
|
||||||
[services_auth]: api/services.auth.html
|
[services_auth]: ../api/services.auth.html
|
||||||
[HubAuth]: api/services.auth.html#jupyterhub.services.auth.HubAuth
|
[HubAuth]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth
|
||||||
[HubAuthenticated]: api/services.auth.html#jupyterhub.services.auth.HubAuthenticated
|
[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
|
@@ -36,8 +36,7 @@ Some examples include:
|
|||||||
Information about the user can be retrieved from `self.user`,
|
Information about the user can be retrieved from `self.user`,
|
||||||
an object encapsulating the user's name, authentication, and server info.
|
an object encapsulating the user's name, authentication, and server info.
|
||||||
|
|
||||||
When `Spawner.start` returns, it should have stored the IP and port
|
The return value of `Spawner.start` should be the (ip, port) of the running server.
|
||||||
of the single-user server in `self.user.server`.
|
|
||||||
|
|
||||||
**NOTE:** When writing coroutines, *never* `yield` in between a database change and a commit.
|
**NOTE:** When writing coroutines, *never* `yield` in between a database change and a commit.
|
||||||
|
|
||||||
@@ -45,10 +44,19 @@ Most `Spawner.start` functions will look similar to this example:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
def start(self):
|
def start(self):
|
||||||
self.user.server.ip = 'localhost' # or other host or IP address, as seen by the Hub
|
self.ip = '127.0.0.1'
|
||||||
self.user.server.port = 1234 # port selected somehow
|
self.port = random_port()
|
||||||
self.db.commit() # always commit before yield, if modifying db values
|
# get environment variables,
|
||||||
yield self._actually_start_server_somehow()
|
# 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)
|
||||||
```
|
```
|
||||||
|
|
||||||
When `Spawner.start` returns, the single-user server process should actually be running,
|
When `Spawner.start` returns, the single-user server process should actually be running,
|
||||||
@@ -114,7 +122,7 @@ This feature is enabled by setting `Spawner.options_form`, which is an HTML form
|
|||||||
inserted unmodified into the spawn form.
|
inserted unmodified into the spawn form.
|
||||||
If the `Spawner.options_form` is defined, when a user tries to start their server, they will be directed to a form page, like this:
|
If the `Spawner.options_form` is defined, when a user tries to start their server, they will be directed to a form page, like this:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
If `Spawner.options_form` is undefined, the user's server is spawned directly, and no spawn page is rendered.
|
If `Spawner.options_form` is undefined, the user's server is spawned directly, and no spawn page is rendered.
|
||||||
|
|
||||||
@@ -171,9 +179,12 @@ If you are interested in building a custom spawner, you can read [this tutorial]
|
|||||||
Some spawners of the single-user notebook servers allow setting limits or
|
Some spawners of the single-user notebook servers allow setting limits or
|
||||||
guarantees on resources, such as CPU and memory. To provide a consistent
|
guarantees on resources, such as CPU and memory. To provide a consistent
|
||||||
experience for sysadmins and users, we provide a standard way to set and
|
experience for sysadmins and users, we provide a standard way to set and
|
||||||
discover these resource limits and guarantees, such as for memory and CPU. For
|
discover these resource limits and guarantees, such as for memory and CPU.
|
||||||
the limits and guarantees to be useful, the spawner must implement support for
|
For the limits and guarantees to be useful, **the spawner must implement
|
||||||
them.
|
support for them**. For example, LocalProcessSpawner, the default
|
||||||
|
spawner, does not support limits and guarantees. One of the spawners
|
||||||
|
that supports limits and guarantees is the `systemdspawner`.
|
||||||
|
|
||||||
|
|
||||||
### Memory Limits & Guarantees
|
### Memory Limits & Guarantees
|
||||||
|
|
||||||
@@ -191,8 +202,8 @@ to provide a guarantee that at minimum this much memory will always be
|
|||||||
available for the single-user notebook server to use. The environment variable
|
available for the single-user notebook server to use. The environment variable
|
||||||
`MEM_GUARANTEE` will also be set in the single-user notebook server.
|
`MEM_GUARANTEE` will also be set in the single-user notebook server.
|
||||||
|
|
||||||
The spawner's underlying system or cluster is responsible for enforcing these
|
**The spawner's underlying system or cluster is responsible for enforcing these
|
||||||
limits and providing these guarantees. If these values are set to `None`, no
|
limits and providing these guarantees.** If these values are set to `None`, no
|
||||||
limits or guarantees are provided, and no environment values are set.
|
limits or guarantees are provided, and no environment values are set.
|
||||||
|
|
||||||
### CPU Limits & Guarantees
|
### CPU Limits & Guarantees
|
||||||
@@ -209,6 +220,6 @@ higher priority applications might be taking up CPU.
|
|||||||
guarantee for CPU usage. The environment variable `CPU_GUARANTEE` will be set
|
guarantee for CPU usage. The environment variable `CPU_GUARANTEE` will be set
|
||||||
in the single-user notebook server when a guarantee is being provided.
|
in the single-user notebook server when a guarantee is being provided.
|
||||||
|
|
||||||
The spawner's underlying system or cluster is responsible for enforcing these
|
**The spawner's underlying system or cluster is responsible for enforcing these
|
||||||
limits and providing these guarantees. If these values are set to `None`, no
|
limits and providing these guarantees.** If these values are set to `None`, no
|
||||||
limits or guarantees are provided, and no environment values are set.
|
limits or guarantees are provided, and no environment values are set.
|
133
docs/source/reference/technical-overview.md
Normal file
133
docs/source/reference/technical-overview.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Technical Overview
|
||||||
|
|
||||||
|
The **Technical Overview** section gives you a high-level view of:
|
||||||
|
|
||||||
|
- JupyterHub's Subsystems: Hub, Proxy, Single-User Notebook Server
|
||||||
|
- how the subsystems interact
|
||||||
|
- the process from JupyterHub access to user login
|
||||||
|
- JupyterHub's default behavior
|
||||||
|
- customizing JupyterHub
|
||||||
|
|
||||||
|
The goal of this section is to share a deeper technical understanding of
|
||||||
|
JupyterHub and how it works.
|
||||||
|
|
||||||
|
## The Subsystems: Hub, Proxy, Single-User Notebook Server
|
||||||
|
|
||||||
|
JupyterHub is a set of processes that together provide a single user Jupyter
|
||||||
|
Notebook server for each person in a group. Three major subsystems are started
|
||||||
|
by the `jupyterhub` command line program:
|
||||||
|
|
||||||
|
- **Hub** (Python/Tornado): manages user accounts, authentication, and
|
||||||
|
coordinates Single User Notebook Servers using a Spawner.
|
||||||
|
|
||||||
|
- **Proxy**: the public facing part of JupyterHub that uses a dynamic proxy
|
||||||
|
to route HTTP requests to the Hub and Single User Notebook Servers.
|
||||||
|
[configurable http proxy](https://github.com/jupyterhub/configurable-http-proxy)
|
||||||
|
(node-http-proxy) is the default proxy.
|
||||||
|
|
||||||
|
- **Single-User Notebook Server** (Python/Tornado): a dedicated,
|
||||||
|
single-user, Jupyter Notebook server is started for each user on the system
|
||||||
|
when the user logs in. The object that starts the single-user notebook
|
||||||
|
servers is called a **Spawner**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## How the Subsystems Interact
|
||||||
|
|
||||||
|
Users access JupyterHub through a web browser, by going to the IP address or
|
||||||
|
the domain name of the server.
|
||||||
|
|
||||||
|
The basic principles of operation are:
|
||||||
|
|
||||||
|
- The Hub spawns the proxy (in the default JupyterHub configuration)
|
||||||
|
- The proxy forwards all requests to the Hub by default
|
||||||
|
- The Hub handles login, and spawns single-user notebook servers on demand
|
||||||
|
- The Hub configures the proxy to forward url prefixes to single-user notebook
|
||||||
|
servers
|
||||||
|
|
||||||
|
The proxy is the only process that listens on a public interface. The Hub sits
|
||||||
|
behind the proxy at `/hub`. Single-user servers sit behind the proxy at
|
||||||
|
`/user/[username]`.
|
||||||
|
|
||||||
|
Different **[authenticators](./authenticators.html)** control access
|
||||||
|
to JupyterHub. The default one (PAM) uses the user accounts on the server where
|
||||||
|
JupyterHub is running. If you use this, you will need to create a user account
|
||||||
|
on the system for each user on your team. Using other authenticators, you can
|
||||||
|
allow users to sign in with e.g. a GitHub account, or with any single-sign-on
|
||||||
|
system your organization has.
|
||||||
|
|
||||||
|
Next, **[spawners](./spawners.html)** control how JupyterHub starts
|
||||||
|
the individual notebook server for each user. The default spawner will
|
||||||
|
start a notebook server on the same machine running under their system username.
|
||||||
|
The other main option is to start each server in a separate container, often
|
||||||
|
using Docker.
|
||||||
|
|
||||||
|
## The Process from JupyterHub Access to User Login
|
||||||
|
|
||||||
|
When a user accesses JupyterHub, the following events take place:
|
||||||
|
|
||||||
|
- Login data is handed to the [Authenticator](./authenticators.html) instance for
|
||||||
|
validation
|
||||||
|
- The Authenticator returns the username if the login information is valid
|
||||||
|
- A single-user notebook server instance is [spawned](./spawners.html) for the
|
||||||
|
logged-in user
|
||||||
|
- When the single-user notebook server starts, the proxy is notified to forward
|
||||||
|
requests to `/user/[username]/*` to the single-user notebook server.
|
||||||
|
- A cookie is set on `/hub/`, containing an encrypted token. (Prior to version
|
||||||
|
0.8, a cookie for `/user/[username]` was used too.)
|
||||||
|
- The browser is redirected to `/user/[username]`, and the request is handled by
|
||||||
|
the single-user notebook server.
|
||||||
|
|
||||||
|
The single-user server identifies the user with the Hub via OAuth:
|
||||||
|
|
||||||
|
- on request, the single-user server checks a cookie
|
||||||
|
- if no cookie is set, redirect to the Hub for verification via OAuth
|
||||||
|
- after verification at the Hub, the browser is redirected back to the
|
||||||
|
single-user server
|
||||||
|
- the token is verified and stored in a cookie
|
||||||
|
- if no user is identified, the browser is redirected back to `/hub/login`
|
||||||
|
|
||||||
|
## Default Behavior
|
||||||
|
|
||||||
|
By default, the **Proxy** listens on all public interfaces on port 8000.
|
||||||
|
Thus you can reach JupyterHub through either:
|
||||||
|
|
||||||
|
- `http://localhost:8000`
|
||||||
|
- or any other public IP or domain pointing to your system.
|
||||||
|
|
||||||
|
In their default configuration, the other services, the **Hub** and
|
||||||
|
**Single-User Notebook Servers**, all communicate with each other on localhost
|
||||||
|
only.
|
||||||
|
|
||||||
|
By default, starting JupyterHub will write two files to disk in the current
|
||||||
|
working directory:
|
||||||
|
|
||||||
|
- `jupyterhub.sqlite` is the SQLite database containing all of the state of the
|
||||||
|
**Hub**. This file allows the **Hub** to remember which users are running and
|
||||||
|
where, as well as storing other information enabling you to restart parts of
|
||||||
|
JupyterHub separately. It is important to note that this database contains
|
||||||
|
**no** sensitive information other than **Hub** usernames.
|
||||||
|
- `jupyterhub_cookie_secret` is the encryption key used for securing cookies.
|
||||||
|
This file needs to persist so that a **Hub** server restart will avoid
|
||||||
|
invalidating cookies. Conversely, deleting this file and restarting the server
|
||||||
|
effectively invalidates all login cookies. The cookie secret file is discussed
|
||||||
|
in the [Cookie Secret section of the Security Settings document](../getting-started/security-basics.html).
|
||||||
|
|
||||||
|
The location of these files can be specified via configuration settings. It is
|
||||||
|
recommended that these files be stored in standard UNIX filesystem locations,
|
||||||
|
such as `/etc/jupyterhub` for all configuration files and `/srv/jupyterhub` for
|
||||||
|
all security and runtime files.
|
||||||
|
|
||||||
|
## Customizing JupyterHub
|
||||||
|
|
||||||
|
There are two basic extension points for JupyterHub:
|
||||||
|
|
||||||
|
- How users are authenticated by [Authenticators](./authenticators.html)
|
||||||
|
- How user's single-user notebook server processes are started by
|
||||||
|
[Spawners](./spawners.html)
|
||||||
|
|
||||||
|
Each is governed by a customizable class, and JupyterHub ships with basic
|
||||||
|
defaults for each.
|
||||||
|
|
||||||
|
To enable custom authentication and/or spawning, subclass `Authenticator` or
|
||||||
|
`Spawner`, and override the relevant methods.
|
93
docs/source/reference/templates.md
Normal file
93
docs/source/reference/templates.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Working with templates and UI
|
||||||
|
|
||||||
|
The pages of the JupyterHub application are generated from
|
||||||
|
[Jinja](http://jinja.pocoo.org/) templates. These allow the header, for
|
||||||
|
example, to be defined once and incorporated into all pages. By providing
|
||||||
|
your own templates, you can have complete control over JupyterHub's
|
||||||
|
appearance.
|
||||||
|
|
||||||
|
## Custom Templates
|
||||||
|
|
||||||
|
JupyterHub will look for custom templates in all of the paths in the
|
||||||
|
`JupyterHub.template_paths` configuration option, falling back on the
|
||||||
|
[default templates](https://github.com/jupyterhub/jupyterhub/tree/master/share/jupyterhub/templates)
|
||||||
|
if no custom template with that name is found. This fallback
|
||||||
|
behavior is new in version 0.9; previous versions searched only those paths
|
||||||
|
explicitly included in `template_paths`. You may override as many
|
||||||
|
or as few templates as you desire.
|
||||||
|
|
||||||
|
## Extending Templates
|
||||||
|
|
||||||
|
Jinja provides a mechanism to [extend templates](http://jinja.pocoo.org/docs/2.10/templates/#template-inheritance).
|
||||||
|
A base template can define a `block`, and child templates can replace or
|
||||||
|
supplement the material in the block. The
|
||||||
|
[JupyterHub templates](https://github.com/jupyterhub/jupyterhub/tree/master/share/jupyterhub/templates)
|
||||||
|
make extensive use of blocks, which allows you to customize parts of the
|
||||||
|
interface easily.
|
||||||
|
|
||||||
|
In general, a child template can extend a base template, `base.html`, by beginning with:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% extends "base.html" %}
|
||||||
|
```
|
||||||
|
|
||||||
|
This works, unless you are trying to extend the default template for the same
|
||||||
|
file name. Starting in version 0.9, you may refer to the base file with a
|
||||||
|
`templates/` prefix. Thus, if you are writing a custom `base.html`, start the
|
||||||
|
file with this block:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% extends "templates/base.html" %}
|
||||||
|
```
|
||||||
|
|
||||||
|
By defining `block`s with same name as in the base template, child templates
|
||||||
|
can replace those sections with custom content. The content from the base
|
||||||
|
template can be included with the `{{ super() }}` directive.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
To add an additional message to the spawn-pending page, below the existing
|
||||||
|
text about the server starting up, place this content in a file named
|
||||||
|
`spawn_pending.html` in a directory included in the
|
||||||
|
`JupyterHub.template_paths` configuration option.
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% extends "templates/spawn_pending.html" %}
|
||||||
|
|
||||||
|
{% block message %}
|
||||||
|
{{ super() }}
|
||||||
|
<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).
|
@@ -2,33 +2,25 @@
|
|||||||
|
|
||||||
From time to time, you may wish to upgrade JupyterHub to take advantage
|
From time to time, you may wish to upgrade JupyterHub to take advantage
|
||||||
of new releases. Much of this process is automated using scripts,
|
of new releases. Much of this process is automated using scripts,
|
||||||
such as those generated by alembic for database upgrades. Before upgrading a
|
such as those generated by alembic for database upgrades. Whether you
|
||||||
JupyterHub deployment, it's critical to backup your data and configurations
|
are using the default SQLite database or an RDBMS, such as PostgreSQL or
|
||||||
before shutting down the JupyterHub process and server.
|
MySQL, the process follows similar steps.
|
||||||
|
|
||||||
## Databases: SQLite (default) or RDBMS (PostgreSQL, MySQL)
|
**Before upgrading a JupyterHub deployment**, it's critical to backup your data
|
||||||
|
and configurations before shutting down the JupyterHub process and server.
|
||||||
|
|
||||||
The default database for JupyterHub is a [SQLite](https://sqlite.org) database.
|
## Note about upgrading the SQLite database
|
||||||
We have chosen SQLite as JupyterHub's default for its lightweight simplicity
|
|
||||||
in certain uses such as testing, small deployments and workshops.
|
|
||||||
|
|
||||||
When running a long term deployment or a production system, we recommend using
|
When used in production systems, SQLite has some disadvantages when it
|
||||||
a traditional RDBMS database, such as [PostgreSQL](https://www.postgresql.org)
|
comes to upgrading JupyterHub. These are:
|
||||||
or [MySQL](https://www.mysql.com), that supports the SQL `ALTER TABLE`
|
|
||||||
statement.
|
|
||||||
|
|
||||||
For production systems, SQLite has some disadvantages when used with JupyterHub:
|
|
||||||
|
|
||||||
- `upgrade-db` may not work, and you may need to start with a fresh database
|
- `upgrade-db` may not work, and you may need to start with a fresh database
|
||||||
- `downgrade-db` **will not** work if you want to rollback to an earlier
|
- `downgrade-db` **will not** work if you want to rollback to an earlier
|
||||||
version, so backup the `jupyterhub.sqlite` file before upgrading
|
version, so backup the `jupyterhub.sqlite` file before upgrading
|
||||||
|
|
||||||
The sqlite documentation provides a helpful page about [when to use sqlite and
|
|
||||||
where traditional RDBMS may be a better choice](https://sqlite.org/whentouse.html).
|
|
||||||
|
|
||||||
## The upgrade process
|
## The upgrade process
|
||||||
|
|
||||||
Four fundamental process steps are needed when upgrading JupyterHub and its
|
Five fundamental process steps are needed when upgrading JupyterHub and its
|
||||||
database:
|
database:
|
||||||
|
|
||||||
1. Backup JupyterHub database
|
1. Backup JupyterHub database
|
112
docs/source/reference/websecurity.md
Normal file
112
docs/source/reference/websecurity.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Security Overview
|
||||||
|
|
||||||
|
The **Security Overview** section helps you learn about:
|
||||||
|
|
||||||
|
- the design of JupyterHub with respect to web security
|
||||||
|
- the semi-trusted user
|
||||||
|
- the available mitigations to protect untrusted users from each other
|
||||||
|
- the value of periodic security audits.
|
||||||
|
|
||||||
|
This overview also helps you obtain a deeper understanding of how JupyterHub
|
||||||
|
works.
|
||||||
|
|
||||||
|
## Semi-trusted and untrusted users
|
||||||
|
|
||||||
|
JupyterHub is designed to be a *simple multi-user server for modestly sized
|
||||||
|
groups* of **semi-trusted** users. While the design reflects serving semi-trusted
|
||||||
|
users, JupyterHub is not necessarily unsuitable for serving **untrusted** users.
|
||||||
|
|
||||||
|
Using JupyterHub with **untrusted** users does mean more work by the
|
||||||
|
administrator. Much care is required to secure a Hub, with extra caution on
|
||||||
|
protecting users from each other as the Hub is serving untrusted users.
|
||||||
|
|
||||||
|
One aspect of JupyterHub's *design simplicity* for **semi-trusted** users is that
|
||||||
|
the Hub and single-user servers are placed in a *single domain*, behind a
|
||||||
|
[*proxy*][configurable-http-proxy]. If the Hub is serving untrusted
|
||||||
|
users, many of the web's cross-site protections are not applied between
|
||||||
|
single-user servers and the Hub, or between single-user servers and each
|
||||||
|
other, since browsers see the whole thing (proxy, Hub, and single user
|
||||||
|
servers) as a single website (i.e. single domain).
|
||||||
|
|
||||||
|
## Protect users from each other
|
||||||
|
|
||||||
|
To protect users from each other, a user must **never** be able to write arbitrary
|
||||||
|
HTML and serve it to another user on the Hub's domain. JupyterHub's
|
||||||
|
authentication setup prevents a user writing arbitrary HTML and serving it to
|
||||||
|
another user because only the owner of a given single-user notebook server is
|
||||||
|
allowed to view user-authored pages served by the given single-user notebook
|
||||||
|
server.
|
||||||
|
|
||||||
|
To protect all users from each other, JupyterHub administrators must
|
||||||
|
ensure that:
|
||||||
|
|
||||||
|
* A user **does not have permission** to modify their single-user notebook server,
|
||||||
|
including:
|
||||||
|
- A user **may not** install new packages in the Python environment that runs
|
||||||
|
their single-user server.
|
||||||
|
- If the `PATH` is used to resolve the single-user executable (instead of
|
||||||
|
using an absolute path), a user **may not** create new files in any `PATH`
|
||||||
|
directory that precedes the directory containing `jupyterhub-singleuser`.
|
||||||
|
- A user may not modify environment variables (e.g. PATH, PYTHONPATH) for
|
||||||
|
their single-user server.
|
||||||
|
* A user **may not** modify the configuration of the notebook server
|
||||||
|
(the `~/.jupyter` or `JUPYTER_CONFIG_DIR` directory).
|
||||||
|
|
||||||
|
If any additional services are run on the same domain as the Hub, the services
|
||||||
|
**must never** display user-authored HTML that is neither *sanitized* nor *sandboxed*
|
||||||
|
(e.g. IFramed) to any user that lacks authentication as the author of a file.
|
||||||
|
|
||||||
|
## Mitigate security issues
|
||||||
|
|
||||||
|
Several approaches to mitigating these issues with configuration
|
||||||
|
options provided by JupyterHub include:
|
||||||
|
|
||||||
|
### Enable subdomains
|
||||||
|
|
||||||
|
JupyterHub provides the ability to run single-user servers on their own
|
||||||
|
subdomains. This means the cross-origin protections between servers has the
|
||||||
|
desired effect, and user servers and the Hub are protected from each other. A
|
||||||
|
user's single-user server will be at `username.jupyter.mydomain.com`. This also
|
||||||
|
requires all user subdomains to point to the same address, which is most easily
|
||||||
|
accomplished with wildcard DNS. Since this spreads the service across multiple
|
||||||
|
domains, you will need wildcard SSL, as well. Unfortunately, for many
|
||||||
|
institutional domains, wildcard DNS and SSL are not available. **If you do plan
|
||||||
|
to serve untrusted users, enabling subdomains is highly encouraged**, as it
|
||||||
|
resolves the cross-site issues.
|
||||||
|
|
||||||
|
### Disable user config
|
||||||
|
|
||||||
|
If subdomains are not available or not desirable, JupyterHub provides a a
|
||||||
|
configuration option `Spawner.disable_user_config`, which can be set to prevent
|
||||||
|
the user-owned configuration files from being loaded. After implementing this
|
||||||
|
option, PATHs and package installation and PATHs are the other things that the
|
||||||
|
admin must enforce.
|
||||||
|
|
||||||
|
### Prevent spawners from evaluating shell configuration files
|
||||||
|
|
||||||
|
For most Spawners, `PATH` is not something users can influence, but care should
|
||||||
|
be taken to ensure that the Spawner does *not* evaluate shell configuration
|
||||||
|
files prior to launching the server.
|
||||||
|
|
||||||
|
### Isolate packages using virtualenv
|
||||||
|
|
||||||
|
Package isolation is most easily handled by running the single-user server in
|
||||||
|
a virtualenv with disabled system-site-packages. The user should not have
|
||||||
|
permission to install packages into this environment.
|
||||||
|
|
||||||
|
It is important to note that the control over the environment only affects the
|
||||||
|
single-user server, and not the environment(s) in which the user's kernel(s)
|
||||||
|
may run. Installing additional packages in the kernel environment does not
|
||||||
|
pose additional risk to the web application's security.
|
||||||
|
|
||||||
|
## Security audits
|
||||||
|
|
||||||
|
We recommend that you do periodic reviews of your deployment's security. It's
|
||||||
|
good practice to keep JupyterHub, configurable-http-proxy, and nodejs
|
||||||
|
versions up to date.
|
||||||
|
|
||||||
|
A handy website for testing your deployment is
|
||||||
|
[Qualsys' SSL analyzer tool](https://www.ssllabs.com/ssltest/analyze.html).
|
||||||
|
|
||||||
|
|
||||||
|
[configurable-http-proxy]: https://github.com/jupyterhub/configurable-http-proxy
|
@@ -1,70 +0,0 @@
|
|||||||
# Using JupyterHub's REST API
|
|
||||||
|
|
||||||
Using the [JupyterHub REST API][], you can perform actions on the Hub,
|
|
||||||
such as:
|
|
||||||
|
|
||||||
- checking which users are active
|
|
||||||
- adding or removing users
|
|
||||||
- stopping or starting single user notebook servers
|
|
||||||
- authenticating services
|
|
||||||
|
|
||||||
A [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)
|
|
||||||
API provides a standard way for users to get and send information to the
|
|
||||||
Hub.
|
|
||||||
|
|
||||||
|
|
||||||
## Creating an API token
|
|
||||||
To send requests using JupyterHub API, you must pass an API token with the
|
|
||||||
request. You can create a token for an individual user using the following
|
|
||||||
command:
|
|
||||||
|
|
||||||
jupyterhub token USERNAME
|
|
||||||
|
|
||||||
|
|
||||||
## Adding tokens to the config file
|
|
||||||
You may also add a dictionary of API tokens and usernames to the hub's
|
|
||||||
configuration file, `jupyterhub_config.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
c.JupyterHub.api_tokens = {
|
|
||||||
'secret-token': 'username',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Making an API request
|
|
||||||
|
|
||||||
To authenticate your requests, pass the API token in the request's
|
|
||||||
Authorization header.
|
|
||||||
|
|
||||||
**Example: List the hub's users**
|
|
||||||
|
|
||||||
Using the popular Python requests library, the following code sends an API
|
|
||||||
request and an API token for authorization:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
api_url = 'http://127.0.0.1:8081/hub/api'
|
|
||||||
|
|
||||||
r = requests.get(api_url + '/users',
|
|
||||||
headers={
|
|
||||||
'Authorization': 'token %s' % token,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
r.raise_for_status()
|
|
||||||
users = r.json()
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Learning more about the API
|
|
||||||
|
|
||||||
You can see the full [JupyterHub REST API][] for details.
|
|
||||||
The same REST API Spec can be viewed in a more interactive style [on swagger's petstore][].
|
|
||||||
Both resources contain the same information and differ only in its display.
|
|
||||||
Note: The Swagger specification is being renamed the [OpenAPI Initiative][].
|
|
||||||
|
|
||||||
[on swagger's petstore]: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default
|
|
||||||
[OpenAPI Initiative]: https://www.openapis.org/
|
|
||||||
[JupyterHub REST API]: ./_static/rest-api/index.html
|
|
@@ -7,6 +7,9 @@ problem and how to resolve it.
|
|||||||
[*Behavior*](#behavior)
|
[*Behavior*](#behavior)
|
||||||
- JupyterHub proxy fails to start
|
- JupyterHub proxy fails to start
|
||||||
- sudospawner fails to run
|
- 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)
|
[*Errors*](#errors)
|
||||||
- 500 error after spawning my single-user server
|
- 500 error after spawning my single-user server
|
||||||
@@ -18,6 +21,9 @@ problem and how to resolve it.
|
|||||||
- How do I increase the number of pySpark executors on YARN?
|
- How do I increase the number of pySpark executors on YARN?
|
||||||
- How do I use JupyterLab's prerelease version with JupyterHub?
|
- How do I use JupyterLab's prerelease version with JupyterHub?
|
||||||
- How do I set up JupyterHub for a workshop (when users are not known ahead of time)?
|
- How do I set up JupyterHub for a workshop (when users are not known ahead of time)?
|
||||||
|
- How do I set up rotating daily logs?
|
||||||
|
- Toree integration with HDFS rack awareness script
|
||||||
|
- Where do I find Docker images and Dockerfiles related to JupyterHub?
|
||||||
|
|
||||||
[*Troubleshooting commands*](#troubleshooting-commands)
|
[*Troubleshooting commands*](#troubleshooting-commands)
|
||||||
|
|
||||||
@@ -31,6 +37,10 @@ If you have tried to start the JupyterHub proxy and it fails to start:
|
|||||||
``c.JupyterHub.ip = '*'``; if it is, try ``c.JupyterHub.ip = ''``
|
``c.JupyterHub.ip = '*'``; if it is, try ``c.JupyterHub.ip = ''``
|
||||||
- Try starting with ``jupyterhub --ip=0.0.0.0``
|
- Try starting with ``jupyterhub --ip=0.0.0.0``
|
||||||
|
|
||||||
|
**Note**: If this occurs on Ubuntu/Debian, check that the you are using a
|
||||||
|
recent version of node. Some versions of Ubuntu/Debian come with a version
|
||||||
|
of node that is very old, and it is necessary to update node.
|
||||||
|
|
||||||
### sudospawner fails to run
|
### sudospawner fails to run
|
||||||
|
|
||||||
If the sudospawner script is not found in the path, sudospawner will not run.
|
If the sudospawner script is not found in the path, sudospawner will not run.
|
||||||
@@ -45,6 +55,27 @@ or add:
|
|||||||
|
|
||||||
to the config file, `jupyterhub_config.py`.
|
to the config file, `jupyterhub_config.py`.
|
||||||
|
|
||||||
|
### What is the default behavior when none of the lists (admin, whitelist, group whitelist) are set?
|
||||||
|
|
||||||
|
When nothing is given for these lists, there will be no admins, and all users
|
||||||
|
who can authenticate on the system (i.e. all the unix users on the server with
|
||||||
|
a password) will be allowed to start a server. The whitelist lets you limit
|
||||||
|
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
|
## Errors
|
||||||
|
|
||||||
### 500 error after spawning my single-user server
|
### 500 error after spawning my single-user server
|
||||||
@@ -70,7 +101,7 @@ check if the cookie corresponds to the right user. This request is logged.
|
|||||||
If everything is working, the response logged will be similar to this:
|
If everything is working, the response logged will be similar to this:
|
||||||
|
|
||||||
```
|
```
|
||||||
200 GET /hub/api/authorizations/cookie/jupyter-hub-token-name/[secret] (@10.0.1.4) 6.10ms
|
200 GET /hub/api/authorizations/cookie/jupyterhub-token-name/[secret] (@10.0.1.4) 6.10ms
|
||||||
```
|
```
|
||||||
|
|
||||||
You should see a similar 200 message, as above, in the Hub log when you first
|
You should see a similar 200 message, as above, in the Hub log when you first
|
||||||
@@ -80,7 +111,7 @@ may mean that your single-user notebook server isn't connecting to your Hub.
|
|||||||
If you see 403 (forbidden) like this, it's a token problem:
|
If you see 403 (forbidden) like this, it's a token problem:
|
||||||
|
|
||||||
```
|
```
|
||||||
403 GET /hub/api/authorizations/cookie/jupyter-hub-token-name/[secret] (@10.0.1.4) 4.14ms
|
403 GET /hub/api/authorizations/cookie/jupyterhub-token-name/[secret] (@10.0.1.4) 4.14ms
|
||||||
```
|
```
|
||||||
|
|
||||||
Check the logs of the single-user notebook server, which may have more detailed
|
Check the logs of the single-user notebook server, which may have more detailed
|
||||||
@@ -226,6 +257,31 @@ notebook servers to default to JupyterLab:
|
|||||||
|
|
||||||
Users will need a GitHub account to login and be authenticated by the Hub.
|
Users will need a GitHub account to login and be authenticated by the Hub.
|
||||||
|
|
||||||
|
### How do I set up rotating daily logs?
|
||||||
|
|
||||||
|
You can do this with [logrotate](https://linux.die.net/man/8/logrotate),
|
||||||
|
or pipe to `logger` to use syslog instead of directly to a file.
|
||||||
|
|
||||||
|
For example, with this logrotate config file:
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/log/jupyterhub.log {
|
||||||
|
copytruncate
|
||||||
|
daily
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and run this daily by putting a script in `/etc/cron.daily/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
logrotate /path/to/above-config
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use syslog:
|
||||||
|
|
||||||
|
jupyterhub | logger -t jupyterhub
|
||||||
|
|
||||||
|
|
||||||
## Troubleshooting commands
|
## Troubleshooting commands
|
||||||
|
|
||||||
The following commands provide additional detail about installed packages,
|
The following commands provide additional detail about installed packages,
|
||||||
@@ -250,7 +306,7 @@ jupyter kernelspec list
|
|||||||
jupyterhub --debug
|
jupyterhub --debug
|
||||||
```
|
```
|
||||||
|
|
||||||
## Toree integration with HDFS rack awareness script
|
### Toree integration with HDFS rack awareness script
|
||||||
|
|
||||||
The Apache Toree kernel will an issue, when running with JupyterHub, if the standard HDFS
|
The Apache Toree kernel will an issue, when running with JupyterHub, if the standard HDFS
|
||||||
rack awareness script is used. This will materialize in the logs as a repeated WARN:
|
rack awareness script is used. This will materialize in the logs as a repeated WARN:
|
||||||
@@ -272,3 +328,12 @@ script (e.g. /etc/hadoop/conf/custom_topology_script.py). Copy the original scri
|
|||||||
to a python two installation (e.g. /usr/bin/python).
|
to a python two installation (e.g. /usr/bin/python).
|
||||||
2. In spark-env.sh add a Python 2 installation to your path (e.g. export PATH=/opt/anaconda2/bin:$PATH).
|
2. In spark-env.sh add a Python 2 installation to your path (e.g. export PATH=/opt/anaconda2/bin:$PATH).
|
||||||
|
|
||||||
|
### Where do I find Docker images and Dockerfiles related to JupyterHub?
|
||||||
|
|
||||||
|
Docker images can be found at the [JupyterHub organization on DockerHub](https://hub.docker.com/u/jupyterhub/).
|
||||||
|
The Docker image [jupyterhub/singleuser](https://hub.docker.com/r/jupyterhub/singleuser/)
|
||||||
|
provides an example single user notebook server for use with DockerSpawner.
|
||||||
|
|
||||||
|
Additional single user notebook server images can be found at the [Jupyter
|
||||||
|
organization on DockerHub](https://hub.docker.com/r/jupyter/) and information
|
||||||
|
about each image at the [jupyter/docker-stacks repo](https://github.com/jupyter/docker-stacks).
|
||||||
|
14
docs/source/tutorials/index.rst
Normal file
14
docs/source/tutorials/index.rst
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Tutorials
|
||||||
|
=========
|
||||||
|
|
||||||
|
This section provides links to documentation that helps a user do a specific
|
||||||
|
task.
|
||||||
|
|
||||||
|
* :doc:`upgrade-dot-eight`
|
||||||
|
* `Zero to JupyterHub with Kubernetes <https://zero-to-jupyterhub.readthedocs.io/en/latest/>`_
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:hidden:
|
||||||
|
|
||||||
|
upgrade-dot-eight
|
93
docs/source/tutorials/upgrade-dot-eight.rst
Normal file
93
docs/source/tutorials/upgrade-dot-eight.rst
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
.. _upgrade-dot-eight:
|
||||||
|
|
||||||
|
Upgrading to JupyterHub version 0.8
|
||||||
|
===================================
|
||||||
|
|
||||||
|
This document will assist you in upgrading an existing JupyterHub deployment
|
||||||
|
from version 0.7 to version 0.8.
|
||||||
|
|
||||||
|
Upgrade checklist
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
0. Review the release notes. Review any deprecated features and pay attention
|
||||||
|
to any backwards incompatible changes
|
||||||
|
1. Backup JupyterHub database:
|
||||||
|
- ``jupyterhub.sqlite`` when using the default sqlite database
|
||||||
|
- Your JupyterHub database when using an RDBMS
|
||||||
|
2. Backup the existing JupyterHub configuration file: ``jupyterhub_config.py``
|
||||||
|
3. Shutdown the Hub
|
||||||
|
4. Upgrade JupyterHub
|
||||||
|
- ``pip install -U jupyterhub`` when using ``pip``
|
||||||
|
- ``conda upgrade jupyterhub`` when using ``conda``
|
||||||
|
5. Upgrade the database using run ```jupyterhub upgrade-db``
|
||||||
|
6. Update the JupyterHub configuration file ``jupyterhub_config.py``
|
||||||
|
|
||||||
|
Backup JupyterHub database
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
To prevent unintended loss of data or configuration information, you should
|
||||||
|
back up the JupyterHub database (the default SQLite database or a RDBMS
|
||||||
|
database using PostgreSQL, MySQL, or others supported by SQLAlchemy):
|
||||||
|
|
||||||
|
- If using the default SQLite database, back up the ``jupyterhub.sqlite``
|
||||||
|
database.
|
||||||
|
- If using an RDBMS database such as PostgreSQL, MySQL, or other supported by
|
||||||
|
SQLAlchemy, back up the JupyterHub database.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Losing the Hub database is often not a big deal. Information that resides only
|
||||||
|
in the Hub database includes:
|
||||||
|
|
||||||
|
- active login tokens (user cookies, service tokens)
|
||||||
|
- users added via GitHub UI, instead of config files
|
||||||
|
- info about running servers
|
||||||
|
|
||||||
|
If the following conditions are true, you should be fine clearing the Hub
|
||||||
|
database and starting over:
|
||||||
|
|
||||||
|
- users specified in config file
|
||||||
|
- user servers are stopped during upgrade
|
||||||
|
- don't mind causing users to login again after upgrade
|
||||||
|
|
||||||
|
Backup JupyterHub configuration file
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
Backup up your configuration file, ``jupyterhub_config.py``, to a secure
|
||||||
|
location.
|
||||||
|
|
||||||
|
Shutdown JupyterHub
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Prior to shutting down JupyterHub, you should notify the Hub users of the
|
||||||
|
scheduled downtime.
|
||||||
|
- Shutdown the JupyterHub service.
|
||||||
|
|
||||||
|
Upgrade JupyterHub
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Follow directions that correspond to your package manager, ``pip`` or ``conda``,
|
||||||
|
for the new JupyterHub release:
|
||||||
|
|
||||||
|
- ``pip install -U jupyterhub`` for ``pip``
|
||||||
|
- ``conda upgrade jupyterhub`` for ``conda``
|
||||||
|
|
||||||
|
Upgrade the proxy, authenticator, or spawner if needed.
|
||||||
|
|
||||||
|
Upgrade JupyterHub database
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
To run the upgrade process for JupyterHub databases, enter::
|
||||||
|
|
||||||
|
jupyterhub upgrade-db
|
||||||
|
|
||||||
|
Update the JupyterHub configuration file
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
Create a new JupyterHub configuration file or edit a copy of the existing
|
||||||
|
file ``jupyterhub_config.py``.
|
||||||
|
|
||||||
|
Start JupyterHub
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Start JupyterHub with the same command that you used before the upgrade.
|
@@ -1,80 +0,0 @@
|
|||||||
# Web Security in JupyterHub
|
|
||||||
|
|
||||||
JupyterHub is designed to be a simple multi-user server for modestly sized
|
|
||||||
groups of semi-trusted users. While the design reflects serving semi-trusted
|
|
||||||
users, JupyterHub is not necessarily unsuitable for serving untrusted users.
|
|
||||||
Using JupyterHub with untrusted users does mean more work and much care is
|
|
||||||
required to secure a Hub against untrusted users, with extra caution on
|
|
||||||
protecting users from each other as the Hub is serving untrusted users.
|
|
||||||
|
|
||||||
One aspect of JupyterHub's design simplicity for semi-trusted users is that
|
|
||||||
the Hub and single-user servers are placed in a single domain, behind a
|
|
||||||
[proxy][configurable-http-proxy]. As a result, if the Hub is serving untrusted
|
|
||||||
users, many of the web's cross-site protections are not applied between
|
|
||||||
single-user servers and the Hub, or between single-user servers and each
|
|
||||||
other, since browsers see the whole thing (proxy, Hub, and single user
|
|
||||||
servers) as a single website.
|
|
||||||
|
|
||||||
To protect users from each other, a user must never be able to write arbitrary
|
|
||||||
HTML and serve it to another user on the Hub's domain. JupyterHub's
|
|
||||||
authentication setup prevents this because only the owner of a given
|
|
||||||
single-user server is allowed to view user-authored pages served by their
|
|
||||||
server. To protect all users from each other, JupyterHub administrators must
|
|
||||||
ensure that:
|
|
||||||
|
|
||||||
* A user does not have permission to modify their single-user server:
|
|
||||||
- A user may not install new packages in the Python environment that runs
|
|
||||||
their server.
|
|
||||||
- If the PATH is used to resolve the single-user executable (instead of an
|
|
||||||
absolute path), a user may not create new files in any PATH directory
|
|
||||||
that precedes the directory containing jupyterhub-singleuser.
|
|
||||||
- A user may not modify environment variables (e.g. PATH, PYTHONPATH) for
|
|
||||||
their single-user server.
|
|
||||||
* A user may not modify the configuration of the notebook server
|
|
||||||
(the ~/.jupyter or JUPYTER_CONFIG_DIR directory).
|
|
||||||
|
|
||||||
If any additional services are run on the same domain as the Hub, the services
|
|
||||||
must never display user-authored HTML that is neither sanitized nor sandboxed
|
|
||||||
(e.g. IFramed) to any user that lacks authentication as the author of a file.
|
|
||||||
|
|
||||||
|
|
||||||
## Mitigations
|
|
||||||
|
|
||||||
There are two main configuration options provided by JupyterHub to mitigate
|
|
||||||
these issues:
|
|
||||||
|
|
||||||
### Subdomains
|
|
||||||
|
|
||||||
JupyterHub 0.5 adds the ability to run single-user servers on their own
|
|
||||||
subdomains, which means the cross-origin protections between servers has the
|
|
||||||
desired effect, and user servers and the Hub are protected from each other. A
|
|
||||||
user's server will be at `username.jupyter.mydomain.com`, etc. This requires
|
|
||||||
all user subdomains to point to the same address, which is most easily
|
|
||||||
accomplished with wildcard DNS. Since this spreads the service across multiple
|
|
||||||
domains, you will need wildcard SSL, as well. Unfortunately, for many
|
|
||||||
institutional domains, wildcard DNS and SSL are not available, but if you do
|
|
||||||
plan to serve untrusted users, enabling subdomains is highly encouraged, as it
|
|
||||||
resolves all of the cross-site issues.
|
|
||||||
|
|
||||||
### Disabling user config
|
|
||||||
|
|
||||||
If subdomains are not available or not desirable, 0.5 also adds an option
|
|
||||||
`Spawner.disable_user_config`, which you can set to prevent the user-owned
|
|
||||||
configuration files from being loaded. This leaves only package installation
|
|
||||||
and PATHs as things the admin must enforce.
|
|
||||||
|
|
||||||
For most Spawners, PATH is not something users can influence, but care should
|
|
||||||
be taken to ensure that the Spawn does *not* evaluate shell configuration
|
|
||||||
files prior to launching the server.
|
|
||||||
|
|
||||||
Package isolation is most easily handled by running the single-user server in
|
|
||||||
a virtualenv with disabled system-site-packages.
|
|
||||||
|
|
||||||
## Extra notes
|
|
||||||
|
|
||||||
It is important to note that the control over the environment only affects the
|
|
||||||
single-user server, and not the environment(s) in which the user's kernel(s)
|
|
||||||
may run. Installing additional packages in the kernel environment does not
|
|
||||||
pose additional risk to the web application's security.
|
|
||||||
|
|
||||||
[configurable-http-proxy]: https://github.com/jupyterhub/configurable-http-proxy
|
|
55
docs/sphinxext/autodoc_traits.py
Normal file
55
docs/sphinxext/autodoc_traits.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""autodoc extension for configurable traits"""
|
||||||
|
|
||||||
|
from traitlets import TraitType, Undefined
|
||||||
|
from sphinx.domains.python import PyClassmember
|
||||||
|
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
|
||||||
|
)
|
||||||
|
trait_members = []
|
||||||
|
for name, trait in sorted(get_traits(config=True).items()):
|
||||||
|
# put help in __doc__ where autodoc will look for it
|
||||||
|
trait.__doc__ = trait.help
|
||||||
|
trait_members.append((name, trait))
|
||||||
|
return check, trait_members + members
|
||||||
|
|
||||||
|
|
||||||
|
class TraitDocumenter(AttributeDocumenter):
|
||||||
|
objtype = 'trait'
|
||||||
|
directivetype = 'attribute'
|
||||||
|
member_order = 1
|
||||||
|
priority = 100
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def can_document_member(cls, member, membername, isattr, parent):
|
||||||
|
return isinstance(member, TraitType)
|
||||||
|
|
||||||
|
def format_name(self):
|
||||||
|
return 'config c.' + super().format_name()
|
||||||
|
|
||||||
|
def add_directive_header(self, sig):
|
||||||
|
default = self.object.get_default_value()
|
||||||
|
if default is Undefined:
|
||||||
|
default_s = ''
|
||||||
|
else:
|
||||||
|
default_s = repr(default)
|
||||||
|
sig = ' = {}({})'.format(self.object.__class__.__name__, default_s)
|
||||||
|
return super().add_directive_header(sig)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
app.add_autodocumenter(ConfigurableDocumenter)
|
||||||
|
app.add_autodocumenter(TraitDocumenter)
|
133
examples/bootstrap-script/README.md
Normal file
133
examples/bootstrap-script/README.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Bootstrapping your users
|
||||||
|
|
||||||
|
Before spawning a notebook to the user, it could be useful to
|
||||||
|
do some preparation work in a bootstrapping process.
|
||||||
|
|
||||||
|
Common use cases are:
|
||||||
|
|
||||||
|
*Providing writeable storage for LDAP users*
|
||||||
|
|
||||||
|
Your Jupyterhub is configured to use the LDAPAuthenticator and DockerSpawer.
|
||||||
|
|
||||||
|
* The user has no file directory on the host since your are using LDAP.
|
||||||
|
* When a user has no directory and DockerSpawner wants to mount a volume,
|
||||||
|
the spawner will use docker to create a directory.
|
||||||
|
Since the docker daemon is running as root, the generated directory for the volume
|
||||||
|
mount will not be writeable by the `jovyan` user inside of the container.
|
||||||
|
For the directory to be useful to the user, the permissions on the directory
|
||||||
|
need to be modified for the user to have write access.
|
||||||
|
|
||||||
|
*Prepopulating Content*
|
||||||
|
|
||||||
|
Another use would be to copy initial content, such as tutorial files or reference
|
||||||
|
material, into the user's space when a notebook server is newly spawned.
|
||||||
|
|
||||||
|
You can define your own bootstrap process by implementing a `pre_spawn_hook` on any spawner.
|
||||||
|
The Spawner itself is passed as parameter to your hook and you can easily get the contextual information out of the spawning process.
|
||||||
|
|
||||||
|
Similarly, there may be cases where you would like to clean up after a spawner stops.
|
||||||
|
You may implement a `post_stop_hook` that is always executed after the spawner stops.
|
||||||
|
|
||||||
|
If you implement a hook, make sure that it is *idempotent*. It will be executed every time
|
||||||
|
a notebook server is spawned to the user. That means you should somehow
|
||||||
|
ensure that things which should run only once are not running again and again.
|
||||||
|
For example, before you create a directory, check if it exists.
|
||||||
|
|
||||||
|
Bootstrapping examples:
|
||||||
|
|
||||||
|
### Example #1 - Create a user directory
|
||||||
|
|
||||||
|
Create a directory for the user, if none exists
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
# in jupyterhub_config.py
|
||||||
|
import os
|
||||||
|
def create_dir_hook(spawner):
|
||||||
|
username = spawner.user.name # get the username
|
||||||
|
volume_path = os.path.join('/volumes/jupyterhub', username)
|
||||||
|
if not os.path.exists(volume_path):
|
||||||
|
# create a directory with umask 0755
|
||||||
|
# hub and container user must have the same UID to be writeable
|
||||||
|
# still readable by other users on the system
|
||||||
|
os.mkdir(volume_path, 0o755)
|
||||||
|
# now do whatever you think your user needs
|
||||||
|
# ...
|
||||||
|
pass
|
||||||
|
|
||||||
|
# attach the hook function to the spawner
|
||||||
|
c.Spawner.pre_spawn_hook = create_dir_hook
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example #2 - Run a shell script
|
||||||
|
|
||||||
|
You can specify a plain ole' shell script (or any other executable) to be run
|
||||||
|
by the bootstrap process.
|
||||||
|
|
||||||
|
For example, you can execute a shell script and as first parameter pass the name
|
||||||
|
of the user:
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
# in jupyterhub_config.py
|
||||||
|
from subprocess import check_call
|
||||||
|
import os
|
||||||
|
def my_script_hook(spawner):
|
||||||
|
username = spawner.user.name # get the username
|
||||||
|
script = os.path.join(os.path.dirname(__file__), 'bootstrap.sh')
|
||||||
|
check_call([script, username])
|
||||||
|
|
||||||
|
# attach the hook function to the spawner
|
||||||
|
c.Spawner.pre_spawn_hook = my_script_hook
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's an example on what you could do in your shell script. See also
|
||||||
|
`/examples/bootstrap-script/`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Bootstrap example script
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
# - The first parameter for the Bootstrap Script is the USER.
|
||||||
|
USER=$1
|
||||||
|
if ["$USER" == ""]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
# This example script will do the following:
|
||||||
|
# - create one directory for the user $USER in a BASE_DIRECTORY (see below)
|
||||||
|
# - create a "tutorials" directory within and download and unzip
|
||||||
|
# the PythonDataScienceHandbook from GitHub
|
||||||
|
|
||||||
|
# Start the Bootstrap Process
|
||||||
|
echo "bootstrap process running for user $USER ..."
|
||||||
|
|
||||||
|
# Base Directory: All Directories for the user will be below this point
|
||||||
|
BASE_DIRECTORY=/volumes/jupyterhub/
|
||||||
|
|
||||||
|
# User Directory: That's the private directory for the user to be created, if none exists
|
||||||
|
USER_DIRECTORY=$BASE_DIRECTORY/$USER
|
||||||
|
|
||||||
|
if [ -d "$USER_DIRECTORY" ]; then
|
||||||
|
echo "...directory for user already exists. skipped"
|
||||||
|
exit 0 # all good. nothing to do.
|
||||||
|
else
|
||||||
|
echo "...creating a directory for the user: $USER_DIRECTORY"
|
||||||
|
mkdir $USER_DIRECTORY
|
||||||
|
|
||||||
|
echo "...initial content loading for user ..."
|
||||||
|
mkdir $USER_DIRECTORY/tutorials
|
||||||
|
cd $USER_DIRECTORY/tutorials
|
||||||
|
wget https://github.com/jakevdp/PythonDataScienceHandbook/archive/master.zip
|
||||||
|
unzip -o master.zip
|
||||||
|
rm master.zip
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
```
|
48
examples/bootstrap-script/bootstrap.sh
Executable file
48
examples/bootstrap-script/bootstrap.sh
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Bootstrap example script
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
# - The first parameter for the Bootstrap Script is the USER.
|
||||||
|
USER=$1
|
||||||
|
if ["$USER" == ""]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
# This example script will do the following:
|
||||||
|
# - create one directory for the user $USER in a BASE_DIRECTORY (see below)
|
||||||
|
# - create a "tutorials" directory within and download and unzip the PythonDataScienceHandbook from GitHub
|
||||||
|
|
||||||
|
# Start the Bootstrap Process
|
||||||
|
echo "bootstrap process running for user $USER ..."
|
||||||
|
|
||||||
|
# Base Directory: All Directories for the user will be below this point
|
||||||
|
BASE_DIRECTORY=/volumes/jupyterhub
|
||||||
|
|
||||||
|
# User Directory: That's the private directory for the user to be created, if none exists
|
||||||
|
USER_DIRECTORY=$BASE_DIRECTORY/$USER
|
||||||
|
|
||||||
|
if [ -d "$USER_DIRECTORY" ]; then
|
||||||
|
echo "...directory for user already exists. skipped"
|
||||||
|
exit 0 # all good. nothing to do.
|
||||||
|
else
|
||||||
|
echo "...creating a directory for the user: $USER_DIRECTORY"
|
||||||
|
mkdir $USER_DIRECTORY
|
||||||
|
|
||||||
|
# mkdir did not succeed?
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "...initial content loading for user ..."
|
||||||
|
mkdir $USER_DIRECTORY/tutorials
|
||||||
|
cd $USER_DIRECTORY/tutorials
|
||||||
|
wget https://github.com/jakevdp/PythonDataScienceHandbook/archive/master.zip
|
||||||
|
unzip -o master.zip
|
||||||
|
rm master.zip
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
34
examples/bootstrap-script/jupyterhub_config.py
Normal file
34
examples/bootstrap-script/jupyterhub_config.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Example for a Spawner.pre_spawn_hook
|
||||||
|
# create a directory for the user before the spawner starts
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
def create_dir_hook(spawner):
|
||||||
|
username = spawner.user.name # get the username
|
||||||
|
volume_path = os.path.join('/volumes/jupyterhub', username)
|
||||||
|
if not os.path.exists(volume_path):
|
||||||
|
os.mkdir(volume_path, 0o755)
|
||||||
|
# now do whatever you think your user needs
|
||||||
|
# ...
|
||||||
|
|
||||||
|
def clean_dir_hook(spawner):
|
||||||
|
username = spawner.user.name # get the username
|
||||||
|
temp_path = os.path.join('/volumes/jupyterhub', username, 'temp')
|
||||||
|
if os.path.exists(temp_path) and os.path.isdir(temp_path):
|
||||||
|
shutil.rmtree(temp_path)
|
||||||
|
|
||||||
|
# attach the hook functions to the spawner
|
||||||
|
c.Spawner.pre_spawn_hook = create_dir_hook
|
||||||
|
c.Spawner.post_stop_hook = clean_dir_hook
|
||||||
|
|
||||||
|
# Use the DockerSpawner to serve your users' notebooks
|
||||||
|
c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
|
||||||
|
from jupyter_client.localinterfaces import public_ips
|
||||||
|
c.JupyterHub.hub_ip = public_ips()[0]
|
||||||
|
c.DockerSpawner.hub_ip_connect = public_ips()[0]
|
||||||
|
c.DockerSpawner.container_ip = "0.0.0.0"
|
||||||
|
|
||||||
|
# You can now mount the volume to the docker container as we've
|
||||||
|
# made sure the directory exists
|
||||||
|
c.DockerSpawner.volumes = { '/volumes/jupyterhub/{username}/': '/home/jovyan/work' }
|
||||||
|
|
@@ -15,7 +15,7 @@ c.JupyterHub.services = [
|
|||||||
{
|
{
|
||||||
'name': 'cull-idle',
|
'name': 'cull-idle',
|
||||||
'admin': True,
|
'admin': True,
|
||||||
'command': 'python cull_idle_servers.py --timeout=3600'.split(),
|
'command': 'python3 cull_idle_servers.py --timeout=3600'.split(),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
@@ -37,5 +37,5 @@ variable. Run `cull_idle_servers.py` manually.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
export JUPYTERHUB_API_TOKEN=`jupyterhub token`
|
export JUPYTERHUB_API_TOKEN=`jupyterhub token`
|
||||||
python cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api]
|
python3 cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api]
|
||||||
```
|
```
|
||||||
|
330
examples/cull-idle/cull_idle_servers.py
Normal file → Executable file
330
examples/cull-idle/cull_idle_servers.py
Normal file → Executable file
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
"""script to monitor and cull idle single-user servers
|
"""script to monitor and cull idle single-user servers
|
||||||
|
|
||||||
Caveats:
|
Caveats:
|
||||||
@@ -16,75 +16,348 @@ You can run this as a service managed by JupyterHub with this in your config::
|
|||||||
{
|
{
|
||||||
'name': 'cull-idle',
|
'name': 'cull-idle',
|
||||||
'admin': True,
|
'admin': True,
|
||||||
'command': 'python cull_idle_servers.py --timeout=3600'.split(),
|
'command': 'python3 cull_idle_servers.py --timeout=3600'.split(),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
Or run it manually by generating an API token and storing it in `JUPYTERHUB_API_TOKEN`:
|
Or run it manually by generating an API token and storing it in `JUPYTERHUB_API_TOKEN`:
|
||||||
|
|
||||||
export JUPYTERHUB_API_TOKEN=`jupyterhub token`
|
export JUPYTERHUB_API_TOKEN=`jupyterhub token`
|
||||||
python cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api]
|
python3 cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api]
|
||||||
|
|
||||||
|
This script uses the same ``--timeout`` and ``--max-age`` values for
|
||||||
|
culling users and users' servers. If you want a different value for
|
||||||
|
users and servers, you should add this script to the services list
|
||||||
|
twice, just with different ``name``s, different values, and one with
|
||||||
|
the ``--cull-users`` option.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
from datetime import datetime, timezone
|
||||||
|
from functools import partial
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from dateutil.parser import parse as parse_date
|
try:
|
||||||
|
from urllib.parse import quote
|
||||||
|
except ImportError:
|
||||||
|
from urllib import quote
|
||||||
|
|
||||||
from tornado.gen import coroutine
|
import dateutil.parser
|
||||||
|
|
||||||
|
from tornado.gen import coroutine, multi
|
||||||
|
from tornado.locks import Semaphore
|
||||||
from tornado.log import app_log
|
from tornado.log import app_log
|
||||||
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
||||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||||
from tornado.options import define, options, parse_command_line
|
from tornado.options import define, options, parse_command_line
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date(date_string):
|
||||||
|
"""Parse a timestamp
|
||||||
|
|
||||||
|
If it doesn't have a timezone, assume utc
|
||||||
|
|
||||||
|
Returned datetime object will always be timezone-aware
|
||||||
|
"""
|
||||||
|
dt = dateutil.parser.parse(date_string)
|
||||||
|
if not dt.tzinfo:
|
||||||
|
# assume naïve timestamps are UTC
|
||||||
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
|
return dt
|
||||||
|
|
||||||
|
|
||||||
|
def format_td(td):
|
||||||
|
"""
|
||||||
|
Nicely format a timedelta object
|
||||||
|
|
||||||
|
as HH:MM:SS
|
||||||
|
"""
|
||||||
|
if td is None:
|
||||||
|
return "unknown"
|
||||||
|
if isinstance(td, str):
|
||||||
|
return td
|
||||||
|
seconds = int(td.total_seconds())
|
||||||
|
h = seconds // 3600
|
||||||
|
seconds = seconds % 3600
|
||||||
|
m = seconds // 60
|
||||||
|
seconds = seconds % 60
|
||||||
|
return "{h:02}:{m:02}:{seconds:02}".format(h=h, m=m, seconds=seconds)
|
||||||
|
|
||||||
|
|
||||||
@coroutine
|
@coroutine
|
||||||
def cull_idle(url, api_token, timeout):
|
def cull_idle(url, api_token, inactive_limit, cull_users=False, max_age=0, concurrency=10):
|
||||||
"""cull idle single-user servers"""
|
"""Shutdown idle single-user servers
|
||||||
|
|
||||||
|
If cull_users, inactive *users* will be deleted as well.
|
||||||
|
"""
|
||||||
auth_header = {
|
auth_header = {
|
||||||
'Authorization': 'token %s' % api_token
|
'Authorization': 'token %s' % api_token,
|
||||||
}
|
}
|
||||||
req = HTTPRequest(url=url + '/users',
|
req = HTTPRequest(
|
||||||
|
url=url + '/users',
|
||||||
headers=auth_header,
|
headers=auth_header,
|
||||||
)
|
)
|
||||||
now = datetime.datetime.utcnow()
|
now = datetime.now(timezone.utc)
|
||||||
cull_limit = now - datetime.timedelta(seconds=timeout)
|
|
||||||
client = AsyncHTTPClient()
|
client = AsyncHTTPClient()
|
||||||
resp = yield client.fetch(req)
|
|
||||||
|
if concurrency:
|
||||||
|
semaphore = Semaphore(concurrency)
|
||||||
|
@coroutine
|
||||||
|
def fetch(req):
|
||||||
|
"""client.fetch wrapped in a semaphore to limit concurrency"""
|
||||||
|
yield semaphore.acquire()
|
||||||
|
try:
|
||||||
|
return (yield client.fetch(req))
|
||||||
|
finally:
|
||||||
|
yield semaphore.release()
|
||||||
|
else:
|
||||||
|
fetch = client.fetch
|
||||||
|
|
||||||
|
resp = yield fetch(req)
|
||||||
users = json.loads(resp.body.decode('utf8', 'replace'))
|
users = json.loads(resp.body.decode('utf8', 'replace'))
|
||||||
futures = []
|
futures = []
|
||||||
for user in users:
|
|
||||||
last_activity = parse_date(user['last_activity'])
|
@coroutine
|
||||||
if user['server'] and last_activity < cull_limit:
|
def handle_server(user, server_name, server):
|
||||||
app_log.info("Culling %s (inactive since %s)", user['name'], last_activity)
|
"""Handle (maybe) culling a single server
|
||||||
req = HTTPRequest(url=url + '/users/%s/server' % user['name'],
|
|
||||||
|
Returns True if server is now stopped (user removable),
|
||||||
|
False otherwise.
|
||||||
|
"""
|
||||||
|
log_name = user['name']
|
||||||
|
if server_name:
|
||||||
|
log_name = '%s/%s' % (user['name'], server_name)
|
||||||
|
if server.get('pending'):
|
||||||
|
app_log.warning(
|
||||||
|
"Not culling server %s with pending %s",
|
||||||
|
log_name, server['pending'])
|
||||||
|
return False
|
||||||
|
|
||||||
|
# jupyterhub < 0.9 defined 'server.url' once the server was ready
|
||||||
|
# as an *implicit* signal that the server was ready.
|
||||||
|
# 0.9 adds a dedicated, explicit 'ready' field.
|
||||||
|
# By current (0.9) definitions, servers that have no pending
|
||||||
|
# events and are not ready shouldn't be in the model,
|
||||||
|
# but let's check just to be safe.
|
||||||
|
|
||||||
|
if not server.get('ready', bool(server['url'])):
|
||||||
|
app_log.warning(
|
||||||
|
"Not culling not-ready not-pending server %s: %s",
|
||||||
|
log_name, server)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if server.get('started'):
|
||||||
|
age = now - parse_date(server['started'])
|
||||||
|
else:
|
||||||
|
# started may be undefined on jupyterhub < 0.9
|
||||||
|
age = None
|
||||||
|
|
||||||
|
# check last activity
|
||||||
|
# last_activity can be None in 0.9
|
||||||
|
if server['last_activity']:
|
||||||
|
inactive = now - parse_date(server['last_activity'])
|
||||||
|
else:
|
||||||
|
# no activity yet, use start date
|
||||||
|
# last_activity may be None with jupyterhub 0.9,
|
||||||
|
# which introduces the 'started' field which is never None
|
||||||
|
# for running servers
|
||||||
|
inactive = age
|
||||||
|
|
||||||
|
should_cull = (inactive is not None and
|
||||||
|
inactive.total_seconds() >= inactive_limit)
|
||||||
|
if should_cull:
|
||||||
|
app_log.info(
|
||||||
|
"Culling server %s (inactive for %s)",
|
||||||
|
log_name, format_td(inactive))
|
||||||
|
|
||||||
|
if max_age and not should_cull:
|
||||||
|
# only check started if max_age is specified
|
||||||
|
# so that we can still be compatible with jupyterhub 0.8
|
||||||
|
# which doesn't define the 'started' field
|
||||||
|
if age is not None and age.total_seconds() >= max_age:
|
||||||
|
app_log.info(
|
||||||
|
"Culling server %s (age: %s, inactive for %s)",
|
||||||
|
log_name, format_td(age), format_td(inactive))
|
||||||
|
should_cull = True
|
||||||
|
|
||||||
|
if not should_cull:
|
||||||
|
app_log.debug(
|
||||||
|
"Not culling server %s (age: %s, inactive for %s)",
|
||||||
|
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=delete_url, method='DELETE', headers=auth_header,
|
||||||
|
)
|
||||||
|
resp = yield fetch(req)
|
||||||
|
if resp.code == 202:
|
||||||
|
app_log.warning(
|
||||||
|
"Server %s is slow to stop",
|
||||||
|
log_name,
|
||||||
|
)
|
||||||
|
# return False to prevent culling user with pending shutdowns
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@coroutine
|
||||||
|
def handle_user(user):
|
||||||
|
"""Handle one user.
|
||||||
|
|
||||||
|
Create a list of their servers, and async exec them. Wait for
|
||||||
|
that to be done, and if all servers are stopped, possibly cull
|
||||||
|
the user.
|
||||||
|
"""
|
||||||
|
# shutdown servers first.
|
||||||
|
# Hub doesn't allow deleting users with running servers.
|
||||||
|
# jupyterhub 0.9 always provides a 'servers' model.
|
||||||
|
# 0.8 only does this when named servers are enabled.
|
||||||
|
if 'servers' in user:
|
||||||
|
servers = user['servers']
|
||||||
|
else:
|
||||||
|
# jupyterhub < 0.9 without named servers enabled.
|
||||||
|
# create servers dict with one entry for the default server
|
||||||
|
# from the user model.
|
||||||
|
# only if the server is running.
|
||||||
|
servers = {}
|
||||||
|
if user['server']:
|
||||||
|
servers[''] = {
|
||||||
|
'last_activity': user['last_activity'],
|
||||||
|
'pending': user['pending'],
|
||||||
|
'url': user['server'],
|
||||||
|
}
|
||||||
|
server_futures = [
|
||||||
|
handle_server(user, server_name, server)
|
||||||
|
for server_name, server in servers.items()
|
||||||
|
]
|
||||||
|
results = yield multi(server_futures)
|
||||||
|
if not cull_users:
|
||||||
|
return
|
||||||
|
# some servers are still running, cannot cull users
|
||||||
|
still_alive = len(results) - sum(results)
|
||||||
|
if still_alive:
|
||||||
|
app_log.debug(
|
||||||
|
"Not culling user %s with %i servers still alive",
|
||||||
|
user['name'], still_alive)
|
||||||
|
return False
|
||||||
|
|
||||||
|
should_cull = False
|
||||||
|
if user.get('created'):
|
||||||
|
age = now - parse_date(user['created'])
|
||||||
|
else:
|
||||||
|
# created may be undefined on jupyterhub < 0.9
|
||||||
|
age = None
|
||||||
|
|
||||||
|
# check last activity
|
||||||
|
# last_activity can be None in 0.9
|
||||||
|
if user['last_activity']:
|
||||||
|
inactive = now - parse_date(user['last_activity'])
|
||||||
|
else:
|
||||||
|
# no activity yet, use start date
|
||||||
|
# last_activity may be None with jupyterhub 0.9,
|
||||||
|
# which introduces the 'created' field which is never None
|
||||||
|
inactive = age
|
||||||
|
|
||||||
|
should_cull = (inactive is not None and
|
||||||
|
inactive.total_seconds() >= inactive_limit)
|
||||||
|
if should_cull:
|
||||||
|
app_log.info(
|
||||||
|
"Culling user %s (inactive for %s)",
|
||||||
|
user['name'], inactive)
|
||||||
|
|
||||||
|
if max_age and not should_cull:
|
||||||
|
# only check created if max_age is specified
|
||||||
|
# so that we can still be compatible with jupyterhub 0.8
|
||||||
|
# which doesn't define the 'started' field
|
||||||
|
if age is not None and age.total_seconds() >= max_age:
|
||||||
|
app_log.info(
|
||||||
|
"Culling user %s (age: %s, inactive for %s)",
|
||||||
|
user['name'], format_td(age), format_td(inactive))
|
||||||
|
should_cull = True
|
||||||
|
|
||||||
|
if not should_cull:
|
||||||
|
app_log.debug(
|
||||||
|
"Not culling user %s (created: %s, last active: %s)",
|
||||||
|
user['name'], format_td(age), format_td(inactive))
|
||||||
|
return False
|
||||||
|
|
||||||
|
req = HTTPRequest(
|
||||||
|
url=url + '/users/%s' % user['name'],
|
||||||
method='DELETE',
|
method='DELETE',
|
||||||
headers=auth_header,
|
headers=auth_header,
|
||||||
)
|
)
|
||||||
futures.append((user['name'], client.fetch(req)))
|
yield fetch(req)
|
||||||
elif user['server'] and last_activity > cull_limit:
|
return True
|
||||||
app_log.debug("Not culling %s (active since %s)", user['name'], last_activity)
|
|
||||||
|
for user in users:
|
||||||
|
futures.append((user['name'], handle_user(user)))
|
||||||
|
|
||||||
for (name, f) in futures:
|
for (name, f) in futures:
|
||||||
yield f
|
try:
|
||||||
|
result = yield f
|
||||||
|
except Exception:
|
||||||
|
app_log.exception("Error processing %s", name)
|
||||||
|
else:
|
||||||
|
if result:
|
||||||
app_log.debug("Finished culling %s", name)
|
app_log.debug("Finished culling %s", name)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
define('url', default=os.environ.get('JUPYTERHUB_API_URL'), help="The JupyterHub API URL")
|
define(
|
||||||
|
'url',
|
||||||
|
default=os.environ.get('JUPYTERHUB_API_URL'),
|
||||||
|
help="The JupyterHub API URL",
|
||||||
|
)
|
||||||
define('timeout', default=600, help="The idle timeout (in seconds)")
|
define('timeout', default=600, help="The idle timeout (in seconds)")
|
||||||
define('cull_every', default=0, help="The interval (in seconds) for checking for idle servers to cull")
|
define('cull_every', default=0,
|
||||||
|
help="The interval (in seconds) for checking for idle servers to cull")
|
||||||
|
define('max_age', default=0,
|
||||||
|
help="The maximum age (in seconds) of servers that should be culled even if they are active")
|
||||||
|
define('cull_users', default=False,
|
||||||
|
help="""Cull users in addition to servers.
|
||||||
|
This is for use in temporary-user cases such as tmpnb.""",
|
||||||
|
)
|
||||||
|
define('concurrency', default=10,
|
||||||
|
help="""Limit the number of concurrent requests made to the Hub.
|
||||||
|
|
||||||
|
Deleting a lot of users at the same time can slow down the Hub,
|
||||||
|
so limit the number of API requests we have outstanding at any given time.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
parse_command_line()
|
parse_command_line()
|
||||||
if not options.cull_every:
|
if not options.cull_every:
|
||||||
options.cull_every = options.timeout // 2
|
options.cull_every = options.timeout // 2
|
||||||
|
|
||||||
api_token = os.environ['JUPYTERHUB_API_TOKEN']
|
api_token = os.environ['JUPYTERHUB_API_TOKEN']
|
||||||
|
|
||||||
|
try:
|
||||||
|
AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
|
||||||
|
except ImportError as e:
|
||||||
|
app_log.warning(
|
||||||
|
"Could not load pycurl: %s\n"
|
||||||
|
"pycurl is recommended if you have a large number of users.",
|
||||||
|
e)
|
||||||
|
|
||||||
loop = IOLoop.current()
|
loop = IOLoop.current()
|
||||||
cull = lambda : cull_idle(options.url, api_token, options.timeout)
|
cull = partial(
|
||||||
# run once before scheduling periodic call
|
cull_idle,
|
||||||
loop.run_sync(cull)
|
url=options.url,
|
||||||
|
api_token=api_token,
|
||||||
|
inactive_limit=options.timeout,
|
||||||
|
cull_users=options.cull_users,
|
||||||
|
max_age=options.max_age,
|
||||||
|
concurrency=options.concurrency,
|
||||||
|
)
|
||||||
|
# schedule first cull immediately
|
||||||
|
# because PeriodicCallback doesn't start until the end of the first interval
|
||||||
|
loop.add_callback(cull)
|
||||||
# schedule periodic cull
|
# schedule periodic cull
|
||||||
pc = PeriodicCallback(cull, 1e3 * options.cull_every)
|
pc = PeriodicCallback(cull, 1e3 * options.cull_every)
|
||||||
pc.start()
|
pc.start()
|
||||||
@@ -92,4 +365,3 @@ if __name__ == '__main__':
|
|||||||
loop.start()
|
loop.start()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|
@@ -3,6 +3,6 @@ c.JupyterHub.services = [
|
|||||||
{
|
{
|
||||||
'name': 'cull-idle',
|
'name': 'cull-idle',
|
||||||
'admin': True,
|
'admin': True,
|
||||||
'command': 'python cull_idle_servers.py --timeout=3600'.split(),
|
'command': 'python3 cull_idle_servers.py --timeout=3600'.split(),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
90
examples/external-oauth/README.md
Normal file
90
examples/external-oauth/README.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Using JupyterHub as an OAuth provider
|
||||||
|
|
||||||
|
JupyterHub 0.9 introduces the ability to use JupyterHub as an OAuth provider
|
||||||
|
for external services that may not be otherwise integrated with JupyterHub.
|
||||||
|
The main feature this enables is using JupyterHub like a 'regular' OAuth 2
|
||||||
|
provider for services running anywhere.
|
||||||
|
|
||||||
|
There are two examples here. `whoami-oauth` (in the service-whoami directory) uses `jupyterhub.services.HubOAuthenticated`
|
||||||
|
to authenticate requests with the Hub for a service run on its own host.
|
||||||
|
This is an implementation of OAuth 2.0 provided by the jupyterhub package,
|
||||||
|
which configures all of the necessary URLs from environment variables.
|
||||||
|
|
||||||
|
The second is `whoami-oauth-basic`, which implements the full OAuth process
|
||||||
|
without any inheritance, so it can be used as a reference for OAuth
|
||||||
|
implementations in other web servers or languages.
|
||||||
|
|
||||||
|
## Run the example
|
||||||
|
|
||||||
|
1. generate an API token:
|
||||||
|
|
||||||
|
export JUPYTERHUB_API_TOKEN=`openssl rand -hex 32`
|
||||||
|
|
||||||
|
2. launch a version of the the whoami service.
|
||||||
|
For `whoami-oauth`:
|
||||||
|
|
||||||
|
bash launch-service.sh &
|
||||||
|
|
||||||
|
or for `whoami-oauth-basic`:
|
||||||
|
|
||||||
|
bash launch-service-basic.sh &
|
||||||
|
|
||||||
|
3. Launch JupyterHub:
|
||||||
|
|
||||||
|
jupyterhub
|
||||||
|
|
||||||
|
4. Visit http://127.0.0.1:5555/
|
||||||
|
|
||||||
|
After logging in with your local-system credentials, you should see a JSON dump of your user info:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"admin": false,
|
||||||
|
"last_activity": "2016-05-27T14:05:18.016372",
|
||||||
|
"name": "queequeg",
|
||||||
|
"pending": null,
|
||||||
|
"server": "/user/queequeg"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The essential pieces for using JupyterHub as an OAuth provider are:
|
||||||
|
|
||||||
|
1. registering your service with jupyterhub:
|
||||||
|
|
||||||
|
```python
|
||||||
|
c.JupyterHub.services = [
|
||||||
|
{
|
||||||
|
# the name of your service
|
||||||
|
# should be simple and unique.
|
||||||
|
# mostly used to identify your service in logging
|
||||||
|
"name": "my-service",
|
||||||
|
# the oauth client id of your service
|
||||||
|
# must be unique but isn't private
|
||||||
|
# can be randomly generated or hand-written
|
||||||
|
"oauth_client_id": "abc123",
|
||||||
|
# the API token and client secret of the service
|
||||||
|
# should be generated securely,
|
||||||
|
# e.g. via `openssl rand -hex 32`
|
||||||
|
"api_token": "abc123...",
|
||||||
|
# the redirect target for jupyterhub to send users
|
||||||
|
# after successful authentication
|
||||||
|
"oauth_redirect_uri": "https://service-host/oauth_callback"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Telling your service how to authenticate with JupyterHub.
|
||||||
|
|
||||||
|
The relevant OAuth URLs and keys for using JupyterHub as an OAuth provider are:
|
||||||
|
|
||||||
|
1. the client_id, used in oauth requests
|
||||||
|
2. the api token registered with jupyterhub is the client_secret for oauth requests
|
||||||
|
3. oauth url of the Hub, which is "/hub/api/oauth2/authorize", e.g. `https://myhub.horse/hub/api/oauth2/authorize`
|
||||||
|
4. a redirect handler to receive the authenticated response
|
||||||
|
(at `oauth_redirect_uri` registered in jupyterhub config)
|
||||||
|
5. the token URL for completing the oauth process is "/hub/api/oauth2/token",
|
||||||
|
e.g. `https://myhub.horse/hub/api/oauth2/token`.
|
||||||
|
The reply is JSON and the token is in the field `access_token`.
|
||||||
|
6. Users can be identified by oauth token by making a request to `/hub/api/user`
|
||||||
|
with the new token in the `Authorization` header.
|
18
examples/external-oauth/jupyterhub_config.py
Normal file
18
examples/external-oauth/jupyterhub_config.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# get the oauth client's API token.
|
||||||
|
# this could come from anywhere
|
||||||
|
api_token = os.getenv("JUPYTERHUB_API_TOKEN")
|
||||||
|
if not api_token:
|
||||||
|
raise ValueError("Make sure to `export JUPYTERHUB_API_TOKEN=$(openssl rand -hex 32)`")
|
||||||
|
|
||||||
|
# tell JupyterHub to register the service as an external oauth client
|
||||||
|
|
||||||
|
c.JupyterHub.services = [
|
||||||
|
{
|
||||||
|
'name': 'external-oauth',
|
||||||
|
'oauth_client_id': "whoami-oauth-client-test",
|
||||||
|
'api_token': api_token,
|
||||||
|
'oauth_redirect_uri': 'http://127.0.0.1:5555/oauth_callback',
|
||||||
|
},
|
||||||
|
]
|
20
examples/external-oauth/launch-service-basic.sh
Normal file
20
examples/external-oauth/launch-service-basic.sh
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# script to launch whoami-oauth-basic service
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# the service needs to know:
|
||||||
|
# 1. API token
|
||||||
|
if [[ -z "${JUPYTERHUB_API_TOKEN}" ]]; then
|
||||||
|
echo 'set API token with export JUPYTERHUB_API_TOKEN=$(openssl rand -hex 32)'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. oauth client ID
|
||||||
|
export JUPYTERHUB_CLIENT_ID='whoami-oauth-client-test'
|
||||||
|
# 3. where the Hub is
|
||||||
|
export JUPYTERHUB_URL='http://127.0.0.1:8000'
|
||||||
|
|
||||||
|
# 4. where to run
|
||||||
|
export JUPYTERHUB_SERVICE_URL='http://127.0.0.1:5555'
|
||||||
|
|
||||||
|
# launch the service
|
||||||
|
exec python3 whoami-oauth-basic.py
|
21
examples/external-oauth/launch-service.sh
Normal file
21
examples/external-oauth/launch-service.sh
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# script to launch whoami-oauth service
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# the service needs to know:
|
||||||
|
# 1. API token
|
||||||
|
if [[ -z "${JUPYTERHUB_API_TOKEN}" ]]; then
|
||||||
|
echo 'set API token with export JUPYTERHUB_API_TOKEN=$(openssl rand -hex 32)'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. oauth client ID
|
||||||
|
export JUPYTERHUB_CLIENT_ID="whoami-oauth-client-test"
|
||||||
|
# 3. what URL to run on
|
||||||
|
export JUPYTERHUB_SERVICE_PREFIX='/'
|
||||||
|
export JUPYTERHUB_SERVICE_URL='http://127.0.0.1:5555'
|
||||||
|
export JUPYTERHUB_OAUTH_CALLBACK_URL="$JUPYTERHUB_SERVICE_URL/oauth_callback"
|
||||||
|
# 4. where the Hub is
|
||||||
|
export JUPYTERHUB_HOST='http://127.0.0.1:8000'
|
||||||
|
|
||||||
|
# launch the service
|
||||||
|
exec python3 ../service-whoami/whoami-oauth.py
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user