mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 02:24:08 +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
|
||||
sudo: false
|
||||
python:
|
||||
- 3.5
|
||||
- 3.4
|
||||
- 3.3
|
||||
before_install:
|
||||
@@ -10,7 +11,7 @@ before_install:
|
||||
- git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels
|
||||
install:
|
||||
- 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:
|
||||
- py.test --cov jupyterhub jupyterhub/tests -v
|
||||
after_success:
|
||||
|
15
README.md
15
README.md
@@ -48,16 +48,10 @@ Then install javascript dependencies:
|
||||
|
||||
## Installation
|
||||
|
||||
As usual start with cloning the code:
|
||||
JupyterHub can be installed with pip:
|
||||
|
||||
git clone https://github.com/jupyter/jupyterhub.git
|
||||
cd jupyterhub
|
||||
pip3 install 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:
|
||||
|
||||
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]"
|
||||
|
||||
|
||||
This will fetch client-side javascript dependencies and compile CSS,
|
||||
and install these files to `sys.prefix`/share/jupyter, as well as
|
||||
install any Python dependencies.
|
||||
@@ -74,8 +67,10 @@ install any Python dependencies.
|
||||
|
||||
### 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 .
|
||||
|
||||
In which case you may need to manually update javascript and css after some updates, with:
|
||||
|
@@ -1,4 +1,4 @@
|
||||
-r requirements.txt
|
||||
coveralls
|
||||
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:
|
||||
raise web.HTTPError(400, "%s's server is not running" % name)
|
||||
self.set_server_cookie(user)
|
||||
current.other_user_cookies.add(name)
|
||||
|
||||
|
||||
default_handlers = [
|
||||
|
@@ -628,10 +628,8 @@ class JupyterHub(Application):
|
||||
admin_users = self.authenticator.admin_users
|
||||
|
||||
if not admin_users:
|
||||
# add current user as admin if there aren't any others
|
||||
admins = db.query(orm.User).filter(orm.User.admin==True)
|
||||
if admins.first() is None:
|
||||
admin_users.add(getuser())
|
||||
self.log.warning("No admin users, admin interface will be unavailable.")
|
||||
self.log.warning("Add any administrative users to `c.Authenticator.admin_users` in config.")
|
||||
|
||||
new_users = []
|
||||
|
||||
@@ -711,6 +709,7 @@ class JupyterHub(Application):
|
||||
self.log.debug("Loading state for %s from db", user.name)
|
||||
user.spawner = spawner = self.spawner_class(
|
||||
user=user, hub=self.hub, config=self.config, db=self.db,
|
||||
authenticator=self.authenticator,
|
||||
)
|
||||
status = yield spawner.poll()
|
||||
if status is None:
|
||||
|
@@ -8,7 +8,7 @@ import pwd
|
||||
from subprocess import check_call, check_output, CalledProcessError
|
||||
|
||||
from tornado import gen
|
||||
import simplepam
|
||||
import pamela
|
||||
|
||||
from traitlets.config import LoggingConfigurable
|
||||
from traitlets import Bool, Set, Unicode, Any
|
||||
@@ -58,6 +58,18 @@ class Authenticator(LoggingConfigurable):
|
||||
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):
|
||||
"""
|
||||
Return True if the whitelist is empty or user is in the whitelist.
|
||||
@@ -208,10 +220,24 @@ class PAMAuthenticator(LocalAuthenticator):
|
||||
username = data['username']
|
||||
if not self.check_whitelist(username):
|
||||
return
|
||||
# simplepam wants bytes, not unicode
|
||||
# see simplepam#3
|
||||
busername = username.encode(self.encoding)
|
||||
bpassword = data['password'].encode(self.encoding)
|
||||
if simplepam.authenticate(busername, bpassword, service=self.service):
|
||||
try:
|
||||
pamela.authenticate(username, data['password'], service=self.service)
|
||||
except pamela.PAMError as e:
|
||||
self.log.warn("PAM Authentication failed: %s", e)
|
||||
else:
|
||||
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()
|
||||
return user
|
||||
|
||||
def clear_login_cookie(self):
|
||||
user = self.get_current_user()
|
||||
def clear_login_cookie(self, name=None):
|
||||
if name is None:
|
||||
user = self.get_current_user()
|
||||
else:
|
||||
user = self.find_user(name)
|
||||
if user and user.server:
|
||||
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)
|
||||
@@ -262,6 +265,7 @@ class BaseHandler(RequestHandler):
|
||||
base_url=self.base_url,
|
||||
hub=self.hub,
|
||||
config=self.config,
|
||||
authenticator=self.authenticator,
|
||||
)
|
||||
@gen.coroutine
|
||||
def finish_user_spawn(f=None):
|
||||
|
@@ -16,6 +16,9 @@ class LogoutHandler(BaseHandler):
|
||||
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.redirect(self.hub.server.base_url, permanent=False)
|
||||
|
||||
|
||||
@@ -28,6 +31,7 @@ class LoginHandler(BaseHandler):
|
||||
username=username,
|
||||
login_error=login_error,
|
||||
custom_login_form=self.authenticator.custom_html,
|
||||
login_url=self.settings['login_url'],
|
||||
)
|
||||
|
||||
def get(self):
|
||||
@@ -57,9 +61,8 @@ class LoginHandler(BaseHandler):
|
||||
for arg in self.request.arguments:
|
||||
data[arg] = self.get_argument(arg)
|
||||
|
||||
username = data['username']
|
||||
authorized = yield self.authenticate(data)
|
||||
if authorized:
|
||||
username = yield self.authenticate(data)
|
||||
if username:
|
||||
user = self.user_from_username(username)
|
||||
already_running = False
|
||||
if user.spawner:
|
||||
@@ -85,5 +88,6 @@ class LoginHandler(BaseHandler):
|
||||
|
||||
# Only logout is a default handler.
|
||||
default_handlers = [
|
||||
(r"/login", LoginHandler),
|
||||
(r"/logout", LogoutHandler),
|
||||
]
|
||||
|
@@ -8,6 +8,7 @@ from tornado import web
|
||||
from .. import orm
|
||||
from ..utils import admin_only, url_path_join
|
||||
from .base import BaseHandler
|
||||
from .login import LoginHandler
|
||||
|
||||
|
||||
class RootHandler(BaseHandler):
|
||||
@@ -25,15 +26,15 @@ class RootHandler(BaseHandler):
|
||||
if user:
|
||||
if user.running:
|
||||
url = user.server.base_url
|
||||
self.log.debug("User is running: %s", url)
|
||||
else:
|
||||
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
|
||||
html = self.render_template('login.html',
|
||||
login_url=self.settings['login_url'],
|
||||
custom_html=self.authenticator.custom_html,
|
||||
)
|
||||
self.finish(html)
|
||||
url = url_path_join(self.hub.server.base_url, 'login')
|
||||
self.redirect(url)
|
||||
|
||||
|
||||
class HomeHandler(BaseHandler):
|
||||
"""Render the user's home page."""
|
||||
|
@@ -285,6 +285,8 @@ class User(Base):
|
||||
spawner = None
|
||||
spawn_pending = False
|
||||
stop_pending = False
|
||||
|
||||
other_user_cookies = set([])
|
||||
|
||||
def __repr__(self):
|
||||
if self.server:
|
||||
@@ -334,7 +336,7 @@ class User(Base):
|
||||
return db.query(cls).filter(cls.name==name).first()
|
||||
|
||||
@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"""
|
||||
db = inspect(self).session
|
||||
if hub is None:
|
||||
@@ -355,11 +357,15 @@ class User(Base):
|
||||
user=self,
|
||||
hub=hub,
|
||||
db=db,
|
||||
authenticator=authenticator,
|
||||
)
|
||||
# we are starting a new server, make sure it doesn't restore state
|
||||
spawner.clear_state()
|
||||
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
|
||||
# wait for spawner.start to return
|
||||
try:
|
||||
@@ -426,21 +432,27 @@ class User(Base):
|
||||
and cleanup after it.
|
||||
"""
|
||||
self.spawn_pending = False
|
||||
if self.spawner is None:
|
||||
spawner = self.spawner
|
||||
if spawner is None:
|
||||
return
|
||||
self.spawner.stop_polling()
|
||||
spawner.stop_polling()
|
||||
self.stop_pending = True
|
||||
try:
|
||||
status = yield self.spawner.poll()
|
||||
status = yield spawner.poll()
|
||||
if status is None:
|
||||
yield self.spawner.stop()
|
||||
self.spawner.clear_state()
|
||||
self.state = self.spawner.get_state()
|
||||
spawner.clear_state()
|
||||
self.state = spawner.get_state()
|
||||
self.server = None
|
||||
inspect(self).session.commit()
|
||||
finally:
|
||||
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):
|
||||
"""An API token"""
|
||||
|
@@ -24,18 +24,28 @@ from IPython.utils.traitlets import (
|
||||
CUnicode,
|
||||
)
|
||||
|
||||
from IPython.html.notebookapp import NotebookApp, aliases as notebook_aliases
|
||||
from IPython.html.auth.login import LoginHandler
|
||||
from IPython.html.auth.logout import LogoutHandler
|
||||
try:
|
||||
import notebook
|
||||
# 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,
|
||||
# which authenticate via the central auth server.
|
||||
|
@@ -7,11 +7,10 @@ import errno
|
||||
import os
|
||||
import pipes
|
||||
import pwd
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
import grp
|
||||
from subprocess import Popen, check_output, PIPE, CalledProcessError
|
||||
from subprocess import Popen
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from tornado import gen
|
||||
@@ -19,14 +18,12 @@ from tornado.ioloop import IOLoop, PeriodicCallback
|
||||
|
||||
from traitlets.config import LoggingConfigurable
|
||||
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 .utils import random_port
|
||||
|
||||
NUM_PAT = re.compile(r'\d+')
|
||||
|
||||
class Spawner(LoggingConfigurable):
|
||||
"""Base class for spawning single-user notebook servers.
|
||||
|
||||
@@ -42,6 +39,7 @@ class Spawner(LoggingConfigurable):
|
||||
db = Any()
|
||||
user = Any()
|
||||
hub = Any()
|
||||
authenticator = Any()
|
||||
api_token = Unicode()
|
||||
ip = Unicode('localhost', config=True,
|
||||
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:
|
||||
break
|
||||
else:
|
||||
yield gen.Task(loop.add_timeout, loop.time() + self.death_interval)
|
||||
yield gen.sleep(self.death_interval)
|
||||
|
||||
def _try_setcwd(path):
|
||||
"""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 .. import orm
|
||||
|
||||
from pamela import PAMError
|
||||
|
||||
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
|
||||
if password == username:
|
||||
return True
|
||||
else:
|
||||
raise PAMError("Fake")
|
||||
|
||||
|
||||
def mock_open_session(username, service):
|
||||
pass
|
||||
|
||||
|
||||
class MockSpawner(LocalProcessSpawner):
|
||||
@@ -51,12 +53,12 @@ class SlowSpawner(MockSpawner):
|
||||
|
||||
@gen.coroutine
|
||||
def start(self):
|
||||
yield gen.Task(IOLoop.current().add_timeout, timedelta(seconds=2))
|
||||
yield gen.sleep(2)
|
||||
yield super().start()
|
||||
|
||||
@gen.coroutine
|
||||
def stop(self):
|
||||
yield gen.Task(IOLoop.current().add_timeout, timedelta(seconds=2))
|
||||
yield gen.sleep(2)
|
||||
yield super().stop()
|
||||
|
||||
|
||||
@@ -80,7 +82,9 @@ class MockPAMAuthenticator(PAMAuthenticator):
|
||||
return not user.name.startswith('dne')
|
||||
|
||||
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)
|
||||
|
||||
class MockHub(JupyterHub):
|
||||
|
@@ -363,11 +363,10 @@ def test_slow_spawn(app, io_loop):
|
||||
assert app_user.spawn_pending
|
||||
assert not app_user.stop_pending
|
||||
|
||||
dt = timedelta(seconds=0.1)
|
||||
@gen.coroutine
|
||||
def wait_spawn():
|
||||
while app_user.spawn_pending:
|
||||
yield gen.Task(io_loop.add_timeout, dt)
|
||||
yield gen.sleep(0.1)
|
||||
|
||||
io_loop.run_sync(wait_spawn)
|
||||
assert not app_user.spawn_pending
|
||||
@@ -377,7 +376,7 @@ def test_slow_spawn(app, io_loop):
|
||||
@gen.coroutine
|
||||
def wait_stop():
|
||||
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.raise_for_status()
|
||||
@@ -410,11 +409,10 @@ def test_never_spawn(app, io_loop):
|
||||
assert app_user.spawner is not None
|
||||
assert app_user.spawn_pending
|
||||
|
||||
dt = timedelta(seconds=0.1)
|
||||
@gen.coroutine
|
||||
def wait_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)
|
||||
assert not app_user.spawn_pending
|
||||
|
@@ -3,7 +3,6 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from getpass import getuser
|
||||
from subprocess import check_output
|
||||
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
||||
|
||||
@@ -16,7 +15,9 @@ def test_token_app():
|
||||
cmd = [sys.executable, '-m', 'jupyterhub', 'token']
|
||||
out = check_output(cmd + ['--help-all']).decode('utf8', 'replace')
|
||||
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)
|
||||
|
||||
def test_generate_config():
|
||||
|
@@ -18,7 +18,7 @@ def test_root_no_auth(app, io_loop):
|
||||
print(app.hub.server)
|
||||
r = requests.get(app.proxy.public_server.host)
|
||||
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):
|
||||
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",
|
||||
ip, port, e
|
||||
)
|
||||
yield gen.Task(loop.add_timeout, loop.time() + 0.1)
|
||||
yield gen.sleep(0.1)
|
||||
else:
|
||||
return
|
||||
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,
|
||||
# but 502 or other proxy error is conceivable
|
||||
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:
|
||||
app_log.debug("Server at %s responded with %s", url, e.code)
|
||||
return
|
||||
except (OSError, socket.error) as e:
|
||||
if e.errno not in {errno.ECONNABORTED, errno.ECONNREFUSED, errno.ECONNRESET}:
|
||||
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:
|
||||
return
|
||||
|
||||
|
@@ -5,8 +5,9 @@
|
||||
|
||||
version_info = (
|
||||
0,
|
||||
2,
|
||||
3,
|
||||
0,
|
||||
# 'dev',
|
||||
)
|
||||
|
||||
__version__ = '.'.join(map(str, version_info))
|
||||
|
@@ -1,6 +1,6 @@
|
||||
traitlets>=4
|
||||
tornado>=4
|
||||
tornado>=4.1
|
||||
jinja2
|
||||
simplepam
|
||||
pamela
|
||||
sqlalchemy
|
||||
requests
|
||||
|
Reference in New Issue
Block a user