Compare commits

...

24 Commits
0.6.0 ... 0.6.1

Author SHA1 Message Date
Min RK
2985562c2f release 0.6.1 2016-05-04 14:08:39 +02:00
Min RK
754f850e95 changelog for 0.6.1 2016-05-04 14:08:39 +02:00
Min RK
dccb85d225 plural add-users ids 2016-05-04 13:57:16 +02:00
Min RK
a0e401bc87 Merge pull request #551 from minrk/proxy-error
Serve proxy error pages from the Hub
2016-05-04 12:34:10 +02:00
Min RK
c6885a2124 Merge pull request #552 from minrk/poll-and-notify
notice dead servers more often
2016-05-04 12:33:09 +02:00
Min RK
7528fb7d9b notice dead servers more often
call poll_and_notify to ensure triggering of dead-server events in a few places:

- `/hub/home` page view
- user start and stop API endpoints

This should avoid the failure to stop a server that's died uncleanly because the server hasn't noticed yet
2016-05-04 11:07:28 +02:00
Carol Willing
e7df5a299c Merge pull request #556 from minrk/shutdown-all
Add Stop All button to admin page
2016-05-03 05:58:41 -07:00
Min RK
ff997bbce5 Add Stop All button to admin page
for stopping all single-user servers at once
2016-05-03 13:25:12 +02:00
Min RK
1e21e00e1a return status from poll_and_notify
allows calling it directly
2016-04-27 14:28:23 +02:00
Min RK
77d3ee98f9 allow logo_url in template namespace to set the logo link 2016-04-27 14:06:51 +02:00
Min RK
1f861b2c90 server proxy error pages from the Hub 2016-04-27 14:06:29 +02:00
Carol Willing
14a00e67b4 Merge pull request #550 from daradib/typo
Fix docs typo for Spawner.disable_user_config
2016-04-26 15:28:02 -07:00
Dara Adib
14f63c168d Fix docs typo for Spawner.disable_user_config 2016-04-26 11:36:48 -07:00
Kyle Kelley
e70dbb3d32 Merge pull request #549 from minrk/optional-statsd
Make statsd an optional dependency
2016-04-26 07:46:28 -05:00
Min RK
b679275a68 remove unneeded codecov.yml
codecov team config suffices
2016-04-26 13:44:29 +02:00
Min RK
0c1478a67e Make statsd an optional dependency
only import it if it's used
2016-04-26 13:37:39 +02:00
Min RK
d26e2346a2 Merge pull request #548 from minrk/jupyterhub-urls
fix a few more jupyter->jupyterhub URLs
2016-04-26 12:41:19 +02:00
Min RK
9a09c841b9 Merge pull request #547 from minrk/disable-codecov-comments
disable codecov PR comments
2016-04-26 12:41:02 +02:00
Min RK
f1d4f5a733 fix a few more jupyter->jupyterhub URLs
in README
2016-04-26 11:58:27 +02:00
Min RK
d970dd4c89 disable CodeCov PR comments
The've removed web app config, in favor of codecov.yml,
discarding our existing config,
which means coverage reports are showing up in most Jupyter PRs now.
2016-04-26 11:55:52 +02:00
Min RK
f3279bf849 Merge pull request #544 from rafael-ladislau/master
Fix multiple windows logout error
2016-04-26 11:41:53 +02:00
Rafael Ladislau
db0878a495 Fix multiple windows logout error
When you have two JupyterHub windows and log out successfully in one of them. If you try to click the logout button in the other window, you will receive a 500 error.

It happened because there were operations being done in a None user object.
2016-04-25 13:31:39 -04:00
Min RK
c9b1042791 back to dev 2016-04-25 14:34:15 +02:00
Min RK
cd81320d8f push tags on circleci 2016-04-25 14:25:34 +02:00
16 changed files with 123 additions and 44 deletions

View File

