Compare commits
1267 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4692d6638d | ||
![]() |
7829070e1c | ||
![]() |
5e4b935322 | ||
![]() |
4c445c7a88 | ||
![]() |
8e2965df6a | ||
![]() |
7a41d24606 | ||
![]() |
5f84a006dc | ||
![]() |
e19296a230 | ||
![]() |
89ba97f413 | ||
![]() |
fe2157130b | ||
![]() |
e3b17e8176 | ||
![]() |
027f2f95c6 | ||
![]() |
210975324a | ||
![]() |
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 | ||
![]() |
aa132cade7 | ||
![]() |
dd35ffbe86 | ||
![]() |
8edcf8be81 | ||
![]() |
11196443ac | ||
![]() |
29b02b7bcb | ||
![]() |
0383bc27b2 | ||
![]() |
65d5102b49 | ||
![]() |
8a226e6f46 | ||
![]() |
0bd34e0a10 | ||
![]() |
186107d959 | ||
![]() |
91b07b7ea4 | ||
![]() |
f5b30fd2b4 | ||
![]() |
0234396c2c | ||
![]() |
a43d677ae4 | ||
![]() |
dcfe71e7f0 | ||
![]() |
5d41376c2e | ||
![]() |
dd083359ec | ||
![]() |
e6d54960ba | ||
![]() |
a9295bc5c2 | ||
![]() |
2015c701fa | ||
![]() |
3e9c18f50a | ||
![]() |
7cac874afc | ||
![]() |
a7b6bd8d32 | ||
![]() |
1649a98656 | ||
![]() |
6694cb42c8 | ||
![]() |
b6e293c38e | ||
![]() |
02090c953b | ||
![]() |
ecbe51f60f | ||
![]() |
fed14abed3 | ||
![]() |
94978ea9e0 | ||
![]() |
bf6999e439 | ||
![]() |
020ee7378f | ||
![]() |
e4a0569961 | ||
![]() |
4ff525d5bd | ||
![]() |
37a31b01b2 | ||
![]() |
1604cb1b0b | ||
![]() |
45702ac18c | ||
![]() |
c81e9d60e4 | ||
![]() |
224865b894 | ||
![]() |
3b3bc8224b | ||
![]() |
c56dc2ea6f | ||
![]() |
62202bbb74 | ||
![]() |
7ba28c0207 | ||
![]() |
9392a29dad | ||
![]() |
72ab8f99ec | ||
![]() |
fcf32c7e50 | ||
![]() |
da451d6552 | ||
![]() |
dbe8bf5428 | ||
![]() |
662b1a4d4a | ||
![]() |
732adea997 | ||
![]() |
7e1dbf3515 | ||
![]() |
65b92ec246 | ||
![]() |
dc42ee4779 | ||
![]() |
c04441c1b2 | ||
![]() |
c3faef8e2a | ||
![]() |
d2175635af | ||
![]() |
1f7401cd14 | ||
![]() |
c94b3e34d2 | ||
![]() |
566e1d05ea | ||
![]() |
0488d0bd73 | ||
![]() |
ca31d9b426 | ||
![]() |
8721f9010f | ||
![]() |
88de48ebac | ||
![]() |
d5a6e2b2ac | ||
![]() |
2152a94156 | ||
![]() |
bc3824e9bf | ||
![]() |
60bc92cf78 | ||
![]() |
3b15467738 | ||
![]() |
4970fe0a1c | ||
![]() |
7dbe2425b8 | ||
![]() |
433d44a642 | ||
![]() |
7733d320d0 | ||
![]() |
20d367c2a8 | ||
![]() |
4687fbe075 | ||
![]() |
b0dc52781e | ||
![]() |
4f1f7d6b8f | ||
![]() |
41f8608f4e | ||
![]() |
ba3a8f2e76 | ||
![]() |
12e3a5496d | ||
![]() |
280644bab5 | ||
![]() |
bf28371356 | ||
![]() |
ce237181f2 | ||
![]() |
85ca5a052e | ||
![]() |
db8b3dbce9 | ||
![]() |
9c2d56f015 | ||
![]() |
d244a1e02f | ||
![]() |
9f134277a9 | ||
![]() |
ef9aca7bcb | ||
![]() |
32f39f23eb | ||
![]() |
c9b2beb821 | ||
![]() |
e9ad82e350 | ||
![]() |
347dd3cc0f | ||
![]() |
798346dbe8 | ||
![]() |
fd94c6de17 | ||
![]() |
3fc6fc32c5 | ||
![]() |
a1b6aa5537 | ||
![]() |
f9965bb3c3 | ||
![]() |
541997371c | ||
![]() |
522c3e5bee | ||
![]() |
1baf434695 | ||
![]() |
92db71f293 | ||
![]() |
b985f8384d | ||
![]() |
4c2d049e70 | ||
![]() |
605c4f121c | ||
![]() |
4baf5035cb | ||
![]() |
f8a57eb7d9 | ||
![]() |
93ac343493 | ||
![]() |
dc092186f0 | ||
![]() |
6b7c319351 | ||
![]() |
ef5885f769 | ||
![]() |
0ffd53424d | ||
![]() |
5f464d01b4 | ||
![]() |
0a054cc651 | ||
![]() |
348af48d45 | ||
![]() |
4d03c00dab | ||
![]() |
7a71074a55 | ||
![]() |
5527a3e7dd | ||
![]() |
f961800fa4 | ||
![]() |
adbf961433 | ||
![]() |
73e130cb2c | ||
![]() |
a44f178b64 | ||
![]() |
057fe32e3b | ||
![]() |
cad9ffa453 | ||
![]() |
a11193a240 | ||
![]() |
ea61a580b3 | ||
![]() |
0bf6db92dd | ||
![]() |
b0f38e7626 | ||
![]() |
0f237f28e7 | ||
![]() |
d63bd944ac | ||
![]() |
54e28d759d | ||
![]() |
a00c13ba67 | ||
![]() |
b4bc5437dd | ||
![]() |
13bc0397f6 | ||
![]() |
9eb30f6ff6 | ||
![]() |
17f20d8593 | ||
![]() |
cd23e086a8 | ||
![]() |
03087f20fe | ||
![]() |
f536eb4629 | ||
![]() |
f3e814aa8a | ||
![]() |
5fb0a6dffe | ||
![]() |
c7ba86d1d8 | ||
![]() |
38dcc694b7 | ||
![]() |
fdfffefefa | ||
![]() |
4e7704afd9 | ||
![]() |
b52fcf4936 | ||
![]() |
539be2f08e | ||
![]() |
29b2836c50 | ||
![]() |
3a757d003a | ||
![]() |
236802be1f | ||
![]() |
4a2c9e97c6 | ||
![]() |
0444d8465c | ||
![]() |
faef34e4ff | ||
![]() |
c174ec42f0 | ||
![]() |
d484728de9 | ||
![]() |
7da7f7e074 | ||
![]() |
53bdcd7d74 | ||
![]() |
1849964699 | ||
![]() |
5163c7a97f | ||
![]() |
b9daef9947 | ||
![]() |
f16e0488ab | ||
![]() |
adc16be4dc | ||
![]() |
3e4b4149de | ||
![]() |
c392bae7e4 | ||
![]() |
2e5373aa37 | ||
![]() |
5412cd414f | ||
![]() |
d957c5158f | ||
![]() |
4a622cb964 | ||
![]() |
69e721de46 | ||
![]() |
f3f130f452 | ||
![]() |
fd4a04e3f3 | ||
![]() |
85c040ab8e | ||
![]() |
2bb4cd4739 | ||
![]() |
4c3b134f10 | ||
![]() |
bb8536b553 | ||
![]() |
8998fd480c | ||
![]() |
d948fed0b5 | ||
![]() |
fcfe6314ac | ||
![]() |
dcfe2aa792 | ||
![]() |
85790ab9d8 | ||
![]() |
adda2fcd90 | ||
![]() |
5604e983db | ||
![]() |
386563a10a | ||
![]() |
0e3c5cf625 | ||
![]() |
a3eb2d2b9a | ||
![]() |
b6a8860a44 | ||
![]() |
b8a649ae86 | ||
![]() |
7774bfc612 | ||
![]() |
9f76613aed | ||
![]() |
f1ccbe4bed | ||
![]() |
668d78f729 | ||
![]() |
0009b9a3d6 | ||
![]() |
b2be07ea6a | ||
![]() |
74649eaad0 | ||
![]() |
f33086aa13 | ||
![]() |
9c1cd960fc | ||
![]() |
3a5226ffa0 | ||
![]() |
96a53f9921 | ||
![]() |
ff92ac9dad | ||
![]() |
933478bfff | ||
![]() |
7d996f91b0 | ||
![]() |
c818cbb644 | ||
![]() |
e638e5b684 | ||
![]() |
625e76ea40 | ||
![]() |
f8229c9fb6 | ||
![]() |
47da422a93 | ||
![]() |
3dd98bc0fc | ||
![]() |
fa6e4aa449 | ||
![]() |
182472f921 | ||
![]() |
d99afe531d | ||
![]() |
b6b238073f | ||
![]() |
a4c696d3bd | ||
![]() |
bce767120c | ||
![]() |
6a9f346b21 | ||
![]() |
d4646e1caa | ||
![]() |
77f0e00695 | ||
![]() |
26a6c89b3a | ||
![]() |
34297b82b3 | ||
![]() |
70727c4940 | ||
![]() |
56080e5436 | ||
![]() |
309b1bda75 | ||
![]() |
f3ebb694b4 | ||
![]() |
f35c14318a | ||
![]() |
b60f2e8233 | ||
![]() |
f1a55e31ce | ||
![]() |
2432611264 | ||
![]() |
729b608eff | ||
![]() |
eb3252da28 | ||
![]() |
a9e9338ee4 | ||
![]() |
aad063e3cd | ||
![]() |
be00265d1a | ||
![]() |
335ba4f453 | ||
![]() |
5a4f3a4910 | ||
![]() |
7ee4be0f13 | ||
![]() |
10c3fbe5cf | ||
![]() |
13826a41a1 | ||
![]() |
cb35026637 | ||
![]() |
24c080cf4a | ||
![]() |
e9fc629285 | ||
![]() |
150b67c1c9 | ||
![]() |
acdee0ac29 | ||
![]() |
193b236ef1 | ||
![]() |
1851e6a29d | ||
![]() |
74f086629c | ||
![]() |
33a59c8352 | ||
![]() |
08644fea74 | ||
![]() |
f878bf6ad3 | ||
![]() |
651c457266 | ||
![]() |
2dd3463ea8 | ||
![]() |
ad93af8cc8 | ||
![]() |
080cf7a29b | ||
![]() |
b8f4803ef4 | ||
![]() |
4a8f51ed6d | ||
![]() |
7923074ed5 | ||
![]() |
834b2ba77d | ||
![]() |
7897a13ca5 | ||
![]() |
7987011372 | ||
![]() |
d7a76077bd | ||
![]() |
62731cf489 | ||
![]() |
5d501bc465 | ||
![]() |
63a6841848 | ||
![]() |
403241bd98 | ||
![]() |
de3fe88df6 | ||
![]() |
6a370286e1 | ||
![]() |
491b7e7d11 | ||
![]() |
0b0db97117 | ||
![]() |
42a993fd08 | ||
![]() |
fd1544bf41 | ||
![]() |
ed36207328 | ||
![]() |
a0b8ccf805 | ||
![]() |
9d2278d29b | ||
![]() |
df42385d7e | ||
![]() |
02796d4daa | ||
![]() |
80c5f67335 | ||
![]() |
0b14e89404 | ||
![]() |
f595b1ad59 | ||
![]() |
80ca1eacc5 | ||
![]() |
5b3ac6c840 | ||
![]() |
0000b7447a | ||
![]() |
a22060ca7f | ||
![]() |
8ca321ecc3 | ||
![]() |
862cb3640b | ||
![]() |
51908c9673 | ||
![]() |
9aa4046093 | ||
![]() |
acb49adfea | ||
![]() |
f345ad5422 | ||
![]() |
5ad618bfc1 | ||
![]() |
26b00578a1 | ||
![]() |
c3111b04bb | ||
![]() |
a61ba74360 | ||
![]() |
4de93fd1d5 | ||
![]() |
46bb7b05f4 | ||
![]() |
1aa2cb1921 | ||
![]() |
c4bfa63fd6 | ||
![]() |
4c5d6167bd | ||
![]() |
9a002c2445 | ||
![]() |
f97d32c5bd | ||
![]() |
bac311677f | ||
![]() |
94cb5b3a05 | ||
![]() |
ed4f0ba014 | ||
![]() |
fd219b5fff | ||
![]() |
140c4f2909 | ||
![]() |
a1c787ba5f | ||
![]() |
54c808fe98 | ||
![]() |
eaeec9f19b | ||
![]() |
21d25ac130 | ||
![]() |
eda21642bd | ||
![]() |
aace54d5b2 | ||
![]() |
e460c00759 | ||
![]() |
678fd1cd08 | ||
![]() |
42c78f3c43 | ||
![]() |
548e0f6153 | ||
![]() |
31f63c737f | ||
![]() |
71b35602d7 | ||
![]() |
7c41a024ba | ||
![]() |
51097de43d | ||
![]() |
44e16d538d | ||
![]() |
f6517d01db | ||
![]() |
039b925cf6 | ||
![]() |
bba5460236 | ||
![]() |
e5d3705a1a | ||
![]() |
7b80b95a49 | ||
![]() |
75cb487ab3 | ||
![]() |
eba4b3e8c7 | ||
![]() |
712b895d8e | ||
![]() |
635fd9b2c3 | ||
![]() |
afcbdd9bc4 | ||
![]() |
80fa5418b7 | ||
![]() |
b0a09c027d | ||
![]() |
4edf59efeb | ||
![]() |
9f0dec1247 | ||
![]() |
2c47fd4a02 | ||
![]() |
9878f1e32d | ||
![]() |
5c396668ff | ||
![]() |
5f12f9f2c3 | ||
![]() |
4974775cd9 | ||
![]() |
0cb777cd0f | ||
![]() |
a4bb25a75f | ||
![]() |
b3f117bc59 | ||
![]() |
499ba89f07 | ||
![]() |
05d743f725 | ||
![]() |
a347d56623 | ||
![]() |
172976208e | ||
![]() |
b6db3f59a2 | ||
![]() |
4b31279fc8 | ||
![]() |
bfef83cefc | ||
![]() |
07d599fed2 | ||
![]() |
0412407558 | ||
![]() |
4c568b46d6 | ||
![]() |
d92fcf5827 | ||
![]() |
36f3abbfc7 | ||
![]() |
49a45b13e6 | ||
![]() |
dfa13cb2c5 | ||
![]() |
fd3b959771 | ||
![]() |
39a80edb74 | ||
![]() |
2a35d1c8a6 | ||
![]() |
81350322d7 | ||
![]() |
50c2528359 | ||
![]() |
77bac30654 | ||
![]() |
41fafc74cf | ||
![]() |
c6281160fa | ||
![]() |
3159b61ae7 | ||
![]() |
11278ddb26 | ||
![]() |
e299a6c279 | ||
![]() |
22ff5f3d91 | ||
![]() |
a3e8bd346f | ||
![]() |
592a084a28 | ||
![]() |
c27e59b0f9 | ||
![]() |
1c9bc1b133 | ||
![]() |
be4f4853cf | ||
![]() |
7d8895c2fb | ||
![]() |
5b8913be5b | ||
![]() |
d03a1ee490 | ||
![]() |
19ae38c108 | ||
![]() |
9b71f11213 | ||
![]() |
8fbaedf4d7 | ||
![]() |
87ab07b322 | ||
![]() |
f36a1e10e6 | ||
![]() |
5944671663 | ||
![]() |
27dfd0edca | ||
![]() |
9dfc043352 | ||
![]() |
e8bd1520b2 | ||
![]() |
a30b9976f5 | ||
![]() |
954e5b3d5e | ||
![]() |
7cd8aa266b | ||
![]() |
d0449d136c | ||
![]() |
ff9aeb70b4 | ||
![]() |
2eaecd22ba | ||
![]() |
4801d647c1 | ||
![]() |
b7e6fa3abe | ||
![]() |
d590024c47 | ||
![]() |
f3f71c38c3 | ||
![]() |
27125a169c | ||
![]() |
3f9205d405 | ||
![]() |
96861dc2b0 | ||
![]() |
cedaa184f1 | ||
![]() |
f491791081 | ||
![]() |
6bba1c474f | ||
![]() |
357f6799b0 | ||
![]() |
ce3ea270f5 | ||
![]() |
992717adc0 | ||
![]() |
993101710f | ||
![]() |
ac6fe61804 | ||
![]() |
37aa1a291a | ||
![]() |
c6294f2763 | ||
![]() |
6e9a77f55f | ||
![]() |
799b407d89 | ||
![]() |
3ddfa5f939 | ||
![]() |
5968661742 | ||
![]() |
34592e3da5 | ||
![]() |
5aea7eda96 | ||
![]() |
08024be1c0 | ||
![]() |
39daff3099 | ||
![]() |
d4c0fe8679 | ||
![]() |
c9ae45bef3 | ||
![]() |
503f21fd37 | ||
![]() |
6d106b24f4 | ||
![]() |
71f47b7a70 | ||
![]() |
844381e7c9 | ||
![]() |
267994b191 | ||
![]() |
cc2202c188 | ||
![]() |
4996a84ca0 | ||
![]() |
3cefc2951c | ||
![]() |
835b4afc06 | ||
![]() |
146bef1d88 | ||
![]() |
ef9656eb8b | ||
![]() |
84868a6475 | ||
![]() |
9e9c6f2761 | ||
![]() |
19e8bdacfe | ||
![]() |
c6640aa51d | ||
![]() |
1514a2f2e2 | ||
![]() |
9edb282067 | ||
![]() |
9ffe5e6187 | ||
![]() |
14662111a8 | ||
![]() |
a7ea5774d9 | ||
![]() |
c998458362 | ||
![]() |
07ddede40c | ||
![]() |
b8a6ac62e8 | ||
![]() |
86e9a3217c | ||
![]() |
f591e6e3fb | ||
![]() |
64dd1db327 | ||
![]() |
b68569f61c | ||
![]() |
3a52e3f4df | ||
![]() |
05c268e190 | ||
![]() |
98937de278 | ||
![]() |
ff35e3b93e | ||
![]() |
4eebc95109 | ||
![]() |
c708c2a3a0 | ||
![]() |
35f8190128 | ||
![]() |
78b268ddef | ||
![]() |
eb99060a25 | ||
![]() |
8e99f659f5 | ||
![]() |
5c9e9d65b5 | ||
![]() |
3e768b7297 | ||
![]() |
aa2999210d | ||
![]() |
be95a27597 | ||
![]() |
5edcdd4fb2 | ||
![]() |
b81586de0a | ||
![]() |
e0f3e3b954 | ||
![]() |
3037d264c3 | ||
![]() |
17f1346c08 | ||
![]() |
276aba9f85 | ||
![]() |
0ba63c42fd | ||
![]() |
2985562c2f | ||
![]() |
754f850e95 | ||
![]() |
dccb85d225 | ||
![]() |
a0e401bc87 | ||
![]() |
c6885a2124 | ||
![]() |
7528fb7d9b | ||
![]() |
e7df5a299c | ||
![]() |
ff997bbce5 | ||
![]() |
1e21e00e1a | ||
![]() |
77d3ee98f9 | ||
![]() |
1f861b2c90 | ||
![]() |
14a00e67b4 | ||
![]() |
14f63c168d | ||
![]() |
e70dbb3d32 | ||
![]() |
b679275a68 | ||
![]() |
0c1478a67e | ||
![]() |
d26e2346a2 | ||
![]() |
9a09c841b9 | ||
![]() |
f1d4f5a733 | ||
![]() |
d970dd4c89 | ||
![]() |
f3279bf849 | ||
![]() |
db0878a495 | ||
![]() |
c9b1042791 | ||
![]() |
cd81320d8f |
15
.coveragerc
@@ -1,4 +1,17 @@
|
||||
[run]
|
||||
branch = False
|
||||
omit =
|
||||
jupyterhub/tests/*
|
||||
jupyterhub/singleuser.py
|
||||
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_config.py
|
||||
node_modules
|
||||
docs
|
||||
.git
|
||||
dist
|
||||
build
|
||||
|
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
|
29
.github/issue_template.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
|
||||
|
||||
```
|
4
.gitignore
vendored
@@ -1,10 +1,12 @@
|
||||
node_modules
|
||||
*.py[co]
|
||||
*~
|
||||
.cache
|
||||
.DS_Store
|
||||
build
|
||||
/build
|
||||
dist
|
||||
docs/_build
|
||||
docs/source/_static/rest-api
|
||||
.ipynb_checkpoints
|
||||
# ignore config file at the top-level of the repo
|
||||
# but not sub-dirs
|
||||
|
52
.travis.yml
@@ -1,21 +1,49 @@
|
||||
# http://travis-ci.org/#!/jupyter/jupyterhub
|
||||
language: python
|
||||
sudo: false
|
||||
python:
|
||||
- 3.5
|
||||
- 3.4
|
||||
- 3.3
|
||||
- nightly
|
||||
- 3.6
|
||||
- 3.5
|
||||
- 3.4
|
||||
env:
|
||||
global:
|
||||
- ASYNC_TEST_TIMEOUT=15
|
||||
services:
|
||||
- mysql
|
||||
- postgresql
|
||||
|
||||
# installing dependencies
|
||||
before_install:
|
||||
- npm install
|
||||
- npm install -g configurable-http-proxy
|
||||
- git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels
|
||||
- nvm install 6; nvm use 6
|
||||
- npm install
|
||||
- npm install -g configurable-http-proxy
|
||||
- |
|
||||
if [[ $JUPYTERHUB_TEST_DB_URL == mysql* ]]; then
|
||||
mysql -e 'CREATE DATABASE jupyterhub CHARACTER SET utf8 COLLATE utf8_general_ci;'
|
||||
pip install 'mysql-connector<2.2'
|
||||
elif [[ $JUPYTERHUB_TEST_DB_URL == postgresql* ]]; then
|
||||
psql -c 'create database jupyterhub;' -U postgres
|
||||
pip install psycopg2
|
||||
fi
|
||||
install:
|
||||
- pip install -f travis-wheels/wheelhouse -r dev-requirements.txt .
|
||||
- pip install -U pip
|
||||
- pip install --pre -r dev-requirements.txt .
|
||||
- pip freeze
|
||||
|
||||
# running tests
|
||||
script:
|
||||
- travis_retry py.test --cov jupyterhub jupyterhub/tests -v
|
||||
- pytest -v --maxfail=2 --cov=jupyterhub jupyterhub/tests
|
||||
after_success:
|
||||
- codecov
|
||||
- codecov
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: 3.5
|
||||
env: JUPYTERHUB_TEST_SUBDOMAIN_HOST=http://127.0.0.1.xip.io:8000
|
||||
- python: 3.6
|
||||
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/jupyterhub
|
||||
- python: 3.6
|
||||
env: JUPYTERHUB_TEST_DB_URL=postgresql://postgres@127.0.0.1/jupyterhub
|
||||
allow_failures:
|
||||
- python: nightly
|
||||
|
26
CHECKLIST-Release.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Release checklist
|
||||
|
||||
- [ ] Upgrade Docs prior to Release
|
||||
|
||||
- [ ] Change log
|
||||
- [ ] New features documented
|
||||
- [ ] Update the contributor list - thank you page
|
||||
|
||||
- [ ] Upgrade and test Reference Deployments
|
||||
|
||||
- [ ] Release software
|
||||
|
||||
- [ ] Make sure 0 issues in milestone
|
||||
- [ ] Follow release process steps
|
||||
- [ ] Send builds to PyPI (Warehouse) and Conda Forge
|
||||
|
||||
- [ ] Blog post and/or release note
|
||||
|
||||
- [ ] Notify users of release
|
||||
|
||||
- [ ] Email Jupyter and Jupyter In Education mailing lists
|
||||
- [ ] Tweet (optional)
|
||||
|
||||
- [ ] Increment the version number for the next release
|
||||
|
||||
- [ ] Update roadmap
|
@@ -1,3 +1,3 @@
|
||||
# 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).
|
||||
|
21
Dockerfile
@@ -24,11 +24,13 @@
|
||||
FROM debian:jessie
|
||||
MAINTAINER Jupyter Project <jupyter@googlegroups.com>
|
||||
|
||||
# install nodejs, utf8 locale
|
||||
# install nodejs, utf8 locale, set CDN because default httpredir is unreliable
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get -y update && \
|
||||
RUN REPO=http://cdn-fastly.deb.debian.org && \
|
||||
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 install npm nodejs nodejs-legacy wget locales git &&\
|
||||
apt-get -y install wget locales git bzip2 &&\
|
||||
/usr/sbin/update-locale LANG=C.UTF-8 && \
|
||||
locale-gen C.UTF-8 && \
|
||||
apt-get remove -y locales && \
|
||||
@@ -36,18 +38,17 @@ RUN apt-get -y update && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
# install Python with conda
|
||||
RUN wget -q https://repo.continuum.io/miniconda/Miniconda3-4.0.5-Linux-x86_64.sh -O /tmp/miniconda.sh && \
|
||||
echo 'a7bcd0425d8b6688753946b59681572f63c2241aed77bf0ec6de4c5edc5ceeac */tmp/miniconda.sh' | shasum -a 256 -c - && \
|
||||
# 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 && \
|
||||
echo 'd0c7c71cc5659e54ab51f2005a8d96f3 */tmp/miniconda.sh' | md5sum -c - && \
|
||||
bash /tmp/miniconda.sh -f -b -p /opt/conda && \
|
||||
/opt/conda/bin/conda install --yes python=3.5 sqlalchemy tornado jinja2 traitlets requests pip && \
|
||||
/opt/conda/bin/conda install --yes -c conda-forge \
|
||||
python=3.5 sqlalchemy tornado jinja2 traitlets requests pip pycurl \
|
||||
nodejs configurable-http-proxy && \
|
||||
/opt/conda/bin/pip install --upgrade pip && \
|
||||
rm /tmp/miniconda.sh
|
||||
ENV PATH=/opt/conda/bin:$PATH
|
||||
|
||||
# install js dependencies
|
||||
RUN npm install -g configurable-http-proxy && rm -rf ~/.npm
|
||||
|
||||
ADD . /src/jupyterhub
|
||||
WORKDIR /src/jupyterhub
|
||||
|
||||
|
@@ -10,9 +10,11 @@ graft onbuild
|
||||
graft jupyterhub
|
||||
graft scripts
|
||||
graft share
|
||||
graft singleuser
|
||||
|
||||
# Documentation
|
||||
graft docs
|
||||
prune docs/node_modules
|
||||
|
||||
# prune some large unused files from components
|
||||
prune share/jupyter/hub/static/components/bootstrap/css
|
||||
|
325
README.md
@@ -1,158 +1,253 @@
|
||||
# JupyterHub: A multi-user server for Jupyter notebooks
|
||||
**[Technical Overview](#technical-overview)** |
|
||||
**[Installation](#installation)** |
|
||||
**[Configuration](#configuration)** |
|
||||
**[Docker](#docker)** |
|
||||
**[Contributing](#contributing)** |
|
||||
**[License](#license)** |
|
||||
**[Help and Resources](#help-and-resources)**
|
||||
|
||||
Questions, comments? Visit our Google Group:
|
||||
|
||||
[](https://groups.google.com/forum/#!forum/jupyter)
|
||||
# [JupyterHub](https://github.com/jupyterhub/jupyterhub)
|
||||
|
||||
|
||||
[](https://pypi.python.org/pypi/jupyterhub)
|
||||
[](http://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
||||
[](http://jupyterhub.readthedocs.io/en/0.7.2/?badge=0.7.2)
|
||||
[](https://travis-ci.org/jupyterhub/jupyterhub)
|
||||
[](https://circleci.com/gh/jupyterhub/jupyterhub)
|
||||
[](http://jupyterhub.readthedocs.org/en/latest/?badge=latest)
|
||||
[](https://codecov.io/github/jupyter/jupyterhub?branch=master)
|
||||
[](https://codecov.io/github/jupyterhub/jupyterhub?branch=master)
|
||||
[](https://groups.google.com/forum/#!forum/jupyter)
|
||||
|
||||
With [JupyterHub](https://jupyterhub.readthedocs.io) you can create a
|
||||
**multi-user Hub** which spawns, manages, and proxies multiple instances of the
|
||||
single-user [Jupyter notebook (IPython notebook)](https://jupyter-notebook.readthedocs.io)
|
||||
server.
|
||||
|
||||
JupyterHub, a multi-user server, manages and proxies multiple instances of the single-user <del>IPython</del> Jupyter notebook server.
|
||||
[Project Jupyter](https://jupyter.org) created JupyterHub to support many
|
||||
users. The Hub can offer notebook servers to a class of students, a corporate
|
||||
data science workgroup, a scientific research project, or a high performance
|
||||
computing group.
|
||||
|
||||
Three actors:
|
||||
## Technical overview
|
||||
|
||||
- multi-user Hub (tornado process)
|
||||
- configurable http proxy (node-http-proxy)
|
||||
- multiple single-user IPython notebook servers (Python/IPython/tornado)
|
||||
Three main actors make up JupyterHub:
|
||||
|
||||
Basic principles:
|
||||
- multi-user **Hub** (tornado process)
|
||||
- configurable http **proxy** (node-http-proxy)
|
||||
- multiple **single-user Jupyter notebook servers** (Python/IPython/tornado)
|
||||
|
||||
- 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
|
||||
Basic principles for operation are:
|
||||
|
||||
- Hub spawns a 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 the single-user notebook
|
||||
servers.
|
||||
|
||||
## Dependencies
|
||||
|
||||
JupyterHub itself requires [Python](https://www.python.org/downloads/) ≥ 3.3. To run the single-user servers (which may be on the same system as the Hub or not), [Jupyter Notebook](https://jupyter.readthedocs.org/en/latest/install.html) ≥ 4 is required.
|
||||
|
||||
Install [nodejs/npm](https://www.npmjs.com/), 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.)
|
||||
|
||||
Next, install JavaScript dependencies:
|
||||
|
||||
sudo npm install -g configurable-http-proxy
|
||||
|
||||
### (Optional) Installation Prerequisite (pip)
|
||||
|
||||
Notes on the `pip` command used in the installation directions below:
|
||||
- `sudo` may be needed for `pip install`, depending on the user's filesystem permissions.
|
||||
- JupyterHub requires Python >= 3.3, so `pip3` may be required on some machines for package installation instead of `pip` (especially when both Python 2 and Python 3 are installed on a machine). If `pip3` is not found, install it using (on Linux Debian/Ubuntu):
|
||||
|
||||
sudo apt-get install python3-pip
|
||||
|
||||
JupyterHub also provides a
|
||||
[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 its users.
|
||||
|
||||
## Installation
|
||||
|
||||
JupyterHub can be installed with pip, and the proxy with npm:
|
||||
### Check prerequisites
|
||||
|
||||
npm install -g configurable-http-proxy
|
||||
pip3 install jupyterhub
|
||||
A Linux/Unix based system with the following:
|
||||
|
||||
If you plan to run notebook servers locally, you may also need to install the
|
||||
Jupyter ~~IPython~~ notebook:
|
||||
- [Python](https://www.python.org/downloads/) 3.4 or greater
|
||||
- [nodejs/npm](https://www.npmjs.com/) Install a recent version of
|
||||
[nodejs/npm](https://docs.npmjs.com/getting-started/installing-node)
|
||||
For example, install it on Linux (Debian/Ubuntu) using:
|
||||
|
||||
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
|
||||
|
||||
JupyterHub can be installed with `pip`, and the proxy with `npm`:
|
||||
|
||||
```bash
|
||||
npm install -g configurable-http-proxy
|
||||
pip3 install jupyterhub
|
||||
```
|
||||
|
||||
If you plan to run notebook servers locally, you will need to install the
|
||||
[Jupyter notebook](https://jupyter.readthedocs.io/en/latest/install.html)
|
||||
package:
|
||||
|
||||
pip3 install --upgrade notebook
|
||||
|
||||
### Run the Hub server
|
||||
|
||||
### Development install
|
||||
To start the Hub server, run the command:
|
||||
|
||||
For a development install, clone the repository and then install from source:
|
||||
jupyterhub
|
||||
|
||||
git clone https://github.com/jupyter/jupyterhub
|
||||
cd jupyterhub
|
||||
pip3 install -r dev-requirements.txt -e .
|
||||
Visit `https://localhost:8000` in your browser, and sign in with your unix
|
||||
PAM credentials.
|
||||
|
||||
If the `pip3 install` command fails and complains about `lessc` being unavailable, you may need to explicitly install some additional JavaScript dependencies:
|
||||
*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.
|
||||
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 configuration of the system.
|
||||
|
||||
## Configuration
|
||||
|
||||
The [Getting Started](http://jupyterhub.readthedocs.io/en/latest/getting-started/index.html) section of the
|
||||
documentation explains the common steps in setting up JupyterHub.
|
||||
|
||||
The [**JupyterHub tutorial**](https://github.com/jupyterhub/jupyterhub-tutorial)
|
||||
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
|
||||
|
||||
### Start the Hub
|
||||
|
||||
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
|
||||
|
||||
### Authenticators
|
||||
|
||||
| Authenticator | Description |
|
||||
| --------------------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| 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
|
||||
|
||||
A starter [**docker image for JupyterHub**](https://hub.docker.com/r/jupyterhub/jupyterhub/)
|
||||
gives a baseline deployment of JupyterHub using Docker.
|
||||
|
||||
**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.
|
||||
|
||||
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 by 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.
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
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:
|
||||
You may also need to manually update JavaScript and CSS after some development
|
||||
updates, with:
|
||||
|
||||
python3 setup.py js # fetch updated client-side js (changes rarely)
|
||||
python3 setup.py css # recompile CSS from LESS sources
|
||||
```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 **running tests**:
|
||||
|
||||
## Running the server
|
||||
```bash
|
||||
pytest jupyterhub/tests
|
||||
```
|
||||
|
||||
To start the server, run the command:
|
||||
### A note about platform support
|
||||
|
||||
jupyterhub
|
||||
JupyterHub is supported on Linux/Unix based systems.
|
||||
|
||||
and then visit `http://localhost:8000`, and sign in with your unix credentials.
|
||||
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.
|
||||
|
||||
To allow multiple users to sign into the server, you will need to
|
||||
run the `jupyterhub` command as a *privileged user*, such as root.
|
||||
The [wiki](https://github.com/jupyter/jupyterhub/wiki/Using-sudo-to-run-JupyterHub-without-root-privileges)
|
||||
describes how to run the server as a *less privileged user*, which requires more
|
||||
configuration of the system.
|
||||
[Additional Reference:](http://www.tornadoweb.org/en/stable/#installation) Tornado's documentation on Windows platform support
|
||||
|
||||
## Getting started
|
||||
## License
|
||||
|
||||
See the [getting started document](docs/source/getting-started.md) for the
|
||||
basics of configuring your JupyterHub deployment.
|
||||
We use a shared copyright model that enables all contributors to maintain the
|
||||
copyright on their contributions.
|
||||
|
||||
### Some examples
|
||||
All code is licensed under the terms of the revised BSD license.
|
||||
|
||||
Generate a default config file:
|
||||
## Help and resources
|
||||
|
||||
jupyterhub --generate-config
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
- Using GitHub OAuth instead of PAM with [OAuthenticator](https://github.com/jupyter/oauthenticator)
|
||||
- Spawning single-user servers with Docker, using the [DockerSpawner](https://github.com/jupyter/dockerspawner)
|
||||
|
||||
### Docker
|
||||
|
||||
There is a ready to go [docker image for JupyterHub](https://hub.docker.com/r/jupyter/jupyterhub/).
|
||||
[Note: This `jupyter/jupyterhub` docker image is only an image for running the Hub service itself.
|
||||
It does not require the other Jupyter components, 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, installation of Jupyter Notebook ≥ 4 is required.]
|
||||
|
||||
The JupyterHub docker image can be started with the following command:
|
||||
|
||||
docker run -d --name jupyterhub jupyter/jupyterhub jupyterhub
|
||||
|
||||
This command will create a container named `jupyterhub` that you can stop and resume with `docker stop/start`.
|
||||
It will be listening on all interfaces at port 8000, so this is perfect to test 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 it to create system users in the container. These accounts will be used for authentication
|
||||
in jupyterhub's default configuration. In order to run without SSL (for testing purposes only), you'll need to set `--no-ssl` explicitly.
|
||||
|
||||
# Getting help
|
||||
|
||||
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/jupyter/jupyterhub?utm_source=badge&utm_medium=badge)
|
||||
|
||||
## Resources
|
||||
- [Reporting Issues](https://github.com/jupyterhub/jupyterhub/issues)
|
||||
- [JupyterHub tutorial](https://github.com/jupyterhub/jupyterhub-tutorial)
|
||||
- [Documentation for JupyterHub](http://jupyterhub.readthedocs.io/en/latest/) | [PDF (latest)](https://media.readthedocs.org/pdf/jupyterhub/latest/jupyterhub.pdf) | [PDF (stable)](https://media.readthedocs.org/pdf/jupyterhub/stable/jupyterhub.pdf)
|
||||
- [Documentation for JupyterHub's REST API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/master/docs/rest-api.yml#/default)
|
||||
- [Documentation for Project Jupyter](http://jupyter.readthedocs.io/en/latest/index.html) | [PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)
|
||||
- [Project Jupyter website](https://jupyter.org)
|
||||
- [Documentation for JupyterHub](http://jupyterhub.readthedocs.org/en/latest/) [[PDF](https://media.readthedocs.org/pdf/jupyterhub/latest/jupyterhub.pdf)]
|
||||
- [Documentation for Project Jupyter](http://jupyter.readthedocs.org/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)]
|
||||
- [Issues](https://github.com/jupyter/jupyterhub/issues)
|
||||
- [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter)
|
||||
|
||||
---
|
||||
|
||||
**[Technical Overview](#technical-overview)** |
|
||||
**[Installation](#installation)** |
|
||||
**[Configuration](#configuration)** |
|
||||
**[Docker](#docker)** |
|
||||
**[Contributing](#contributing)** |
|
||||
**[License](#license)** |
|
||||
**[Help and Resources](#help-and-resources)**
|
||||
|
10
bower.json
@@ -2,10 +2,10 @@
|
||||
"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"
|
||||
"bootstrap": "components/bootstrap#~3.3",
|
||||
"font-awesome": "components/font-awesome#~4.7",
|
||||
"jquery": "components/jquery#~3.2",
|
||||
"moment": "~2.18",
|
||||
"requirejs": "~2.3"
|
||||
}
|
||||
}
|
||||
|
@@ -16,4 +16,9 @@ deployment:
|
||||
branch: master
|
||||
commands:
|
||||
- docker login -u $DOCKER_USER -p $DOCKER_PASS -e unused@example.com
|
||||
- docker push jupyterhub/jupyterhub-onbuild:${CIRCLE_TAG:-latest}
|
||||
- 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,5 +1,9 @@
|
||||
-r requirements.txt
|
||||
mock
|
||||
codecov
|
||||
cryptography
|
||||
pytest-cov
|
||||
pytest-tornado
|
||||
pytest>=2.8
|
||||
notebook
|
||||
requests-mock
|
||||
|
@@ -47,11 +47,20 @@ help:
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
@echo " spelling to run spell check on documentation"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
node_modules: package.json
|
||||
npm install && touch node_modules
|
||||
|
||||
rest-api: source/_static/rest-api/index.html
|
||||
|
||||
source/_static/rest-api/index.html: rest-api.yml node_modules
|
||||
npm run rest-api
|
||||
|
||||
html: rest-api
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
@@ -171,6 +180,11 @@ linkcheck:
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
spelling:
|
||||
$(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling
|
||||
@echo
|
||||
@echo "Spell check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/spelling/output.txt."
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
|
19
docs/environment.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
name: jhub_docs
|
||||
channels:
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- nodejs
|
||||
- python=3.5
|
||||
- alembic
|
||||
- jinja2
|
||||
- pamela
|
||||
- requests
|
||||
- sqlalchemy>=1
|
||||
- tornado>=4.1
|
||||
- traitlets>=4.1
|
||||
- sphinx>=1.4, !=1.5.4
|
||||
- sphinx_rtd_theme
|
||||
- pip:
|
||||
- jupyter_alabaster_theme
|
||||
- python-oauth2
|
||||
- recommonmark==0.4.0
|
14
docs/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "jupyterhub-docs-build",
|
||||
"version": "0.8.0",
|
||||
"description": "build JupyterHub swagger docs",
|
||||
"scripts": {
|
||||
"rest-api": "bootprint openapi ./rest-api.yml source/_static/rest-api"
|
||||
},
|
||||
"author": "",
|
||||
"license": "BSD-3-Clause",
|
||||
"devDependencies": {
|
||||
"bootprint": "^1.0.0",
|
||||
"bootprint-openapi": "^1.0.0"
|
||||
}
|
||||
}
|
@@ -1,3 +1,3 @@
|
||||
-r ../requirements.txt
|
||||
sphinx>=1.3.6
|
||||
sphinx>=1.4
|
||||
recommonmark==0.4.0
|
@@ -3,9 +3,11 @@ swagger: '2.0'
|
||||
info:
|
||||
title: JupyterHub
|
||||
description: The REST API for JupyterHub
|
||||
version: 0.4.0
|
||||
version: 0.8.0dev
|
||||
license:
|
||||
name: BSD-3-Clause
|
||||
schemes:
|
||||
- http
|
||||
- [http, https]
|
||||
securityDefinitions:
|
||||
token:
|
||||
type: apiKey
|
||||
@@ -13,18 +15,73 @@ securityDefinitions:
|
||||
in: header
|
||||
security:
|
||||
- token: []
|
||||
basePath: /hub/api/
|
||||
basePath: /hub/api
|
||||
produces:
|
||||
- application/json
|
||||
consumes:
|
||||
- application/json
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
summary: Get JupyterHub version
|
||||
description: |
|
||||
This endpoint is not authenticated for the purpose of clients and user
|
||||
to identify the JupyterHub version before setting up authentication.
|
||||
responses:
|
||||
'200':
|
||||
description: The JupyterHub version
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
description: The version of JupyterHub itself
|
||||
/info:
|
||||
get:
|
||||
summary: Get detailed info about JupyterHub
|
||||
description: |
|
||||
Detailed JupyterHub information, including Python version,
|
||||
JupyterHub's version and executable path,
|
||||
and which Authenticator and Spawner are active.
|
||||
responses:
|
||||
'200':
|
||||
description: Detailed JupyterHub info
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
description: The version of JupyterHub itself
|
||||
python:
|
||||
type: string
|
||||
description: The Python version, as returned by sys.version
|
||||
sys_executable:
|
||||
type: string
|
||||
description: The path to sys.executable running JupyterHub
|
||||
authenticator:
|
||||
type: object
|
||||
properties:
|
||||
class:
|
||||
type: string
|
||||
description: The Python class currently active for JupyterHub Authentication
|
||||
version:
|
||||
type: string
|
||||
description: The version of the currently active Authenticator
|
||||
spawner:
|
||||
type: object
|
||||
properties:
|
||||
class:
|
||||
type: string
|
||||
description: The Python class currently active for spawning single-user notebook servers
|
||||
version:
|
||||
type: string
|
||||
description: The version of the currently active Spawner
|
||||
/users:
|
||||
get:
|
||||
summary: List users
|
||||
responses:
|
||||
'200':
|
||||
description: The user list
|
||||
description: The Hub's user list
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
@@ -40,7 +97,7 @@ paths:
|
||||
properties:
|
||||
usernames:
|
||||
type: array
|
||||
description: list of usernames to create
|
||||
description: list of usernames to create on the Hub
|
||||
items:
|
||||
type: string
|
||||
admin:
|
||||
@@ -81,17 +138,6 @@ paths:
|
||||
description: The user has been created
|
||||
schema:
|
||||
$ref: '#/definitions/User'
|
||||
delete:
|
||||
summary: Delete a user
|
||||
parameters:
|
||||
- name: name
|
||||
description: username
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
'204':
|
||||
description: The user has been deleted
|
||||
patch:
|
||||
summary: Modify a user
|
||||
description: Change a user's name or admin status
|
||||
@@ -104,24 +150,35 @@ paths:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
description: Updated user info. At least one of name and admin is required.
|
||||
description: Updated user info. At least one key to be updated (name or admin) is required.
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: the new name (optional)
|
||||
description: the new name (optional, if another key is updated i.e. admin)
|
||||
admin:
|
||||
type: boolean
|
||||
description: update admin (optional)
|
||||
description: update admin (optional, if another key is updated i.e. name)
|
||||
responses:
|
||||
'200':
|
||||
description: The updated user info
|
||||
schema:
|
||||
$ref: '#/definitions/User'
|
||||
delete:
|
||||
summary: Delete a user
|
||||
parameters:
|
||||
- name: name
|
||||
description: username
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
'204':
|
||||
description: The user has been deleted
|
||||
/users/{name}/server:
|
||||
post:
|
||||
summary: Start a user's server
|
||||
summary: Start a user's single-user notebook server
|
||||
parameters:
|
||||
- name: name
|
||||
description: username
|
||||
@@ -130,9 +187,9 @@ paths:
|
||||
type: string
|
||||
responses:
|
||||
'201':
|
||||
description: The server has started
|
||||
description: The user's notebook server has started
|
||||
'202':
|
||||
description: The server has been requested, but has not yet started
|
||||
description: The user's notebook server has not yet started, but has been requested
|
||||
delete:
|
||||
summary: Stop a user's server
|
||||
parameters:
|
||||
@@ -143,12 +200,12 @@ paths:
|
||||
type: string
|
||||
responses:
|
||||
'204':
|
||||
description: The server has stopped
|
||||
description: The user's notebook server has stopped
|
||||
'202':
|
||||
description: The server has been asked to stop, but is taking a while
|
||||
description: The user's notebook server has not yet stopped as it is taking a while to stop
|
||||
/users/{name}/admin-access:
|
||||
post:
|
||||
summary: Grant an admin access to this user's server
|
||||
summary: Grant admin access to this user's notebook server
|
||||
parameters:
|
||||
- name: name
|
||||
description: username
|
||||
@@ -157,25 +214,153 @@ paths:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Sets a cookie granting the requesting admin access to the user's server
|
||||
description: Sets a cookie granting the requesting administrator access to the user's notebook server
|
||||
/user:
|
||||
summary: Return authenticated user's model
|
||||
description:
|
||||
parameters:
|
||||
responses:
|
||||
'200':
|
||||
description: The authenticated user's model is returned.
|
||||
/groups:
|
||||
get:
|
||||
summary: List groups
|
||||
responses:
|
||||
'200':
|
||||
description: The list of groups
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Group'
|
||||
/groups/{name}:
|
||||
get:
|
||||
summary: Get a group by name
|
||||
parameters:
|
||||
- name: name
|
||||
description: group name
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: The group model
|
||||
schema:
|
||||
$ref: '#/definitions/Group'
|
||||
post:
|
||||
summary: Create a group
|
||||
parameters:
|
||||
- name: name
|
||||
description: group name
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
'201':
|
||||
description: The group has been created
|
||||
schema:
|
||||
$ref: '#/definitions/Group'
|
||||
delete:
|
||||
summary: Delete a group
|
||||
parameters:
|
||||
- name: name
|
||||
description: group name
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
'204':
|
||||
description: The group has been deleted
|
||||
/groups/{name}/users:
|
||||
post:
|
||||
summary: Add users to a group
|
||||
parameters:
|
||||
- name: name
|
||||
description: group name
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
description: The users to add to the group
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
description: List of usernames to add to the group
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: The users have been added to the group
|
||||
schema:
|
||||
$ref: '#/definitions/Group'
|
||||
delete:
|
||||
summary: Remove users from a group
|
||||
parameters:
|
||||
- name: name
|
||||
description: group name
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
description: The users to remove from the group
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
description: List of usernames to remove from the group
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: The users have been removed from the group
|
||||
/services:
|
||||
get:
|
||||
summary: List services
|
||||
responses:
|
||||
'200':
|
||||
description: The service list
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Service'
|
||||
/services/{name}:
|
||||
get:
|
||||
summary: Get a service by name
|
||||
parameters:
|
||||
- name: name
|
||||
description: service name
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: The Service model
|
||||
schema:
|
||||
$ref: '#/definitions/Service'
|
||||
/proxy:
|
||||
get:
|
||||
summary: Get the proxy's routing table
|
||||
description: A convenience alias for getting the info directly from the proxy
|
||||
description: A convenience alias for getting the routing table directly from the proxy
|
||||
responses:
|
||||
'200':
|
||||
description: Routing table
|
||||
schema:
|
||||
type: object
|
||||
description: configurable-http-proxy routing table (see CHP docs for details)
|
||||
description: configurable-http-proxy routing table (see configurable-http-proxy docs for details)
|
||||
post:
|
||||
summary: Force the Hub to sync with the proxy
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
patch:
|
||||
summary: Tell the Hub about a new proxy
|
||||
description: If you have started a new proxy and would like the Hub to switch over to it, this allows you to notify the Hub of the new proxy.
|
||||
summary: Notify the Hub about a new proxy
|
||||
description: Notifies the Hub of a new proxy to use.
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
@@ -199,9 +384,38 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
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}:
|
||||
get:
|
||||
summary: Identify a user from an API token
|
||||
summary: Identify a user or service from an API token
|
||||
parameters:
|
||||
- name: token
|
||||
in: path
|
||||
@@ -209,13 +423,13 @@ paths:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: The user identified by the API token
|
||||
schema:
|
||||
$ref: '#!/definitions/User'
|
||||
description: The user or service identified by the API token
|
||||
'404':
|
||||
description: A user or service is not found.
|
||||
/authorizations/cookie/{cookie_name}/{cookie_value}:
|
||||
get:
|
||||
summary: Identify a user from a cookie
|
||||
description: Used by single-user servers to hand off cookie authentication to the Hub
|
||||
description: Used by single-user notebook servers to hand off cookie authentication to the Hub
|
||||
parameters:
|
||||
- name: cookie_name
|
||||
in: path
|
||||
@@ -229,13 +443,94 @@ paths:
|
||||
'200':
|
||||
description: The user identified by the cookie
|
||||
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:
|
||||
post:
|
||||
summary: Shutdown the Hub
|
||||
responses:
|
||||
'200':
|
||||
description: Hub has shutdown
|
||||
parameters:
|
||||
- name: proxy
|
||||
in: body
|
||||
type: boolean
|
||||
description: Whether the proxy should be shutdown as well (default from Hub config)
|
||||
- name: servers
|
||||
in: body
|
||||
type: boolean
|
||||
description: Whether users' notebook servers should be shutdown as well (default from Hub config)
|
||||
definitions:
|
||||
User:
|
||||
type: object
|
||||
@@ -246,14 +541,53 @@ definitions:
|
||||
admin:
|
||||
type: boolean
|
||||
description: Whether the user is an admin
|
||||
groups:
|
||||
type: array
|
||||
description: The names of groups where this user is a member
|
||||
items:
|
||||
type: string
|
||||
server:
|
||||
type: string
|
||||
description: The user's server's base URL, if running; null if not.
|
||||
description: The user's notebook server's base URL, if running; null if not.
|
||||
pending:
|
||||
type: string
|
||||
enum: ["spawn", "stop"]
|
||||
description: The currently pending action, if any
|
||||
last_activity:
|
||||
type: string
|
||||
format: ISO8601 Timestamp
|
||||
format: date-time
|
||||
description: Timestamp of last-seen activity from the user
|
||||
Group:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The group's name
|
||||
users:
|
||||
type: array
|
||||
description: The names of users who are members of this group
|
||||
items:
|
||||
type: string
|
||||
Service:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The service's name
|
||||
admin:
|
||||
type: boolean
|
||||
description: Whether the service is an admin
|
||||
url:
|
||||
type: string
|
||||
description: The internal url where the service is running
|
||||
prefix:
|
||||
type: string
|
||||
description: The proxied URL prefix to the service's url
|
||||
pid:
|
||||
type: number
|
||||
description: The PID of the service process (if managed)
|
||||
command:
|
||||
type: array
|
||||
description: The command used to start the service (if managed)
|
||||
items:
|
||||
type: string
|
||||
|
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
|
||||
|
||||
:class:`Authenticator`
|
||||
----------------------
|
||||
|
||||
|
||||
.. autoclass:: Authenticator
|
||||
.. autoconfigurable:: Authenticator
|
||||
:members:
|
||||
|
||||
.. autoclass:: LocalAuthenticator
|
||||
:class:`LocalAuthenticator`
|
||||
---------------------------
|
||||
|
||||
.. autoconfigurable:: LocalAuthenticator
|
||||
:members:
|
||||
|
||||
.. autoclass:: PAMAuthenticator
|
||||
:class:`PAMAuthenticator`
|
||||
-------------------------
|
||||
|
||||
.. autoconfigurable:: PAMAuthenticator
|
||||
|
||||
|
@@ -1,14 +1,38 @@
|
||||
.. _api-index:
|
||||
|
||||
####################
|
||||
The JupyterHub API
|
||||
####################
|
||||
##################
|
||||
The JupyterHub API
|
||||
##################
|
||||
|
||||
:Release: |release|
|
||||
:Date: |today|
|
||||
|
||||
JupyterHub also provides a REST API for administration of the Hub and users.
|
||||
The documentation on `Using JupyterHub's REST API <../reference/rest.html>`_ provides
|
||||
information on:
|
||||
|
||||
- what you can do with the API
|
||||
- creating an API token
|
||||
- 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
|
||||
`here (on swagger's petstore) <http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default>`__.
|
||||
The `OpenAPI Initiative`_ (fka Swagger™) is a project used to describe
|
||||
and document RESTful APIs.
|
||||
|
||||
JupyterHub API Reference:
|
||||
|
||||
.. toctree::
|
||||
|
||||
app
|
||||
auth
|
||||
spawner
|
||||
proxy
|
||||
user
|
||||
service
|
||||
services.auth
|
||||
|
||||
|
||||
.. _OpenAPI Initiative: https://www.openapis.org/
|
||||
|
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
@@ -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
|
||||
|
41
docs/source/api/services.auth.rst
Normal file
@@ -0,0 +1,41 @@
|
||||
=======================
|
||||
Services Authentication
|
||||
=======================
|
||||
|
||||
Module: :mod:`jupyterhub.services.auth`
|
||||
=======================================
|
||||
|
||||
.. automodule:: jupyterhub.services.auth
|
||||
|
||||
.. currentmodule:: jupyterhub.services.auth
|
||||
|
||||
|
||||
:class:`HubAuth`
|
||||
----------------
|
||||
|
||||
.. autoconfigurable:: HubAuth
|
||||
:members:
|
||||
|
||||
:class:`HubOAuth`
|
||||
----------------
|
||||
|
||||
.. autoconfigurable:: HubOAuth
|
||||
:members:
|
||||
|
||||
|
||||
:class:`HubAuthenticated`
|
||||
-------------------------
|
||||
|
||||
.. autoclass:: HubAuthenticated
|
||||
:members:
|
||||
|
||||
:class:`HubOAuthenticated`
|
||||
-------------------------
|
||||
|
||||
.. autoclass:: HubOAuthenticated
|
||||
|
||||
:class:`HubOAuthCallbackHandler`
|
||||
--------------------------------
|
||||
|
||||
.. autoclass:: HubOAuthCallbackHandler
|
||||
|
@@ -1,6 +1,6 @@
|
||||
==============
|
||||
Spawners
|
||||
==============
|
||||
========
|
||||
Spawners
|
||||
========
|
||||
|
||||
Module: :mod:`jupyterhub.spawner`
|
||||
=================================
|
||||
@@ -12,7 +12,11 @@ Module: :mod:`jupyterhub.spawner`
|
||||
:class:`Spawner`
|
||||
----------------
|
||||
|
||||
.. autoclass:: Spawner
|
||||
:members: options_from_form, poll, start, stop, get_args, get_env, get_state
|
||||
.. autoconfigurable:: Spawner
|
||||
:members: options_from_form, poll, start, stop, get_args, get_env, get_state, template_namespace, format_string
|
||||
|
||||
:class:`LocalProcessSpawner`
|
||||
----------------------------
|
||||
|
||||
.. autoconfigurable:: LocalProcessSpawner
|
||||
|
||||
.. autoclass:: LocalProcessSpawner
|
||||
|
@@ -1,6 +1,6 @@
|
||||
=============
|
||||
Users
|
||||
=============
|
||||
=====
|
||||
Users
|
||||
=====
|
||||
|
||||
Module: :mod:`jupyterhub.user`
|
||||
==============================
|
||||
@@ -9,11 +9,16 @@ Module: :mod:`jupyterhub.user`
|
||||
|
||||
.. currentmodule:: jupyterhub.user
|
||||
|
||||
:class:`UserDict`
|
||||
-----------------
|
||||
|
||||
.. autoclass:: UserDict
|
||||
:members:
|
||||
|
||||
|
||||
:class:`User`
|
||||
-------------
|
||||
|
||||
.. class:: Server
|
||||
|
||||
.. autoclass:: User
|
||||
:members: escaped_name
|
||||
|
||||
@@ -29,3 +34,4 @@ Module: :mod:`jupyterhub.user`
|
||||
.. attribute:: spawner
|
||||
|
||||
The user's :class:`~.Spawner` instance.
|
||||
|
||||
|
@@ -1,9 +1,178 @@
|
||||
# Summary of changes in JupyterHub
|
||||
# Changelog
|
||||
|
||||
See `git log` for a more detailed summary.
|
||||
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
|
||||
command line for details.
|
||||
|
||||
|
||||
## [Unreleased] 0.8
|
||||
|
||||
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.2] - 2017-01-09
|
||||
|
||||
#### Added
|
||||
|
||||
- Support service environment variables and defaults in `jupyterhub-singleuser`
|
||||
for easier deployment of notebook servers as a Service.
|
||||
- Add `--group` parameter for deploying `jupyterhub-singleuser` as a Service with group authentication.
|
||||
- Include URL parameters when redirecting through `/user-redirect/`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix group authentication for HubAuthenticated services
|
||||
|
||||
### [0.7.1] - 2017-01-02
|
||||
|
||||
#### Added
|
||||
|
||||
- `Spawner.will_resume` for signaling that a single-user server is paused instead of stopped.
|
||||
This is needed for cases like `DockerSpawner.remove_containers = False`,
|
||||
where the first API token is re-used for subsequent spawns.
|
||||
- Warning on startup about single-character usernames,
|
||||
caused by common `set('string')` typo in config.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Removed spurious warning about empty `next_url`, which is AOK.
|
||||
|
||||
### [0.7.0] - 2016-12-2
|
||||
|
||||
#### Added
|
||||
|
||||
- Implement Services API [\#705](https://github.com/jupyterhub/jupyterhub/pull/705)
|
||||
- Add `/api/` and `/api/info` endpoints [\#675](https://github.com/jupyterhub/jupyterhub/pull/675)
|
||||
- Add documentation for JupyterLab, pySpark configuration, troubleshooting,
|
||||
and more.
|
||||
- Add logging of error if adding users already in database. [\#689](https://github.com/jupyterhub/jupyterhub/pull/689)
|
||||
- Add HubAuth class for authenticating with JupyterHub. This class can
|
||||
be used by any application, even outside tornado.
|
||||
- Add user groups.
|
||||
- Add `/hub/user-redirect/...` URL for redirecting users to a file on their own server.
|
||||
|
||||
|
||||
#### Changed
|
||||
|
||||
- Always install with setuptools but not eggs (effectively require
|
||||
`pip install .`) [\#722](https://github.com/jupyterhub/jupyterhub/pull/722)
|
||||
- Updated formatting of changelog. [\#711](https://github.com/jupyterhub/jupyterhub/pull/711)
|
||||
- Single-user server is provided by JupyterHub package, so single-user servers depend on JupyterHub now.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Fix docker repository location [\#719](https://github.com/jupyterhub/jupyterhub/pull/719)
|
||||
- Fix swagger spec conformance and timestamp type in API spec
|
||||
- Various redirect-loop-causing bugs have been fixed.
|
||||
|
||||
|
||||
#### Removed
|
||||
|
||||
- Deprecate `--no-ssl` command line option. It has no meaning and warns if
|
||||
used. [\#789](https://github.com/jupyterhub/jupyterhub/pull/789)
|
||||
- Deprecate `%U` username substitution in favor of `{username}`. [\#748](https://github.com/jupyterhub/jupyterhub/pull/748)
|
||||
- Removed deprecated SwarmSpawner link. [\#699](https://github.com/jupyterhub/jupyterhub/pull/699)
|
||||
|
||||
## 0.6
|
||||
|
||||
### [0.6.1] - 2016-05-04
|
||||
|
||||
Bugfixes on 0.6:
|
||||
|
||||
- statsd is an optional dependency, only needed if in use
|
||||
- Notice more quickly when servers have crashed
|
||||
- Better error pages for proxy errors
|
||||
- Add Stop All button to admin panel for stopping all servers at once
|
||||
|
||||
### [0.6.0] - 2016-04-25
|
||||
|
||||
- JupyterHub has moved to a new `jupyterhub` namespace on GitHub and Docker. What was `juptyer/jupyterhub` is now `jupyterhub/jupyterhub`, etc.
|
||||
- `jupyterhub/jupyterhub` image on DockerHub no longer loads the jupyterhub_config.py in an ONBUILD step. A new `jupyterhub/jupyterhub-onbuild` image does this
|
||||
- Add statsd support, via `c.JupyterHub.statsd_{host,port,prefix}`
|
||||
@@ -16,7 +185,7 @@ See `git log` for a more detailed summary.
|
||||
- Various fixes for user URLs and redirects
|
||||
|
||||
|
||||
## 0.5
|
||||
## [0.5] - 2016-03-07
|
||||
|
||||
|
||||
- Single-user server must be run with Jupyter Notebook ≥ 4.0
|
||||
@@ -30,11 +199,11 @@ See `git log` for a more detailed summary.
|
||||
|
||||
## 0.4
|
||||
|
||||
### 0.4.1
|
||||
### [0.4.1] - 2016-02-03
|
||||
|
||||
Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
|
||||
|
||||
### 0.4.0
|
||||
### [0.4.0] - 2016-02-01
|
||||
|
||||
- Add `Spawner.user_options_form` for specifying an HTML form to present to users,
|
||||
allowing users to influence the spawning of their own servers.
|
||||
@@ -45,7 +214,7 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
|
||||
- 0.4 will be the last JupyterHub release where single-user servers running IPython 3 is supported instead of Notebook ≥ 4.0.
|
||||
|
||||
|
||||
## 0.3
|
||||
## [0.3] - 2015-11-04
|
||||
|
||||
- No longer make the user starting the Hub an admin
|
||||
- start PAM sessions on login
|
||||
@@ -53,13 +222,25 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
|
||||
allowing deeper interaction between Spawner/Authenticator pairs.
|
||||
- login redirect fixes
|
||||
|
||||
## 0.2
|
||||
## [0.2] - 2015-07-12
|
||||
|
||||
- Based on standalone traitlets instead of IPython.utils.traitlets
|
||||
- multiple users in admin panel
|
||||
- Fixes for usernames that require escaping
|
||||
|
||||
## 0.1
|
||||
## 0.1 - 2015-03-07
|
||||
|
||||
First preview release
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/0.7.2...HEAD
|
||||
[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.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.0]: https://github.com/jupyterhub/jupyterhub/compare/0.5.0...0.6.0
|
||||
[0.5]: https://github.com/jupyterhub/jupyterhub/compare/0.4.1...0.5.0
|
||||
[0.4.1]: https://github.com/jupyterhub/jupyterhub/compare/0.4.0...0.4.1
|
||||
[0.4.0]: https://github.com/jupyterhub/jupyterhub/compare/0.3.0...0.4.0
|
||||
[0.3]: https://github.com/jupyterhub/jupyterhub/compare/0.2.0...0.3.0
|
||||
[0.2]: https://github.com/jupyterhub/jupyterhub/compare/0.1.0...0.2.0
|
||||
|
@@ -1,59 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# JupyterHub documentation build configuration file, created by
|
||||
# sphinx-quickstart on Mon Jan 4 16:31:09 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shlex
|
||||
|
||||
# Needed for conversion from markdown to html
|
||||
# For conversion from markdown to html
|
||||
import recommonmark.parser
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
# Set paths
|
||||
sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
needs_sphinx = '1.3'
|
||||
# Minimal Sphinx version
|
||||
needs_sphinx = '1.4'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
# Sphinx extension modules
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.napoleon',
|
||||
'autodoc_traits',
|
||||
'jupyter_alabaster_theme',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# Jupyter uses recommonmark's parser to convert markdown
|
||||
source_parsers = {
|
||||
'.md': 'recommonmark.parser.CommonMarkParser',
|
||||
}
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = ['.rst', '.md']
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
@@ -62,13 +34,12 @@ project = u'JupyterHub'
|
||||
copyright = u'2016, Project Jupyter team'
|
||||
author = u'Project Jupyter team'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
# Project Jupyter uses the following to autopopulate version
|
||||
# Autopopulate version
|
||||
from os.path import dirname
|
||||
root = dirname(dirname(dirname(__file__)))
|
||||
docs = dirname(dirname(__file__))
|
||||
root = dirname(docs)
|
||||
sys.path.insert(0, root)
|
||||
sys.path.insert(0, os.path.join(docs, 'sphinxext'))
|
||||
|
||||
import jupyterhub
|
||||
# The short X.Y version.
|
||||
@@ -76,162 +47,62 @@ version = '%i.%i' % jupyterhub.version_info[:2]
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = jupyterhub.__version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
# Set the default role so we can use `foo` instead of ``foo``
|
||||
default_role = 'literal'
|
||||
|
||||
# -- Source -------------------------------------------------------------
|
||||
|
||||
source_parsers = {
|
||||
'.md': 'recommonmark.parser.CommonMarkParser',
|
||||
}
|
||||
|
||||
source_suffix = ['.rst', '.md']
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
# The theme to use for HTML and HTML Help pages.
|
||||
html_theme = 'jupyter_alabaster_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
# Paths that contain custom static files (such as style sheets)
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
|
||||
#html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# Now only 'ja' uses this config value
|
||||
#html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'JupyterHubdoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
@@ -243,28 +114,15 @@ latex_documents = [
|
||||
u'Project Jupyter team', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
# -- manual page output -------------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
@@ -273,11 +131,10 @@ man_pages = [
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
# -- Texinfo output -----------------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
@@ -288,20 +145,13 @@ texinfo_documents = [
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
||||
|
||||
|
||||
# -- Options for Epub output ----------------------------------------------
|
||||
# -- Epub output --------------------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
epub_title = project
|
||||
@@ -309,78 +159,33 @@ epub_author = author
|
||||
epub_publisher = author
|
||||
epub_copyright = copyright
|
||||
|
||||
# The basename for the epub file. It defaults to the project name.
|
||||
#epub_basename = project
|
||||
|
||||
# The HTML theme for the epub output. Since the default themes are not optimized
|
||||
# for small screen space, using the same theme for HTML and epub output is
|
||||
# usually not wise. This defaults to 'epub', a theme designed to save visual
|
||||
# space.
|
||||
#epub_theme = 'epub'
|
||||
|
||||
# The language of the text. It defaults to the language option
|
||||
# or 'en' if the language is not set.
|
||||
#epub_language = ''
|
||||
|
||||
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
||||
#epub_scheme = ''
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# or the project homepage.
|
||||
#epub_identifier = ''
|
||||
|
||||
# A unique identification for the text.
|
||||
#epub_uid = ''
|
||||
|
||||
# A tuple containing the cover image and cover page html template filenames.
|
||||
#epub_cover = ()
|
||||
|
||||
# A sequence of (type, uri, title) tuples for the guide element of content.opf.
|
||||
#epub_guide = ()
|
||||
|
||||
# HTML files that should be inserted before the pages created by sphinx.
|
||||
# The format is a list of tuples containing the path and title.
|
||||
#epub_pre_files = []
|
||||
|
||||
# HTML files shat should be inserted after the pages created by sphinx.
|
||||
# The format is a list of tuples containing the path and title.
|
||||
#epub_post_files = []
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
# The depth of the table of contents in toc.ncx.
|
||||
#epub_tocdepth = 3
|
||||
# -- Intersphinx ----------------------------------------------------------
|
||||
|
||||
# Allow duplicate toc entries.
|
||||
#epub_tocdup = True
|
||||
intersphinx_mapping = {'https://docs.python.org/3/': None}
|
||||
|
||||
# Choose between 'default' and 'includehidden'.
|
||||
#epub_tocscope = 'default'
|
||||
# -- Read The Docs --------------------------------------------------------
|
||||
|
||||
# Fix unsupported image types using the Pillow.
|
||||
#epub_fix_images = False
|
||||
|
||||
# Scale large images.
|
||||
#epub_max_image_width = 0
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#epub_show_urls = 'inline'
|
||||
|
||||
# If false, no index is generated.
|
||||
#epub_use_index = True
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'https://docs.python.org/': None}
|
||||
|
||||
# Read The Docs
|
||||
# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
if not on_rtd:
|
||||
import jupyter_alabaster_theme
|
||||
html_theme = 'jupyter_alabaster_theme'
|
||||
html_theme_path = [jupyter_alabaster_theme.get_path()]
|
||||
else:
|
||||
# readthedocs.org uses their theme by default, so no need to specify it
|
||||
# build rest-api, since RTD doesn't run make
|
||||
from subprocess import check_call as sh
|
||||
sh(['make', 'rest-api'], cwd=docs)
|
||||
|
||||
if not on_rtd: # only import and set the theme if we're building docs locally
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
# -- Spell checking -------------------------------------------------------
|
||||
|
||||
# otherwise, readthedocs.org uses their theme by default, so no need to specify it
|
||||
try:
|
||||
import sphinxcontrib.spelling
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
extensions.append("sphinxcontrib.spelling")
|
||||
|
||||
spelling_word_list_filename='spelling_wordlist.txt'
|
||||
|
77
docs/source/contributor-list.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Contributors
|
||||
|
||||
Project Jupyter thanks the following people for their help and
|
||||
contribution on JupyterHub:
|
||||
|
||||
- anderbubble
|
||||
- apetresc
|
||||
- barrachri
|
||||
- betatim
|
||||
- Carreau
|
||||
- charnpreetsingh
|
||||
- ckald
|
||||
- CRegenschein
|
||||
- cwaldbieser
|
||||
- danielballen
|
||||
- danoventa
|
||||
- daradib
|
||||
- datapolitan
|
||||
- dblockow-d2dcrc
|
||||
- DeepHorizons
|
||||
- dhirschfeld
|
||||
- dietmarw
|
||||
- dmartzol
|
||||
- DominicFollettSmith
|
||||
- dsblank
|
||||
- ellisonbg
|
||||
- evanlinde
|
||||
- Fokko
|
||||
- fperez
|
||||
- iamed18
|
||||
- JamiesHQ
|
||||
- jbweston
|
||||
- jdavidheiser
|
||||
- jencabral
|
||||
- jhamrick
|
||||
- josephtate
|
||||
- kinuax
|
||||
- KrishnaPG
|
||||
- kroq-gar78
|
||||
- ksolan
|
||||
- mbmilligan
|
||||
- mgeplf
|
||||
- minrk
|
||||
- mistercrunch
|
||||
- Mistobaan
|
||||
- mwmarkland
|
||||
- nthiery
|
||||
- ObiWahn
|
||||
- ozancaglayan
|
||||
- parente
|
||||
- PeterDaveHello
|
||||
- peterruppel
|
||||
- pjamason
|
||||
- prasadkatti
|
||||
- rafael-ladislau
|
||||
- rgbkrk
|
||||
- robnagler
|
||||
- ryanlovett
|
||||
- Scrypy
|
||||
- shreddd
|
||||
- spoorthyv
|
||||
- ssanderson
|
||||
- takluyver
|
||||
- temogen
|
||||
- ThomasMChen
|
||||
- TimShawver
|
||||
- Todd-Z-Li
|
||||
- toobaz
|
||||
- tsaeger
|
||||
- tschaume
|
||||
- vilhelmen
|
||||
- whitead
|
||||
- willingc
|
||||
- YannBrrd
|
||||
- yuvipanda
|
||||
- zoltan-fedor
|
||||
- zonca
|
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/#/
|
||||
|
||||
### Red Hat
|
||||
|
||||
|
||||
## 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,439 +0,0 @@
|
||||
# Getting started with JupyterHub
|
||||
|
||||
This document describes some of the basics of configuring JupyterHub to do what you want.
|
||||
JupyterHub is highly customizable, so there's a lot to cover.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
See [the readme](https://github.com/jupyter/jupyterhub/blob/master/README.md) for help installing JupyterHub.
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
JupyterHub is a set of processes that together provide a multiuser Jupyter Notebook server.
|
||||
There are three main categories of processes run by the `jupyterhub` command line program:
|
||||
|
||||
- **Single User Server**: a dedicated, single-user, Jupyter Notebook is started for each user on the system
|
||||
when they log in. The object that starts these processes is called a Spawner.
|
||||
- **Proxy**: the public facing part of the server that uses a dynamic proxy to route HTTP requests
|
||||
to the Hub and Single User Servers.
|
||||
- **Hub**: manages user accounts and authentication and coordinates Single Users Servers using a Spawner.
|
||||
|
||||
## JupyterHub's default behavior
|
||||
|
||||
**IMPORTANT:** In its default configuration, JupyterHub requires SSL encryption (HTTPS) to run.
|
||||
**You should not run JupyterHub without SSL encryption on a public network.**
|
||||
See [Security documentation](#Security) for how to configure JupyterHub to use SSL, and in
|
||||
certain cases, e.g. behind SSL termination in nginx, allowing the hub to run with no SSL
|
||||
by requiring `--no-ssl` (as of [version 0.5](./changelog.html)).
|
||||
|
||||
To start JupyterHub in its default configuration, type the following at the command line:
|
||||
|
||||
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.
|
||||
- `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.
|
||||
|
||||
|
||||
## How to configure JupyterHub
|
||||
|
||||
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:
|
||||
|
||||
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:
|
||||
|
||||
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:
|
||||
|
||||
jupyterhub -h
|
||||
|
||||
or:
|
||||
|
||||
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
|
||||
c.Spawner.notebook_dir = '~/assignments' from the command-line:
|
||||
|
||||
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:
|
||||
|
||||
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:** In its default configuration, JupyterHub requires SSL encryption (HTTPS) to run.
|
||||
**You should not run JupyterHub without SSL encryption on a public network.**
|
||||
|
||||
Security is the most important aspect of configuring Jupyter. There are three 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)
|
||||
|
||||
## 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 `your.domain.com` by your fully qualified domain name):
|
||||
|
||||
```python
|
||||
c.JupyterHub.ssl_key = '/etc/letsencrypt/live/your.domain.com/privkey.pem'
|
||||
c.JupyterHub.ssl_cert = '/etc/letsencrypt/live/your.domain.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: In certain cases, e.g. behind SSL termination in nginx, allowing no SSL
|
||||
running on the hub may be desired. To run the Hub without SSL, you must opt
|
||||
in by configuring and confirming the `--no-ssl` option, added as of [version 0.5](./changelog.html).
|
||||
|
||||
## 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 secret in the configuration file itself 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:
|
||||
|
||||
```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.
|
||||
|
||||
## Configuring authentication
|
||||
|
||||
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.
|
||||
You can restrict which users are allowed to login with `Authenticator.whitelist`:
|
||||
|
||||
|
||||
```python
|
||||
c.Authenticator.whitelist = {'mal', 'zoe', 'inara', 'kaylee'}
|
||||
```
|
||||
|
||||
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.**
|
||||
|
||||
### Adding and removing users
|
||||
|
||||
Users can be added and removed to the Hub via the admin panel or REST API. These users will be
|
||||
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. This means that
|
||||
after starting the Hub once, it is not sufficient to remove users from the whitelist in your
|
||||
config file. You must also remove them from the database, either by discarding the database file,
|
||||
or via the admin UI.
|
||||
|
||||
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.
|
||||
|
||||
## Configuring single-user 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 to run external services.
|
||||
More detail on this API will be added in the future.
|
||||
|
||||
## 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
|
||||
|
||||
## Example
|
||||
|
||||
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][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.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
|
||||
```
|
||||
|
||||
# Further reading
|
||||
|
||||
- [Custom Authenticators](./authenticators.html)
|
||||
- [Custom Spawners](./spawners.html)
|
||||
- [Troubleshooting](./troubleshooting.html)
|
||||
|
||||
|
||||
[oauth-setup]: https://github.com/jupyter/oauthenticator#setup
|
||||
[oauthenticator]: https://github.com/jupyter/oauthenticator
|
||||
[PAM]: https://en.wikipedia.org/wiki/Pluggable_authentication_module
|
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
@@ -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
@@ -0,0 +1,12 @@
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
config-basics
|
||||
networking-basics
|
||||
security-basics
|
||||
authenticators-users-basics
|
||||
spawners-basics
|
||||
services-basics
|
88
docs/source/getting-started/networking-basics.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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.
|
||||
|
||||
## 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.
|
||||
```
|
181
docs/source/getting-started/security-basics.rst
Normal file
@@ -0,0 +1,181 @@
|
||||
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, e.g. behind `SSL termination in NGINX <https://www.nginx.com/resources/admin-guide/nginx-ssl-termination/>`_,
|
||||
allowing no SSL running on the hub may be the desired configuration option.
|
||||
|
||||
.. _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
@@ -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': 'python 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'
|
||||
python 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
@@ -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 <del>IPython</del> Jupyter notebook server.
|
||||
|
||||
There are three basic processes involved:
|
||||
|
||||
- multi-user Hub (Python/Tornado)
|
||||
- [configurable http proxy](https://github.com/jupyter/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.md).
|
||||
|
||||
See a list of custom Authenticators [on the wiki](https://github.com/jupyter/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.md).
|
||||
|
||||
See a list of custom Spawners [on the wiki](https://github.com/jupyter/jupyterhub/wiki/Spawners).
|
BIN
docs/source/images/hub-pieces.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
docs/source/images/instance.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
docs/source/images/jhub-parts.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
docs/source/images/security.png
Normal file
After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 27 KiB |
BIN
docs/source/images/token-request-success.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
docs/source/images/token-request.png
Normal file
After Width: | Height: | Size: 52 KiB |
@@ -1,94 +1,120 @@
|
||||
JupyterHub
|
||||
==========
|
||||
|
||||
JupyterHub is a server that gives multiple users access to Jupyter notebooks,
|
||||
running an independent Jupyter notebook server for each user.
|
||||
`JupyterHub`_, a multi-user **Hub**, spawns, manages, and proxies multiple
|
||||
instances of the single-user `Jupyter notebook`_ server.
|
||||
JupyterHub can be used to serve notebooks to a class of students, a corporate
|
||||
data science group, or a scientific research group.
|
||||
|
||||
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 organisation, or it can run on the public
|
||||
internet (in which case, take care with `security <getting-started.html#security>`__).
|
||||
Users access JupyterHub in a web browser, by going to the IP address or
|
||||
domain name of the server.
|
||||
.. image:: images/jhub-parts.png
|
||||
:alt: JupyterHub subsystems
|
||||
:width: 40%
|
||||
:align: right
|
||||
|
||||
Different :doc:`authenticators <authenticators>` 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 organisation has.
|
||||
Three subsystems make up JupyterHub:
|
||||
|
||||
Next, :doc:`spawners <spawners>` 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.
|
||||
* a multi-user **Hub** (tornado process)
|
||||
* a **configurable http proxy** (node-http-proxy)
|
||||
* multiple **single-user Jupyter notebook servers** (Python/IPython/tornado)
|
||||
|
||||
JupyterHub runs as three separate parts:
|
||||
JupyterHub performs the following functions:
|
||||
|
||||
* The multi-user Hub (Python & Tornado)
|
||||
* A `configurable http proxy <https://github.com/jupyter/configurable-http-proxy>`_ (NodeJS)
|
||||
* Multiple single-user Jupyter notebook servers (Python & Tornado)
|
||||
- The Hub spawns a proxy
|
||||
- The proxy forwards all requests to the Hub by default
|
||||
- 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
|
||||
|
||||
Basic principles:
|
||||
For convenient administration of the Hub, its users, and services,
|
||||
JupyterHub also provides a `REST API`_.
|
||||
|
||||
* 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
|
||||
Contents
|
||||
--------
|
||||
|
||||
**Installation Guide**
|
||||
|
||||
Contents:
|
||||
* :doc:`installation-guide`
|
||||
* :doc:`quickstart`
|
||||
* :doc:`quickstart-docker`
|
||||
* :doc:`installation-basics`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: User Documentation
|
||||
**Getting Started**
|
||||
|
||||
getting-started
|
||||
howitworks
|
||||
websecurity
|
||||
* :doc:`getting-started/index`
|
||||
* :doc:`getting-started/config-basics`
|
||||
* :doc:`getting-started/networking-basics`
|
||||
* :doc:`getting-started/security-basics`
|
||||
* :doc:`getting-started/authenticators-users-basics`
|
||||
* :doc:`getting-started/spawners-basics`
|
||||
* :doc:`getting-started/services-basics`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Configuration
|
||||
**Technical Reference**
|
||||
|
||||
authenticators
|
||||
spawners
|
||||
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/config-examples`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Developer Documentation
|
||||
**API Reference**
|
||||
|
||||
api/index
|
||||
* :doc:`api/index`
|
||||
|
||||
**Tutorials**
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Community documentation
|
||||
* :doc:`tutorials/index`
|
||||
* :doc:`tutorials/upgrade-dot-eight`
|
||||
* `Zero to JupyterHub with Kubernetes <https://zero-to-jupyterhub.readthedocs.io/en/latest/>`_
|
||||
|
||||
**Troubleshooting**
|
||||
|
||||
* :doc:`troubleshooting`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: About JupyterHub
|
||||
**About JupyterHub**
|
||||
|
||||
changelog
|
||||
* :doc:`contributor-list`
|
||||
* :doc:`gallery-jhub-deployments`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Questions? Suggestions?
|
||||
|
||||
Jupyter mailing list <https://groups.google.com/forum/#!forum/jupyter>
|
||||
Jupyter website <https://jupyter.org>
|
||||
Stack Overflow - Jupyter <https://stackoverflow.com/questions/tagged/jupyter>
|
||||
Stack Overflow - Jupyter-notebook <https://stackoverflow.com/questions/tagged/jupyter-notebook>
|
||||
**Changelog**
|
||||
|
||||
* :doc:`changelog`
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
------------------
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
|
||||
Questions? Suggestions?
|
||||
-----------------------
|
||||
|
||||
- `Jupyter mailing list <https://groups.google.com/forum/#!forum/jupyter>`_
|
||||
- `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
@@ -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
@@ -0,0 +1,9 @@
|
||||
Installation Guide
|
||||
==================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
quickstart
|
||||
quickstart-docker
|
||||
installation-basics
|
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/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.
|
||||
|
||||
.. _Zero to JupyterHub: https://zero-to-jupyterhub.readthedocs.io/en/latest/
|
78
docs/source/quickstart.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Quickstart
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing JupyterHub, you will need:
|
||||
|
||||
- a Linux/Unix based system
|
||||
- [Python](https://www.python.org/downloads/) 3.4 or greater. An understanding
|
||||
of using [`pip`](https://pip.pypa.io/en/stable/) or
|
||||
[`conda`](https://conda.io/docs/get-started.html) for
|
||||
installing Python packages is helpful.
|
||||
- [nodejs/npm](https://www.npmjs.com/). [Install nodejs/npm](https://docs.npmjs.com/getting-started/installing-node),
|
||||
using your operating system's package manager. For example, install on Linux
|
||||
Debian/Ubuntu using:
|
||||
|
||||
```bash
|
||||
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), you will need:
|
||||
|
||||
- [Jupyter Notebook](https://jupyter.readthedocs.io/en/latest/install.html)
|
||||
version 4 or greater
|
||||
|
||||
## Installation
|
||||
|
||||
JupyterHub can be installed with `pip` (and the proxy with `npm`) or `conda`:
|
||||
|
||||
**pip, npm:**
|
||||
|
||||
```bash
|
||||
python3 -m pip install jupyterhub
|
||||
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):
|
||||
|
||||
```bash
|
||||
conda install -c conda-forge jupyterhub # installs jupyterhub and proxy
|
||||
conda install notebook # needed if running the notebook servers locally
|
||||
```
|
||||
|
||||
Test your installation. If installed, these commands should return the packages'
|
||||
help contents:
|
||||
|
||||
```bash
|
||||
jupyterhub -h
|
||||
configurable-http-proxy -h
|
||||
```
|
||||
|
||||
## Start the Hub server
|
||||
|
||||
To start the Hub server, run the command:
|
||||
|
||||
```bash
|
||||
jupyterhub
|
||||
```
|
||||
|
||||
Visit `https://localhost:8000` in your browser, and sign in with your unix
|
||||
credentials.
|
||||
|
||||
To **allow multiple users to sign in** to the Hub server, you must start
|
||||
`jupyterhub` as a *privileged user*, such as root:
|
||||
|
||||
```bash
|
||||
sudo jupyterhub
|
||||
```
|
||||
|
||||
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*. This requires
|
||||
additional configuration of the system.
|
@@ -1,38 +1,67 @@
|
||||
# Writing a custom Authenticator
|
||||
# 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.
|
||||
The [Authenticator][] is the mechanism for authorizing users to use the
|
||||
Hub and single user notebook servers.
|
||||
|
||||
You can use custom Authenticator subclasses to enable authentication via other systems.
|
||||
One such example is using [GitHub OAuth][].
|
||||
## The default PAM Authenticator
|
||||
|
||||
Because the username is passed from the Authenticator to the Spawner,
|
||||
a custom Authenticator and Spawner are often used together.
|
||||
JupyterHub ships only with the default [PAM][]-based Authenticator,
|
||||
for logging in with local user accounts via a username and password.
|
||||
|
||||
See a list of custom Authenticators [on the wiki](https://github.com/jupyter/jupyterhub/wiki/Authenticators).
|
||||
## 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.
|
||||
|
||||
## Basics of Authenticators
|
||||
You can see an example implementation of an Authenticator that uses
|
||||
[GitHub OAuth][] at [OAuthenticator][].
|
||||
|
||||
A basic Authenticator has one central method:
|
||||
JupyterHub's [OAuthenticator][] currently supports the following
|
||||
popular services:
|
||||
|
||||
- Auth0
|
||||
- Bitbucket
|
||||
- CILogon
|
||||
- GitHub
|
||||
- GitLab
|
||||
- Globus
|
||||
- Google
|
||||
- MediaWiki
|
||||
- Okpy
|
||||
- OpenShift
|
||||
|
||||
### Authenticator.authenticate
|
||||
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
|
||||
|
||||
## 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 the login form.
|
||||
Unless the login form has been customized, `data` will have two keys:
|
||||
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` (self-explanatory)
|
||||
- `password` (also self-explanatory)
|
||||
- `username`
|
||||
- `password`
|
||||
|
||||
`authenticate`'s job is simple:
|
||||
The `authenticate` method's job is simple:
|
||||
|
||||
- return a username (non-empty str)
|
||||
of the authenticated user if authentication is successful
|
||||
- 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
|
||||
@@ -55,15 +84,7 @@ class DictionaryAuthenticator(Authenticator):
|
||||
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
|
||||
#### Normalize usernames
|
||||
|
||||
Since the Authenticator and Spawner both use the same username,
|
||||
sometimes you want to transform the name coming from the authentication service
|
||||
@@ -79,7 +100,7 @@ c.Authenticator.username_map = {
|
||||
}
|
||||
```
|
||||
|
||||
### Validating usernames
|
||||
#### Validate usernames
|
||||
|
||||
In most cases, there is a very limited set of acceptable usernames.
|
||||
Authenticators can define `validate_username(username)`,
|
||||
@@ -91,21 +112,31 @@ which is a regular expression string for validation.
|
||||
|
||||
To only allow usernames that start with 'w':
|
||||
|
||||
c.Authenticator.username_pattern = r'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.
|
||||
|
||||
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).
|
||||
|
||||
|
||||
## OAuth and other non-password logins
|
||||
## JupyterHub as an OAuth provider
|
||||
|
||||
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][].
|
||||
Beginning with version 0.8, JupyterHub is an OAuth provider.
|
||||
|
||||
|
||||
[Authenticator]: https://github.com/jupyter/jupyterhub/blob/master/jupyterhub/auth.py
|
||||
[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/jupyter/oauthenticator
|
||||
|
||||
[OAuthenticator]: https://github.com/jupyterhub/oauthenticator
|
211
docs/source/reference/config-examples.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Configuration examples
|
||||
|
||||
This section provides examples, including configuration files and tips, for the
|
||||
following configurations:
|
||||
|
||||
- Using GitHub OAuth
|
||||
- Using nginx reverse proxy
|
||||
|
||||
## Using 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
|
||||
* 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
|
||||
|
||||
# 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/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
|
||||
jupyterhub -f /etc/jupyterhub/jupyterhub_config.py
|
||||
```
|
||||
|
||||
## Using 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 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` 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 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'
|
||||
```
|
||||
|
||||
The **`nginx` server config file** is 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`.
|
14
docs/source/reference/index.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
Technical Reference
|
||||
===================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
technical-overview
|
||||
websecurity
|
||||
authenticators
|
||||
spawners
|
||||
services
|
||||
rest
|
||||
upgrading
|
||||
config-examples
|
132
docs/source/reference/rest.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 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()
|
||||
```
|
||||
|
||||
Note that the API token authorizes **JupyterHub** REST API requests. The same
|
||||
token does **not** authorize access to the [Jupyter Notebook REST API][]
|
||||
provided by notebook servers managed by JupyterHub. A different token is used
|
||||
to access the **Jupyter Notebook** API.
|
||||
|
||||
## 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
|
361
docs/source/reference/services.md
Normal file
@@ -0,0 +1,361 @@
|
||||
# Services
|
||||
|
||||
With version 0.7, JupyterHub adds support for **Services**.
|
||||
|
||||
This section provides the following information about Services:
|
||||
|
||||
- [Definition of a Service](#definition-of-a-service)
|
||||
- [Properties of a Service](#properties-of-a-service)
|
||||
- [Hub-Managed Services](#hub-managed-services)
|
||||
- [Launching a Hub-Managed Service](#launching-a-hub-managed-service)
|
||||
- [Externally-Managed Services](#externally-managed-services)
|
||||
- [Writing your own Services](#writing-your-own-services)
|
||||
- [Hub Authentication and Services](#hub-authentication-and-services)
|
||||
|
||||
## Definition of a Service
|
||||
|
||||
When working with JupyterHub, a **Service** is defined as a process that interacts
|
||||
with the Hub's REST API. A Service may perform a specific or
|
||||
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
|
||||
for some time
|
||||
- registering additional web servers which should use the Hub's authentication
|
||||
and be served behind the Hub's proxy.
|
||||
|
||||
Two key features help define a Service:
|
||||
|
||||
- Is the Service **managed** by JupyterHub?
|
||||
- Does the Service have a web server that should be added to the proxy's
|
||||
table?
|
||||
|
||||
Currently, these characteristics distinguish two types of Services:
|
||||
|
||||
- A **Hub-Managed Service** which is managed by JupyterHub
|
||||
- An **Externally-Managed Service** which runs its own web server and
|
||||
communicates operation instructions via the Hub's API.
|
||||
|
||||
## Properties of a Service
|
||||
|
||||
A Service may have the following properties:
|
||||
|
||||
- `name: str` - the name of the service
|
||||
- `admin: bool (default - false)` - whether the service should have
|
||||
administrative privileges
|
||||
- `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,
|
||||
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:
|
||||
|
||||
- `command: (str/Popen list`) - Command for JupyterHub to spawn the service.
|
||||
- Only use this if the service should be a subprocess.
|
||||
- If command is not specified, the Service is assumed to be managed
|
||||
externally.
|
||||
- If a command is specified for launching the Service, the Service will
|
||||
be started and managed by the Hub.
|
||||
- `environment: dict` - additional environment variables for the Service.
|
||||
- `user: str` - the name of a system user to manage the Service. If
|
||||
unspecified, run as the same user as the Hub.
|
||||
|
||||
## Hub-Managed Services
|
||||
|
||||
A **Hub-Managed Service** is started by the Hub, and the Hub is responsible
|
||||
for the Service's actions. A Hub-Managed Service can only be a local
|
||||
subprocess of the Hub. The Hub will take care of starting the process and
|
||||
restarts it if it stops.
|
||||
|
||||
While Hub-Managed Services share some similarities with notebook Spawners,
|
||||
there are no plans for Hub-Managed Services to support the same spawning
|
||||
abstractions as a notebook Spawner.
|
||||
|
||||
If you wish to run a Service in a Docker container or other deployment
|
||||
environments, the Service can be registered as an
|
||||
**Externally-Managed Service**, as described below.
|
||||
|
||||
## Launching a Hub-Managed Service
|
||||
|
||||
A Hub-Managed Service is characterized by its specified `command` for launching
|
||||
the Service. For example, a 'cull idle' notebook server task configured as a
|
||||
Hub-Managed Service would include:
|
||||
|
||||
- the Service name,
|
||||
- admin permissions, and
|
||||
- the `command` to launch the Service which will cull idle servers after a
|
||||
timeout interval
|
||||
|
||||
This example would be configured as follows in `jupyterhub_config.py`:
|
||||
|
||||
```python
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'cull-idle',
|
||||
'admin': True,
|
||||
'command': ['python', '/path/to/cull-idle.py', '--timeout']
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
A Hub-Managed Service may also be configured with additional optional
|
||||
parameters, which describe the environment needed to start the Service process:
|
||||
|
||||
- `environment: dict` - additional environment variables for the Service.
|
||||
- `user: str` - name of the user to run the server if different from the Hub.
|
||||
Requires Hub to be root.
|
||||
- `cwd: path` directory in which to run the Service, if different from the
|
||||
Hub directory.
|
||||
|
||||
The Hub will pass the following environment variables to launch the Service:
|
||||
|
||||
```bash
|
||||
JUPYTERHUB_SERVICE_NAME: The name of the service
|
||||
JUPYTERHUB_API_TOKEN: API token assigned to the service
|
||||
JUPYTERHUB_API_URL: URL for the JupyterHub API (default, http://127.0.0.1:8080/hub/api)
|
||||
JUPYTERHUB_BASE_URL: Base URL of the Hub (https://mydomain[:port]/)
|
||||
JUPYTERHUB_SERVICE_PREFIX: URL path prefix of this service (/services/:service-name/)
|
||||
JUPYTERHUB_SERVICE_URL: Local URL where the service is expected to be listening.
|
||||
Only for proxied web services.
|
||||
```
|
||||
|
||||
For the previous 'cull idle' Service example, these environment variables
|
||||
would be passed to the Service when the Hub starts the 'cull idle' Service:
|
||||
|
||||
```bash
|
||||
JUPYTERHUB_SERVICE_NAME: 'cull-idle'
|
||||
JUPYTERHUB_API_TOKEN: API token assigned to the service
|
||||
JUPYTERHUB_API_URL: http://127.0.0.1:8080/hub/api
|
||||
JUPYTERHUB_BASE_URL: https://mydomain[:port]
|
||||
JUPYTERHUB_SERVICE_PREFIX: /services/cull-idle/
|
||||
```
|
||||
|
||||
See the JupyterHub GitHub repo for additional information about the
|
||||
[`cull-idle` example](https://github.com/jupyterhub/jupyterhub/tree/master/examples/cull-idle).
|
||||
|
||||
## Externally-Managed Services
|
||||
|
||||
You may prefer to use your own service management tools, such as Docker or
|
||||
systemd, to manage a JupyterHub Service. These **Externally-Managed
|
||||
Services**, unlike Hub-Managed Services, are not subprocesses of the Hub. You
|
||||
must tell JupyterHub which API token the Externally-Managed Service is using
|
||||
to perform its API requests. Each Externally-Managed Service will need a
|
||||
unique API token, because the Hub authenticates each API request and the API
|
||||
token is used to identify the originating Service or user.
|
||||
|
||||
A configuration example of an Externally-Managed Service with admin access and
|
||||
running its own web server is:
|
||||
|
||||
```python
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'my-web-service',
|
||||
'url': 'https://10.0.1.1:1984',
|
||||
'api_token': 'super-secret',
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In this case, the `url` field will be passed along to the Service as
|
||||
`JUPYTERHUB_SERVICE_URL`.
|
||||
|
||||
## Writing your own Services
|
||||
|
||||
When writing your own services, you have a few decisions to make (in addition
|
||||
to what your service does!):
|
||||
|
||||
1. Does my service need a public URL?
|
||||
2. Do I want JupyterHub to start/stop the service?
|
||||
3. Does my service need to authenticate users?
|
||||
|
||||
When a Service is managed by JupyterHub, the Hub will pass the necessary
|
||||
information to the Service via the environment variables described above. A
|
||||
flexible Service, whether managed by the Hub or not, can make use of these
|
||||
same environment variables.
|
||||
|
||||
When you run a service that has a url, it will be accessible under a
|
||||
`/services/` prefix, such as `https://myhub.horse/services/my-service/`. For
|
||||
your service to route proxied requests properly, it must take
|
||||
`JUPYTERHUB_SERVICE_PREFIX` into account when routing requests. For example, a
|
||||
web service would normally service its root handler at `'/'`, but the proxied
|
||||
service would need to serve `JUPYTERHUB_SERVICE_PREFIX + '/'`.
|
||||
|
||||
## Hub Authentication and Services
|
||||
|
||||
JupyterHub 0.7 introduces some utilities for using the Hub's authentication
|
||||
mechanism to govern access to your service. When a user logs into JupyterHub,
|
||||
the Hub sets a **cookie (`jupyterhub-services`)**. The service can use this
|
||||
cookie to authenticate requests.
|
||||
|
||||
JupyterHub ships with a reference implementation of Hub authentication that
|
||||
can be used by services. You may go beyond this reference implementation and
|
||||
create custom hub-authenticating clients and services. We describe the process
|
||||
below.
|
||||
|
||||
The reference, or base, implementation is the [`HubAuth`][HubAuth] class,
|
||||
which implements the requests to the Hub.
|
||||
|
||||
To use HubAuth, you must set the `.api_token`, either programmatically when constructing the class,
|
||||
or via the `JUPYTERHUB_API_TOKEN` environment variable.
|
||||
|
||||
Most of the logic for authentication implementation is found in the
|
||||
[`HubAuth.user_for_cookie`](services.auth.html#jupyterhub.services.auth.HubAuth.user_for_cookie)
|
||||
method, which makes a request of the Hub, and returns:
|
||||
|
||||
- None, if no user could be identified, or
|
||||
- a dict of the following form:
|
||||
|
||||
```python
|
||||
{
|
||||
"name": "username",
|
||||
"groups": ["list", "of", "groups"],
|
||||
"admin": False, # or True
|
||||
}
|
||||
```
|
||||
|
||||
You are then free to use the returned user information to take appropriate
|
||||
action.
|
||||
|
||||
HubAuth also caches the Hub's response for a number of seconds,
|
||||
configurable by the `cookie_cache_max_age` setting (default: five minutes).
|
||||
|
||||
### Flask Example
|
||||
|
||||
For example, you have a Flask service that returns information about a user.
|
||||
JupyterHub's HubAuth class can be used to authenticate requests to the Flask
|
||||
service. See the `service-whoami-flask` example in the
|
||||
[JupyterHub GitHub repo](https://github.com/jupyterhub/jupyterhub/tree/master/examples/service-whoami-flask)
|
||||
for more details.
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
import json
|
||||
import os
|
||||
from urllib.parse import quote
|
||||
|
||||
from flask import Flask, redirect, request, Response
|
||||
|
||||
from jupyterhub.services.auth import HubAuth
|
||||
|
||||
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
|
||||
|
||||
auth = HubAuth(
|
||||
api_token=os.environ['JUPYTERHUB_API_TOKEN'],
|
||||
cookie_cache_max_age=60,
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def authenticated(f):
|
||||
"""Decorator for authenticating with the Hub"""
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
cookie = request.cookies.get(auth.cookie_name)
|
||||
if cookie:
|
||||
user = auth.user_for_cookie(cookie)
|
||||
else:
|
||||
user = None
|
||||
if user:
|
||||
return f(user, *args, **kwargs)
|
||||
else:
|
||||
# redirect to login url on failed auth
|
||||
return redirect(auth.login_url + '?next=%s' % quote(request.path))
|
||||
return decorated
|
||||
|
||||
|
||||
@app.route(prefix + '/')
|
||||
@authenticated
|
||||
def whoami(user):
|
||||
return Response(
|
||||
json.dumps(user, indent=1, sort_keys=True),
|
||||
mimetype='application/json',
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
### Authenticating tornado services with JupyterHub
|
||||
|
||||
Since most Jupyter services are written with tornado,
|
||||
we include a mixin class, [`HubAuthenticated`][HubAuthenticated],
|
||||
for quickly authenticating your own tornado services with JupyterHub.
|
||||
|
||||
Tornado's `@web.authenticated` method calls a Handler's `.get_current_user`
|
||||
method to identify the user. Mixing in `HubAuthenticated` defines
|
||||
`get_current_user` to use HubAuth. If you want to configure the HubAuth
|
||||
instance beyond the default, you'll want to define an `initialize` method,
|
||||
such as:
|
||||
|
||||
```python
|
||||
class MyHandler(HubAuthenticated, web.RequestHandler):
|
||||
hub_users = {'inara', 'mal'}
|
||||
|
||||
def initialize(self, hub_auth):
|
||||
self.hub_auth = hub_auth
|
||||
|
||||
@web.authenticated
|
||||
def get(self):
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
The HubAuth will automatically load the desired configuration from the Service
|
||||
environment variables.
|
||||
|
||||
If you want to limit user access, you can whitelist users through either the
|
||||
`.hub_users` attribute or `.hub_groups`. These are sets that check against the
|
||||
username and user group list, respectively. If a user matches neither the user
|
||||
list nor the group list, they will not be allowed access. If both are left
|
||||
undefined, then any user will be allowed.
|
||||
|
||||
|
||||
### Implementing your own Authentication with JupyterHub
|
||||
|
||||
If you don't want to use the reference implementation
|
||||
(e.g. you find the implementation a poor fit for your Flask app),
|
||||
you can implement authentication via the Hub yourself.
|
||||
We recommend looking at the [`HubAuth`][HubAuth] class implementation for reference,
|
||||
and taking note of the following process:
|
||||
|
||||
1. retrieve the cookie `jupyterhub-services` from the request.
|
||||
2. Make an API request `GET /hub/api/authorizations/cookie/jupyterhub-services/cookie-value`,
|
||||
where cookie-value is the url-encoded value of the `jupyterhub-services` cookie.
|
||||
This request must be authenticated with a Hub API token in the `Authorization` header.
|
||||
For example, with [requests][]:
|
||||
|
||||
```python
|
||||
r = requests.get(
|
||||
'/'.join((["http://127.0.0.1:8081/hub/api",
|
||||
"authorizations/cookie/jupyterhub-services",
|
||||
quote(encrypted_cookie, safe=''),
|
||||
]),
|
||||
headers = {
|
||||
'Authorization' : 'token %s' % api_token,
|
||||
},
|
||||
)
|
||||
r.raise_for_status()
|
||||
user = r.json()
|
||||
```
|
||||
|
||||
3. On success, the reply will be a JSON model describing the user:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inara",
|
||||
"groups": ["serenity", "guild"],
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
An example of using an Externally-Managed Service and authentication is
|
||||
in [nbviewer README]_ section on securing the notebook viewer,
|
||||
and an example of its configuration is found [here](https://github.com/jupyter/nbviewer/blob/master/nbviewer/providers/base.py#L94).
|
||||
nbviewer can also be run as a Hub-Managed Service as described [nbviewer README]_
|
||||
section on securing the notebook viewer.
|
||||
|
||||
|
||||
[requests]: http://docs.python-requests.org/en/master/
|
||||
[services_auth]: ../api/services.auth.html
|
||||
[HubAuth]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth
|
||||
[HubAuthenticated]: ../api/services.auth.html#jupyterhub.services.auth.HubAuthenticated
|
||||
[nbviewer example]: https://github.com/jupyter/nbviewer#securing-the-notebook-viewer
|
@@ -1,4 +1,4 @@
|
||||
# Writing a custom Spawner
|
||||
# Spawners
|
||||
|
||||
A [Spawner][] starts each single-user notebook server.
|
||||
The Spawner represents an abstract interface to a process,
|
||||
@@ -8,21 +8,25 @@ and a custom Spawner needs to be able to take three actions:
|
||||
- poll whether the process is still running
|
||||
- stop the process
|
||||
|
||||
|
||||
## Examples
|
||||
Custom Spawners for JupyterHub can be found on the [JupyterHub wiki](https://github.com/jupyter/jupyterhub/wiki/Spawners). Some examples include:
|
||||
- [DockerSpawner](https://github.com/jupyter/dockerspawner) for spawning user servers in Docker containers
|
||||
* dockerspawner.DockerSpawner for spawning identical Docker containers for
|
||||
Custom Spawners for JupyterHub can be found on the [JupyterHub wiki](https://github.com/jupyterhub/jupyterhub/wiki/Spawners).
|
||||
Some examples include:
|
||||
|
||||
- [DockerSpawner](https://github.com/jupyterhub/dockerspawner) for spawning user servers in Docker containers
|
||||
* `dockerspawner.DockerSpawner` for spawning identical Docker containers for
|
||||
each users
|
||||
* dockerspawner.SystemUserSpawner for spawning Docker containers with an
|
||||
* `dockerspawner.SystemUserSpawner` for spawning Docker containers with an
|
||||
environment and home directory for each users
|
||||
- [SudoSpawner](https://github.com/jupyter/sudospawner) enables JupyterHub to
|
||||
* both `DockerSpawner` and `SystemUserSpawner` also work with Docker Swarm for
|
||||
launching containers on remote machines
|
||||
- [SudoSpawner](https://github.com/jupyterhub/sudospawner) enables JupyterHub to
|
||||
run without being root, by spawning an intermediate process via `sudo`
|
||||
- [BatchSpawner](https://github.com/mbmilligan/batchspawner) for spawning remote
|
||||
- [BatchSpawner](https://github.com/jupyterhub/batchspawner) for spawning remote
|
||||
servers using batch systems
|
||||
- [RemoteSpawner](https://github.com/zonca/remotespawner) to spawn notebooks
|
||||
and a remote server and tunnel the port via SSH
|
||||
- [SwarmSpawner](https://github.com/compmodels/jupyterhub/blob/master/swarmspawner.py)
|
||||
for spawning containers using Docker Swarm
|
||||
|
||||
|
||||
## Spawner control methods
|
||||
|
||||
@@ -32,8 +36,7 @@ Custom Spawners for JupyterHub can be found on the [JupyterHub wiki](https://git
|
||||
Information about the user can be retrieved from `self.user`,
|
||||
an object encapsulating the user's name, authentication, and server info.
|
||||
|
||||
When `Spawner.start` returns, it should have stored the IP and port
|
||||
of the single-user server in `self.user.server`.
|
||||
The return value of `Spawner.start` should be the (ip, port) of the running server.
|
||||
|
||||
**NOTE:** When writing coroutines, *never* `yield` in between a database change and a commit.
|
||||
|
||||
@@ -41,10 +44,10 @@ Most `Spawner.start` functions will look similar to this example:
|
||||
|
||||
```python
|
||||
def start(self):
|
||||
self.user.server.ip = 'localhost' # or other host or IP address, as seen by the Hub
|
||||
self.user.server.port = 1234 # port selected somehow
|
||||
self.db.commit() # always commit before yield, if modifying db values
|
||||
self.ip = '127.0.0.1'
|
||||
self.port = random_port()
|
||||
yield self._actually_start_server_somehow()
|
||||
return (self.ip, self.port)
|
||||
```
|
||||
|
||||
When `Spawner.start` returns, the single-user server process should actually be running,
|
||||
@@ -61,11 +64,11 @@ and an integer exit status, otherwise.
|
||||
For the local process case, `Spawner.poll` uses `os.kill(PID, 0)`
|
||||
to check if the local process is still running.
|
||||
|
||||
|
||||
### Spawner.stop
|
||||
|
||||
`Spawner.stop` should stop the process. It must be a tornado coroutine, which should return when the process has finished exiting.
|
||||
|
||||
|
||||
## Spawner state
|
||||
|
||||
JupyterHub should be able to stop and restart without tearing down
|
||||
@@ -97,6 +100,7 @@ def clear_state(self):
|
||||
self.pid = 0
|
||||
```
|
||||
|
||||
|
||||
## Spawner options form
|
||||
|
||||
(new in 0.4)
|
||||
@@ -109,12 +113,11 @@ This feature is enabled by setting `Spawner.options_form`, which is an HTML 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 `Spawner.options_form` is undefined, the user's server is spawned directly, and no spawn page is rendered.
|
||||
|
||||
See [this example](https://github.com/jupyter/jupyterhub/blob/master/examples/spawn-form/jupyterhub_config.py) for a form that allows custom CLI args for the local spawner.
|
||||
|
||||
See [this example](https://github.com/jupyterhub/jupyterhub/blob/master/examples/spawn-form/jupyterhub_config.py) for a form that allows custom CLI args for the local spawner.
|
||||
|
||||
### `Spawner.options_from_form`
|
||||
|
||||
@@ -153,8 +156,58 @@ which would return:
|
||||
}
|
||||
```
|
||||
|
||||
When `Spawner.spawn` is called, this dictionary is accessible as `self.user_options`.
|
||||
When `Spawner.start` is called, this dictionary is accessible as `self.user_options`.
|
||||
|
||||
|
||||
[Spawner]: https://github.com/jupyterhub/jupyterhub/blob/master/jupyterhub/spawner.py
|
||||
|
||||
[Spawner]: https://github.com/jupyter/jupyterhub/blob/master/jupyterhub/spawner.py
|
||||
## Writing a custom spawner
|
||||
|
||||
If you are interested in building a custom spawner, you can read [this tutorial](http://jupyterhub-tutorial.readthedocs.io/en/latest/spawners.html).
|
||||
|
||||
## Spawners, resource limits, and guarantees (Optional)
|
||||
|
||||
Some spawners of the single-user notebook servers allow setting limits or
|
||||
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
|
||||
discover these resource limits and guarantees, such as for memory and CPU. For
|
||||
the limits and guarantees to be useful, the spawner must implement support for
|
||||
them.
|
||||
|
||||
### Memory Limits & Guarantees
|
||||
|
||||
`c.Spawner.mem_limit`: A **limit** specifies the *maximum amount of memory*
|
||||
that may be allocated, though there is no promise that the maximum amount will
|
||||
be available. In supported spawners, you can set `c.Spawner.mem_limit` to
|
||||
limit the total amount of memory that a single-user notebook server can
|
||||
allocate. Attempting to use more memory than this limit will cause errors. The
|
||||
single-user notebook server can discover its own memory limit by looking at
|
||||
the environment variable `MEM_LIMIT`, which is specified in absolute bytes.
|
||||
|
||||
`c.Spawner.mem_guarantee`: Sometimes, a **guarantee** of a *minumum amount of
|
||||
memory* is desirable. In this case, you can set `c.Spawner.mem_guarantee` to
|
||||
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
|
||||
`MEM_GUARANTEE` will also be set in the single-user notebook server.
|
||||
|
||||
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 or guarantees are provided, and no environment values are set.
|
||||
|
||||
### CPU Limits & Guarantees
|
||||
|
||||
`c.Spawner.cpu_limit`: In supported spawners, you can set
|
||||
`c.Spawner.cpu_limit` to limit the total number of cpu-cores that a
|
||||
single-user notebook server can use. These can be fractional - `0.5` means 50%
|
||||
of one CPU core, `4.0` is 4 cpu-cores, etc. This value is also set in the
|
||||
single-user notebook server's environment variable `CPU_LIMIT`. The limit does
|
||||
not claim that you will be able to use all the CPU up to your limit as other
|
||||
higher priority applications might be taking up CPU.
|
||||
|
||||
`c.Spawner.cpu_guarantee`: You can set `c.Spawner.cpu_guarantee` to provide a
|
||||
guarantee for CPU usage. The environment variable `CPU_GUARANTEE` will be set
|
||||
in the single-user notebook server when a guarantee is being provided.
|
||||
|
||||
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 or guarantees are provided, and no environment values are set.
|
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.
|
106
docs/source/reference/upgrading.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Upgrading JupyterHub and its database
|
||||
|
||||
From time to time, you may wish to upgrade JupyterHub to take advantage
|
||||
of new releases. Much of this process is automated using scripts,
|
||||
such as those generated by alembic for database upgrades. Before upgrading a
|
||||
JupyterHub deployment, it's critical to backup your data and configurations
|
||||
before shutting down the JupyterHub process and server.
|
||||
|
||||
## Databases: SQLite (default) or RDBMS (PostgreSQL, MySQL)
|
||||
|
||||
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.
|
||||
|
||||
When running a long term deployment or a production system, we recommend using
|
||||
a traditional RDBMS database, such as [PostgreSQL](https://www.postgresql.org)
|
||||
or [MySQL](https://www.mysql.com), that supports the SQL `ALTER TABLE`
|
||||
statement.
|
||||
|
||||
For production systems, SQLite has some disadvantages when used with JupyterHub:
|
||||
|
||||
- `upgrade-db` may not work, and you may need to start with a fresh database
|
||||
- `downgrade-db` **will not** work if you want to rollback to an earlier
|
||||
version, so backup the `jupyterhub.sqlite` file before upgrading
|
||||
|
||||
The sqlite documentation provides a helpful page about [when to use sqlite and
|
||||
where traditional RDBMS may be a better choice](https://sqlite.org/whentouse.html).
|
||||
|
||||
## The upgrade process
|
||||
|
||||
Five fundamental process steps are needed when upgrading JupyterHub and its
|
||||
database:
|
||||
|
||||
1. Backup JupyterHub database
|
||||
2. Backup JupyterHub configuration file
|
||||
3. Shutdown the Hub
|
||||
4. Upgrade JupyterHub
|
||||
5. Upgrade the database using run `jupyterhub upgrade-db`
|
||||
|
||||
Let's take a closer look at each step in the upgrade process as well as some
|
||||
additional information about JupyterHub databases.
|
||||
|
||||
### 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.
|
||||
|
||||
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
|
||||
|
||||
Additionally, backing 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. This gives users the opportunity to finish any outstanding
|
||||
work in process.
|
||||
|
||||
Next, shutdown the JupyterHub service.
|
||||
|
||||
### Upgrade JupyterHub
|
||||
|
||||
Follow directions that correspond to your package manager, `pip` or `conda`,
|
||||
for the new JupyterHub release. These directions will guide you to the
|
||||
specific command. In general, `pip install -U jupyterhub` or
|
||||
`conda upgrade jupyterhub`
|
||||
|
||||
### Upgrade JupyterHub databases
|
||||
|
||||
To run the upgrade process for JupyterHub databases, enter:
|
||||
|
||||
```
|
||||
jupyterhub upgrade-db
|
||||
```
|
||||
|
||||
## Upgrade checklist
|
||||
|
||||
1. Backup JupyterHub database:
|
||||
- `jupyterhub.sqlite` when using the default sqlite database
|
||||
- Your JupyterHub database when using an RDBMS
|
||||
2. Backup 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`
|
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
|
213
docs/source/spelling_wordlist.txt
Normal file
@@ -0,0 +1,213 @@
|
||||
admin
|
||||
Afterwards
|
||||
alchemyst
|
||||
alope
|
||||
api
|
||||
API
|
||||
apps
|
||||
args
|
||||
asctime
|
||||
auth
|
||||
authenticator
|
||||
Authenticator
|
||||
authenticators
|
||||
Authenticators
|
||||
Autograde
|
||||
autograde
|
||||
autogradeapp
|
||||
autograded
|
||||
Autograded
|
||||
autograder
|
||||
Autograder
|
||||
autograding
|
||||
backends
|
||||
Bitdiddle
|
||||
bugfix
|
||||
Bugfixes
|
||||
bugtracker
|
||||
Carreau
|
||||
Changelog
|
||||
changelog
|
||||
checksum
|
||||
checksums
|
||||
cmd
|
||||
cogsci
|
||||
conda
|
||||
config
|
||||
coroutine
|
||||
coroutines
|
||||
crt
|
||||
customizable
|
||||
datefmt
|
||||
decrypted
|
||||
dev
|
||||
DockerSpawner
|
||||
dockerspawner
|
||||
dropdown
|
||||
duedate
|
||||
Duedate
|
||||
ellachao
|
||||
ellisonbg
|
||||
entrypoint
|
||||
env
|
||||
Filenames
|
||||
filesystem
|
||||
formatters
|
||||
formdata
|
||||
formgrade
|
||||
formgrader
|
||||
gif
|
||||
GitHub
|
||||
Gradebook
|
||||
gradebook
|
||||
Granger
|
||||
hardcoded
|
||||
hOlle
|
||||
Homebrew
|
||||
html
|
||||
http
|
||||
https
|
||||
hubapi
|
||||
Indices
|
||||
IFramed
|
||||
inline
|
||||
iopub
|
||||
ip
|
||||
ipynb
|
||||
IPython
|
||||
ischurov
|
||||
ivanslapnicar
|
||||
jdfreder
|
||||
jhamrick
|
||||
jklymak
|
||||
jonathanmorgan
|
||||
joschu
|
||||
JUPYTER
|
||||
Jupyter
|
||||
jupyter
|
||||
jupyterhub
|
||||
Kerberos
|
||||
kerberos
|
||||
letsencrypt
|
||||
lgpage
|
||||
linkcheck
|
||||
linux
|
||||
localhost
|
||||
logfile
|
||||
login
|
||||
logins
|
||||
logout
|
||||
lookup
|
||||
lphk
|
||||
mandli
|
||||
Marr
|
||||
mathjax
|
||||
matplotlib
|
||||
metadata
|
||||
mikebolt
|
||||
minrk
|
||||
Mitigations
|
||||
mixin
|
||||
Mixin
|
||||
multi
|
||||
multiuser
|
||||
namespace
|
||||
nbconvert
|
||||
nbgrader
|
||||
neuroscience
|
||||
nginx
|
||||
np
|
||||
npm
|
||||
oauth
|
||||
OAuth
|
||||
oauthenticator
|
||||
ok
|
||||
olgabot
|
||||
osx
|
||||
PAM
|
||||
phantomjs
|
||||
Phantomjs
|
||||
plugin
|
||||
plugins
|
||||
Popen
|
||||
positionally
|
||||
postgres
|
||||
pregenerated
|
||||
prepend
|
||||
prepopulate
|
||||
preprocessor
|
||||
Preprocessor
|
||||
prev
|
||||
Programmatically
|
||||
programmatically
|
||||
ps
|
||||
py
|
||||
Qualys
|
||||
quickstart
|
||||
readonly
|
||||
redSlug
|
||||
reinstall
|
||||
resize
|
||||
rst
|
||||
runtime
|
||||
rw
|
||||
sandboxed
|
||||
sansary
|
||||
singleuser
|
||||
smeylan
|
||||
spawner
|
||||
Spawner
|
||||
spawners
|
||||
Spawners
|
||||
spellcheck
|
||||
SQL
|
||||
sqlite
|
||||
startup
|
||||
statsd
|
||||
stdin
|
||||
stdout
|
||||
stoppped
|
||||
subclasses
|
||||
subcommand
|
||||
subdomain
|
||||
subdomains
|
||||
Subdomains
|
||||
suchow
|
||||
suprocesses
|
||||
svurens
|
||||
sys
|
||||
SystemUserSpawner
|
||||
systemwide
|
||||
tasilb
|
||||
teardown
|
||||
threadsafe
|
||||
timestamp
|
||||
timestamps
|
||||
TLD
|
||||
todo
|
||||
toolbar
|
||||
traitlets
|
||||
travis
|
||||
tuples
|
||||
undeletable
|
||||
unicode
|
||||
uninstall
|
||||
UNIX
|
||||
unix
|
||||
untracked
|
||||
untrusted
|
||||
url
|
||||
username
|
||||
usernames
|
||||
utcnow
|
||||
utils
|
||||
vinaykola
|
||||
virtualenv
|
||||
whitelist
|
||||
whitespace
|
||||
wildcard
|
||||
Wildcards
|
||||
willingc
|
||||
wordlist
|
||||
Workflow
|
||||
workflow
|
@@ -1,20 +1,34 @@
|
||||
# Troubleshooting
|
||||
|
||||
This document is under active development.
|
||||
|
||||
When troubleshooting, you may see unexpected behaviors or receive an error
|
||||
message. These two lists provide links to identifying the cause of the
|
||||
message. This section provide links for identifying the cause of the
|
||||
problem and how to resolve it.
|
||||
|
||||
## Behavior problems
|
||||
- [JupyterHub proxy fails to start](#jupyterhub-proxy-fails-to-start)
|
||||
[*Behavior*](#behavior)
|
||||
- JupyterHub proxy fails to start
|
||||
- sudospawner fails to run
|
||||
- What is the default behavior when none of the lists (admin, whitelist,
|
||||
group whitelist) are set?
|
||||
|
||||
## Errors
|
||||
- [500 error after spawning a single-user server](#500-error-after-spawning-my-single-user-server)
|
||||
[*Errors*](#errors)
|
||||
- 500 error after spawning my single-user server
|
||||
|
||||
----
|
||||
[*How do I...?*](#how-do-i)
|
||||
- Use a chained SSL certificate
|
||||
- Install JupyterHub without a network connection
|
||||
- I want access to the whole filesystem, but still default users to their home directory
|
||||
- How do I increase the number of pySpark executors on YARN?
|
||||
- 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 rotating daily logs?
|
||||
- Toree integration with HDFS rack awareness script
|
||||
- Where do I find Docker images and Dockerfiles related to JupyterHub?
|
||||
|
||||
## JupyterHub proxy fails to start
|
||||
[*Troubleshooting commands*](#troubleshooting-commands)
|
||||
|
||||
## Behavior
|
||||
|
||||
### JupyterHub proxy fails to start
|
||||
|
||||
If you have tried to start the JupyterHub proxy and it fails to start:
|
||||
|
||||
@@ -22,13 +36,41 @@ If you have tried to start the JupyterHub proxy and it fails to start:
|
||||
``c.JupyterHub.ip = '*'``; if it is, try ``c.JupyterHub.ip = ''``
|
||||
- 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.
|
||||
|
||||
## 500 error after spawning my single-user server
|
||||
### sudospawner fails to run
|
||||
|
||||
If the sudospawner script is not found in the path, sudospawner will not run.
|
||||
To avoid this, specify sudospawner's absolute path. For example, start
|
||||
jupyterhub with:
|
||||
|
||||
jupyterhub --SudoSpawner.sudospawner_path='/absolute/path/to/sudospawner'
|
||||
|
||||
or add:
|
||||
|
||||
c.SudoSpawner.sudospawner_path = '/absolute/path/to/sudospawner'
|
||||
|
||||
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).
|
||||
|
||||
|
||||
You receive a 500 error when accessing the URL `/user/you/...`. This is often
|
||||
seen when your single-user server cannot check your cookies with the Hub.
|
||||
## Errors
|
||||
|
||||
### 500 error after spawning my single-user server
|
||||
|
||||
You receive a 500 error when accessing the URL `/user/<your_name>/...`.
|
||||
This is often seen when your single-user server cannot verify your user cookie
|
||||
with the Hub.
|
||||
|
||||
There are two likely reasons for this:
|
||||
|
||||
@@ -36,23 +78,23 @@ There are two likely reasons for this:
|
||||
configuration problems)
|
||||
2. The single-user server cannot *authenticate* its requests (invalid token)
|
||||
|
||||
### Symptoms:
|
||||
#### Symptoms
|
||||
|
||||
The main symptom is a failure to load *any* page served by the single-user
|
||||
server, met with a 500 error. This is typically the first page at `/user/you`
|
||||
after logging in or clicking "Start my server". When a single-user server
|
||||
receives a request, it makes an API request to the Hub to check if the cookie
|
||||
corresponds to the right user. This request is logged.
|
||||
server, met with a 500 error. This is typically the first page at `/user/<your_name>`
|
||||
after logging in or clicking "Start my server". When a single-user notebook server
|
||||
receives a request, the notebook server makes an API request to the Hub to
|
||||
check if the cookie corresponds to the right user. This request is logged.
|
||||
|
||||
If everything is working, it will look like 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
|
||||
```
|
||||
|
||||
You should see a similar 200 message, as above, in the Hub log when you first
|
||||
visit your single-user server. If you don't see this message in the log, it
|
||||
may mean that your single-user server isn't connecting to your Hub.
|
||||
visit your single-user notebook server. If you don't see this message in the log, it
|
||||
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:
|
||||
|
||||
@@ -60,12 +102,12 @@ 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
|
||||
```
|
||||
|
||||
Check the logs of the single-user server, which may have more detailed
|
||||
Check the logs of the single-user notebook server, which may have more detailed
|
||||
information on the cause.
|
||||
|
||||
### Causes and resolutions:
|
||||
#### Causes and resolutions
|
||||
|
||||
#### No authorization request
|
||||
##### No authorization request
|
||||
|
||||
If you make an API request and it is not received by the server, you likely
|
||||
have a network configuration issue. Often, this happens when the Hub is only
|
||||
@@ -78,7 +120,7 @@ that all single-user servers can connect to, e.g.:
|
||||
c.JupyterHub.hub_ip = '10.0.0.1'
|
||||
```
|
||||
|
||||
#### 403 GET /hub/api/authorizations/cookie
|
||||
##### 403 GET /hub/api/authorizations/cookie
|
||||
|
||||
If you receive a 403 error, the API token for the single-user server is likely
|
||||
invalid. Commonly, the 403 error is caused by resetting the JupyterHub
|
||||
@@ -90,10 +132,196 @@ the container every time. This means that the same API token is used by the
|
||||
server for its whole life, until the container is rebuilt.
|
||||
|
||||
The fix for this Docker case is to remove any Docker containers seeing this
|
||||
issue (typicaly all containers created before a certain point in time):
|
||||
issue (typically all containers created before a certain point in time):
|
||||
|
||||
docker rm -f jupyter-name
|
||||
|
||||
After this, when you start your server via JupyterHub, it will build a
|
||||
new container. If this was the underlying cause of the issue, you should see
|
||||
your server again.
|
||||
|
||||
|
||||
## How do I...?
|
||||
|
||||
### Use a chained SSL certificate
|
||||
|
||||
Some certificate providers, i.e. Entrust, may provide you with a chained
|
||||
certificate that contains multiple files. If you are using a chained
|
||||
certificate you will need to concatenate the individual files by appending the
|
||||
chain cert and root cert to your host cert:
|
||||
|
||||
cat your_host.crt chain.crt root.crt > your_host-chained.crt
|
||||
|
||||
You would then set in your `jupyterhub_config.py` file the `ssl_key` and
|
||||
`ssl_cert` as follows:
|
||||
|
||||
c.JupyterHub.ssl_cert = your_host-chained.crt
|
||||
c.JupyterHub.ssl_key = your_host.key
|
||||
|
||||
|
||||
#### Example
|
||||
|
||||
Your certificate provider gives you the following files: `example_host.crt`,
|
||||
`Entrust_L1Kroot.txt` and `Entrust_Root.txt`.
|
||||
|
||||
Concatenate the files appending the chain cert and root cert to your host cert:
|
||||
|
||||
cat example_host.crt Entrust_L1Kroot.txt Entrust_Root.txt > example_host-chained.crt
|
||||
|
||||
You would then use the `example_host-chained.crt` as the value for
|
||||
JupyterHub's `ssl_cert`. You may pass this value as a command line option
|
||||
when starting JupyterHub or more conveniently set the `ssl_cert` variable in
|
||||
JupyterHub's configuration file, `jupyterhub_config.py`. In `jupyterhub_config.py`,
|
||||
set:
|
||||
|
||||
c.JupyterHub.ssl_cert = /path/to/example_host-chained.crt
|
||||
c.JupyterHub.ssl_key = /path/to/example_host.key
|
||||
|
||||
where `ssl_cert` is example-chained.crt and ssl_key to your private key.
|
||||
|
||||
Then restart JupyterHub.
|
||||
|
||||
See also [JupyterHub SSL encryption](getting-started.md#ssl-encryption).
|
||||
|
||||
### Install JupyterHub without a network connection
|
||||
|
||||
Both conda and pip can be used without a network connection. You can make your
|
||||
own repository (directory) of conda packages and/or wheels, and then install
|
||||
from there instead of the internet.
|
||||
|
||||
For instance, you can install JupyterHub with pip and configurable-http-proxy
|
||||
with npmbox:
|
||||
|
||||
pip wheel jupyterhub
|
||||
npmbox configurable-http-proxy
|
||||
|
||||
### I want access to the whole filesystem, but still default users to their home directory
|
||||
|
||||
Setting the following in `jupyterhub_config.py` will configure access to
|
||||
the entire filesystem and set the default to the user's home directory.
|
||||
|
||||
c.Spawner.notebook_dir = '/'
|
||||
c.Spawner.default_url = '/home/%U' # %U will be replaced with the username
|
||||
|
||||
### How do I increase the number of pySpark executors on YARN?
|
||||
|
||||
From the command line, pySpark executors can be configured using a command
|
||||
similar to this one:
|
||||
|
||||
pyspark --total-executor-cores 2 --executor-memory 1G
|
||||
|
||||
[Cloudera documentation for configuring spark on YARN applications](https://www.cloudera.com/documentation/enterprise/latest/topics/cdh_ig_running_spark_on_yarn.html#spark_on_yarn_config_apps)
|
||||
provides additional information. The [pySpark configuration documentation](https://spark.apache.org/docs/0.9.0/configuration.html)
|
||||
is also helpful for programmatic configuration examples.
|
||||
|
||||
### How do I use JupyterLab's prerelease version with JupyterHub?
|
||||
|
||||
While JupyterLab is still under active development, we have had users
|
||||
ask about how to try out JupyterLab with JupyterHub.
|
||||
|
||||
You need to install and enable the JupyterLab extension system-wide,
|
||||
then you can change the default URL to `/lab`.
|
||||
|
||||
For instance:
|
||||
|
||||
pip install jupyterlab
|
||||
jupyter serverextension enable --py jupyterlab --sys-prefix
|
||||
|
||||
The important thing is that jupyterlab is installed and enabled in the
|
||||
single-user notebook server environment. For system users, this means
|
||||
system-wide, as indicated above. For Docker containers, it means inside
|
||||
the single-user docker image, etc.
|
||||
|
||||
In `jupyterhub_config.py`, configure the Spawner to tell the single-user
|
||||
notebook servers to default to JupyterLab:
|
||||
|
||||
c.Spawner.default_url = '/lab'
|
||||
|
||||
### How do I set up JupyterHub for a workshop (when users are not known ahead of time)?
|
||||
|
||||
1. Set up JupyterHub using OAuthenticator for GitHub authentication
|
||||
2. Configure whitelist to be an empty list in` jupyterhub_config.py`
|
||||
3. Configure admin list to have workshop leaders be listed with administrator privileges.
|
||||
|
||||
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
|
||||
|
||||
The following commands provide additional detail about installed packages,
|
||||
versions, and system information that may be helpful when troubleshooting
|
||||
a JupyterHub deployment. The commands are:
|
||||
|
||||
- System and deployment information
|
||||
|
||||
```bash
|
||||
jupyter troubleshooting
|
||||
```
|
||||
|
||||
- Kernel information
|
||||
|
||||
```bash
|
||||
jupyter kernelspec list
|
||||
```
|
||||
|
||||
- Debug logs when running JupyterHub
|
||||
|
||||
```bash
|
||||
jupyterhub --debug
|
||||
```
|
||||
|
||||
### Toree integration with HDFS rack awareness script
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
16/11/29 16:24:20 WARN ScriptBasedMapping: Exception running /etc/hadoop/conf/topology_script.py some.ip.address
|
||||
ExitCodeException exitCode=1: File "/etc/hadoop/conf/topology_script.py", line 63
|
||||
print rack
|
||||
^
|
||||
SyntaxError: Missing parentheses in call to 'print'
|
||||
|
||||
at `org.apache.hadoop.util.Shell.runCommand(Shell.java:576)`
|
||||
```
|
||||
|
||||
In order to resolve this issue, there are two potential options.
|
||||
|
||||
1. Update HDFS core-site.xml, so the parameter "net.topology.script.file.name" points to a custom
|
||||
script (e.g. /etc/hadoop/conf/custom_topology_script.py). Copy the original script and change the first line point
|
||||
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).
|
||||
|
||||
### 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
@@ -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
@@ -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,63 +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_use_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 an 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
|
54
docs/sphinxext/autodoc_traits.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""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)
|
130
examples/bootstrap-script/README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# 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.
|
||||
|
||||
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
@@ -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
|
26
examples/bootstrap-script/jupyterhub_config.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Example for a Spawner.pre_spawn_hook
|
||||
# create a directory for the user before the spawner starts
|
||||
|
||||
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):
|
||||
os.mkdir(volume_path, 0o755)
|
||||
# now do whatever you think your user needs
|
||||
# ...
|
||||
|
||||
# attach the hook function to the spawner
|
||||
c.Spawner.pre_spawn_hook = create_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' }
|
||||
|
41
examples/cull-idle/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# `cull-idle` Example
|
||||
|
||||
The `cull_idle_servers.py` file provides a script to cull and shut down idle
|
||||
single-user notebook servers. This script is used when `cull-idle` is run as
|
||||
a Service or when it is run manually as a standalone script.
|
||||
|
||||
|
||||
## 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': 'python 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 managed by the Hub.
|
||||
|
||||
## Run `cull-idle` manually as a standalone script
|
||||
|
||||
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=`jupyterhub token`
|
||||
python cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api]
|
||||
```
|
@@ -9,10 +9,21 @@ so cull timeout should be greater than the sum of:
|
||||
- single-user websocket ping interval (default: 30s)
|
||||
- JupyterHub.last_activity_interval (default: 5 minutes)
|
||||
|
||||
Generate an API token and store it in `JPY_API_TOKEN`:
|
||||
You can run this as a service managed by JupyterHub with this in your config::
|
||||
|
||||
export JPY_API_TOKEN=`jupyterhub token`
|
||||
python cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub]
|
||||
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'cull-idle',
|
||||
'admin': True,
|
||||
'command': 'python cull_idle_servers.py --timeout=3600'.split(),
|
||||
}
|
||||
]
|
||||
|
||||
Or run it manually by generating an API token and storing it in `JUPYTERHUB_API_TOKEN`:
|
||||
|
||||
export JUPYTERHUB_API_TOKEN=`jupyterhub token`
|
||||
python cull_idle_servers.py [--timeout=900] [--url=http://127.0.0.1:8081/hub/api]
|
||||
"""
|
||||
|
||||
import datetime
|
||||
@@ -34,7 +45,7 @@ def cull_idle(url, api_token, timeout):
|
||||
auth_header = {
|
||||
'Authorization': 'token %s' % api_token
|
||||
}
|
||||
req = HTTPRequest(url=url + '/api/users',
|
||||
req = HTTPRequest(url=url + '/users',
|
||||
headers=auth_header,
|
||||
)
|
||||
now = datetime.datetime.utcnow()
|
||||
@@ -47,7 +58,7 @@ def cull_idle(url, api_token, timeout):
|
||||
last_activity = parse_date(user['last_activity'])
|
||||
if user['server'] and last_activity < cull_limit:
|
||||
app_log.info("Culling %s (inactive since %s)", user['name'], last_activity)
|
||||
req = HTTPRequest(url=url + '/api/users/%s/server' % user['name'],
|
||||
req = HTTPRequest(url=url + '/users/%s/server' % user['name'],
|
||||
method='DELETE',
|
||||
headers=auth_header,
|
||||
)
|
||||
@@ -60,7 +71,7 @@ def cull_idle(url, api_token, timeout):
|
||||
app_log.debug("Finished culling %s", name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
define('url', default='http://127.0.0.1:8081/hub', 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('cull_every', default=0, help="The interval (in seconds) for checking for idle servers to cull")
|
||||
|
||||
@@ -68,7 +79,7 @@ if __name__ == '__main__':
|
||||
if not options.cull_every:
|
||||
options.cull_every = options.timeout // 2
|
||||
|
||||
api_token = os.environ['JPY_API_TOKEN']
|
||||
api_token = os.environ['JUPYTERHUB_API_TOKEN']
|
||||
|
||||
loop = IOLoop.current()
|
||||
cull = lambda : cull_idle(options.url, api_token, options.timeout)
|
||||
|
8
examples/cull-idle/jupyterhub_config.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# run cull-idle as a service
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'cull-idle',
|
||||
'admin': True,
|
||||
'command': 'python cull_idle_servers.py --timeout=3600'.split(),
|
||||
}
|
||||
]
|
25
examples/service-notebook/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Running a shared notebook as a service
|
||||
|
||||
This directory contains two examples of running a shared notebook server as a service,
|
||||
one as a 'managed' service, and one as an external service with supervisor.
|
||||
|
||||
These examples require jupyterhub >= 0.7.2.
|
||||
|
||||
A single-user notebook server is run as a service,
|
||||
and uses groups to authenticate a collection of users with the Hub.
|
||||
|
||||
In these examples, a JupyterHub group `'shared'` is created,
|
||||
and a notebook server is spawned at `/services/shared-notebook`.
|
||||
Any user in the `'shared'` group will be able to access the notebook server at `/services/shared-notebook/`.
|
||||
|
||||
In both examples, you will want to select the name of the group,
|
||||
and the name of the shared-notebook service.
|
||||
|
||||
In the external example, some extra steps are required to set up supervisor:
|
||||
|
||||
1. select a system user to run the service. This is a user on the system, and does not need to be a Hub user. Add this to the user field in `shared-notebook.conf`, replacing `someuser`.
|
||||
2. generate a secret token for authentication, and replace the `super-secret` fields in `shared-notebook-service` and `jupyterhub_config.py`
|
||||
3. install `shared-notebook-service` somewhere on your system, and update `/path/to/shared-notebook-service` to the absolute path of this destination
|
||||
3. copy `shared-notebook.conf` to `/etc/supervisor/conf.d/`
|
||||
4. `supervisorctl reload`
|
||||
|
24
examples/service-notebook/external/jupyterhub_config.py
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# our user list
|
||||
c.Authenticator.whitelist = [
|
||||
'minrk',
|
||||
'ellisonbg',
|
||||
'willingc',
|
||||
]
|
||||
|
||||
# ellisonbg and willingc have access to a shared server:
|
||||
|
||||
c.JupyterHub.load_groups = {
|
||||
'shared': [
|
||||
'ellisonbg',
|
||||
'willingc',
|
||||
]
|
||||
}
|
||||
|
||||
# start the notebook server as a service
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'shared-notebook',
|
||||
'url': 'http://127.0.0.1:9999',
|
||||
'api_token': 'super-secret',
|
||||
}
|
||||
]
|
9
examples/service-notebook/external/shared-notebook-service
vendored
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash -l
|
||||
set -e
|
||||
|
||||
export JUPYTERHUB_API_TOKEN=super-secret
|
||||
export JUPYTERHUB_SERVICE_URL=http://127.0.0.1:9999
|
||||
export JUPYTERHUB_SERVICE_NAME=shared-notebook
|
||||
|
||||
jupyterhub-singleuser \
|
||||
--group='shared'
|
14
examples/service-notebook/external/shared-notebook.conf
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
[program:jupyterhub-shared-notebook]
|
||||
user=someuser
|
||||
command=bash -l /path/to/shared-notebook-service
|
||||
directory=/home/someuser
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=1
|
||||
exitcodes=0,2
|
||||
stopsignal=TERM
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/jupyterhub-service-shared-notebook.log
|
||||
stdout_logfile_maxbytes=1MB
|
||||
stdout_logfile_backups=10
|
||||
stdout_capture_maxbytes=1MB
|
32
examples/service-notebook/managed/jupyterhub_config.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# our user list
|
||||
c.Authenticator.whitelist = [
|
||||
'minrk',
|
||||
'ellisonbg',
|
||||
'willingc',
|
||||
]
|
||||
|
||||
# ellisonbg and willingc have access to a shared server:
|
||||
|
||||
c.JupyterHub.load_groups = {
|
||||
'shared': [
|
||||
'ellisonbg',
|
||||
'willingc',
|
||||
]
|
||||
}
|
||||
|
||||
service_name = 'shared-notebook'
|
||||
service_port = 9999
|
||||
group_name = 'shared'
|
||||
|
||||
# start the notebook server as a service
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': service_name,
|
||||
'url': 'http://127.0.0.1:{}'.format(service_port),
|
||||
'command': [
|
||||
'jupyterhub-singleuser',
|
||||
'--group=shared',
|
||||
'--debug',
|
||||
],
|
||||
}
|
||||
]
|
33
examples/service-whoami-flask/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Authenticating a flask service with JupyterHub
|
||||
|
||||
Uses `jupyterhub.services.HubAuth` to authenticate requests with the Hub in a [flask][] application.
|
||||
|
||||
## Run
|
||||
|
||||
1. Launch JupyterHub and the `whoami service` with
|
||||
|
||||
jupyterhub --ip=127.0.0.1
|
||||
|
||||
2. Visit http://127.0.0.1:8000/services/whoami or http://127.0.0.1:8000/services/whoami-oauth
|
||||
|
||||
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"
|
||||
}
|
||||
```
|
||||
|
||||
This relies on the Hub starting the whoami service, via config (see [jupyterhub_config.py](./jupyterhub_config.py)).
|
||||
|
||||
A similar service could be run externally, by setting the JupyterHub service environment variables:
|
||||
|
||||
JUPYTERHUB_API_TOKEN
|
||||
JUPYTERHUB_SERVICE_PREFIX
|
||||
|
||||
|
||||
[flask]: http://flask.pocoo.org
|
21
examples/service-whoami-flask/jupyterhub_config.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'whoami',
|
||||
'url': 'http://127.0.0.1:10101',
|
||||
'command': ['flask', 'run', '--port=10101'],
|
||||
'environment': {
|
||||
'FLASK_APP': 'whoami-flask.py',
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'whoami-oauth',
|
||||
'url': 'http://127.0.0.1:10201',
|
||||
'command': ['flask', 'run', '--port=10201'],
|
||||
'environment': {
|
||||
'FLASK_APP': 'whoami-oauth.py',
|
||||
}
|
||||
},
|
||||
]
|
4
examples/service-whoami-flask/launch.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
export CONFIGPROXY_AUTH_TOKEN=`openssl rand -hex 32`
|
||||
|
||||
# start JupyterHub
|
||||
jupyterhub --ip=127.0.0.1
|
50
examples/service-whoami-flask/whoami-flask.py
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
whoami service authentication with the Hub
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
import json
|
||||
import os
|
||||
from urllib.parse import quote
|
||||
|
||||
from flask import Flask, redirect, request, Response
|
||||
|
||||
from jupyterhub.services.auth import HubAuth
|
||||
|
||||
|
||||
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
|
||||
|
||||
auth = HubAuth(
|
||||
api_token=os.environ['JUPYTERHUB_API_TOKEN'],
|
||||
cache_max_age=60,
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def authenticated(f):
|
||||
"""Decorator for authenticating with the Hub"""
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
cookie = request.cookies.get(auth.cookie_name)
|
||||
if cookie:
|
||||
user = auth.user_for_cookie(cookie)
|
||||
else:
|
||||
user = None
|
||||
if user:
|
||||
return f(user, *args, **kwargs)
|
||||
else:
|
||||
# redirect to login url on failed auth
|
||||
return redirect(auth.login_url + '?next=%s' % quote(request.path))
|
||||
return decorated
|
||||
|
||||
|
||||
@app.route(prefix + '/')
|
||||
@authenticated
|
||||
def whoami(user):
|
||||
return Response(
|
||||
json.dumps(user, indent=1, sort_keys=True),
|
||||
mimetype='application/json',
|
||||
)
|
||||
|
70
examples/service-whoami-flask/whoami-oauth.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
whoami service authentication with the Hub
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
import json
|
||||
import os
|
||||
|
||||
from flask import Flask, redirect, request, Response, make_response
|
||||
|
||||
from jupyterhub.services.auth import HubOAuth
|
||||
|
||||
|
||||
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
|
||||
|
||||
auth = HubOAuth(
|
||||
api_token=os.environ['JUPYTERHUB_API_TOKEN'],
|
||||
cache_max_age=60,
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def authenticated(f):
|
||||
"""Decorator for authenticating with the Hub via OAuth"""
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
token = request.cookies.get(auth.cookie_name)
|
||||
if token:
|
||||
user = auth.user_for_token(token)
|
||||
else:
|
||||
user = None
|
||||
if user:
|
||||
return f(user, *args, **kwargs)
|
||||
else:
|
||||
# redirect to login url on failed auth
|
||||
state = auth.generate_state(next_url=request.path)
|
||||
response = make_response(redirect(auth.login_url + '&state=%s' % state))
|
||||
response.set_cookie(auth.state_cookie_name, state)
|
||||
return response
|
||||
return decorated
|
||||
|
||||
|
||||
@app.route(prefix)
|
||||
@authenticated
|
||||
def whoami(user):
|
||||
return Response(
|
||||
json.dumps(user, indent=1, sort_keys=True),
|
||||
mimetype='application/json',
|
||||
)
|
||||
|
||||
@app.route(prefix + 'oauth_callback')
|
||||
def oauth_callback():
|
||||
code = request.args.get('code', None)
|
||||
if code is None:
|
||||
return 403
|
||||
|
||||
# validate state field
|
||||
arg_state = request.args.get('state', None)
|
||||
cookie_state = request.cookies.get(auth.state_cookie_name)
|
||||
if arg_state != cookie_state:
|
||||
# state doesn't match
|
||||
return 403
|
||||
|
||||
token = auth.token_for_code(code)
|
||||
next_url = auth.get_next_url(cookie_state) or prefix
|
||||
response = make_response(redirect(next_url))
|
||||
response.set_cookie(auth.cookie_name, token)
|
||||
return response
|
BIN
examples/service-whoami-flask/whoami.png
Normal file
After Width: | Height: | Size: 35 KiB |
34
examples/service-whoami/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Authenticating a service with JupyterHub
|
||||
|
||||
Uses `jupyterhub.services.HubAuthenticated` to authenticate requests with the Hub.
|
||||
|
||||
There is an implementation each of cookie-based `HubAuthenticated` and OAuth-based `HubOAuthenticated`.
|
||||
|
||||
## Run
|
||||
|
||||
1. Launch JupyterHub and the `whoami service` with
|
||||
|
||||
jupyterhub --ip=127.0.0.1
|
||||
|
||||
2. Visit http://127.0.0.1:8000/services/whoami or http://127.0.0.1:8000/services/whoami-oauth
|
||||
|
||||
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"
|
||||
}
|
||||
```
|
||||
|
||||
This relies on the Hub starting the whoami services, via config (see [jupyterhub_config.py](./jupyterhub_config.py)).
|
||||
|
||||
A similar service could be run externally, by setting the JupyterHub service environment variables:
|
||||
|
||||
JUPYTERHUB_API_TOKEN
|
||||
JUPYTERHUB_SERVICE_PREFIX
|
||||
|
||||
or instantiating and configuring a HubAuth object yourself, and attaching it as `self.hub_auth` in your HubAuthenticated handlers.
|
15
examples/service-whoami/jupyterhub_config.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'whoami',
|
||||
'url': 'http://127.0.0.1:10101',
|
||||
'command': [sys.executable, './whoami.py'],
|
||||
},
|
||||
{
|
||||
'name': 'whoami-oauth',
|
||||
'url': 'http://127.0.0.1:10102',
|
||||
'command': [sys.executable, './whoami-oauth.py'],
|
||||
},
|
||||
]
|
43
examples/service-whoami/whoami-oauth.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""An example service authenticating with the Hub.
|
||||
|
||||
This example service serves `/services/whoami/`,
|
||||
authenticated with the Hub,
|
||||
showing the user their own info.
|
||||
"""
|
||||
from getpass import getuser
|
||||
import json
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.web import RequestHandler, Application, authenticated
|
||||
|
||||
from jupyterhub.services.auth import HubOAuthenticated, HubOAuthCallbackHandler
|
||||
from jupyterhub.utils import url_path_join
|
||||
|
||||
class WhoAmIHandler(HubOAuthenticated, RequestHandler):
|
||||
hub_users = {getuser()} # the users allowed to access this service
|
||||
|
||||
@authenticated
|
||||
def get(self):
|
||||
user_model = self.get_current_user()
|
||||
self.set_header('content-type', 'application/json')
|
||||
self.write(json.dumps(user_model, indent=1, sort_keys=True))
|
||||
|
||||
def main():
|
||||
app = Application([
|
||||
(os.environ['JUPYTERHUB_SERVICE_PREFIX'], WhoAmIHandler),
|
||||
(url_path_join(os.environ['JUPYTERHUB_SERVICE_PREFIX'], 'oauth_callback'), HubOAuthCallbackHandler),
|
||||
(r'.*', WhoAmIHandler),
|
||||
], cookie_secret=os.urandom(32))
|
||||
|
||||
http_server = HTTPServer(app)
|
||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||
|
||||
http_server.listen(url.port, url.hostname)
|
||||
|
||||
IOLoop.current().start()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
BIN
examples/service-whoami/whoami.png
Normal file
After Width: | Height: | Size: 35 KiB |
40
examples/service-whoami/whoami.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""An example service authenticating with the Hub.
|
||||
|
||||
This serves `/services/whoami/`, authenticated with the Hub, showing the user their own info.
|
||||
"""
|
||||
from getpass import getuser
|
||||
import json
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.web import RequestHandler, Application, authenticated
|
||||
|
||||
from jupyterhub.services.auth import HubAuthenticated
|
||||
|
||||
|
||||
class WhoAmIHandler(HubAuthenticated, RequestHandler):
|
||||
hub_users = {getuser()} # the users allowed to access me
|
||||
|
||||
@authenticated
|
||||
def get(self):
|
||||
user_model = self.get_current_user()
|
||||
self.set_header('content-type', 'application/json')
|
||||
self.write(json.dumps(user_model, indent=1, sort_keys=True))
|
||||
|
||||
def main():
|
||||
app = Application([
|
||||
(os.environ['JUPYTERHUB_SERVICE_PREFIX'] + '/?', WhoAmIHandler),
|
||||
(r'.*', WhoAmIHandler),
|
||||
])
|
||||
|
||||
http_server = HTTPServer(app)
|
||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
||||
|
||||
http_server.listen(url.port, url.hostname)
|
||||
|
||||
IOLoop.current().start()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@@ -1,2 +1 @@
|
||||
from .version import version_info, __version__
|
||||
|
||||
from ._version import version_info, __version__
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"""Get the data files for this package."""
|
||||
|
||||
|
||||
def get_data_files():
|
||||
"""Walk up until we find share/jupyter/hub"""
|
||||
import sys
|
||||
@@ -21,4 +22,3 @@ def get_data_files():
|
||||
|
||||
# Package managers can just override this with the appropriate constant
|
||||
DATA_FILES_PATH = get_data_files()
|
||||
|
||||
|
46
jupyterhub/_version.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""JupyterHub version info"""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
version_info = (
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
'b4',
|
||||
)
|
||||
|
||||
__version__ = '.'.join(map(str, version_info))
|
||||
|
||||
|
||||
def _check_version(hub_version, singleuser_version, log):
|
||||
"""Compare Hub and single-user server versions"""
|
||||
if not hub_version:
|
||||
log.warning("Hub has no version header, which means it is likely < 0.8. Expected %s", __version__)
|
||||
return
|
||||
|
||||
if not singleuser_version:
|
||||
log.warning("Single-user server has no version header, which means it is likely < 0.8. Expected %s", __version__)
|
||||
return
|
||||
|
||||
# compare minor X.Y versions
|
||||
if hub_version != singleuser_version:
|
||||
from distutils.version import LooseVersion as V
|
||||
hub_major_minor = V(hub_version).version[:2]
|
||||
singleuser_major_minor = V(singleuser_version).version[:2]
|
||||
extra = ""
|
||||
if singleuser_major_minor == hub_major_minor:
|
||||
# patch-level mismatch or lower, log difference at debug-level
|
||||
# because this should be fine
|
||||
log_method = log.debug
|
||||
else:
|
||||
# log warning-level for more significant mismatch, such as 0.8 vs 0.9, etc.
|
||||
log_method = log.warning
|
||||
extra = " This could cause failure to authenticate and result in redirect loops!"
|
||||
log_method(
|
||||
"jupyterhub version %s != jupyterhub-singleuser version %s." + extra,
|
||||
hub_version,
|
||||
singleuser_version,
|
||||
)
|
||||
else:
|
||||
log.debug("jupyterhub and jupyterhub-singleuser both on version %s" % hub_version)
|
67
jupyterhub/alembic.ini
Normal file
@@ -0,0 +1,67 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
script_location = {alembic_dir}
|
||||
sqlalchemy.url = {db_url}
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
#truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; this defaults
|
||||
# to jupyterhub/alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path
|
||||
# version_locations = %(here)s/bar %(here)s/bat jupyterhub/alembic/versions
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
class = jupyterhub.log.CoroutineLogFormatter
|
||||
format = %(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s %(module)s:%(lineno)d]%(end_color)s %(message)s
|
||||
datefmt = %Y-%m-%d %H:%M:%S
|
1
jupyterhub/alembic/README
Normal file
@@ -0,0 +1 @@
|
||||
This is the alembic configuration for JupyterHub data base migrations.
|
82
jupyterhub/alembic/env.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import sys
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if 'jupyterhub' in sys.modules:
|
||||
from jupyterhub.app import JupyterHub
|
||||
if JupyterHub.initialized():
|
||||
app = JupyterHub.instance()
|
||||
alembic_logger = logging.getLogger('alembic')
|
||||
alembic_logger.propagate = True
|
||||
alembic_logger.parent = app.log
|
||||
else:
|
||||
fileConfig(config.config_file_name)
|
||||
else:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = None
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
jupyterhub/alembic/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
@@ -0,0 +1,24 @@
|
||||
"""base revision for 0.5
|
||||
|
||||
Revision ID: 19c0846f6344
|
||||
Revises:
|
||||
Create Date: 2016-04-11 16:05:34.873288
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '19c0846f6344'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@@ -0,0 +1,64 @@
|
||||
"""0.8 changes
|
||||
|
||||
- encrypted auth_state
|
||||
- remove proxy/hub data from db
|
||||
|
||||
OAuth data was also added in this revision,
|
||||
but no migration to do because they are entirely new tables,
|
||||
which will be created on launch.
|
||||
|
||||
Revision ID: 3ec6993fe20c
|
||||
Revises: af4cbdb2d13c
|
||||
Create Date: 2017-07-28 16:44:40.413648
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3ec6993fe20c'
|
||||
down_revision = 'af4cbdb2d13c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('alembic')
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from jupyterhub.orm import JSONDict
|
||||
|
||||
|
||||
def upgrade():
|
||||
# proxy/table info is no longer in the database
|
||||
op.drop_table('proxies')
|
||||
op.drop_table('hubs')
|
||||
|
||||
# drop some columns no longer in use
|
||||
try:
|
||||
op.drop_column('users', 'auth_state')
|
||||
op.drop_column('users', '_server_id')
|
||||
except sa.exc.OperationalError:
|
||||
# this won't be a problem moving forward, but downgrade will fail
|
||||
if op.get_context().dialect.name == 'sqlite':
|
||||
logger.warning("sqlite cannot drop columns. Leaving unused old columns in place.")
|
||||
else:
|
||||
raise
|
||||
|
||||
op.add_column('users', sa.Column('encrypted_auth_state', sa.types.LargeBinary))
|
||||
|
||||
|
||||
def downgrade():
|
||||
# drop all the new tables
|
||||
engine = op.get_bind().engine
|
||||
for table in ('oauth_clients',
|
||||
'oauth_codes',
|
||||
'oauth_access_tokens',
|
||||
'spawners'):
|
||||
if engine.has_table(table):
|
||||
op.drop_table(table)
|
||||
|
||||
op.drop_column('users', 'encrypted_auth_state')
|
||||
|
||||
op.add_column('users', sa.Column('auth_state', JSONDict))
|
||||
op.add_column('users', sa.Column('_server_id', sa.Integer, sa.ForeignKey('servers.id')))
|
||||
|
25
jupyterhub/alembic/versions/af4cbdb2d13c_services.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""services
|
||||
|
||||
Revision ID: af4cbdb2d13c
|
||||
Revises: eeb276e51423
|
||||
Create Date: 2016-07-28 16:16:38.245348
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'af4cbdb2d13c'
|
||||
down_revision = 'eeb276e51423'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('api_tokens', sa.Column('service_id', sa.Integer))
|
||||
|
||||
|
||||
def downgrade():
|
||||
# sqlite cannot downgrade because of limited ALTER TABLE support (no DROP COLUMN)
|
||||
op.drop_column('api_tokens', 'service_id')
|
26
jupyterhub/alembic/versions/eeb276e51423_auth_state.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""auth_state
|
||||
|
||||
Adds auth_state column to Users table.
|
||||
|
||||
Revision ID: eeb276e51423
|
||||
Revises: 19c0846f6344
|
||||
Create Date: 2016-04-11 16:06:49.239831
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'eeb276e51423'
|
||||
down_revision = '19c0846f6344'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from jupyterhub.orm import JSONDict
|
||||
|
||||
def upgrade():
|
||||
op.add_column('users', sa.Column('auth_state', JSONDict))
|
||||
|
||||
|
||||
def downgrade():
|
||||
# sqlite cannot downgrade because of limited ALTER TABLE support (no DROP COLUMN)
|
||||
op.drop_column('users', 'auth_state')
|