From 59eb1490d8745c6e13d13b9615d9ffe99eeba7f5 Mon Sep 17 00:00:00 2001 From: Sebastian Wagner Date: Wed, 24 Jan 2018 11:50:43 +0100 Subject: [PATCH] mgr/dashboard_v2: Added `RESTResource` Python class. This is the base class for providing a RESTful interface to a resource. Signed-off-by: Sebastian Wagner --- src/pybind/mgr/dashboard_v2/restresource.py | 98 +++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/pybind/mgr/dashboard_v2/restresource.py diff --git a/src/pybind/mgr/dashboard_v2/restresource.py b/src/pybind/mgr/dashboard_v2/restresource.py new file mode 100644 index 0000000000000..db9c89b051daa --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/restresource.py @@ -0,0 +1,98 @@ +import json + +import cherrypy + + +def _takes_json(func): + def inner(*args, **kwargs): + body = cherrypy.request.body.read() + try: + data = json.loads(body.decode('utf-8')) + except json.JSONDecodeError as e: + raise cherrypy.HTTPError(400, 'Failed to decode JSON: {}' + .format(str(e))) + return func(data, *args, **kwargs) + return inner + + +def _returns_json(func): + def inner(*args, **kwargs): + cherrypy.serving.response.headers['Content-Type'] = "application/json" + ret = func(*args, **kwargs) + return json.dumps(ret).encode('utf8') + return inner + + +def json_error_page(status, message, traceback, version): + return json.dumps(dict(status=status, detail=message, traceback=traceback, + version=version)) + + +class RESTResource(object): + """ + Base class for providing a RESTful interface to a resource. + + To use this class, simply derive a class from it and implement the methods + you want to support. The list of possible methods are: + + * list() + * bulk_set(data) + * create(data) + * bulk_delete() + * get(key) + * set(data, key) + * delete(key) + + Test with curl: + + curl -H "Content-Type: application/json" -X POST \ + -d '{"username":"xyz","password":"xyz"}' http://127.0.0.1:8080/foo + curl http://127.0.0.1:8080/foo + curl http://127.0.0.1:8080/foo/0 + + """ + + _cp_config = { + 'request.error_page': {'default': json_error_page}, + } + + def _not_implemented(self, is_element): + methods = [method + for ((method, _is_element), (meth, _)) + in self._method_mapping.items() + if _is_element == is_element and hasattr(self, meth)] + cherrypy.response.headers["Allow"] = ",".join(methods) + raise cherrypy.HTTPError(405, "Method not implemented.") + + _method_mapping = { + ('GET', False): ('list', 200), + ('PUT', False): ('bulk_set', 200), + ('PATCH', False): ('bulk_set', 200), + ('POST', False): ('create', 201), + ('DELETE', False): ('bulk_delete', 204), + ('GET', True): ('get', 200), + ('PUT', True): ('set', 200), + ('PATCH', True): ('set', 200), + ('DELETE', True): ('delete', 204), + } + + @cherrypy.expose + def default(self, *vpath, **params): + cherrypy.config.update({'error_page.default': json_error_page}) + is_element = len(vpath) > 0 + + (method_name, status_code) = self._method_mapping[ + (cherrypy.request.method, is_element)] + method = getattr(self, method_name, None) + if not method: + self._not_implemented(is_element) + + if cherrypy.request.method != 'DELETE': + method = _returns_json(method) + + if cherrypy.request.method not in ['GET', 'DELETE']: + method = _takes_json(method) + + cherrypy.response.status = status_code + + return method(*vpath, **params) -- 2.39.5