diff --git a/docs/rest-api.yml b/docs/rest-api.yml index bc1a1db7..4ea54864 100644 --- a/docs/rest-api.yml +++ b/docs/rest-api.yml @@ -652,6 +652,9 @@ definitions: type: string format: date-time description: UTC timestamp last-seen activity on this server. + state: + type: object + description: Arbitrary internal state from this server's spawner. Only available on the hub's users list or get-user-by-name method, and only if a hub admin. None otherwise. Group: type: object properties: diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index b887d5b4..c307c1a1 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -90,7 +90,7 @@ class APIHandler(BaseHandler): 'message': message or status_message, })) - def server_model(self, spawner): + def server_model(self, spawner, include_state=False): """Get the JSON model for a Spawner""" return { 'name': spawner.name, @@ -98,6 +98,7 @@ class APIHandler(BaseHandler): 'started': isoformat(spawner.orm_spawner.started), 'pending': spawner.pending, 'ready': spawner.ready, + 'state': spawner.get_state() if include_state else None, 'url': url_path_join(spawner.user.url, spawner.name, '/'), 'progress_url': spawner._progress_url, } @@ -137,7 +138,7 @@ class APIHandler(BaseHandler): model.update(extra) return model - def user_model(self, user, include_servers=False): + def user_model(self, user, include_servers=False, include_state=False): """Get the JSON model for a User object""" if isinstance(user, orm.User): user = self.users[user.id] @@ -164,7 +165,7 @@ class APIHandler(BaseHandler): # include 'active' servers, not just ready # (this includes pending events) if spawner.active: - servers[name] = self.server_model(spawner) + servers[name] = self.server_model(spawner, include_state=include_state) return model def group_model(self, group): diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 32829231..c8c0146a 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -36,7 +36,7 @@ class UserListAPIHandler(APIHandler): @admin_only def get(self): data = [ - self.user_model(u, include_servers=True) + self.user_model(u, include_servers=True, include_state=True) for u in self.db.query(orm.User) ] self.write(json.dumps(data)) @@ -116,7 +116,7 @@ class UserAPIHandler(APIHandler): @admin_or_self async def get(self, name): user = self.find_user(name) - model = self.user_model(user, include_servers=True) + model = self.user_model(user, include_servers=True, include_state=self.get_current_user().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 # are admins, Hub admins often are also marked as admins so they diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index 060db3d6..93258fa9 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -207,6 +207,9 @@ def normalize_user(user): for key in ('started', 'last_activity'): server[key] = normalize_timestamp(server[key]) server['progress_url'] = re.sub(r'.*/hub/api', 'PREFIX/hub/api', server['progress_url']) + if (isinstance(server['state'], dict) + and isinstance(server['state'].get('pid', None), int)): + server['state']['pid'] = 0 return user def fill_user(model): diff --git a/jupyterhub/tests/test_named_servers.py b/jupyterhub/tests/test_named_servers.py index 04f07410..04ae83df 100644 --- a/jupyterhub/tests/test_named_servers.py +++ b/jupyterhub/tests/test_named_servers.py @@ -43,6 +43,7 @@ def test_default_server(app, named_servers): 'pending': None, 'ready': True, 'progress_url': 'PREFIX/hub/api/users/{}/server/progress'.format(username), + 'state': {'pid': 0}, }, }, }) @@ -102,6 +103,7 @@ def test_create_named_server(app, named_servers): 'ready': True, 'progress_url': 'PREFIX/hub/api/users/{}/servers/{}/progress'.format( username, servername), + 'state': {'pid': 0}, } for name in [servername] },