support named servers in REST API

and exercise them in tests
This commit is contained in:
Min RK
2017-07-24 16:55:17 +02:00
parent 9a555d8a6e
commit 2cec124b4f
7 changed files with 115 additions and 32 deletions

View File

@@ -108,18 +108,18 @@ class APIHandler(BaseHandler):
model['pending'] = 'spawn'
elif user.spawners['']._stop_pending:
model['pending'] = 'stop'
return model
# TODO: named servers
servers = model['servers'] = {}
for name, spawner in user.spawners.items():
if user.running(name):
servers[name] = s = {'name': name}
if spawner._spawn_pending:
s['pending'] = 'spawn'
elif spawner._stop_pending:
s['pending'] = 'stop'
if spawner.server:
s['url'] = spawner.server.url
if self.allow_named_servers:
servers = model['servers'] = {}
for name, spawner in user.spawners.items():
if user.running(name):
servers[name] = s = {'name': name}
if spawner._spawn_pending:
s['pending'] = 'spawn'
elif spawner._stop_pending:
s['pending'] = 'stop'
if spawner.server:
s['url'] = user.url + name
return model
def group_model(self, group):

View File

@@ -221,7 +221,9 @@ class UserNamedServerAPIHandler(APIHandler):
"""
@gen.coroutine
@admin_or_self
def post(self, name, server_name=''):
def post(self, name, server_name):
if not self.allow_named_servers:
raise web.HTTPError(400, "Named servers are not enabled.")
user = self.find_user(name)
if user is None:
raise web.HTTPError(404, "No such user %r" % name)
@@ -235,17 +237,23 @@ class UserNamedServerAPIHandler(APIHandler):
@gen.coroutine
@admin_or_self
def delete(self, name, server_name):
if not self.allow_named_servers:
raise web.HTTPError(400, "Named servers are not enabled.")
user = self.find_user(name)
if user is None:
raise web.HTTPError(404, "No such user %r" % name)
if server_name not in user.spawners:
raise web.HTTPError(404, "%s has no server named %r" % (name, server_name))
spawner = user.spawners[server_name]
if spawner._stop_pending:
self.set_status(202)
return
if not user.running(name):
raise web.HTTPError(400, "%s's server is not running" % name)
if not user.running(server_name):
raise web.HTTPError(400, "%s's server %r is not running" % (name, server_name))
# include notify, so that a server that died is noticed immediately
status = yield spawner.poll_and_notify()
if status is not None:
raise web.HTTPError(400, "%s's server is not running" % name)
raise web.HTTPError(400, "%s's server %r is not running" % (name, server_name))
yield self.stop_single_user(user, server_name)
status = 202 if spawner._stop_pending else 204
self.set_status(status)

View File

@@ -400,11 +400,14 @@ class BaseHandler(RequestHandler):
yield user.stop()
@gen.coroutine
def stop_single_user(self, user):
if user.spawner._stop_pending:
raise RuntimeError("Stop already pending for: %s" % user.name)
def stop_single_user(self, user, name=''):
if name not in user.spawners:
raise KeyError("User %s has no such spawner %r", user.name, name)
spawner = user.spawners[name]
if spawner._stop_pending:
raise RuntimeError("Stop already pending for: %s:%s" % (user.name, name))
tic = IOLoop.current().time()
yield self.proxy.delete_user(user)
yield self.proxy.delete_user(user, name)
f = user.stop()
@gen.coroutine
def finish_stop(f=None):
@@ -422,9 +425,9 @@ class BaseHandler(RequestHandler):
try:
yield gen.with_timeout(timedelta(seconds=self.slow_stop_timeout), f)
except gen.TimeoutError:
if user.spawner._stop_pending:
if spawner._stop_pending:
# hit timeout, but stop is still pending
self.log.warning("User %s server is slow to stop", user.name)
self.log.warning("User %s:%s server is slow to stop", user.name, name)
# schedule finish for when the server finishes stopping
IOLoop.current().add_future(f, finish_stop)
else:

View File

@@ -276,7 +276,7 @@ class Proxy(LoggingConfigurable):
futures = []
for orm_user in db.query(User):
user = user_dict[orm_user]
for name, spawner in user.spawners:
for name, spawner in user.spawners.items():
if user.running(name):
futures.append(self.add_user(user, name))
# wait after submitting them all

View File

@@ -229,7 +229,7 @@ def public_url(app, user_or_service=None, path=''):
host = user_or_service.host
else:
host = public_host(app)
prefix = user_or_service.server.base_url
prefix = user_or_service.prefix
else:
host = public_host(app)
prefix = Server.from_url(app.proxy.public_url).base_url

View File

@@ -1,10 +1,82 @@
from .test_api import api_request, add_user
"""Tests for named servers"""
import pytest
import requests
def test_create_named_server(app):
return
app.allow_named_servers = True
username = 'user'
servername = 'foo'
from ..utils import url_path_join
from .test_api import api_request, add_user, find_user
from .mocking import public_url
@pytest.fixture
def named_servers(app):
key = 'allow_named_servers'
app.tornado_application.settings[key] = app.tornado_settings[key] = True
try:
yield True
finally:
app.tornado_application.settings[key] = app.tornado_settings[key] = False
def test_create_named_server(app, named_servers):
username = 'walnut'
user = add_user(app.db, app, name=username)
# assert user.allow_named_servers == True
cookies = app.login_user(username)
servername = 'trevor'
r = api_request(app, 'users', username, 'servers', servername, method='post')
r.raise_for_status()
assert r.status_code == 201
assert r.text == ''
url = url_path_join(public_url(app, user), servername, 'env')
r = requests.get(url, cookies=cookies)
r.raise_for_status()
assert r.url == url
env = r.json()
prefix = env.get('JUPYTERHUB_SERVICE_PREFIX')
assert prefix == user.spawners[servername].server.base_url
assert prefix.endswith('/user/%s/%s/' % (username, servername))
def test_delete_named_server(app, named_servers):
username = 'donaar'
user = add_user(app.db, app, name=username)
assert user.allow_named_servers
cookies = app.login_user(username)
servername = 'splugoth'
r = api_request(app, 'users', username, 'servers', servername, method='post')
r.raise_for_status()
assert r.status_code == 201
r = api_request(app, 'users', username, 'servers', servername, method='delete')
r.raise_for_status()
assert r.status_code == 204
r = api_request(app, 'users', username)
r.raise_for_status()
user_model = r.json()
user_model.pop('last_activity')
assert user_model == {
'name': username,
'groups': [],
'kind': 'user',
'admin': False,
'pending': None,
'server': None,
'servers': {
name: {
'name': name,
'url': url_path_join(user.url, name),
}
for name in ['1', servername]
},
}
def test_named_server_disabled(app):
username = 'user'
servername = 'okay'
r = api_request(app, 'users', username, 'servers', servername, method='post')
assert r.status_code == 400
r = api_request(app, 'users', username, 'servers', servername, method='delete')
assert r.status_code == 400

View File

@@ -134,7 +134,7 @@ class User(HasTraits):
self.allow_named_servers = self.settings.get('allow_named_servers', False)
self.base_url = url_path_join(
self.base_url = self.prefix = url_path_join(
self.settings.get('base_url', '/'), 'user', self.escaped_name) + '/'
self.spawners = _SpawnerDict(self._new_spawner)