Compare commits

..

37 Commits
0.2.0 ... 0.3.0

Author SHA1 Message Date
Min RK
4931684a2c release 0.3 2015-11-04 17:10:36 +01:00
Min RK
62d3cc53ef changelog for 0.3 2015-11-04 17:09:34 +01:00
Min RK
bd002e5340 Merge pull request #325 from minrk/authenticator-hooks
add pre/post-spawn hooks for Authenticators
2015-11-04 16:07:01 +00:00
Min RK
6f2aefb990 add pre/post-spawn hooks for Authenticators
allows setup/cleanup to be performed by the authenticator

use this to open PAM sessions at spawn
and close them at stop,
rather than open at login and never close.
2015-10-16 12:02:44 +02:00
Min RK
bd3c878c67 Merge pull request #320 from minrk/authenticator-username
get username from authenticator
2015-10-06 15:43:22 +02:00
Min RK
c1de376b6a Merge pull request #310 from minrk/singleuser-notebook
single-user imports notebook package directly
2015-10-06 14:08:35 +02:00
Min RK
4cc74d287e get username from authenticator 2015-10-06 13:36:34 +02:00
Min RK
411a7a0bd8 single-user imports notebook package directly
instead of relying on IPython.html shims

when should we drop support for IPython 3?
2015-09-24 16:13:28 +02:00
Min RK
498c062ee0 Merge pull request #309 from minrk/gen.sleep
use gen.sleep
2015-09-24 16:09:55 +02:00
Min RK
d1edbddb77 use gen.sleep
instead of elaborate `gen.Task(add_timeout...)`

requires tornado 4.1
2015-09-23 17:04:01 +02:00
Min RK
0c9214ffb7 Merge pull request #307 from minrk/test-3.5
test on 3.5
2015-09-22 14:17:30 +02:00
Min RK
db0aaf1027 test on 3.5
requires pytest >= 2.8
2015-09-22 14:09:23 +02:00
Min RK
42681f8512 Merge pull request #306 from minrk/test-token-username
update token app test
2015-09-22 14:08:41 +02:00
Min RK
e5c1414b6a update token app test
now that admin user isn't added by default
2015-09-22 10:14:11 +02:00
Min RK
d857c20de0 Merge pull request #304 from minrk/rm-default-admin
Remove implicit admin of launching user
2015-09-22 08:59:28 +02:00
Min RK
a267174a03 Remove implicit admin of launching user
instead, warn about missing admins and point to config.
2015-09-21 10:52:19 +02:00
Min RK
768eeee470 Merge pull request #298 from minrk/spawner-authenticator
give Spawners a handle on the Authenticator
2015-09-11 14:24:38 +02:00
Min RK
a451f11cd3 give Spawners a handle on the Authenticator
band-aid for spawner-authenticator pairs
2015-09-11 11:57:41 +02:00
Min RK
63a476f9a6 remove some unused cruft from spawner 2015-09-11 11:23:00 +02:00
Min RK
100b17819d Merge pull request #296 from minrk/pamela
use pamela instead of simplepam
2015-09-11 11:02:14 +02:00
Min RK
024d8d7378 update mocking for pamela 2015-09-09 14:24:53 +02:00
Min RK
15e50529ff use pamela instead of simplepam
and open PAM sessions after successful auth
2015-09-09 13:55:02 +02:00
Min RK
a1a10be747 Merge pull request #290 from jhamrick/clear-login-cookies
Unset all login cookies
2015-08-22 18:55:30 -07:00
Jessica B. Hamrick
a91ee67e74 Reset other_user_cookies after clearing them 2015-08-22 13:14:05 -07:00
Jessica B. Hamrick
ea5bfa9999 Unset all login cookies 2015-08-21 19:24:44 -07:00
Min RK
bea58ee622 Merge pull request #288 from minrk/dont-auto-redirect-root
redirect unauthenticated root to *regular* login page
2015-08-19 21:44:00 -07:00
Min RK
b698d4d226 redirect root to *regular* login page
shows "Login with..." button for external services
instead of auto-redirecting to login service
(no good for oauth)
2015-08-19 12:43:32 -07:00
Min RK
139c7ecacb always render login page at /login 2015-08-19 12:30:10 -07:00
Min RK
eefa8fcad7 Merge pull request #284 from minrk/double-base-url
remove double base_url in login redirect
2015-08-06 21:48:49 -07:00
Min RK
acaedcd898 remove double base_url in login redirect
user.server.base_url is already correct,
and shouldn't be joined with the hub url
2015-08-06 21:37:06 -07:00
Min RK
a075661bfb Merge pull request #276 from Crunch-io/redirect-to-login
Redirect unauthenticated root to login
2015-07-23 13:00:16 -07:00
Joseph Tate
f2246df5bb Fix logging and comments 2015-07-23 15:08:53 -04:00
Joseph Tate
1a3c062512 Fix broken test 2015-07-23 15:06:20 -04:00
Joseph Tate
05e4ab41fe Redirect to the loginurl when not logged in, fix the user.running redirect to fix a redirect loop 2015-07-23 15:06:03 -04:00
Min RK
6f3ccb2d3d Merge pull request #275 from jhamrick/installation-instructions
Update installation instructions
2015-07-14 22:06:52 -07:00
Jessica B. Hamrick
6e5ce236c1 Update installation instructions 2015-07-14 15:36:35 -07:00
Min RK
58437057a1 back to dev 2015-07-12 15:30:47 -05:00
20 changed files with 154 additions and 77 deletions

