mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 13:33:00 +00:00
Merge pull request #942 from barrachri/multi_servers
Multiple singleuser servers
This commit is contained in:
@@ -35,6 +35,9 @@ if not os.path.exists(ssl_dir):
|
||||
os.makedirs(ssl_dir)
|
||||
|
||||
|
||||
# Allows multiple single-server per user
|
||||
c.JupyterHub.allow_named_servers = True
|
||||
|
||||
# https on :443
|
||||
c.JupyterHub.port = 443
|
||||
c.JupyterHub.ssl_key = pjoin(ssl_dir, 'ssl.key')
|
||||
|
@@ -76,7 +76,7 @@ class UserListAPIHandler(APIHandler):
|
||||
|
||||
def admin_or_self(method):
|
||||
"""Decorator for restricting access to either the target user or admin"""
|
||||
def m(self, name):
|
||||
def m(self, name, *args, **kwargs):
|
||||
current = self.get_current_user()
|
||||
if current is None:
|
||||
raise web.HTTPError(403)
|
||||
@@ -86,7 +86,7 @@ def admin_or_self(method):
|
||||
# raise 404 if not found
|
||||
if not self.find_user(name):
|
||||
raise web.HTTPError(404)
|
||||
return method(self, name)
|
||||
return method(self, name, *args, **kwargs)
|
||||
return m
|
||||
|
||||
class UserAPIHandler(APIHandler):
|
||||
@@ -161,6 +161,10 @@ class UserAPIHandler(APIHandler):
|
||||
|
||||
|
||||
class UserServerAPIHandler(APIHandler):
|
||||
"""Create and delete single-user servers
|
||||
|
||||
This handler should be used when c.JupyterHub.allow_named_servers = False
|
||||
"""
|
||||
@gen.coroutine
|
||||
@admin_or_self
|
||||
def post(self, name):
|
||||
@@ -193,6 +197,54 @@ class UserServerAPIHandler(APIHandler):
|
||||
status = 202 if user.stop_pending else 204
|
||||
self.set_status(status)
|
||||
|
||||
|
||||
class UserCreateNamedServerAPIHandler(APIHandler):
|
||||
"""Create a named single-user server
|
||||
|
||||
This handler should be used when c.JupyterHub.allow_named_servers = True
|
||||
"""
|
||||
@gen.coroutine
|
||||
@admin_or_self
|
||||
def post(self, name):
|
||||
user = self.find_user(name)
|
||||
if user is None:
|
||||
raise HTTPError(404, "No such user %r" % name)
|
||||
if user.running:
|
||||
# include notify, so that a server that died is noticed immediately
|
||||
state = yield user.spawner.poll_and_notify()
|
||||
if state is None:
|
||||
raise web.HTTPError(400, "%s's server is already running" % name)
|
||||
|
||||
options = self.get_json_body()
|
||||
yield self.spawn_single_user(user, options=options)
|
||||
status = 202 if user.spawn_pending else 201
|
||||
self.set_status(status)
|
||||
|
||||
|
||||
class UserDeleteNamedServerAPIHandler(APIHandler):
|
||||
"""Delete a named single-user server
|
||||
|
||||
Expect a server_name inside the url /user/:user/servers/:server_name
|
||||
|
||||
This handler should be used when c.JupyterHub.allow_named_servers = True
|
||||
"""
|
||||
@gen.coroutine
|
||||
@admin_or_self
|
||||
def delete(self, name, server_name):
|
||||
user = self.find_user(name)
|
||||
if user.stop_pending:
|
||||
self.set_status(202)
|
||||
return
|
||||
if not user.running:
|
||||
raise web.HTTPError(400, "%s's server is not running" % name)
|
||||
# include notify, so that a server that died is noticed immediately
|
||||
status = yield user.spawner.poll_and_notify()
|
||||
if status is not None:
|
||||
raise web.HTTPError(400, "%s's server is not running" % name)
|
||||
yield self.stop_single_user(user)
|
||||
status = 202 if user.stop_pending else 204
|
||||
self.set_status(status)
|
||||
|
||||
class UserAdminAccessAPIHandler(APIHandler):
|
||||
"""Grant admins access to single-user servers
|
||||
|
||||
@@ -223,5 +275,7 @@ default_handlers = [
|
||||
(r"/api/users", UserListAPIHandler),
|
||||
(r"/api/users/([^/]+)", UserAPIHandler),
|
||||
(r"/api/users/([^/]+)/server", UserServerAPIHandler),
|
||||
(r"/api/users/([^/]+)/servers", UserCreateNamedServerAPIHandler),
|
||||
(r"/api/users/([^/]+)/servers/([^/]+)", UserDeleteNamedServerAPIHandler),
|
||||
(r"/api/users/([^/]+)/admin-access", UserAdminAccessAPIHandler),
|
||||
]
|
||||
|
@@ -502,6 +502,10 @@ class JupyterHub(Application):
|
||||
def _authenticator_default(self):
|
||||
return self.authenticator_class(parent=self, db=self.db)
|
||||
|
||||
allow_multiple_servers = Bool(False,
|
||||
help="Allow multiple single-server per user"
|
||||
).tag(config=True)
|
||||
|
||||
# class for spawning single-user servers
|
||||
spawner_class = Type(LocalProcessSpawner, Spawner,
|
||||
help="""The class to use for spawning single-user servers.
|
||||
@@ -1322,6 +1326,7 @@ class JupyterHub(Application):
|
||||
subdomain_host=self.subdomain_host,
|
||||
domain=self.domain,
|
||||
statsd=self.statsd,
|
||||
allow_multiple_servers=self.allow_multiple_servers,
|
||||
)
|
||||
# allow configured settings to have priority
|
||||
settings.update(self.tornado_settings)
|
||||
|
@@ -64,6 +64,8 @@ class Server(Base):
|
||||
"""
|
||||
__tablename__ = 'servers'
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
name = Column(Unicode(32), default='') # must be unique between user's servers
|
||||
proto = Column(Unicode(15), default='http')
|
||||
ip = Column(Unicode(255), default='') # could also be a DNS name
|
||||
port = Column(Integer, default=random_port)
|
||||
|
@@ -9,7 +9,7 @@ from tornado.log import app_log
|
||||
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from .utils import url_path_join
|
||||
from .utils import url_path_join, default_server_name
|
||||
|
||||
from . import orm
|
||||
from traitlets import HasTraits, Any, Dict, observe, default
|
||||
@@ -118,6 +118,8 @@ class User(HasTraits):
|
||||
|
||||
hub = self.db.query(orm.Hub).first()
|
||||
|
||||
self.allow_multiple_servers = self.settings.get('allow_multiple_servers', False)
|
||||
|
||||
self.cookie_name = '%s-%s' % (hub.server.cookie_name, quote(self.name, safe=''))
|
||||
self.base_url = url_path_join(
|
||||
self.settings.get('base_url', '/'), 'user', self.escaped_name)
|
||||
@@ -147,7 +149,7 @@ class User(HasTraits):
|
||||
def __repr__(self):
|
||||
return repr(self.orm_user)
|
||||
|
||||
@property
|
||||
@property # FIX-ME CHECK IF STILL NEEDED
|
||||
def running(self):
|
||||
"""property for whether a user has a running server"""
|
||||
if self.spawn_pending or self.stop_pending:
|
||||
@@ -198,11 +200,36 @@ class User(HasTraits):
|
||||
|
||||
@gen.coroutine
|
||||
def spawn(self, options=None):
|
||||
"""Start the user's spawner"""
|
||||
"""Start the user's spawner
|
||||
|
||||
depending from the value of JupyterHub.allow_multiple_servers
|
||||
|
||||
if False:
|
||||
JupyterHub expects only one single-server per user
|
||||
url of the server will be /user/:name
|
||||
|
||||
if True:
|
||||
JupyterHub expects more than one single-server per user
|
||||
url of the server will be /user/:name/:server_name
|
||||
"""
|
||||
db = self.db
|
||||
|
||||
if self.allow_multiple_servers:
|
||||
if options is not None and 'server_name' in options:
|
||||
server_name = options['server_name']
|
||||
else:
|
||||
server_name = default_server_name(self)
|
||||
base_url = url_path_join(self.base_url, server_name)
|
||||
else:
|
||||
server_name = ''
|
||||
base_url = self.base_url
|
||||
|
||||
|
||||
|
||||
server = orm.Server(
|
||||
name = server_name,
|
||||
cookie_name=self.cookie_name,
|
||||
base_url=self.base_url,
|
||||
base_url=base_url,
|
||||
)
|
||||
self.servers.append(server)
|
||||
db.add(self)
|
||||
@@ -212,6 +239,8 @@ class User(HasTraits):
|
||||
db.commit()
|
||||
|
||||
spawner = self.spawner
|
||||
# Passing server_name to the spawner
|
||||
spawner.server_name = server_name
|
||||
spawner.user_options = options or {}
|
||||
# we are starting a new server, make sure it doesn't restore state
|
||||
spawner.clear_state()
|
||||
|
@@ -214,3 +214,18 @@ def url_path_join(*pieces):
|
||||
result = '/'
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def default_server_name(user):
|
||||
"""Return the default name for a new server for a given user.
|
||||
|
||||
Will be the first available integer string, e.g. '1' or '2'.
|
||||
"""
|
||||
existing_names = { server.name for server in user.servers }
|
||||
# if there are 5 servers, count from 1 to 6
|
||||
for n in range(1, len(existing_names) + 2):
|
||||
name = str(n)
|
||||
if name not in existing_names:
|
||||
return name
|
||||
raise RuntimeError("It should be impossible to get here")
|
||||
|
||||
|
Reference in New Issue
Block a user