mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-18 15:33:02 +00:00
Merge pull request #1817 from minrk/server-model
Always include server sub-models in user api requests
This commit is contained in:
@@ -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:
|
||||||
|
@@ -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'],
|
||||||
|
@@ -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):
|
||||||
|
@@ -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):
|
||||||
|
@@ -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):
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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),
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user