mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 18:44:10 +00:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4931684a2c | ||
![]() |
62d3cc53ef | ||
![]() |
bd002e5340 | ||
![]() |
6f2aefb990 | ||
![]() |
bd3c878c67 | ||
![]() |
c1de376b6a | ||
![]() |
4cc74d287e | ||
![]() |
411a7a0bd8 | ||
![]() |
498c062ee0 | ||
![]() |
d1edbddb77 | ||
![]() |
0c9214ffb7 | ||
![]() |
db0aaf1027 | ||
![]() |
42681f8512 | ||
![]() |
e5c1414b6a | ||
![]() |
d857c20de0 | ||
![]() |
a267174a03 | ||
![]() |
768eeee470 | ||
![]() |
a451f11cd3 | ||
![]() |
63a476f9a6 | ||
![]() |
100b17819d | ||
![]() |
024d8d7378 | ||
![]() |
15e50529ff | ||
![]() |
a1a10be747 | ||
![]() |
a91ee67e74 | ||
![]() |
ea5bfa9999 | ||
![]() |
bea58ee622 | ||
![]() |
b698d4d226 | ||
![]() |
139c7ecacb | ||
![]() |
eefa8fcad7 | ||
![]() |
acaedcd898 | ||
![]() |
a075661bfb | ||
![]() |
f2246df5bb | ||
![]() |
1a3c062512 | ||
![]() |
05e4ab41fe | ||
![]() |
6f3ccb2d3d | ||
![]() |
6e5ce236c1 | ||
![]() |
58437057a1 |
@@ -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:
|
||||||
|
15
README.md
15
README.md
@@ -48,15 +48,9 @@ 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:
|
||||||
|
|
||||||
@@ -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:
|
||||||
|
@@ -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
22
docs/changelog.md
Normal 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
|
||||||
|
|
@@ -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 = [
|
||||||
|
@@ -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:
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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):
|
||||||
|
@@ -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),
|
||||||
]
|
]
|
||||||
|
@@ -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."""
|
||||||
|
@@ -286,6 +286,8 @@ class User(Base):
|
|||||||
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:
|
||||||
return "<{cls}({name}@{ip}:{port})>".format(
|
return "<{cls}({name}@{ip}:{port})>".format(
|
||||||
@@ -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"""
|
||||||
|
@@ -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.
|
||||||
|
@@ -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"""
|
||||||
|
@@ -18,16 +18,18 @@ from ..app import JupyterHub
|
|||||||
from ..auth import PAMAuthenticator
|
from ..auth import PAMAuthenticator
|
||||||
from .. import orm
|
from .. import orm
|
||||||
|
|
||||||
def mock_authenticate(username, password, service='login'):
|
from pamela import PAMError
|
||||||
# mimic simplepam's failure to handle unicode
|
|
||||||
if isinstance(username, str):
|
|
||||||
return False
|
|
||||||
if isinstance(password, str):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
def mock_authenticate(username, password, service='login'):
|
||||||
# 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):
|
||||||
|
@@ -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
|
||||||
|
@@ -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():
|
||||||
|
@@ -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')
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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))
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
traitlets>=4
|
traitlets>=4
|
||||||
tornado>=4
|
tornado>=4.1
|
||||||
jinja2
|
jinja2
|
||||||
simplepam
|
pamela
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
requests
|
requests
|
||||||
|
Reference in New Issue
Block a user