View File

@@ -2,6 +2,7 @@
language: python language: python
sudo: false sudo: false
python: python:
- 3.5
- 3.4 - 3.4
- 3.3 - 3.3
before_install: before_install:
@@ -10,7 +11,7 @@ before_install:
- git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels - git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels
install: install:
- pip install -f travis-wheels/wheelhouse -r dev-requirements.txt . - pip install -f travis-wheels/wheelhouse -r dev-requirements.txt .
- pip install -f travis-wheels/wheelhouse ipython[notebook] - pip install -f travis-wheels/wheelhouse notebook
script: script:
- py.test --cov jupyterhub jupyterhub/tests -v - py.test --cov jupyterhub jupyterhub/tests -v
after_success: after_success:

View File

@@ -48,16 +48,10 @@ Then install javascript dependencies:
## Installation ## Installation
As usual start with cloning the code: JupyterHub can be installed with pip:
git clone https://github.com/jupyter/jupyterhub.git pip3 install jupyterhub
cd jupyterhub
Then you can install the Python package by doing:
pip3 install -r requirements.txt
pip3 install .
If the `pip3 install .` command fails and complains about `lessc` being unavailable, you may need to explicitly install some additional javascript dependencies: If the `pip3 install .` command fails and complains about `lessc` being unavailable, you may need to explicitly install some additional javascript dependencies:
npm install npm install
@@ -66,7 +60,6 @@ If you plan to run notebook servers locally, you may also need to install the IP
pip3 install "ipython[notebook]" pip3 install "ipython[notebook]"
This will fetch client-side javascript dependencies and compile CSS, This will fetch client-side javascript dependencies and compile CSS,
and install these files to `sys.prefix`/share/jupyter, as well as and install these files to `sys.prefix`/share/jupyter, as well as
install any Python dependencies. install any Python dependencies.
@@ -74,8 +67,10 @@ install any Python dependencies.
### Development install ### Development install
For a development install: For a development install, clone the repository and then install from source:
git clone https://github.com/jupyter/jupyterhub
cd jupyterhub
pip3 install -r dev-requirements.txt -e . pip3 install -r dev-requirements.txt -e .
In which case you may need to manually update javascript and css after some updates, with: In which case you may need to manually update javascript and css after some updates, with:

View File

@@ -1,4 +1,4 @@
-r requirements.txt -r requirements.txt
coveralls coveralls
pytest-cov pytest-cov
pytest pytest>=2.8

22
docs/changelog.md Normal file
View File