@@ -6,7 +6,7 @@ Questions, comments? Visit our Google Group:
[![Build Status](https://travis-ci.org/jupyterhub/jupyterhub.svg?branch=master)](https://travis-ci.org/jupyterhub/jupyterhub) [![Build Status](https://travis-ci.org/jupyterhub/jupyterhub.svg?branch=master)](https://travis-ci.org/jupyterhub/jupyterhub)
[![Circle CI](https://circleci.com/gh/jupyterhub/jupyterhub.svg?style=shield&circle-token=b5b65862eb2617b9a8d39e79340b0a6b816da8cc)](https://circleci.com/gh/jupyterhub/jupyterhub) [![Circle CI](https://circleci.com/gh/jupyterhub/jupyterhub.svg?style=shield&circle-token=b5b65862eb2617b9a8d39e79340b0a6b816da8cc)](https://circleci.com/gh/jupyterhub/jupyterhub)
[![Documentation Status](https://readthedocs.org/projects/jupyterhub/badge/?version=latest)](http://jupyterhub.readthedocs.org/en/latest/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/jupyterhub/badge/?version=latest)](http://jupyterhub.readthedocs.org/en/latest/?badge=latest)
[![codecov.io](https://codecov.io/github/jupyter/jupyterhub/coverage.svg?branch=master)](https://codecov.io/github/jupyter/jupyterhub?branch=master) [![codecov.io](https://codecov.io/github/jupyterhub/jupyterhub/coverage.svg?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. 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: 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 cd jupyterhub
pip3 install -r dev-requirements.txt -e . 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 To allow multiple users to sign into the server, you will need to
run the `jupyterhub` command as a *privileged user*, such as root. run the `jupyterhub` command as a *privileged user*, such as root.
The [wiki](https://github.com/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 describes how to run the server as a *less privileged user*, which requires more
configuration of the system. 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. which should allow plugging into a variety of authentication or process control environments.
Some examples, meant as illustration and testing of this concept: Some examples, meant as illustration and testing of this concept:
- Using GitHub OAuth instead of PAM with [OAuthenticator](https://github.com/jupyter/oauthenticator) - 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/jupyter/dockerspawner) - Spawning single-user servers with Docker, using the [DockerSpawner](https://github.com/jupyterhub/dockerspawner)
### Docker ### Docker
There is a ready to go [docker image for JupyterHub](https://hub.docker.com/r/jupyter/jupyterhub/). There is a ready to go [docker image for JupyterHub](https://hub.docker.com/r/jupyterhub/jupyterhub/).
[Note: This `jupyter/jupyterhub` docker image is only an image for running the Hub service itself. [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. 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.] 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: and you may participate in development discussions or get live help on Gitter:
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jupyter/jupyterhub?utm_source=badge&utm_medium=badge) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jupyterhub/jupyterhub?utm_source=badge&utm_medium=badge)
## Resources ## Resources
- [Project Jupyter website](https://jupyter.org) - [Project Jupyter website](https://jupyter.org)

View File

@@ -16,4 +16,9 @@ deployment:
branch: master branch: master
commands: commands:
- docker login -u $DOCKER_USER -p $DOCKER_PASS -e unused@example.com - 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

View File

@@ -4,6 +4,17 @@ See `git log` for a more detailed summary.
## 0.6 ## 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 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 - `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}` - Add statsd support, via `c.JupyterHub.statsd_{host,port,prefix}`

View File

@@ -45,7 +45,7 @@ as it resolves all of the cross-site issues.
### Disabling user config ### Disabling user config
If subdomains are not available or not desirable, 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. 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. This leaves only package installation and PATHs as things the admin must enforce.

View File

@@ -161,8 +161,9 @@ class UserServerAPIHandler(APIHandler):
@admin_or_self @admin_or_self
def post(self, name): def post(self, name):
user = self.find_user(name) user = self.find_user(name)
if user.spawner: if user.running:
state = yield user.spawner.poll() # include notify, so that a server that died is noticed immediately
state = yield user.spawner.poll_and_notify()
if state is None: if state is None:
raise web.HTTPError(400, "%s's server is already running" % name) raise web.HTTPError(400, "%s's server is already running" % name)
@@ -180,7 +181,8 @@ class UserServerAPIHandler(APIHandler):
return return
if not user.running: if not user.running:
raise web.HTTPError(400, "%s's server is not running" % name) 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: if status is not None:
raise web.HTTPError(400, "%s's server is not running" % name) raise web.HTTPError(400, "%s's server is not running" % name)
yield self.stop_single_user(user) yield self.stop_single_user(user)

View File

@@ -12,7 +12,6 @@ import signal
import socket import socket
import sys import sys
import threading import threading
import statsd
from datetime import datetime from datetime import datetime
from distutils.version import LooseVersion as V from distutils.version import LooseVersion as V
from getpass import getuser from getpass import getuser
@@ -507,22 +506,21 @@ class JupyterHub(Application):
Instance(logging.Handler), Instance(logging.Handler),
help="Extra log handlers to set on JupyterHub logger", help="Extra log handlers to set on JupyterHub logger",
).tag(config=True) ).tag(config=True)
@property statsd = Any(allow_none=False, help="The statsd client, if any. A mock will be used if we aren't using statsd")
def statsd(self): @default('statsd')
if hasattr(self, '_statsd'): def _statsd(self):
return self._statsd
if self.statsd_host: if self.statsd_host:
self._statsd = statsd.StatsClient( import statsd
client = statsd.StatsClient(
self.statsd_host, self.statsd_host,
self.statsd_port, self.statsd_port,
self.statsd_prefix self.statsd_prefix
) )
return self._statsd return client
else: else:
# return an empty mock object! # return an empty mock object!
self._statsd = EmptyClass() return EmptyClass()
return self._statsd
def init_logging(self): def init_logging(self):
# This prevents double log messages because tornado use a root logger that # 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-ip', self.proxy.api_server.ip,
'--api-port', str(self.proxy.api_server.port), '--api-port', str(self.proxy.api_server.port),
'--default-target', self.hub.server.host, '--default-target', self.hub.server.host,
'--error-target', url_path_join(self.hub.server.url, 'error'),
] ]
if self.subdomain_host: if self.subdomain_host:
cmd.append('--host-routing') cmd.append('--host-routing')

View File

@@ -410,6 +410,7 @@ class BaseHandler(RequestHandler):
"""render custom error pages""" """render custom error pages"""
exc_info = kwargs.get('exc_info') exc_info = kwargs.get('exc_info')
message = '' message = ''
exception = None
status_message = responses.get(status_code, 'Unknown HTTP Error') status_message = responses.get(status_code, 'Unknown HTTP Error')
if exc_info: if exc_info:
exception = exc_info[1] exception = exc_info[1]

View File

@@ -15,12 +15,12 @@ class LogoutHandler(BaseHandler):
user = self.get_current_user() user = self.get_current_user()
if user: if user:
self.log.info("User logged out: %s", user.name) self.log.info("User logged out: %s", user.name)
self.clear_login_cookie() self.clear_login_cookie()
for name in user.other_user_cookies: for name in user.other_user_cookies:
self.clear_login_cookie(name) self.clear_login_cookie(name)
user.other_user_cookies = set([]) user.other_user_cookies = set([])
self.statsd.incr('logout')
self.redirect(self.hub.server.base_url, permanent=False) self.redirect(self.hub.server.base_url, permanent=False)
self.statsd.incr('logout')
class LoginHandler(BaseHandler): class LoginHandler(BaseHandler):

View File

@@ -3,12 +3,14 @@
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
from http.client import responses
from jinja2 import TemplateNotFound
from tornado import web, gen from tornado import web, gen
from .. import orm from .. import orm
from ..utils import admin_only, url_path_join from ..utils import admin_only, url_path_join
from .base import BaseHandler from .base import BaseHandler
from urllib.parse import quote
class RootHandler(BaseHandler): class RootHandler(BaseHandler):
@@ -41,9 +43,14 @@ class HomeHandler(BaseHandler):
"""Render the user's home page.""" """Render the user's home page."""
@web.authenticated @web.authenticated
@gen.coroutine
def get(self): 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', html = self.render_template('home.html',
user=self.get_current_user(), user=user,
) )
self.finish(html) self.finish(html)
@@ -160,9 +167,43 @@ class AdminHandler(BaseHandler):
self.finish(html) 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 = [ default_handlers = [
(r'/', RootHandler), (r'/', RootHandler),
(r'/home', HomeHandler), (r'/home', HomeHandler),
(r'/admin', AdminHandler), (r'/admin', AdminHandler),
(r'/spawn', SpawnHandler), (r'/spawn', SpawnHandler),
(r'/error/(\d+)', ProxyErrorHandler),
] ]

View File

@@ -15,7 +15,7 @@ from subprocess import Popen
from tempfile import mkdtemp from tempfile import mkdtemp
from tornado import gen from tornado import gen
from tornado.ioloop import IOLoop, PeriodicCallback from tornado.ioloop import PeriodicCallback
from traitlets.config import LoggingConfigurable from traitlets.config import LoggingConfigurable
from traitlets import ( from traitlets import (
@@ -335,15 +335,17 @@ class Spawner(LoggingConfigurable):
self.stop_polling() self.stop_polling()
add_callback = IOLoop.current().add_callback
for callback in self._callbacks: 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) death_interval = Float(0.1)
@gen.coroutine @gen.coroutine
def wait_for_death(self, timeout=10): def wait_for_death(self, timeout=10):
"""wait for the process to die, up to timeout seconds""" """wait for the process to die, up to timeout seconds"""
loop = IOLoop.current()
for i in range(int(timeout / self.death_interval)): for i in range(int(timeout / self.death_interval)):
status = yield self.poll() status = yield self.poll()
if status is not None: if status is not None:

View File

@@ -6,7 +6,7 @@
version_info = ( version_info = (
0, 0,
6, 6,
0, 1,
# 'dev', # 'dev',
) )

View File

@@ -2,6 +2,5 @@ traitlets>=4.1
tornado>=4.1 tornado>=4.1
jinja2 jinja2
pamela pamela
statsd
sqlalchemy>=1.0 sqlalchemy>=1.0
requests requests

View File

@@ -152,15 +152,15 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function ($, bs, mo
}); });
}); });
$("#add-user").click(function () { $("#add-users").click(function () {
var dialog = $("#add-user-dialog"); var dialog = $("#add-users-dialog");
dialog.find(".username-input").val(''); dialog.find(".username-input").val('');
dialog.find(".admin-checkbox").prop("checked", false); dialog.find(".admin-checkbox").prop("checked", false);
dialog.modal(); dialog.modal();
}); });
$("#add-user-dialog").find(".save-button").click(function () { $("#add-users-dialog").find(".save-button").click(function () {
var dialog = $("#add-user-dialog"); var dialog = $("#add-users-dialog");
var lines = dialog.find(".username-input").val().split('\n'); var lines = dialog.find(".username-input").val().split('\n');
var admin = dialog.find(".admin-checkbox").prop("checked"); var admin = dialog.find(".admin-checkbox").prop("checked");
var usernames = []; 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 () { $("#shutdown-hub").click(function () {
var dialog = $("#shutdown-hub-dialog"); var dialog = $("#shutdown-hub-dialog");
dialog.find("input[type=checkbox]").prop("checked", true); dialog.find("input[type=checkbox]").prop("checked", true);

View File

@@ -32,8 +32,9 @@
<tbody> <tbody>
<tr class="user-row add-user-row"> <tr class="user-row add-user-row">
<td colspan="12"> <td colspan="12">
<a id="add-user" class="col-xs-5 btn btn-default">Add User</a> <a id="add-users" class="col-xs-2 btn btn-default">Add Users</a>
<a id="shutdown-hub" class="col-xs-5 col-xs-offset-2 btn btn-danger">Shutdown Hub</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> </td>
</tr> </tr>
{% for u in users %} {% for u in users %}
@@ -71,6 +72,10 @@
This operation cannot be undone. This operation cannot be undone.
{% endcall %} {% 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') %} {% call modal('Shutdown Hub', btn_label='Shutdown', btn_class='btn-danger shutdown-button') %}
Are you sure you want to shutdown the Hub? 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: 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('Edit User') }}
{{ user_modal('Add User', multi=True) }} {{ user_modal('Add Users', multi=True) }}
{% endblock %} {% endblock %}

View File

@@ -17,6 +17,11 @@
{{message}} {{message}}
</p> </p>
{% endif %} {% endif %}
{% if message_html %}
<p>
{{message_html | safe}}
</p>
{% endif %}
{% endblock error_detail %} {% endblock error_detail %}
</div> </div>

View File

@@ -82,7 +82,7 @@
<div id="header" class="navbar navbar-static-top"> <div id="header" class="navbar navbar-static-top">
<div class="container"> <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 %} {% block login_widget %}