Merge pull request #1817 from minrk/server-model

Always include server sub-models in user api requests
This commit is contained in:
Min RK
2018-04-24 14:12:58 +02:00
committed by GitHub
7 changed files with 87 additions and 34 deletions

View File

@@ -3,7 +3,7 @@ swagger: '2.0'
info: info:
title: JupyterHub title: JupyterHub
description: The REST API for JupyterHub description: The REST API for JupyterHub
version: 0.8.0dev version: 0.9.0dev
license: license:
name: BSD-3-Clause name: BSD-3-Clause
schemes: schemes:
@@ -606,12 +606,52 @@ definitions:
description: The user's notebook server's base URL, if running; null if not. description: The user's notebook server's base URL, if running; null if not.
pending: pending:
type: string type: string
enum: ["spawn", "stop"] enum: ["spawn", "stop", null]
description: The currently pending action, if any description: The currently pending action, if any
last_activity: last_activity:
type: string type: string
format: date-time format: date-time
description: Timestamp of last-seen activity from the user description: Timestamp of last-seen activity from the user
servers:
type: object
description: The active servers for this user.
items:
schema:
$ref: '#/definitions/Server'
Server:
type: object
properties:
name:
type: string
description: The server's name. The user's default server has an empty name ('')
ready:
type: boolean
description: |
Whether the server is ready for traffic.
Will always be false when any transition is pending.
pending:
type: string
enum: ["spawn", "stop", null]
description: |
The currently pending action, if any.
A server is not ready if an action is pending.
url:
type: string
description: |
The URL where the server can be accessed
(typically /user/:name/:server.name/).
progress_url:
type: string
description: |
The URL for an event-stream to retrieve events during a spawn.
started:
type: string
format: date-time
description: UTC timestamp when the server was last started.
last_activity:
type: string
format: date-time
description: UTC timestamp last-seen activity on this server.
Group: Group:
type: object type: object
properties: properties:

View File

@@ -127,6 +127,19 @@ def cull_idle(url, api_token, inactive_limit, cull_users=False, max_age=0, concu
log_name, server['pending']) log_name, server['pending'])
return False return False
# jupyterhub < 0.9 defined 'server.url' once the server was ready
# as an *implicit* signal that the server was ready.
# 0.9 adds a dedicated, explicit 'ready' field.
# By current (0.9) definitions, servers that have no pending
# events and are not ready shouldn't be in the model,
# but let's check just to be safe.
if not server.get('ready', bool(server['url'])):
app_log.warning(
"Not culling not-ready not-pending server %s: %s",
log_name, server)
return False
if server.get('started'): if server.get('started'):
age = now - parse_date(server['started']) age = now - parse_date(server['started'])
else: else:
@@ -192,16 +205,18 @@ def cull_idle(url, api_token, inactive_limit, cull_users=False, max_age=0, concu
""" """
# shutdown servers first. # shutdown servers first.
# Hub doesn't allow deleting users with running servers. # Hub doesn't allow deleting users with running servers.
# named servers contain the 'servers' dict # jupyterhub 0.9 always provides a 'servers' model.
# 0.8 only does this when named servers are enabled.
if 'servers' in user: if 'servers' in user:
servers = user['servers'] servers = user['servers']
# Otherwise, server data is intermingled in with the user
# model
else: else:
# jupyterhub < 0.9 without named servers enabled.
# create servers dict with one entry for the default server
# from the user model.
# only if the server is running.
servers = {} servers = {}
if user['server']: if user['server']:
servers[''] = { servers[''] = {
'started': user.get('started'),
'last_activity': user['last_activity'], 'last_activity': user['last_activity'],
'pending': user['pending'], 'pending': user['pending'],
'url': user['server'], 'url': user['server'],

View File

@@ -96,7 +96,8 @@ class APIHandler(BaseHandler):
'name': spawner.name, 'name': spawner.name,
'last_activity': isoformat(spawner.orm_spawner.last_activity), 'last_activity': isoformat(spawner.orm_spawner.last_activity),
'started': isoformat(spawner.orm_spawner.started), 'started': isoformat(spawner.orm_spawner.started),
'pending': spawner.pending or None, 'pending': spawner.pending,
'ready': spawner.ready,
'url': url_path_join(spawner.user.url, spawner.name, '/'), 'url': url_path_join(spawner.user.url, spawner.name, '/'),
'progress_url': spawner._progress_url, 'progress_url': spawner._progress_url,
} }
@@ -136,7 +137,7 @@ class APIHandler(BaseHandler):
model.update(extra) model.update(extra)
return model return model
def user_model(self, user): def user_model(self, user, include_servers=False):
"""Get the JSON model for a User object""" """Get the JSON model for a User object"""
if isinstance(user, orm.User): if isinstance(user, orm.User):
user = self.users[user.id] user = self.users[user.id]
@@ -147,23 +148,23 @@ class APIHandler(BaseHandler):
'admin': user.admin, 'admin': user.admin,
'groups': [ g.name for g in user.groups ], 'groups': [ g.name for g in user.groups ],
'server': user.url if user.running else None, 'server': user.url if user.running else None,
'progress_url': user.progress_url(''),
'pending': None, 'pending': None,
'created': isoformat(user.created), 'created': isoformat(user.created),
'started': None,
'last_activity': isoformat(user.last_activity), 'last_activity': isoformat(user.last_activity),
} }
if '' in user.spawners: if '' in user.spawners:
server_model = self.server_model(user.spawners['']) model['pending'] = user.spawners[''].pending
# copy some values from the default server to the user model
for key in ('started', 'pending'):
model[key] = server_model[key]
if self.allow_named_servers: if not include_servers:
servers = model['servers'] = {} model['servers'] = None
for name, spawner in user.spawners.items(): return model
if spawner.ready:
servers[name] = self.server_model(spawner) servers = model['servers'] = {}
for name, spawner in user.spawners.items():
# include 'active' servers, not just ready
# (this includes pending events)
if spawner.active:
servers[name] = self.server_model(spawner)
return model return model
def group_model(self, group): def group_model(self, group):