@@ -0,0 +1,22 @@
# Summary of changes in JupyterHub
See `git log` for a more detailed summary.
## 0.3.0
- No longer make the user starting the Hub an admin
- start PAM sessions on login
- hooks for Authenticators to fire before spawners start and after they stop,
allowing deeper interaction between Spawner/Authenticator pairs.
- login redirect fixes
## 0.2.0
- Based on standalone traitlets instead of IPython.utils.traitlets
- multiple users in admin panel
- Fixes for usernames that require escaping
## 0.1.0
First preview release

View File

@@ -196,6 +196,7 @@ class UserAdminAccessAPIHandler(APIHandler):
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)
self.set_server_cookie(user) self.set_server_cookie(user)
current.other_user_cookies.add(name)
default_handlers = [ default_handlers = [

View File

@@ -628,10 +628,8 @@ class JupyterHub(Application):
admin_users = self.authenticator.admin_users admin_users = self.authenticator.admin_users
if not admin_users: if not admin_users:
# add current user as admin if there aren't any others self.log.warning("No admin users, admin interface will be unavailable.")
admins = db.query(orm.User).filter(orm.User.admin==True) self.log.warning("Add any administrative users to `c.Authenticator.admin_users` in config.")
if admins.first() is None:
admin_users.add(getuser())
new_users = [] new_users = []
@@ -711,6 +709,7 @@ class JupyterHub(Application):
self.log.debug("Loading state for %s from db", user.name) self.log.debug("Loading state for %s from db", user.name)
user.spawner = spawner = self.spawner_class( user.spawner = spawner = self.spawner_class(
user=user, hub=self.hub, config=self.config, db=self.db, user=user, hub=self.hub, config=self.config, db=self.db,
authenticator=self.authenticator,
) )
status = yield spawner.poll() status = yield spawner.poll()
if status is None: if status is None:

View File

@@ -8,7 +8,7 @@ import pwd
from subprocess import check_call, check_output, CalledProcessError from subprocess import check_call, check_output, CalledProcessError
from tornado import gen from tornado import gen
import simplepam import pamela
from traitlets.config import LoggingConfigurable from traitlets.config import LoggingConfigurable
from traitlets import Bool, Set, Unicode, Any from traitlets import Bool, Set, Unicode, Any
@@ -58,6 +58,18 @@ class Authenticator(LoggingConfigurable):
and return None on failed authentication. and return None on failed authentication.
""" """
def pre_spawn_start(self, user, spawner):
"""Hook called before spawning a user's server.
Can be used to do auth-related startup, e.g. opening PAM sessions.
"""
def post_spawn_stop(self, user, spawner):
"""Hook called after stopping a user container.
Can be used to do auth-related cleanup, e.g. closing PAM sessions.
"""
def check_whitelist(self, user): def check_whitelist(self, user):
""" """
Return True if the whitelist is empty or user is in the whitelist. Return True if the whitelist is empty or user is in the whitelist.
@@ -208,10 +220,24 @@ class PAMAuthenticator(LocalAuthenticator):
username = data['username'] username = data['username']
if not self.check_whitelist(username): if not self.check_whitelist(username):
return return
# simplepam wants bytes, not unicode try:
# see simplepam#3 pamela.authenticate(username, data['password'], service=self.service)
busername = username.encode(self.encoding) except pamela.PAMError as e:
bpassword = data['password'].encode(self.encoding) self.log.warn("PAM Authentication failed: %s", e)
if simplepam.authenticate(busername, bpassword, service=self.service): else:
return username return username
def pre_spawn_start(self, user, spawner):
"""Open PAM session for user"""
try:
pamela.open_session(user.name, service=self.service)
except pamela.PAMError as e:
self.log.warn("Failed to open PAM session for %s: %s", user.name, e)
def post_spawn_stop(self, user, spawner):
"""Close PAM session for user"""
try:
pamela.close_session(user.name, service=self.service)
except pamela.PAMError as e:
self.log.warn("Failed to close PAM session for %s: %s", user.name, e)

View File

@@ -179,8 +179,11 @@ class BaseHandler(RequestHandler):
self.db.commit() self.db.commit()
return user return user
def clear_login_cookie(self): def clear_login_cookie(self, name=None):
user = self.get_current_user() if name is None:
user = self.get_current_user()
else:
user = self.find_user(name)
if user and user.server: if user and user.server:
self.clear_cookie(user.server.cookie_name, path=user.server.base_url) self.clear_cookie(user.server.cookie_name, path=user.server.base_url)
self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url) self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url)
@@ -262,6 +265,7 @@ class BaseHandler(RequestHandler):
base_url=self.base_url, base_url=self.base_url,
hub=self.hub, hub=self.hub,
config=self.config, config=self.config,
authenticator=self.authenticator,
) )
@gen.coroutine @gen.coroutine
def finish_user_spawn(f=None): def finish_user_spawn(f=None):

