diff --git a/docs/source/config-examples.md b/docs/source/config-examples.md index 5ef39fe6..2fc2c73d 100644 --- a/docs/source/config-examples.md +++ b/docs/source/config-examples.md @@ -35,6 +35,9 @@ if not os.path.exists(ssl_dir): os.makedirs(ssl_dir) +# Allows multiple single-server per user +c.JupyterHub.allow_multiple_servers = False + # https on :443 c.JupyterHub.port = 443 c.JupyterHub.ssl_key = pjoin(ssl_dir, 'ssl.key') diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 858c7b4e..0b74c595 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -158,14 +158,54 @@ class UserAPIHandler(APIHandler): setattr(user, key, value) self.db.commit() self.write(json.dumps(self.user_model(user))) + - -class UserCreateServerAPIHandler(APIHandler): - +class UserServerAPIHandler(APIHandler): + """Create and delete single-user servers + + This handler should be used when c.JupyterHub.allow_multiple_servers = False + """ + @gen.coroutine + @admin_or_self + def post(self, name): + user = self.find_user(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) + + @gen.coroutine + @admin_or_self + def delete(self, 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 UserCreateMultiServerAPIHandler(APIHandler): + """Create multi single-user server + + This handler should be used when c.JupyterHub.allow_multiple_servers = True + """ @gen.coroutine @admin_or_self def post(self, name): - user = self.find_user(name) if user.running: # include notify, so that a server that died is noticed immediately @@ -179,8 +219,13 @@ class UserCreateServerAPIHandler(APIHandler): self.set_status(status) -class UserDeleteServerAPIHandler(APIHandler): - +class UserDeleteMultiServerAPIHandler(APIHandler): + """Delete multi single-user server + + Expect a server_name inside the url /user/:user/servers/:server_name + + This handler should be used when c.JupyterHub.allow_multiple_servers = True + """ @gen.coroutine @admin_or_self def delete(self, name, server_name): @@ -227,7 +272,8 @@ class UserAdminAccessAPIHandler(APIHandler): default_handlers = [ (r"/api/users", UserListAPIHandler), (r"/api/users/([^/]+)", UserAPIHandler), - (r"/api/users/([^/]+)/servers", UserCreateServerAPIHandler), - (r"/api/users/([^/]+)/servers/([^/]+)", UserDeleteServerAPIHandler), + (r"/api/users/([^/]+)/server", UserServerAPIHandler), + (r"/api/users/([^/]+)/servers", UserCreateMultiServerAPIHandler), + (r"/api/users/([^/]+)/servers/([^/]+)", UserDeleteMultiServerAPIHandler), (r"/api/users/([^/]+)/admin-access", UserAdminAccessAPIHandler), ] diff --git a/jupyterhub/app.py b/jupyterhub/app.py index f13b2239..929d6340 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -501,7 +501,11 @@ class JupyterHub(Application): @default('authenticator') 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) diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index 0884d9cb..671bd282 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -65,7 +65,7 @@ class Server(Base): __tablename__ = 'servers' id = Column(Integer, primary_key=True) - name = Column(Unicode(32)) # must be unique between user's servers + 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) diff --git a/jupyterhub/user.py b/jupyterhub/user.py index c135271f..fa6bf162 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -120,6 +120,8 @@ class User(HasTraits): super().__init__(**kwargs) 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( @@ -203,18 +205,27 @@ class User(HasTraits): def spawn(self, options=None): """Start the user's spawner - Because there could be more then one server per user - each server has to have a unique name between the servers of a given user + depending from the value of JupyterHub.allow_multiple_servers - base_url is built using user's base url and adding /server/{name} - where name is the server uuid urlsafed + 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 options is not None and 'server_name' in options: - server_name = options['server_name'] + + 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) else: - server_name = default_server_name(self) + server_name = '' + + server = orm.Server( name = server_name, cookie_name=self.cookie_name,