mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 02:24:08 +00:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2985562c2f | ||
![]() |
754f850e95 | ||
![]() |
dccb85d225 | ||
![]() |
a0e401bc87 | ||
![]() |
c6885a2124 | ||
![]() |
7528fb7d9b | ||
![]() |
e7df5a299c | ||
![]() |
ff997bbce5 | ||
![]() |
1e21e00e1a | ||
![]() |
77d3ee98f9 | ||
![]() |
1f861b2c90 | ||
![]() |
14a00e67b4 | ||
![]() |
14f63c168d | ||
![]() |
e70dbb3d32 | ||
![]() |
b679275a68 | ||
![]() |
0c1478a67e | ||
![]() |
d26e2346a2 | ||
![]() |
9a09c841b9 | ||
![]() |
f1d4f5a733 | ||
![]() |
d970dd4c89 | ||
![]() |
f3279bf849 | ||
![]() |
db0878a495 | ||
![]() |
c9b1042791 | ||
![]() |
cd81320d8f |
16
README.md
16
README.md
@@ -6,7 +6,7 @@ Questions, comments? Visit our Google Group:
|
||||
[](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)
|
||||
|
||||
|
||||
JupyterHub, a multi-user server, manages and proxies multiple instances of the single-user <del>IPython</del> Jupyter notebook server.
|
||||
@@ -67,7 +67,7 @@ Jupyter ~~IPython~~ notebook:
|
||||
|
||||
For a development install, clone the repository and then install from source:
|
||||
|
||||
git clone https://github.com/jupyter/jupyterhub
|
||||
git clone https://github.com/jupyterhub/jupyterhub
|
||||
cd jupyterhub
|
||||
pip3 install -r dev-requirements.txt -e .
|
||||
|
||||
@@ -93,7 +93,7 @@ and then visit `http://localhost:8000`, and sign in with your unix credentials.
|
||||
|
||||
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)
|
||||
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.
|
||||
|
||||
@@ -116,13 +116,13 @@ 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)
|
||||
- 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)
|
||||
|
||||
### 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.
|
||||
There is a ready to go [docker image for JupyterHub](https://hub.docker.com/r/jupyterhub/jupyterhub/).
|
||||
[Note: This `jupyterhub/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.]
|
||||
|
||||
@@ -148,7 +148,7 @@ We encourage you to ask questions on the mailing list:
|
||||
|
||||
and you may participate in development discussions or get live help on Gitter:
|
||||
|
||||
[](https://gitter.im/jupyter/jupyterhub?utm_source=badge&utm_medium=badge)
|
||||
[](https://gitter.im/jupyterhub/jupyterhub?utm_source=badge&utm_medium=badge)
|
||||
|
||||
## Resources
|
||||
- [Project Jupyter website](https://jupyter.org)
|
||||
|
@@ -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
|
||||
|
@@ -4,6 +4,17 @@ See `git log` for a more detailed summary.
|
||||
|
||||
## 0.6
|
||||
|
||||
### 0.6.1
|
||||
|
||||
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
|
||||
|
||||
- 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}`
|
||||
|
@@ -45,7 +45,7 @@ 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`,
|
||||
0.5 also adds an option `Spawner.disable_user_config`,
|
||||
which you can set to prevent the user-owned configuration files from being loaded.
|
||||
This leaves only package installation and PATHs as things the admin must enforce.
|
||||
|
||||
|
@@ -161,8 +161,9 @@ class UserServerAPIHandler(APIHandler):
|
||||
@admin_or_self
|
||||
def post(self, name):
|
||||
user = self.find_user(name)
|
||||
if user.spawner:
|
||||
state = yield user.spawner.poll()
|
||||
if user.running:
|
||||
# include notify, so that a server that died is noticed immediately
|
||||
state = yield user.spawner.poll_and_notify()
|
||||
if state is None:
|
||||
raise web.HTTPError(400, "%s's server is already running" % name)
|
||||
|
||||
@@ -180,7 +181,8 @@ class UserServerAPIHandler(APIHandler):
|
||||
return
|
||||
if not user.running:
|
||||
raise web.HTTPError(400, "%s's server is not running" % name)
|
||||
status = yield user.spawner.poll()
|
||||
# include notify, so that a server that died is noticed immediately
|
||||
status = yield user.spawner.poll_and_notify()
|
||||
if status is not None:
|
||||
raise web.HTTPError(400, "%s's server is not running" % name)
|
||||
yield self.stop_single_user(user)
|
||||
|
@@ -12,7 +12,6 @@ import signal
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import statsd
|
||||
from datetime import datetime
|
||||
from distutils.version import LooseVersion as V
|
||||
from getpass import getuser
|
||||
@@ -507,22 +506,21 @@ class JupyterHub(Application):
|
||||
Instance(logging.Handler),
|
||||
help="Extra log handlers to set on JupyterHub logger",
|
||||
).tag(config=True)
|
||||
|
||||
@property
|
||||
def statsd(self):
|
||||
if hasattr(self, '_statsd'):
|
||||
return self._statsd
|
||||
|
||||
statsd = Any(allow_none=False, help="The statsd client, if any. A mock will be used if we aren't using statsd")
|
||||
@default('statsd')
|
||||
def _statsd(self):
|
||||
if self.statsd_host:
|
||||
self._statsd = statsd.StatsClient(
|
||||
import statsd
|
||||
client = statsd.StatsClient(
|
||||
self.statsd_host,
|
||||
self.statsd_port,
|
||||
self.statsd_prefix
|
||||
)
|
||||
return self._statsd
|
||||
return client
|
||||
else:
|
||||
# return an empty mock object!
|
||||
self._statsd = EmptyClass()
|
||||
return self._statsd
|
||||
return EmptyClass()
|
||||
|
||||
def init_logging(self):
|
||||
# This prevents double log messages because tornado use a root logger that
|
||||
@@ -939,6 +937,7 @@ class JupyterHub(Application):
|
||||
'--api-ip', self.proxy.api_server.ip,
|
||||
'--api-port', str(self.proxy.api_server.port),
|
||||
'--default-target', self.hub.server.host,
|
||||
'--error-target', url_path_join(self.hub.server.url, 'error'),
|
||||
]
|
||||
if self.subdomain_host:
|
||||
cmd.append('--host-routing')
|
||||
|
@@ -410,6 +410,7 @@ class BaseHandler(RequestHandler):
|
||||
"""render custom error pages"""
|
||||
exc_info = kwargs.get('exc_info')
|
||||
message = ''
|
||||
exception = None
|
||||
status_message = responses.get(status_code, 'Unknown HTTP Error')
|
||||
if exc_info:
|
||||
exception = exc_info[1]
|
||||
|
@@ -15,12 +15,12 @@ class LogoutHandler(BaseHandler):
|
||||
user = self.get_current_user()
|
||||
if user:
|
||||
self.log.info("User logged out: %s", user.name)
|
||||
self.clear_login_cookie()
|
||||
for name in user.other_user_cookies:
|
||||
self.clear_login_cookie(name)
|
||||
user.other_user_cookies = set([])
|
||||
self.clear_login_cookie()
|
||||
for name in user.other_user_cookies:
|
||||
self.clear_login_cookie(name)
|
||||
user.other_user_cookies = set([])
|
||||
self.statsd.incr('logout')
|
||||
self.redirect(self.hub.server.base_url, permanent=False)
|
||||
self.statsd.incr('logout')
|
||||
|
||||
|
||||
class LoginHandler(BaseHandler):
|
||||
|
@@ -3,12 +3,14 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from http.client import responses
|
||||
|
||||
from jinja2 import TemplateNotFound
|
||||
from tornado import web, gen
|
||||
|
||||
from .. import orm
|
||||
from ..utils import admin_only, url_path_join
|
||||
from .base import BaseHandler
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
class RootHandler(BaseHandler):
|
||||
@@ -41,9 +43,14 @@ class HomeHandler(BaseHandler):
|
||||
"""Render the user's home page."""
|
||||
|
||||
@web.authenticated
|
||||
@gen.coroutine
|
||||
def get(self):
|
||||
user = self.get_current_user()
|
||||
if user.running:
|
||||
# trigger poll_and_notify event in case of a server that died
|
||||
yield user.spawner.poll_and_notify()
|
||||
html = self.render_template('home.html',
|
||||
user=self.get_current_user(),
|
||||
user=user,
|
||||
)
|
||||
self.finish(html)
|
||||
|
||||
@@ -160,9 +167,43 @@ class AdminHandler(BaseHandler):
|
||||
self.finish(html)
|
||||
|
||||
|
||||
class ProxyErrorHandler(BaseHandler):
|
||||
"""Handler for rendering proxy error pages"""
|
||||
|
||||
def get(self, status_code_s):
|
||||
status_code = int(status_code_s)
|
||||
status_message = responses.get(status_code, 'Unknown HTTP Error')
|
||||
# build template namespace
|
||||
|
||||
hub_home = url_path_join(self.hub.server.base_url, 'home')
|
||||
message_html = ''
|
||||
if status_code == 503:
|
||||
message_html = ' '.join([
|
||||
"Your server appears to be down.",
|
||||
"Try restarting it <a href='%s'>from the hub</a>" % hub_home
|
||||
])
|
||||
ns = dict(
|
||||
status_code=status_code,
|
||||
status_message=status_message,
|
||||
message_html=message_html,
|
||||
logo_url=hub_home,
|
||||
)
|
||||
|
||||
self.set_header('Content-Type', 'text/html')
|
||||
# render the template
|
||||
try:
|
||||
html = self.render_template('%s.html' % status_code, **ns)
|
||||
except TemplateNotFound:
|
||||
self.log.debug("No template for %d", status_code)
|
||||
html = self.render_template('error.html', **ns)
|
||||
|
||||
self.write(html)
|
||||
|
||||
|
||||
default_handlers = [
|
||||
(r'/', RootHandler),
|
||||
(r'/home', HomeHandler),
|
||||
(r'/admin', AdminHandler),
|
||||
(r'/spawn', SpawnHandler),
|
||||
(r'/error/(\d+)', ProxyErrorHandler),
|
||||
]
|
||||
|
@@ -15,7 +15,7 @@ from subprocess import Popen
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from tornado import gen
|
||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||
from tornado.ioloop import PeriodicCallback
|
||||
|
||||
from traitlets.config import LoggingConfigurable
|
||||
from traitlets import (
|
||||
@@ -335,15 +335,17 @@ class Spawner(LoggingConfigurable):
|
||||
|
||||
self.stop_polling()
|
||||
|
||||
add_callback = IOLoop.current().add_callback
|
||||
for callback in self._callbacks:
|
||||
add_callback(callback)
|
||||
try:
|
||||
yield gen.maybe_future(callback())
|
||||
except Exception:
|
||||
self.log.exception("Unhandled error in poll callback for %s", self)
|
||||
return status
|
||||
|
||||
death_interval = Float(0.1)
|
||||
@gen.coroutine
|
||||
def wait_for_death(self, timeout=10):
|
||||
"""wait for the process to die, up to timeout seconds"""
|
||||
loop = IOLoop.current()
|
||||
for i in range(int(timeout / self.death_interval)):
|
||||
status = yield self.poll()
|
||||
if status is not None:
|
||||
|
@@ -6,7 +6,7 @@
|
||||
version_info = (
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
1,
|
||||
# 'dev',
|
||||
)
|
||||
|
||||
|
@@ -2,6 +2,5 @@ traitlets>=4.1
|
||||
tornado>=4.1
|
||||
jinja2
|
||||
pamela
|
||||
statsd
|
||||
sqlalchemy>=1.0
|
||||
requests
|
||||
|
@@ -152,15 +152,15 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function ($, bs, mo
|
||||
});
|
||||
});
|
||||
|
||||
$("#add-user").click(function () {
|
||||
var dialog = $("#add-user-dialog");
|
||||
$("#add-users").click(function () {
|
||||
var dialog = $("#add-users-dialog");
|
||||
dialog.find(".username-input").val('');
|
||||
dialog.find(".admin-checkbox").prop("checked", false);
|
||||
dialog.modal();
|
||||
});
|
||||
|
||||
$("#add-user-dialog").find(".save-button").click(function () {
|
||||
var dialog = $("#add-user-dialog");
|
||||
$("#add-users-dialog").find(".save-button").click(function () {
|
||||
var dialog = $("#add-users-dialog");
|
||||
var lines = dialog.find(".username-input").val().split('\n');
|
||||
var admin = dialog.find(".admin-checkbox").prop("checked");
|
||||
var usernames = [];
|
||||
@@ -178,6 +178,15 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function ($, bs, mo
|
||||
});
|
||||
});
|
||||
|
||||
$("#stop-all-servers").click(function () {
|
||||
$("#stop-all-servers-dialog").modal();
|
||||
});
|
||||
|
||||
$("#stop-all-servers-dialog").find(".stop-all-button").click(function () {
|
||||
// stop all clicks all the active stop buttons
|
||||
$('.stop-server').not('.hidden').click();
|
||||
});
|
||||
|
||||
$("#shutdown-hub").click(function () {
|
||||
var dialog = $("#shutdown-hub-dialog");
|
||||
dialog.find("input[type=checkbox]").prop("checked", true);
|
||||
|
@@ -32,8 +32,9 @@
|
||||
<tbody>
|
||||
<tr class="user-row add-user-row">
|
||||
<td colspan="12">
|
||||
<a id="add-user" class="col-xs-5 btn btn-default">Add User</a>
|
||||
<a id="shutdown-hub" class="col-xs-5 col-xs-offset-2 btn btn-danger">Shutdown Hub</a>
|
||||
<a id="add-users" class="col-xs-2 btn btn-default">Add Users</a>
|
||||
<a id="stop-all-servers" class="col-xs-2 col-xs-offset-5 btn btn-danger">Stop All</a>
|
||||
<a id="shutdown-hub" class="col-xs-2 col-xs-offset-1 btn btn-danger">Shutdown Hub</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% for u in users %}
|
||||
@@ -71,6 +72,10 @@
|
||||
This operation cannot be undone.
|
||||
{% endcall %}
|
||||
|
||||
{% call modal('Stop All Servers', btn_label='Stop All', btn_class='btn-danger stop-all-button') %}
|
||||
Are you sure you want to stop all your users' servers? Kernels will be shutdown and unsaved data may be lost.
|
||||
{% endcall %}
|
||||
|
||||
{% call modal('Shutdown Hub', btn_label='Shutdown', btn_class='btn-danger shutdown-button') %}
|
||||
Are you sure you want to shutdown the Hub?
|
||||
You can choose to leave the proxy and/or single-user servers running by unchecking the boxes below:
|
||||
@@ -108,7 +113,7 @@
|
||||
|
||||
{{ user_modal('Edit User') }}
|
||||
|
||||
{{ user_modal('Add User', multi=True) }}
|
||||
{{ user_modal('Add Users', multi=True) }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
@@ -17,6 +17,11 @@
|
||||
{{message}}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if message_html %}
|
||||
<p>
|
||||
{{message_html | safe}}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock error_detail %}
|
||||
</div>
|
||||
|
||||
|
@@ -82,7 +82,7 @@
|
||||
|
||||
<div id="header" class="navbar navbar-static-top">
|
||||
<div class="container">
|
||||
<span id="jupyterhub-logo" class="pull-left"><a href="{{base_url}}"><img src='{{base_url}}logo' alt='JupyterHub' class='jpy-logo' title='Home'/></a></span>
|
||||
<span id="jupyterhub-logo" class="pull-left"><a href="{{logo_url or base_url}}"><img src='{{base_url}}logo' alt='JupyterHub' class='jpy-logo' title='Home'/></a></span>
|
||||
|
||||
{% block login_widget %}
|
||||
|
||||
|
Reference in New Issue
Block a user