View File

@@ -16,6 +16,9 @@ class LogoutHandler(BaseHandler):
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:
self.clear_login_cookie(name)
user.other_user_cookies = set([])
self.redirect(self.hub.server.base_url, permanent=False) self.redirect(self.hub.server.base_url, permanent=False)
@@ -28,6 +31,7 @@ class LoginHandler(BaseHandler):
username=username, username=username,
login_error=login_error, login_error=login_error,
custom_login_form=self.authenticator.custom_html, custom_login_form=self.authenticator.custom_html,
login_url=self.settings['login_url'],
) )
def get(self): def get(self):
@@ -57,9 +61,8 @@ class LoginHandler(BaseHandler):
for arg in self.request.arguments: for arg in self.request.arguments:
data[arg] = self.get_argument(arg) data[arg] = self.get_argument(arg)
username = data['username'] username = yield self.authenticate(data)
authorized = yield self.authenticate(data) if username:
if authorized:
user = self.user_from_username(username) user = self.user_from_username(username)
already_running = False already_running = False
if user.spawner: if user.spawner:
@@ -85,5 +88,6 @@ class LoginHandler(BaseHandler):
# Only logout is a default handler. # Only logout is a default handler.
default_handlers = [ default_handlers = [
(r"/login", LoginHandler),
(r"/logout", LogoutHandler), (r"/logout", LogoutHandler),
] ]

View File

@@ -8,6 +8,7 @@ from tornado import web
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 .login import LoginHandler
class RootHandler(BaseHandler): class RootHandler(BaseHandler):
@@ -25,15 +26,15 @@ class RootHandler(BaseHandler):
if user: if user:
if user.running: if user.running:
url = user.server.base_url url = user.server.base_url
self.log.debug("User is running: %s", url)
else: else:
url = url_path_join(self.hub.server.base_url, 'home') url = url_path_join(self.hub.server.base_url, 'home')
self.redirect(url, permanent=False) self.log.debug("User is not running: %s", url)
self.redirect(url)
return return
html = self.render_template('login.html', url = url_path_join(self.hub.server.base_url, 'login')
login_url=self.settings['login_url'], self.redirect(url)
custom_html=self.authenticator.custom_html,
)
self.finish(html)
class HomeHandler(BaseHandler): class HomeHandler(BaseHandler):
"""Render the user's home page.""" """Render the user's home page."""

View File

