mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 13:33:00 +00:00
drop support for old Python, IPython < 3
Require IPython >= 3.0, Python >= 3.3
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- 3.4
|
- 3.4
|
||||||
- 2.7
|
- 3.3
|
||||||
before_install:
|
before_install:
|
||||||
# workaround for https://github.com/travis-ci/travis-cookbooks/issues/155
|
# workaround for https://github.com/travis-ci/travis-cookbooks/issues/155
|
||||||
- sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm
|
- sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm
|
||||||
|
@@ -18,6 +18,8 @@ Basic principles:
|
|||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
JupyterHub requires IPython >= 3.0 (current master) and Python >= 3.3.
|
||||||
|
|
||||||
You will need nodejs/npm, which you can get from your package manager:
|
You will need nodejs/npm, which you can get from your package manager:
|
||||||
|
|
||||||
sudo apt-get install npm nodejs-legacy
|
sudo apt-get install npm nodejs-legacy
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
mock
|
-r requirements.txt
|
||||||
pytest
|
pytest
|
||||||
|
@@ -1 +1,2 @@
|
|||||||
from .version import *
|
from .version import *
|
||||||
|
|
||||||
|
@@ -4,11 +4,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
try:
|
from http.client import responses
|
||||||
# py3
|
|
||||||
from http.client import responses
|
|
||||||
except ImportError:
|
|
||||||
from httplib import responses
|
|
||||||
|
|
||||||
from tornado import web
|
from tornado import web
|
||||||
|
|
||||||
@@ -19,7 +15,7 @@ class APIHandler(BaseHandler):
|
|||||||
"""Return the body of the request as JSON data."""
|
"""Return the body of the request as JSON data."""
|
||||||
if not self.request.body:
|
if not self.request.body:
|
||||||
return None
|
return None
|
||||||
body = self.request.body.strip().decode(u'utf-8')
|
body = self.request.body.strip().decode('utf-8')
|
||||||
try:
|
try:
|
||||||
model = json.loads(body)
|
model = json.loads(body)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@@ -11,11 +11,6 @@ from .. import orm
|
|||||||
from ..utils import admin_only, authenticated_403
|
from ..utils import admin_only, authenticated_403
|
||||||
from .base import APIHandler
|
from .base import APIHandler
|
||||||
|
|
||||||
try:
|
|
||||||
basestring
|
|
||||||
except NameError:
|
|
||||||
basestring = str # py3
|
|
||||||
|
|
||||||
class BaseUserHandler(APIHandler):
|
class BaseUserHandler(APIHandler):
|
||||||
|
|
||||||
def user_model(self, user):
|
def user_model(self, user):
|
||||||
@@ -26,7 +21,7 @@ class BaseUserHandler(APIHandler):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_model_types = {
|
_model_types = {
|
||||||
'name': basestring,
|
'name': str,
|
||||||
'admin': bool,
|
'admin': bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,20 +5,18 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
import io
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from distutils.version import LooseVersion as V
|
||||||
|
from getpass import getuser
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
|
|
||||||
try:
|
if sys.version_info[:2] < (3,3):
|
||||||
raw_input
|
raise ValueError("Python < 3.3 not supported: %s" % sys.version)
|
||||||
except NameError:
|
|
||||||
# py3
|
|
||||||
raw_input = input
|
|
||||||
|
|
||||||
from six import text_type
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
@@ -30,6 +28,10 @@ from tornado.ioloop import IOLoop, PeriodicCallback
|
|||||||
from tornado.log import LogFormatter, app_log, access_log, gen_log
|
from tornado.log import LogFormatter, app_log, access_log, gen_log
|
||||||
from tornado import gen, web
|
from tornado import gen, web
|
||||||
|
|
||||||
|
import IPython
|
||||||
|
if V(IPython.__version__) < V('3.0'):
|
||||||
|
raise ImportError("JupyterHub Requires IPython >= 3.0, found %s" % IPython.__version__)
|
||||||
|
|
||||||
from IPython.utils.traitlets import (
|
from IPython.utils.traitlets import (
|
||||||
Unicode, Integer, Dict, TraitError, List, Bool, Any,
|
Unicode, Integer, Dict, TraitError, List, Bool, Any,
|
||||||
Type, Set, Instance, Bytes,
|
Type, Set, Instance, Bytes,
|
||||||
@@ -43,8 +45,8 @@ from . import handlers, apihandlers
|
|||||||
from . import orm
|
from . import orm
|
||||||
from ._data import DATA_FILES_PATH
|
from ._data import DATA_FILES_PATH
|
||||||
from .utils import (
|
from .utils import (
|
||||||
url_path_join, TimeoutError,
|
url_path_join,
|
||||||
ISO8601_ms, ISO8601_s, getuser_unicode,
|
ISO8601_ms, ISO8601_s,
|
||||||
)
|
)
|
||||||
# classes for config
|
# classes for config
|
||||||
from .auth import Authenticator, PAMAuthenticator
|
from .auth import Authenticator, PAMAuthenticator
|
||||||
@@ -299,18 +301,11 @@ class JupyterHubApp(Application):
|
|||||||
def _log_datefmt_default(self):
|
def _log_datefmt_default(self):
|
||||||
"""Exclude date from default date format"""
|
"""Exclude date from default date format"""
|
||||||
return "%H:%M:%S"
|
return "%H:%M:%S"
|
||||||
|
|
||||||
def _log_format_default(self):
|
def _log_format_default(self):
|
||||||
"""override default log format to include time"""
|
"""override default log format to include time"""
|
||||||
return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
|
return "%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
|
||||||
|
|
||||||
def _log_format_changed(self, name, old, new):
|
|
||||||
"""Change the log formatter when log_format is set."""
|
|
||||||
# FIXME: IPython < 3 compat
|
|
||||||
_log_handler = self.log.handlers[0]
|
|
||||||
_log_formatter = self._log_formatter_cls(fmt=new, datefmt=self.log_datefmt)
|
|
||||||
_log_handler.setFormatter(_log_formatter)
|
|
||||||
|
|
||||||
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
|
||||||
# self.log is a child of. The logging module dipatches log messages to a log
|
# self.log is a child of. The logging module dipatches log messages to a log
|
||||||
@@ -325,8 +320,6 @@ class JupyterHubApp(Application):
|
|||||||
logger.propagate = True
|
logger.propagate = True
|
||||||
logger.parent = self.log
|
logger.parent = self.log
|
||||||
logger.setLevel(self.log.level)
|
logger.setLevel(self.log.level)
|
||||||
# FIXME: IPython < 3 compat
|
|
||||||
self._log_format_changed('', '', self.log_format)
|
|
||||||
|
|
||||||
def init_ports(self):
|
def init_ports(self):
|
||||||
if self.hub_port == self.port:
|
if self.hub_port == self.port:
|
||||||
@@ -370,7 +363,7 @@ class JupyterHubApp(Application):
|
|||||||
"""More informative log messages for failed filesystem access"""
|
"""More informative log messages for failed filesystem access"""
|
||||||
path = os.path.abspath(path)
|
path = os.path.abspath(path)
|
||||||
parent, fname = os.path.split(path)
|
parent, fname = os.path.split(path)
|
||||||
user = getuser_unicode()
|
user = getuser()
|
||||||
if not os.path.isdir(parent):
|
if not os.path.isdir(parent):
|
||||||
self.log.error("Directory %s does not exist", parent)
|
self.log.error("Directory %s does not exist", parent)
|
||||||
if os.path.exists(parent) and not os.access(parent, os.W_OK):
|
if os.path.exists(parent) and not os.access(parent, os.W_OK):
|
||||||
@@ -399,7 +392,7 @@ class JupyterHubApp(Application):
|
|||||||
self.log.error("Bad permissions on %s", secret_file)
|
self.log.error("Bad permissions on %s", secret_file)
|
||||||
else:
|
else:
|
||||||
self.log.info("Loading %s from %s", trait_name, secret_file)
|
self.log.info("Loading %s from %s", trait_name, secret_file)
|
||||||
with io.open(secret_file) as f:
|
with open(secret_file) as f:
|
||||||
b64_secret = f.read()
|
b64_secret = f.read()
|
||||||
try:
|
try:
|
||||||
secret = binascii.a2b_base64(b64_secret)
|
secret = binascii.a2b_base64(b64_secret)
|
||||||
@@ -413,8 +406,8 @@ class JupyterHubApp(Application):
|
|||||||
if secret_file and secret_from == 'new':
|
if secret_file and secret_from == 'new':
|
||||||
# if we generated a new secret, store it in the secret_file
|
# if we generated a new secret, store it in the secret_file
|
||||||
self.log.info("Writing %s to %s", trait_name, secret_file)
|
self.log.info("Writing %s to %s", trait_name, secret_file)
|
||||||
b64_secret = text_type(binascii.b2a_base64(secret))
|
b64_secret = binascii.b2a_base64(secret).decode('ascii')
|
||||||
with io.open(secret_file, 'w', encoding='utf8') as f:
|
with open(secret_file, 'w') as f:
|
||||||
f.write(b64_secret)
|
f.write(b64_secret)
|
||||||
try:
|
try:
|
||||||
os.chmod(secret_file, 0o600)
|
os.chmod(secret_file, 0o600)
|
||||||
@@ -450,7 +443,7 @@ class JupyterHubApp(Application):
|
|||||||
ip=self.hub_ip,
|
ip=self.hub_ip,
|
||||||
port=self.hub_port,
|
port=self.hub_port,
|
||||||
base_url=self.hub_prefix,
|
base_url=self.hub_prefix,
|
||||||
cookie_name=u'jupyter-hub-token',
|
cookie_name='jupyter-hub-token',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.db.add(self.hub)
|
self.db.add(self.hub)
|
||||||
@@ -470,7 +463,7 @@ class JupyterHubApp(Application):
|
|||||||
# add current user as admin if there aren't any others
|
# add current user as admin if there aren't any others
|
||||||
admins = db.query(orm.User).filter(orm.User.admin==True)
|
admins = db.query(orm.User).filter(orm.User.admin==True)
|
||||||
if admins.first() is None:
|
if admins.first() is None:
|
||||||
self.admin_users.add(getuser_unicode())
|
self.admin_users.add(getuser())
|
||||||
|
|
||||||
for name in self.admin_users:
|
for name in self.admin_users:
|
||||||
# ensure anyone specified as admin in config is admin in db
|
# ensure anyone specified as admin in config is admin in db
|
||||||
@@ -576,7 +569,7 @@ class JupyterHubApp(Application):
|
|||||||
self.proxy.public_server.port = self.port
|
self.proxy.public_server.port = self.port
|
||||||
self.proxy.api_server.ip = self.proxy_api_ip
|
self.proxy.api_server.ip = self.proxy_api_ip
|
||||||
self.proxy.api_server.port = self.proxy_api_port
|
self.proxy.api_server.port = self.proxy_api_port
|
||||||
self.proxy.api_server.base_url = u'/api/routes/'
|
self.proxy.api_server.base_url = '/api/routes/'
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
@@ -694,8 +687,8 @@ class JupyterHubApp(Application):
|
|||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
if self.pid_file:
|
if self.pid_file:
|
||||||
self.log.debug("Writing PID %i to %s", pid, self.pid_file)
|
self.log.debug("Writing PID %i to %s", pid, self.pid_file)
|
||||||
with io.open(self.pid_file, 'w') as f:
|
with open(self.pid_file, 'w') as f:
|
||||||
f.write(u'%i' % pid)
|
f.write('%i' % pid)
|
||||||
|
|
||||||
@catch_config_error
|
@catch_config_error
|
||||||
def initialize(self, *args, **kwargs):
|
def initialize(self, *args, **kwargs):
|
||||||
@@ -756,7 +749,7 @@ class JupyterHubApp(Application):
|
|||||||
def ask():
|
def ask():
|
||||||
prompt = "Overwrite %s with default config? [y/N]" % self.config_file
|
prompt = "Overwrite %s with default config? [y/N]" % self.config_file
|
||||||
try:
|
try:
|
||||||
return raw_input(prompt).lower() or 'n'
|
return input(prompt).lower() or 'n'
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print('') # empty line
|
print('') # empty line
|
||||||
return 'n'
|
return 'n'
|
||||||
@@ -771,7 +764,7 @@ class JupyterHubApp(Application):
|
|||||||
if isinstance(config_text, bytes):
|
if isinstance(config_text, bytes):
|
||||||
config_text = config_text.decode('utf8')
|
config_text = config_text.decode('utf8')
|
||||||
print("Writing default config to: %s" % self.config_file)
|
print("Writing default config to: %s" % self.config_file)
|
||||||
with io.open(self.config_file, encoding='utf8', mode='w') as f:
|
with open(self.config_file, mode='w') as f:
|
||||||
f.write(config_text)
|
f.write(config_text)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
|
@@ -153,5 +153,5 @@ class PAMAuthenticator(LocalAuthenticator):
|
|||||||
busername = username.encode(self.encoding)
|
busername = username.encode(self.encoding)
|
||||||
bpassword = data['password'].encode(self.encoding)
|
bpassword = data['password'].encode(self.encoding)
|
||||||
if simplepam.authenticate(busername, bpassword, service=self.service):
|
if simplepam.authenticate(busername, bpassword, service=self.service):
|
||||||
raise gen.Return(username)
|
return username
|
||||||
|
|
@@ -5,11 +5,7 @@
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
try:
|
from http.client import responses
|
||||||
# py3
|
|
||||||
from http.client import responses
|
|
||||||
except ImportError:
|
|
||||||
from httplib import responses
|
|
||||||
|
|
||||||
from jinja2 import TemplateNotFound
|
from jinja2 import TemplateNotFound
|
||||||
|
|
||||||
@@ -156,7 +152,7 @@ class BaseHandler(RequestHandler):
|
|||||||
auth = self.authenticator
|
auth = self.authenticator
|
||||||
if auth is not None:
|
if auth is not None:
|
||||||
result = yield auth.authenticate(self, data)
|
result = yield auth.authenticate(self, data)
|
||||||
raise gen.Return(result)
|
return result
|
||||||
else:
|
else:
|
||||||
self.log.error("No authentication function, login is impossible!")
|
self.log.error("No authentication function, login is impossible!")
|
||||||
|
|
||||||
@@ -179,7 +175,7 @@ class BaseHandler(RequestHandler):
|
|||||||
)
|
)
|
||||||
yield self.proxy.add_user(user)
|
yield self.proxy.add_user(user)
|
||||||
user.spawner.add_poll_callback(self.user_stopped, user)
|
user.spawner.add_poll_callback(self.user_stopped, user)
|
||||||
raise gen.Return(user)
|
return user
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def user_stopped(self, user):
|
def user_stopped(self, user):
|
||||||
|
@@ -63,11 +63,11 @@ class Server(Base):
|
|||||||
"""
|
"""
|
||||||
__tablename__ = 'servers'
|
__tablename__ = 'servers'
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
proto = Column(Unicode, default=u'http')
|
proto = Column(Unicode, default='http')
|
||||||
ip = Column(Unicode, default=u'localhost')
|
ip = Column(Unicode, default='localhost')
|
||||||
port = Column(Integer, default=random_port)
|
port = Column(Integer, default=random_port)
|
||||||
base_url = Column(Unicode, default=u'/')
|
base_url = Column(Unicode, default='/')
|
||||||
cookie_name = Column(Unicode, default=u'cookie')
|
cookie_name = Column(Unicode, default='cookie')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Server(%s:%s)>" % (self.ip, self.port)
|
return "<Server(%s:%s)>" % (self.ip, self.port)
|
||||||
|
@@ -9,7 +9,6 @@ import os
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from tornado import ioloop
|
from tornado import ioloop
|
||||||
from tornado import web
|
|
||||||
|
|
||||||
from IPython.utils.traitlets import Unicode
|
from IPython.utils.traitlets import Unicode
|
||||||
|
|
||||||
@@ -21,8 +20,8 @@ from IPython.html.utils import url_path_join
|
|||||||
from distutils.version import LooseVersion as V
|
from distutils.version import LooseVersion as V
|
||||||
|
|
||||||
import IPython
|
import IPython
|
||||||
if V(IPython.__version__) < V('2.2'):
|
if V(IPython.__version__) < V('3.0'):
|
||||||
raise ImportError("JupyterHub Requires IPython >= 2.2, found %s" % IPython.__version__)
|
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.
|
||||||
@@ -97,11 +96,6 @@ class SingleUserNotebookApp(NotebookApp):
|
|||||||
# disable the exit confirmation for background notebook processes
|
# disable the exit confirmation for background notebook processes
|
||||||
ioloop.IOLoop.instance().stop()
|
ioloop.IOLoop.instance().stop()
|
||||||
|
|
||||||
def init_kernel_argv(self):
|
|
||||||
"""construct the kernel arguments"""
|
|
||||||
# FIXME: This is 2.x-compat, remove when 3.x is requred
|
|
||||||
self.kernel_argv = ["--profile-dir", self.profile_dir.location]
|
|
||||||
|
|
||||||
def init_webapp(self):
|
def init_webapp(self):
|
||||||
# monkeypatch authentication to use the hub
|
# monkeypatch authentication to use the hub
|
||||||
from IPython.html.base.handlers import AuthenticatedHandler
|
from IPython.html.base.handlers import AuthenticatedHandler
|
||||||
@@ -120,10 +114,7 @@ class SingleUserNotebookApp(NotebookApp):
|
|||||||
|
|
||||||
# load the hub related settings into the tornado settings dict
|
# load the hub related settings into the tornado settings dict
|
||||||
env = os.environ
|
env = os.environ
|
||||||
# FIXME: IPython < 3 compat
|
s = self.webapp_settings
|
||||||
s = getattr(self, 'tornado_settings',
|
|
||||||
getattr(self, 'webapp_settings')
|
|
||||||
)
|
|
||||||
s['cookie_cache'] = {}
|
s['cookie_cache'] = {}
|
||||||
s['user'] = self.user
|
s['user'] = self.user
|
||||||
s['hub_api_key'] = env.pop('JPY_API_TOKEN')
|
s['hub_api_key'] = env.pop('JPY_API_TOKEN')
|
||||||
|
@@ -114,7 +114,7 @@ class Spawner(LoggingConfigurable):
|
|||||||
|
|
||||||
Subclasses should call super, to ensure that state is properly cleared.
|
Subclasses should call super, to ensure that state is properly cleared.
|
||||||
"""
|
"""
|
||||||
self.api_token = u''
|
self.api_token = ''
|
||||||
|
|
||||||
def get_args(self):
|
def get_args(self):
|
||||||
"""Return the arguments to be passed after self.cmd"""
|
"""Return the arguments to be passed after self.cmd"""
|
||||||
@@ -339,13 +339,13 @@ class LocalProcessSpawner(Spawner):
|
|||||||
break
|
break
|
||||||
pids = self._get_pg_pids(ppid)
|
pids = self._get_pg_pids(ppid)
|
||||||
if pids:
|
if pids:
|
||||||
raise gen.Return(pids[0])
|
return pids[0]
|
||||||
else:
|
else:
|
||||||
yield gen.Task(loop.add_timeout, loop.time() + 0.1)
|
yield gen.Task(loop.add_timeout, loop.time() + 0.1)
|
||||||
self.log.error("Failed to get single-user PID")
|
self.log.error("Failed to get single-user PID")
|
||||||
# return sudo pid if we can't get the real PID
|
# return sudo pid if we can't get the real PID
|
||||||
# this shouldn't happen
|
# this shouldn't happen
|
||||||
raise gen.Return(ppid)
|
return ppid
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def start(self):
|
def start(self):
|
||||||
@@ -378,7 +378,7 @@ class LocalProcessSpawner(Spawner):
|
|||||||
if status is not None:
|
if status is not None:
|
||||||
# clear state if the process is done
|
# clear state if the process is done
|
||||||
self.clear_state()
|
self.clear_state()
|
||||||
raise gen.Return(status)
|
return status
|
||||||
|
|
||||||
# if we resumed from stored state,
|
# if we resumed from stored state,
|
||||||
# we don't have the Popen handle anymore, so rely on self.pid
|
# we don't have the Popen handle anymore, so rely on self.pid
|
||||||
@@ -386,16 +386,16 @@ class LocalProcessSpawner(Spawner):
|
|||||||
if not self.pid:
|
if not self.pid:
|
||||||
# no pid, not running
|
# no pid, not running
|
||||||
self.clear_state()
|
self.clear_state()
|
||||||
raise gen.Return(0)
|
return 0
|
||||||
|
|
||||||
# send signal 0 to check if PID exists
|
# send signal 0 to check if PID exists
|
||||||
# this doesn't work on Windows, but that's okay because we don't support Windows.
|
# this doesn't work on Windows, but that's okay because we don't support Windows.
|
||||||
alive = yield self._signal(0)
|
alive = yield self._signal(0)
|
||||||
if not alive:
|
if not alive:
|
||||||
self.clear_state()
|
self.clear_state()
|
||||||
raise gen.Return(0)
|
return 0
|
||||||
else:
|
else:
|
||||||
raise gen.Return(None)
|
return None
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def _signal(self, sig):
|
def _signal(self, sig):
|
||||||
@@ -405,9 +405,9 @@ class LocalProcessSpawner(Spawner):
|
|||||||
"""
|
"""
|
||||||
if self.set_user == 'sudo':
|
if self.set_user == 'sudo':
|
||||||
rc = yield self._signal_sudo(sig)
|
rc = yield self._signal_sudo(sig)
|
||||||
raise gen.Return(rc)
|
return rc
|
||||||
else:
|
else:
|
||||||
raise gen.Return(self._signal_setuid(sig))
|
return self._signal_setuid(sig)
|
||||||
|
|
||||||
def _signal_setuid(self, sig):
|
def _signal_setuid(self, sig):
|
||||||
"""simple implementation of signal, which we can use when we are using setuid (we are root)"""
|
"""simple implementation of signal, which we can use when we are using setuid (we are root)"""
|
||||||
@@ -427,10 +427,10 @@ class LocalProcessSpawner(Spawner):
|
|||||||
try:
|
try:
|
||||||
check_output(['ps', '-p', str(self.pid)], stderr=PIPE)
|
check_output(['ps', '-p', str(self.pid)], stderr=PIPE)
|
||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
raise gen.Return(False) # process is gone
|
return False # process is gone
|
||||||
else:
|
else:
|
||||||
if sig == 0:
|
if sig == 0:
|
||||||
raise gen.Return(True) # process exists
|
return True # process exists
|
||||||
|
|
||||||
# build sudo -u user kill -SIG PID
|
# build sudo -u user kill -SIG PID
|
||||||
cmd = self.sudo_cmd(self.user)
|
cmd = self.sudo_cmd(self.user)
|
||||||
@@ -441,7 +441,7 @@ class LocalProcessSpawner(Spawner):
|
|||||||
check_output(cmd,
|
check_output(cmd,
|
||||||
preexec_fn=self.make_preexec_fn(self.user.name),
|
preexec_fn=self.make_preexec_fn(self.user.name),
|
||||||
)
|
)
|
||||||
raise gen.Return(True) # process exists
|
return True # process exists
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def stop(self, now=False):
|
def stop(self, now=False):
|
||||||
|
@@ -4,12 +4,12 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from getpass import getuser
|
||||||
|
|
||||||
from pytest import fixture
|
from pytest import fixture
|
||||||
from tornado import ioloop
|
from tornado import ioloop
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
from ..utils import getuser_unicode
|
|
||||||
|
|
||||||
from .mocking import MockHubApp
|
from .mocking import MockHubApp
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ def db():
|
|||||||
if _db is None:
|
if _db is None:
|
||||||
_db = orm.new_session_factory('sqlite:///:memory:', echo=True)()
|
_db = orm.new_session_factory('sqlite:///:memory:', echo=True)()
|
||||||
user = orm.User(
|
user = orm.User(
|
||||||
name=getuser_unicode(),
|
name=getuser(),
|
||||||
server=orm.Server(),
|
server=orm.Server(),
|
||||||
)
|
)
|
||||||
hub = orm.Hub(
|
hub = orm.Hub(
|
||||||
|
@@ -1,29 +1,23 @@
|
|||||||
"""mock utilities for testing"""
|
"""mock utilities for testing"""
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
try:
|
from unittest import mock
|
||||||
from unittest import mock
|
|
||||||
except ImportError:
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from tornado.ioloop import IOLoop
|
from tornado.ioloop import IOLoop
|
||||||
|
|
||||||
from six import text_type
|
|
||||||
|
|
||||||
from ..spawner import LocalProcessSpawner
|
from ..spawner import LocalProcessSpawner
|
||||||
from ..app import JupyterHubApp
|
from ..app import JupyterHubApp
|
||||||
from ..auth import PAMAuthenticator, Authenticator
|
from ..auth import PAMAuthenticator
|
||||||
from .. import orm
|
from .. import orm
|
||||||
|
|
||||||
def mock_authenticate(username, password, service='login'):
|
def mock_authenticate(username, password, service='login'):
|
||||||
# mimic simplepam's failure to handle unicode
|
# mimic simplepam's failure to handle unicode
|
||||||
if isinstance(username, text_type):
|
if isinstance(username, str):
|
||||||
return False
|
return False
|
||||||
if isinstance(password, text_type):
|
if isinstance(password, str):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# just use equality for testing
|
# just use equality for testing
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
"""Test the JupyterHubApp entry point"""
|
"""Test the JupyterHubApp entry point"""
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from subprocess import check_output
|
from subprocess import check_output
|
||||||
@@ -8,8 +7,8 @@ from tempfile import NamedTemporaryFile
|
|||||||
|
|
||||||
def test_help_all():
|
def test_help_all():
|
||||||
out = check_output([sys.executable, '-m', 'jupyterhub', '--help-all']).decode('utf8', 'replace')
|
out = check_output([sys.executable, '-m', 'jupyterhub', '--help-all']).decode('utf8', 'replace')
|
||||||
assert u'--ip' in out
|
assert '--ip' in out
|
||||||
assert u'--JupyterHubApp.ip' in out
|
assert '--JupyterHubApp.ip' in out
|
||||||
|
|
||||||
def test_generate_config():
|
def test_generate_config():
|
||||||
with NamedTemporaryFile(prefix='jupyter_hub_config', suffix='.py') as tf:
|
with NamedTemporaryFile(prefix='jupyter_hub_config', suffix='.py') as tf:
|
||||||
@@ -19,7 +18,7 @@ def test_generate_config():
|
|||||||
'--generate-config', '-f', cfg_file]
|
'--generate-config', '-f', cfg_file]
|
||||||
).decode('utf8', 'replace')
|
).decode('utf8', 'replace')
|
||||||
assert os.path.exists(cfg_file)
|
assert os.path.exists(cfg_file)
|
||||||
with io.open(cfg_file) as f:
|
with open(cfg_file) as f:
|
||||||
cfg_text = f.read()
|
cfg_text = f.read()
|
||||||
os.remove(cfg_file)
|
os.remove(cfg_file)
|
||||||
assert cfg_file in out
|
assert cfg_file in out
|
||||||
|
@@ -9,33 +9,33 @@ from .mocking import MockPAMAuthenticator
|
|||||||
def test_pam_auth(io_loop):
|
def test_pam_auth(io_loop):
|
||||||
authenticator = MockPAMAuthenticator()
|
authenticator = MockPAMAuthenticator()
|
||||||
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
||||||
u'username': u'match',
|
'username': 'match',
|
||||||
u'password': u'match',
|
'password': 'match',
|
||||||
}))
|
}))
|
||||||
assert authorized == u'match'
|
assert authorized == 'match'
|
||||||
|
|
||||||
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
||||||
u'username': u'match',
|
'username': 'match',
|
||||||
u'password': u'nomatch',
|
'password': 'nomatch',
|
||||||
}))
|
}))
|
||||||
assert authorized is None
|
assert authorized is None
|
||||||
|
|
||||||
def test_pam_auth_whitelist(io_loop):
|
def test_pam_auth_whitelist(io_loop):
|
||||||
authenticator = MockPAMAuthenticator(whitelist={'wash', 'kaylee'})
|
authenticator = MockPAMAuthenticator(whitelist={'wash', 'kaylee'})
|
||||||
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
||||||
u'username': u'kaylee',
|
'username': 'kaylee',
|
||||||
u'password': u'kaylee',
|
'password': 'kaylee',
|
||||||
}))
|
}))
|
||||||
assert authorized == u'kaylee'
|
assert authorized == 'kaylee'
|
||||||
|
|
||||||
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
||||||
u'username': u'wash',
|
'username': 'wash',
|
||||||
u'password': u'nomatch',
|
'password': 'nomatch',
|
||||||
}))
|
}))
|
||||||
assert authorized is None
|
assert authorized is None
|
||||||
|
|
||||||
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
|
||||||
u'username': u'mal',
|
'username': 'mal',
|
||||||
u'password': u'mal',
|
'password': 'mal',
|
||||||
}))
|
}))
|
||||||
assert authorized is None
|
assert authorized is None
|
||||||
|
@@ -5,48 +5,42 @@
|
|||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
|
|
||||||
try:
|
|
||||||
unicode
|
|
||||||
except NameError:
|
|
||||||
# py3
|
|
||||||
unicode = str
|
|
||||||
|
|
||||||
|
|
||||||
def test_server(db):
|
def test_server(db):
|
||||||
server = orm.Server()
|
server = orm.Server()
|
||||||
db.add(server)
|
db.add(server)
|
||||||
db.commit()
|
db.commit()
|
||||||
assert server.ip == u'localhost'
|
assert server.ip == 'localhost'
|
||||||
assert server.base_url == '/'
|
assert server.base_url == '/'
|
||||||
assert server.proto == 'http'
|
assert server.proto == 'http'
|
||||||
assert isinstance(server.port, int)
|
assert isinstance(server.port, int)
|
||||||
assert isinstance(server.cookie_name, unicode)
|
assert isinstance(server.cookie_name, str)
|
||||||
assert server.url == 'http://localhost:%i/' % server.port
|
assert server.url == 'http://localhost:%i/' % server.port
|
||||||
|
|
||||||
|
|
||||||
def test_proxy(db):
|
def test_proxy(db):
|
||||||
proxy = orm.Proxy(
|
proxy = orm.Proxy(
|
||||||
auth_token=u'abc-123',
|
auth_token='abc-123',
|
||||||
public_server=orm.Server(
|
public_server=orm.Server(
|
||||||
ip=u'192.168.1.1',
|
ip='192.168.1.1',
|
||||||
port=8000,
|
port=8000,
|
||||||
),
|
),
|
||||||
api_server=orm.Server(
|
api_server=orm.Server(
|
||||||
ip=u'127.0.0.1',
|
ip='127.0.0.1',
|
||||||
port=8001,
|
port=8001,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
db.add(proxy)
|
db.add(proxy)
|
||||||
db.commit()
|
db.commit()
|
||||||
assert proxy.public_server.ip == u'192.168.1.1'
|
assert proxy.public_server.ip == '192.168.1.1'
|
||||||
assert proxy.api_server.ip == u'127.0.0.1'
|
assert proxy.api_server.ip == '127.0.0.1'
|
||||||
assert proxy.auth_token == u'abc-123'
|
assert proxy.auth_token == 'abc-123'
|
||||||
|
|
||||||
|
|
||||||
def test_hub(db):
|
def test_hub(db):
|
||||||
hub = orm.Hub(
|
hub = orm.Hub(
|
||||||
server=orm.Server(
|
server=orm.Server(
|
||||||
ip = u'1.2.3.4',
|
ip = '1.2.3.4',
|
||||||
port = 1234,
|
port = 1234,
|
||||||
base_url='/hubtest/',
|
base_url='/hubtest/',
|
||||||
),
|
),
|
||||||
@@ -54,30 +48,30 @@ def test_hub(db):
|
|||||||
)
|
)
|
||||||
db.add(hub)
|
db.add(hub)
|
||||||
db.commit()
|
db.commit()
|
||||||
assert hub.server.ip == u'1.2.3.4'
|
assert hub.server.ip == '1.2.3.4'
|
||||||
hub.server.port == 1234
|
hub.server.port == 1234
|
||||||
assert hub.api_url == u'http://1.2.3.4:1234/hubtest/api'
|
assert hub.api_url == 'http://1.2.3.4:1234/hubtest/api'
|
||||||
|
|
||||||
|
|
||||||
def test_user(db):
|
def test_user(db):
|
||||||
user = orm.User(name=u'kaylee',
|
user = orm.User(name='kaylee',
|
||||||
server=orm.Server(),
|
server=orm.Server(),
|
||||||
state={'pid': 4234},
|
state={'pid': 4234},
|
||||||
)
|
)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
assert user.name == u'kaylee'
|
assert user.name == 'kaylee'
|
||||||
assert user.server.ip == u'localhost'
|
assert user.server.ip == 'localhost'
|
||||||
assert user.state == {'pid': 4234}
|
assert user.state == {'pid': 4234}
|
||||||
|
|
||||||
found = orm.User.find(db, u'kaylee')
|
found = orm.User.find(db, 'kaylee')
|
||||||
assert found.name == user.name
|
assert found.name == user.name
|
||||||
found = orm.User.find(db, u'badger')
|
found = orm.User.find(db, 'badger')
|
||||||
assert found is None
|
assert found is None
|
||||||
|
|
||||||
|
|
||||||
def test_tokens(db):
|
def test_tokens(db):
|
||||||
user = orm.User(name=u'inara')
|
user = orm.User(name='inara')
|
||||||
db.add(user)
|
db.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
token = user.new_api_token()
|
token = user.new_api_token()
|
||||||
|
@@ -5,34 +5,17 @@
|
|||||||
|
|
||||||
from binascii import b2a_hex
|
from binascii import b2a_hex
|
||||||
import errno
|
import errno
|
||||||
import getpass
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from six import text_type
|
|
||||||
from tornado import web, gen, ioloop
|
from tornado import web, gen, ioloop
|
||||||
from tornado.httpclient import AsyncHTTPClient, HTTPError
|
from tornado.httpclient import AsyncHTTPClient, HTTPError
|
||||||
from tornado.log import app_log
|
from tornado.log import app_log
|
||||||
|
|
||||||
from IPython.html.utils import url_path_join
|
from IPython.html.utils import url_path_join
|
||||||
|
|
||||||
try:
|
|
||||||
# make TimeoutError importable on Python >= 3.3
|
|
||||||
TimeoutError = TimeoutError
|
|
||||||
except NameError:
|
|
||||||
# python < 3.3
|
|
||||||
class TimeoutError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def getuser_unicode():
|
|
||||||
"""
|
|
||||||
Call getpass.getuser, ensuring that the output is returned as unicode.
|
|
||||||
"""
|
|
||||||
return text_type(getpass.getuser())
|
|
||||||
|
|
||||||
|
|
||||||
def random_port():
|
def random_port():
|
||||||
"""get a single random port"""
|
"""get a single random port"""
|
||||||
@@ -147,7 +130,7 @@ def new_token(*args, **kwargs):
|
|||||||
|
|
||||||
For now, just UUIDs.
|
For now, just UUIDs.
|
||||||
"""
|
"""
|
||||||
return text_type(uuid.uuid4().hex)
|
return uuid.uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
def hash_token(token, salt=8, rounds=16384, algorithm='sha512'):
|
def hash_token(token, salt=8, rounds=16384, algorithm='sha512'):
|
||||||
@@ -169,7 +152,7 @@ def hash_token(token, salt=8, rounds=16384, algorithm='sha512'):
|
|||||||
h.update(btoken)
|
h.update(btoken)
|
||||||
digest = h.hexdigest()
|
digest = h.hexdigest()
|
||||||
|
|
||||||
return u"{algorithm}:{rounds}:{salt}:{digest}".format(**locals())
|
return "{algorithm}:{rounds}:{salt}:{digest}".format(**locals())
|
||||||
|
|
||||||
|
|
||||||
def compare_token(compare, token):
|
def compare_token(compare, token):
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
ipython[notebook]>=2.2
|
-e git+https://github.com/ipython/ipython.git#egg=ipython[notebook]
|
||||||
tornado>=3.2
|
tornado>=4
|
||||||
jinja2
|
jinja2
|
||||||
simplepam
|
simplepam
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
requests
|
requests
|
||||||
six
|
|
||||||
|
30
setup.py
30
setup.py
@@ -14,12 +14,11 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
v = sys.version_info
|
v = sys.version_info
|
||||||
if v[:2] < (2,7) or (v[0] >= 3 and v[:2] < (3,3)):
|
if v[:2] < (3,3):
|
||||||
error = "ERROR: IPython requires Python version 2.7 or 3.3 or above."
|
error = "ERROR: Jupyter Hub requires Python version 3.3 or above."
|
||||||
print(error, file=sys.stderr)
|
print(error, file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
PY3 = (sys.version_info[0] >= 3)
|
|
||||||
|
|
||||||
if os.name in ('nt', 'dos'):
|
if os.name in ('nt', 'dos'):
|
||||||
error = "ERROR: Windows is not supported"
|
error = "ERROR: Windows is not supported"
|
||||||
@@ -34,14 +33,6 @@ from glob import glob
|
|||||||
from distutils.core import setup
|
from distutils.core import setup
|
||||||
from subprocess import check_call
|
from subprocess import check_call
|
||||||
|
|
||||||
try:
|
|
||||||
execfile
|
|
||||||
except NameError:
|
|
||||||
# py3
|
|
||||||
def execfile(fname, globs, locs=None):
|
|
||||||
locs = locs or globs
|
|
||||||
exec(compile(open(fname).read(), fname, "exec"), globs, locs)
|
|
||||||
|
|
||||||
pjoin = os.path.join
|
pjoin = os.path.join
|
||||||
|
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
@@ -67,7 +58,9 @@ def get_data_files():
|
|||||||
|
|
||||||
|
|
||||||
ns = {}
|
ns = {}
|
||||||
execfile(pjoin(here, 'jupyterhub', 'version.py'), ns)
|
with open(pjoin(here, 'jupyterhub', 'version.py')) as f:
|
||||||
|
exec(f.read(), {}, ns)
|
||||||
|
|
||||||
|
|
||||||
packages = []
|
packages = []
|
||||||
for d, _, _ in os.walk('jupyterhub'):
|
for d, _, _ in os.walk('jupyterhub'):
|
||||||
@@ -92,13 +85,11 @@ setup_args = dict(
|
|||||||
keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
|
keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
|
||||||
classifiers = [
|
classifiers = [
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
|
'Intended Audience :: System Administrators',
|
||||||
'Intended Audience :: Science/Research',
|
'Intended Audience :: Science/Research',
|
||||||
'License :: OSI Approved :: BSD License',
|
'License :: OSI Approved :: BSD License',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 2',
|
|
||||||
'Programming Language :: Python :: 2.7',
|
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Topic :: System :: Shells',
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -194,11 +185,14 @@ if 'setuptools' in sys.modules:
|
|||||||
self.distribution.run_command('css')
|
self.distribution.run_command('css')
|
||||||
develop.run(self)
|
develop.run(self)
|
||||||
setup_args['cmdclass']['develop'] = develop_js_css
|
setup_args['cmdclass']['develop'] = develop_js_css
|
||||||
|
setup_args['install_requires'] = install_requires = []
|
||||||
|
|
||||||
with open('requirements.txt') as f:
|
with open('requirements.txt') as f:
|
||||||
install_requires = [ line.strip() for line in f.readlines() ]
|
for line in f.readlines():
|
||||||
setup_args['install_requires'] = install_requires
|
req = line.strip()
|
||||||
|
if req.startswith('-e'):
|
||||||
|
req = line.split('#egg=', 1)[1]
|
||||||
|
install_requires.append(req)
|
||||||
|
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
# setup
|
# setup
|
||||||
|
Reference in New Issue
Block a user