diff --git a/.gitignore b/.gitignore index 9dd0d6ce..f94b4a6f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ node_modules build dist docs/_build +docs/source/_static/rest-api .ipynb_checkpoints # ignore config file at the top-level of the repo # but not sub-dirs diff --git a/docs/rest-api.yml b/docs/rest-api.yml index 0bd731c0..15482634 100644 --- a/docs/rest-api.yml +++ b/docs/rest-api.yml @@ -21,6 +21,60 @@ produces: consumes: - application/json paths: + /: + get: + summary: See the version of JupyterHub itself + description: | + This endpoint is not authenticated, + so clients can identify the version of JupyterHub before setting up authentication. + responses: + '200': + description: The version of JupyterHub itself + schema: + type: object + properties: + version: + type: string + description: The version of JupyterHub itself + /info: + get: + summary: Get detailed info about JupyterHub + description: | + Detailed information about JupyterHub, + including information about Python, JupyterHub's version, + and what Authenticators and Spawners are in use. + responses: + '200': + schema: + type: object + properties: + version: + type: string + description: The version of JupyterHub itself + python: + type: string + description: The version of Python, as returned by sys.version + sys_executable: + type: string + description: The path to sys.executable running JupyterHub + authenticator: + type: object + properties: + class: + type: string + description: The Python class currently in use + version: + type: string + description: The version of the package providing the Authenticator class + spawner: + type: object + properties: + class: + type: string + description: The Python class currently in use for spawning single-user servers + version: + type: string + description: The version of the package providing the Spawner class /users: get: summary: List users @@ -332,6 +386,15 @@ paths: /shutdown: post: summary: Shutdown the Hub + parameters: + - name: proxy + in: body + type: bool + description: Whether the proxy should be shutdown as well (default from Hub config) + - name: servers + in: body + type: bool + description: Whether users's servers should be shutdown as well (default from Hub config) responses: '200': description: Hub has shutdown diff --git a/jupyterhub/apihandlers/hub.py b/jupyterhub/apihandlers/hub.py index 35880681..77b7b758 100644 --- a/jupyterhub/apihandlers/hub.py +++ b/jupyterhub/apihandlers/hub.py @@ -4,12 +4,15 @@ # Distributed under the terms of the Modified BSD License. import json +import sys from tornado import web from tornado.ioloop import IOLoop from ..utils import admin_only from .base import APIHandler +from ..version import __version__ + class ShutdownAPIHandler(APIHandler): @@ -49,6 +52,56 @@ class ShutdownAPIHandler(APIHandler): loop.add_callback(loop.stop) +class RootAPIHandler(APIHandler): + + def get(self): + """GET /api/ returns info about the Hub and its API. + + It is not an authenticated endpoint. + + For now, it just returns the version of JupyterHub itself. + """ + data = { + 'version': __version__, + } + self.finish(json.dumps(data)) + + +class InfoAPIHandler(APIHandler): + + @admin_only + def get(self): + """GET /api/info returns detailed info about the Hub and its API. + + It is not an authenticated endpoint. + + For now, it just returns the version of JupyterHub itself. + """ + def _class_info(typ): + """info about a class (Spawner or Authenticator)""" + info = { + 'class': '{mod}.{name}'.format(mod=typ.__module__, name=typ.__name__), + } + pkg = typ.__module__.split('.')[0] + try: + version = sys.modules[pkg].__version__ + except (KeyError, AttributeError): + version = 'unknown' + info['version'] = version + return info + + data = { + 'version': __version__, + 'python': sys.version, + 'sys_executable': sys.executable, + 'spawner': _class_info(self.settings['spawner_class']), + 'authenticator': _class_info(self.authenticator.__class__), + } + self.finish(json.dumps(data)) + + default_handlers = [ (r"/api/shutdown", ShutdownAPIHandler), + (r"/api/?", RootAPIHandler), + (r"/api/info", InfoAPIHandler), ] diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index 493f46d1..e14b0662 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -3,12 +3,14 @@ import json import time from queue import Queue +import sys from urllib.parse import urlparse, quote import requests from tornado import gen +import jupyterhub from .. import orm from ..user import User from ..utils import url_path_join as ujoin @@ -657,6 +659,41 @@ def test_group_delete_users(app): assert sorted([ u.name for u in group.users ]) == sorted(names[2:]) +def test_root_api(app): + base_url = app.hub.server.url + url = ujoin(base_url, 'api') + r = requests.get(url) + r.raise_for_status() + expected = { + 'version': jupyterhub.__version__ + } + assert r.json() == expected + + +def test_info(app): + r = api_request(app, 'info') + r.raise_for_status() + data = r.json() + assert data['version'] == jupyterhub.__version__ + assert sorted(data) == [ + 'authenticator', + 'python', + 'spawner', + 'sys_executable', + 'version', + ] + assert data['python'] == sys.version + assert data['sys_executable'] == sys.executable + assert data['authenticator'] == { + 'class': 'jupyterhub.tests.mocking.MockPAMAuthenticator', + 'version': jupyterhub.__version__, + } + assert data['spawner'] == { + 'class': 'jupyterhub.tests.mocking.MockSpawner', + 'version': jupyterhub.__version__, + } + + # general API tests def test_options(app):