@@ -285,6 +285,8 @@ class User(Base):
spawner = None spawner = None
spawn_pending = False spawn_pending = False
stop_pending = False stop_pending = False
other_user_cookies = set([])
def __repr__(self): def __repr__(self):
if self.server: if self.server:
@@ -334,7 +336,7 @@ class User(Base):
return db.query(cls).filter(cls.name==name).first() return db.query(cls).filter(cls.name==name).first()
@gen.coroutine @gen.coroutine
def spawn(self, spawner_class, base_url='/', hub=None, config=None): def spawn(self, spawner_class, base_url='/', hub=None, authenticator=None, config=None):
"""Start the user's spawner""" """Start the user's spawner"""
db = inspect(self).session db = inspect(self).session
if hub is None: if hub is None:
@@ -355,11 +357,15 @@ class User(Base):
user=self, user=self,
hub=hub, hub=hub,
db=db, db=db,
authenticator=authenticator,
) )
# we are starting a new server, make sure it doesn't restore state # we are starting a new server, make sure it doesn't restore state
spawner.clear_state() spawner.clear_state()
spawner.api_token = api_token spawner.api_token = api_token
# trigger pre-spawn hook on authenticator
if (authenticator):
yield gen.maybe_future(authenticator.pre_spawn_start(self, spawner))
self.spawn_pending = True self.spawn_pending = True
# wait for spawner.start to return # wait for spawner.start to return
try: try:
@@ -426,21 +432,27 @@ class User(Base):
and cleanup after it. and cleanup after it.
""" """
self.spawn_pending = False self.spawn_pending = False
if self.spawner is None: spawner = self.spawner
if spawner is None:
return return
self.spawner.stop_polling() spawner.stop_polling()
self.stop_pending = True self.stop_pending = True
try: try:
status = yield self.spawner.poll() status = yield spawner.poll()
if status is None: if status is None:
yield self.spawner.stop() yield self.spawner.stop()
self.spawner.clear_state() spawner.clear_state()
self.state = self.spawner.get_state() self.state = spawner.get_state()
self.server = None self.server = None
inspect(self).session.commit() inspect(self).session.commit()
finally: finally:
self.stop_pending = False self.stop_pending = False
# trigger post-spawner hook on authenticator
auth = spawner.authenticator
if auth:
yield gen.maybe_future(
auth.post_spawn_stop(self, spawner)
)
class APIToken(Base): class APIToken(Base):
"""An API token""" """An API token"""

View File

@@ -24,18 +24,28 @@ from IPython.utils.traitlets import (
CUnicode, CUnicode,
) )
from IPython.html.notebookapp import NotebookApp, aliases as notebook_aliases try:
from IPython.html.auth.login import LoginHandler import notebook
from IPython.html.auth.logout import LogoutHandler # 4.x
except ImportError:
from IPython.html.notebookapp import NotebookApp, aliases as notebook_aliases
from IPython.html.auth.login import LoginHandler
from IPython.html.auth.logout import LogoutHandler
from IPython.html.utils import url_path_join from IPython.html.utils import url_path_join
from distutils.version import LooseVersion as V
from distutils.version import LooseVersion as V import IPython
if V(IPython.__version__) < V('3.0'):
raise ImportError("JupyterHub Requires IPython >= 3.0, found %s" % IPython.__version__)
else:
from notebook.notebookapp import NotebookApp, aliases as notebook_aliases
from notebook.auth.login import LoginHandler
from notebook.auth.logout import LogoutHandler
from notebook.utils import url_path_join
import IPython
if V(IPython.__version__) < V('3.0'):
raise ImportError("JupyterHub Requires IPython >= 3.0, found %s" % IPython.__version__)
# Define two methods to attach to AuthenticatedHandler, # Define two methods to attach to AuthenticatedHandler,
# which authenticate via the central auth server. # which authenticate via the central auth server.

View File