View File

@@ -35,7 +35,10 @@ class SelfAPIHandler(APIHandler):
class UserListAPIHandler(APIHandler): class UserListAPIHandler(APIHandler):
@admin_only @admin_only
def get(self): def get(self):
data = [ self.user_model(u) for u in self.db.query(orm.User) ] data = [
self.user_model(u, include_servers=True)
for u in self.db.query(orm.User)
]
self.write(json.dumps(data)) self.write(json.dumps(data))
@admin_only @admin_only
@@ -113,15 +116,15 @@ class UserAPIHandler(APIHandler):
@admin_or_self @admin_or_self
async def get(self, name): async def get(self, name):
user = self.find_user(name) user = self.find_user(name)
user_ = self.user_model(user) model = self.user_model(user, include_servers=True)
# auth state will only be shown if the requestor is an admin # auth state will only be shown if the requestor is an admin
# this means users can't see their own auth state unless they # this means users can't see their own auth state unless they
# are admins, Hub admins often are also marked as admins so they # are admins, Hub admins often are also marked as admins so they
# will see their auth state but normal users won't # will see their auth state but normal users won't
requestor = self.get_current_user() requestor = self.get_current_user()
if requestor.admin: if requestor.admin:
user_['auth_state'] = await user.get_auth_state() model['auth_state'] = await user.get_auth_state()
self.write(json.dumps(user_)) self.write(json.dumps(model))
@admin_only @admin_only
async def post(self, name): async def post(self, name):

View File

@@ -81,7 +81,7 @@ class Spawner(LoggingConfigurable):
return 'spawn' return 'spawn'
elif self._stop_pending: elif self._stop_pending:
return 'stop' return 'stop'
return False return None
@property @property
def ready(self): def ready(self):

View File

@@ -200,14 +200,8 @@ def normalize_user(user):
smooths out user model with things like timestamps smooths out user model with things like timestamps
for easier comparison for easier comparison
""" """
for key in ('created', 'last_activity', 'started'): for key in ('created', 'last_activity'):
user[key] = normalize_timestamp(user[key]) user[key] = normalize_timestamp(user[key])
if user['progress_url']:
user['progress_url'] = re.sub(
r'.*/hub/api',
'PREFIX/hub/api',
user['progress_url'],
)
if 'servers' in user: if 'servers' in user:
for server in user['servers'].values(): for server in user['servers'].values():
for key in ('started', 'last_activity'): for key in ('started', 'last_activity'):
@@ -228,8 +222,7 @@ def fill_user(model):
model.setdefault('pending', None) model.setdefault('pending', None)
model.setdefault('created', TIMESTAMP) model.setdefault('created', TIMESTAMP)
model.setdefault('last_activity', TIMESTAMP) model.setdefault('last_activity', TIMESTAMP)
model.setdefault('started', None) model.setdefault('servers', {})
model.setdefault('progress_url', 'PREFIX/hub/api/users/{name}/server/progress'.format(**model))
return model return model

View File

@@ -34,7 +34,6 @@ def test_default_server(app, named_servers):
'name': username, 'name': username,
'auth_state': None, 'auth_state': None,
'server': user.url, 'server': user.url,
'started': TIMESTAMP,
'servers': { 'servers': {
'': { '': {
'name': '', 'name': '',
@@ -42,6 +41,7 @@ def test_default_server(app, named_servers):
'last_activity': TIMESTAMP, 'last_activity': TIMESTAMP,
'url': user.url, 'url': user.url,
'pending': None, 'pending': None,
'ready': True,
'progress_url': 'PREFIX/hub/api/users/{}/server/progress'.format(username), 'progress_url': 'PREFIX/hub/api/users/{}/server/progress'.format(username),
}, },
}, },
@@ -99,6 +99,7 @@ def test_create_named_server(app, named_servers):
'last_activity': TIMESTAMP, 'last_activity': TIMESTAMP,
'url': url_path_join(user.url, name, '/'), 'url': url_path_join(user.url, name, '/'),
'pending': None, 'pending': None,
'ready': True,
'progress_url': 'PREFIX/hub/api/users/{}/servers/{}/progress'.format( 'progress_url': 'PREFIX/hub/api/users/{}/servers/{}/progress'.format(
username, servername), username, servername),
} }