@@ -7,11 +7,10 @@ import errno
import os import os
import pipes import pipes
import pwd import pwd
import re
import signal import signal
import sys import sys
import grp import grp
from subprocess import Popen, check_output, PIPE, CalledProcessError from subprocess import Popen
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from tornado import gen from tornado import gen
@@ -19,14 +18,12 @@ from tornado.ioloop import IOLoop, PeriodicCallback
from traitlets.config import LoggingConfigurable from traitlets.config import LoggingConfigurable
from traitlets import ( from traitlets import (
Any, Bool, Dict, Enum, Instance, Integer, Float, List, Unicode, Any, Bool, Dict, Instance, Integer, Float, List, Unicode,
) )
from .traitlets import Command from .traitlets import Command
from .utils import random_port from .utils import random_port
NUM_PAT = re.compile(r'\d+')
class Spawner(LoggingConfigurable): class Spawner(LoggingConfigurable):
"""Base class for spawning single-user notebook servers. """Base class for spawning single-user notebook servers.
@@ -42,6 +39,7 @@ class Spawner(LoggingConfigurable):
db = Any() db = Any()
user = Any() user = Any()
hub = Any() hub = Any()
authenticator = Any()
api_token = Unicode() api_token = Unicode()
ip = Unicode('localhost', config=True, ip = Unicode('localhost', config=True,
help="The IP address (or hostname) the single-user server should listen on" help="The IP address (or hostname) the single-user server should listen on"
@@ -254,7 +252,7 @@ class Spawner(LoggingConfigurable):
if status is not None: if status is not None:
break break
else: else:
yield gen.Task(loop.add_timeout, loop.time() + self.death_interval) yield gen.sleep(self.death_interval)
def _try_setcwd(path): def _try_setcwd(path):
"""Try to set CWD, walking up and ultimately falling back to a temp dir""" """Try to set CWD, walking up and ultimately falling back to a temp dir"""

View File

@@ -18,16 +18,18 @@ from ..app import JupyterHub
from ..auth import PAMAuthenticator from ..auth import PAMAuthenticator
from .. import orm from .. import orm
from pamela import PAMError
def mock_authenticate(username, password, service='login'): def mock_authenticate(username, password, service='login'):
# mimic simplepam's failure to handle unicode
if isinstance(username, str):
return False
if isinstance(password, str):
return False
# just use equality for testing # just use equality for testing
if password == username: if password == username:
return True return True
else:
raise PAMError("Fake")
def mock_open_session(username, service):
pass
class MockSpawner(LocalProcessSpawner): class MockSpawner(LocalProcessSpawner):
@@ -51,12 +53,12 @@ class SlowSpawner(MockSpawner):
@gen.coroutine @gen.coroutine
def start(self): def start(self):
yield gen.Task(IOLoop.current().add_timeout, timedelta(seconds=2)) yield gen.sleep(2)
yield super().start() yield super().start()
@gen.coroutine @gen.coroutine
def stop(self): def stop(self):
yield gen.Task(IOLoop.current().add_timeout, timedelta(seconds=2)) yield gen.sleep(2)
yield super().stop() yield super().stop()
@@ -80,7 +82,9 @@ class MockPAMAuthenticator(PAMAuthenticator):
return not user.name.startswith('dne') return not user.name.startswith('dne')
def authenticate(self, *args, **kwargs): def authenticate(self, *args, **kwargs):
with mock.patch('simplepam.authenticate', mock_authenticate): with mock.patch.multiple('pamela',
authenticate=mock_authenticate,
open_session=mock_open_session):
return super(MockPAMAuthenticator, self).authenticate(*args, **kwargs) return super(MockPAMAuthenticator, self).authenticate(*args, **kwargs)
class MockHub(JupyterHub): class MockHub(JupyterHub):

View File

@@ -363,11 +363,10 @@ def test_slow_spawn(app, io_loop):
assert app_user.spawn_pending assert app_user.spawn_pending
assert not app_user.stop_pending assert not app_user.stop_pending
dt = timedelta(seconds=0.1)
@gen.coroutine @gen.coroutine
def wait_spawn(): def wait_spawn():
while app_user.spawn_pending: while app_user.spawn_pending:
yield gen.Task(io_loop.add_timeout, dt) yield gen.sleep(0.1)
io_loop.run_sync(wait_spawn) io_loop.run_sync(wait_spawn)
assert not app_user.spawn_pending assert not app_user.spawn_pending
@@ -377,7 +376,7 @@ def test_slow_spawn(app, io_loop):
@gen.coroutine @gen.coroutine
def wait_stop(): def wait_stop():
while app_user.stop_pending: while app_user.stop_pending:
yield gen.Task(io_loop.add_timeout, dt) yield gen.sleep(0.1)
r = api_request(app, 'users', name, 'server', method='delete') r = api_request(app, 'users', name, 'server', method='delete')
r.raise_for_status() r.raise_for_status()
@@ -410,11 +409,10 @@ def test_never_spawn(app, io_loop):
assert app_user.spawner is not None assert app_user.spawner is not None
assert app_user.spawn_pending assert app_user.spawn_pending
dt = timedelta(seconds=0.1)
@gen.coroutine @gen.coroutine
def wait_pending(): def wait_pending():
while app_user.spawn_pending: while app_user.spawn_pending:
yield gen.Task(io_loop.add_timeout, dt) yield gen.sleep(0.1)
io_loop.run_sync(wait_pending) io_loop.run_sync(wait_pending)
assert not app_user.spawn_pending assert not app_user.spawn_pending

View File

@@ -3,7 +3,6 @@
import os import os
import re import re
import sys import sys
from getpass import getuser
from subprocess import check_output from subprocess import check_output
from tempfile import NamedTemporaryFile, TemporaryDirectory from tempfile import NamedTemporaryFile, TemporaryDirectory
@@ -16,7 +15,9 @@ def test_token_app():
cmd = [sys.executable, '-m', 'jupyterhub', 'token'] cmd = [sys.executable, '-m', 'jupyterhub', 'token']
out = check_output(cmd + ['--help-all']).decode('utf8', 'replace') out = check_output(cmd + ['--help-all']).decode('utf8', 'replace')
with TemporaryDirectory() as td: with TemporaryDirectory() as td:
out = check_output(cmd + [getuser()], cwd=td).decode('utf8', 'replace').strip() with open(os.path.join(td, 'jupyterhub_config.py'), 'w') as f:
f.write("c.Authenticator.admin_users={'user'}")
out = check_output(cmd + ['user'], cwd=td).decode('utf8', 'replace').strip()
assert re.match(r'^[a-z0-9]+$', out) assert re.match(r'^[a-z0-9]+$', out)
def test_generate_config(): def test_generate_config():

View File

@@ -18,7 +18,7 @@ def test_root_no_auth(app, io_loop):
print(app.hub.server) print(app.hub.server)
r = requests.get(app.proxy.public_server.host) r = requests.get(app.proxy.public_server.host)
r.raise_for_status() r.raise_for_status()
assert r.url == ujoin(app.proxy.public_server.host, app.hub.server.base_url) assert r.url == ujoin(app.proxy.public_server.host, app.hub.server.base_url, 'login')
def test_root_auth(app): def test_root_auth(app):
cookies = app.login_user('river') cookies = app.login_user('river')

View File

@@ -41,7 +41,7 @@ def wait_for_server(ip, port, timeout=10):
app_log.error("Unexpected error waiting for %s:%i %s", app_log.error("Unexpected error waiting for %s:%i %s",
ip, port, e ip, port, e
) )
yield gen.Task(loop.add_timeout, loop.time() + 0.1) yield gen.sleep(0.1)
else: else:
return return
raise TimeoutError("Server at {ip}:{port} didn't respond in {timeout} seconds".format( raise TimeoutError("Server at {ip}:{port} didn't respond in {timeout} seconds".format(
@@ -67,14 +67,14 @@ def wait_for_http_server(url, timeout=10):
# we expect 599 for no connection, # we expect 599 for no connection,
# but 502 or other proxy error is conceivable # but 502 or other proxy error is conceivable
app_log.warn("Server at %s responded with error: %s", url, e.code) app_log.warn("Server at %s responded with error: %s", url, e.code)
yield gen.Task(loop.add_timeout, loop.time() + 0.25) yield gen.sleep(0.1)
else: else:
app_log.debug("Server at %s responded with %s", url, e.code) app_log.debug("Server at %s responded with %s", url, e.code)
return return
except (OSError, socket.error) as e: except (OSError, socket.error) as e:
if e.errno not in {errno.ECONNABORTED, errno.ECONNREFUSED, errno.ECONNRESET}: if e.errno not in {errno.ECONNABORTED, errno.ECONNREFUSED, errno.ECONNRESET}:
app_log.warn("Failed to connect to %s (%s)", url, e) app_log.warn("Failed to connect to %s (%s)", url, e)
yield gen.Task(loop.add_timeout, loop.time() + 0.25) yield gen.sleep(0.1)
else: else:
return return

View File

@@ -5,8 +5,9 @@
version_info = ( version_info = (
0, 0,
2, 3,
0, 0,
# 'dev',
) )
__version__ = '.'.join(map(str, version_info)) __version__ = '.'.join(map(str, version_info))

View File

@@ -1,6 +1,6 @@
traitlets>=4 traitlets>=4
tornado>=4 tornado>=4.1
jinja2 jinja2
simplepam pamela
sqlalchemy sqlalchemy
requests requests