]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: using RoutesDispatcher as HTTP request dispatcher
authorRicardo Dias <rdias@suse.com>
Tue, 3 Apr 2018 16:02:09 +0000 (17:02 +0100)
committerRicardo Dias <rdias@suse.com>
Wed, 4 Apr 2018 16:41:03 +0000 (17:41 +0100)
Signed-off-by: Ricardo Dias <rdias@suse.com>
12 files changed:
ceph.spec.in
src/pybind/mgr/dashboard/controllers/monitor.py
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
src/pybind/mgr/dashboard/controllers/summary.py
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/requirements.txt
src/pybind/mgr/dashboard/tests/helper.py
src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py
src/pybind/mgr/dashboard/tests/test_tcmu_iscsi.py
src/pybind/mgr/dashboard/tests/test_tools.py
src/pybind/mgr/dashboard/tools.py

index 18269d8eaf58ba22e15fc68b6275d93a72e3e514..a0f0c54bdd6e43b5a68ca06c086d309acd557d13 100644 (file)
@@ -219,6 +219,7 @@ BuildRequires:      python3-Cython
 %if 0%{with make_check}
 %if 0%{?fedora} || 0%{?rhel}
 BuildRequires: python%{_python_buildid}-cherrypy
+BuildRequires: python%{_python_buildid}-routes
 BuildRequires: python%{_python_buildid}-pecan
 BuildRequires: python%{_python_buildid}-werkzeug
 BuildRequires: python%{_python_buildid}-tox
@@ -232,6 +233,7 @@ BuildRequires:      py-bcrypt
 %endif
 %if 0%{?suse_version}
 BuildRequires: python%{_python_buildid}-CherryPy
+BuildRequires: python%{_python_buildid}-Routes
 BuildRequires: python%{_python_buildid}-Werkzeug
 BuildRequires: python%{_python_buildid}-pecan
 BuildRequires: python%{_python_buildid}-numpy-devel
@@ -358,6 +360,7 @@ Group:          System/Filesystems
 Requires:       ceph-base = %{_epoch_prefix}%{version}-%{release}
 %if 0%{?fedora} || 0%{?rhel}
 Requires:       python%{_python_buildid}-cherrypy
+Requires:       python%{_python_buildid}-routes
 Requires:       python%{_python_buildid}-jinja2
 Requires:       python%{_python_buildid}-pecan
 Requires:       python%{_python_buildid}-werkzeug
@@ -371,6 +374,7 @@ Requires:   py-bcrypt
 %endif
 %if 0%{?suse_version}
 Requires:       python%{_python_buildid}-CherryPy
+Requires:       python%{_python_buildid}-Routes
 Requires:       python%{_python_buildid}-Jinja2
 Requires:       python%{_python_buildid}-Werkzeug
 Requires:       python%{_python_buildid}-pecan
index b9733fee7fc05e5f0c963e44451699b564190cfd..3a57f55b558df57689a4228a140cbe2eabd08700 100644 (file)
@@ -14,7 +14,7 @@ from ..tools import ApiController, AuthRequired, BaseController
 class Monitor(BaseController):
     @cherrypy.expose
     @cherrypy.tools.json_out()
-    def default(self, *_vpath, **_params):
+    def __call__(self):
         in_quorum, out_quorum = [], []
 
         counters = ['mon.num_sessions']
index 4f877980d16ff47ab47bf33d97d0e20789349620..585d2087405e805a9ff8c59cbb41ae0c65187987 100644 (file)
@@ -26,6 +26,7 @@ class Rbd(RESTController):
     }
 
     def __init__(self):
+        super(Rbd, self).__init__()
         self.rbd = None
 
     @staticmethod
index 2e799817cb2c5af66a0f7c917fca3cb2b7bf695d..118183160ea082f0a5c94c86902444ce361b5913 100644 (file)
@@ -157,11 +157,12 @@ def get_daemons_and_pools():  # pylint: disable=R0915
 class RbdMirror(BaseController):
 
     def __init__(self):
+        super(RbdMirror, self).__init__()
         self.pool_data = {}
 
     @cherrypy.expose
     @cherrypy.tools.json_out()
-    def default(self, *_vpath, **_params):
+    def __call__(self):
         status, content_data = self._get_content_data()
         return {'status': status, 'content_data': content_data}
 
index 0f92d8f6bbd23461259dfc42b553c26b3ed7cb50..002f2b71c18eec6cec4dd4477dff5ad1c94b3681 100644 (file)
@@ -58,7 +58,7 @@ class Summary(BaseController):
 
     @cherrypy.expose
     @cherrypy.tools.json_out()
-    def default(self, *_vpath, **_params):
+    def __call__(self):
         executing_t, finished_t = TaskManager.list_serializable()
         return {
             'rbd_pools': self._rbd_pool_data(),
index e8bbea3fce5fe2a11cf8809194d5e1ff7ea3334d..9221d8a4288e63d29b2872823199c047b47ca889 100644 (file)
@@ -27,7 +27,7 @@ if 'COVERAGE_ENABLED' in os.environ:
 # pylint: disable=wrong-import-position
 from . import logger, mgr
 from .controllers.auth import Auth
-from .tools import load_controllers, json_error_page, SessionExpireAtBrowserCloseTool, \
+from .tools import generate_routes, json_error_page, SessionExpireAtBrowserCloseTool, \
                    NotificationQueue, RequestLoggingTool, TaskManager
 from .settings import options_command_list, handle_option_command
 
@@ -97,6 +97,7 @@ class Module(MgrModule):
         return os.path.join(current_dir, 'frontend/dist')
 
     def configure_cherrypy(self):
+        # pylint: disable=too-many-locals
         server_addr = self.get_localized_config('server_addr', '::')
         server_port = self.get_localized_config('server_port', '8080')
         if server_addr is None:
@@ -125,14 +126,6 @@ class Module(MgrModule):
         }
         cherrypy.config.update(config)
 
-        config = {
-            '/': {
-                'tools.staticdir.on': True,
-                'tools.staticdir.dir': self.get_frontend_path(),
-                'tools.staticdir.index': 'index.html'
-            }
-        }
-
         # Publish the URI that others may use to access the service we're
         # about to start serving
         self.set_uri("http://{0}:{1}{2}/".format(
@@ -141,8 +134,17 @@ class Module(MgrModule):
             self.url_prefix
         ))
 
-        cherrypy.tree.mount(Module.ApiRoot(self), '{}/api'.format(self.url_prefix))
-        cherrypy.tree.mount(Module.StaticRoot(), '{}/'.format(self.url_prefix), config=config)
+        mapper = generate_routes(self.url_prefix)
+
+        config = {
+            '{}/'.format(self.url_prefix): {
+                'tools.staticdir.on': True,
+                'tools.staticdir.dir': self.get_frontend_path(),
+                'tools.staticdir.index': 'index.html'
+            },
+            '{}/api'.format(self.url_prefix): {'request.dispatch': mapper}
+        }
+        cherrypy.tree.mount(None, config=config)
 
     def serve(self):
         if 'COVERAGE_ENABLED' in os.environ:
@@ -183,55 +185,6 @@ class Module(MgrModule):
     def notify(self, notify_type, notify_id):
         NotificationQueue.new_notification(notify_type, notify_id)
 
-    class ApiRoot(object):
-
-        _cp_config = {
-            'tools.sessions.on': True,
-            'tools.authenticate.on': True
-        }
-
-        def __init__(self, mgrmod):
-            self.ctrls = load_controllers()
-            logger.debug('Loaded controllers: %s', self.ctrls)
-
-            first_level_ctrls = [ctrl for ctrl in self.ctrls
-                                 if '/' not in ctrl._cp_path_]
-            multi_level_ctrls = set(self.ctrls).difference(first_level_ctrls)
-
-            for ctrl in first_level_ctrls:
-                logger.info('Adding controller: %s -> /api/%s', ctrl.__name__,
-                            ctrl._cp_path_)
-                inst = ctrl()
-                setattr(Module.ApiRoot, ctrl._cp_path_, inst)
-
-            for ctrl in multi_level_ctrls:
-                path_parts = ctrl._cp_path_.split('/')
-                path = '/'.join(path_parts[:-1])
-                key = path_parts[-1]
-                parent_ctrl_classes = [c for c in self.ctrls
-                                       if c._cp_path_ == path]
-                if len(parent_ctrl_classes) != 1:
-                    logger.error('No parent controller found for %s! '
-                                 'Please check your path in the ApiController '
-                                 'decorator!', ctrl)
-                else:
-                    inst = ctrl()
-                    setattr(parent_ctrl_classes[0], key, inst)
-
-        @cherrypy.expose
-        def index(self):
-            tpl = """API Endpoints:<br>
-            <ul>
-            {lis}
-            </ul>
-            """
-            endpoints = ['<li><a href="{}">{}</a></li>'.format(ctrl._cp_path_, ctrl.__name__) for
-                         ctrl in self.ctrls]
-            return tpl.format(lis='\n'.join(endpoints))
-
-    class StaticRoot(object):
-        pass
-
 
 class StandbyModule(MgrStandbyModule):
     def serve(self):
index 4484ed6609a273501b9dcf0ebbf4601a7c24e442..fc623ade316dd08daca481aff15f374c685df338 100644 (file)
@@ -24,6 +24,7 @@ pytest==3.3.2
 pytest-cov==2.5.1
 pytz==2017.3
 requests==2.18.4
+Routes==2.4.1
 singledispatch==3.4.0.3
 six==1.11.0
 tempora==1.10
index effe21d763273de8b8bcd9bc410ee53ea9da87e4..4557c528c5a0e2c8203dcce208921419da2087ab 100644 (file)
@@ -8,10 +8,23 @@ import cherrypy
 from cherrypy.test import helper
 
 from ..controllers.auth import Auth
-from ..tools import json_error_page, SessionExpireAtBrowserCloseTool
+from ..tools import json_error_page, SessionExpireAtBrowserCloseTool, \
+                    generate_controller_routes
 
 
 class ControllerTestCase(helper.CPWebCase):
+    @classmethod
+    def setup_controllers(cls, ctrl_classes, base_url=''):
+        if not isinstance(ctrl_classes, list):
+            ctrl_classes = [ctrl_classes]
+        mapper = cherrypy.dispatch.RoutesDispatcher()
+        for ctrl in ctrl_classes:
+            generate_controller_routes(ctrl, mapper, base_url)
+        if base_url == '':
+            base_url = '/'
+        cherrypy.tree.mount(None, config={
+            base_url: {'request.dispatch': mapper}})
+
     def __init__(self, *args, **kwargs):
         cherrypy.tools.authenticate = cherrypy.Tool('before_handler', Auth.check_auth)
         cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
index c283c947d84fe17b48e29b2b630f1c9be4668d91..c4d04c224497e00e85fe97806f074314d54d26a7 100644 (file)
@@ -3,8 +3,6 @@ from __future__ import absolute_import
 import json
 import mock
 
-import cherrypy
-
 from .. import mgr
 from ..controllers.summary import Summary
 from ..controllers.rbd_mirroring import RbdMirror
@@ -66,8 +64,7 @@ class RbdMirroringControllerTest(ControllerTestCase):
 
         Summary._cp_config['tools.authenticate.on'] = False  # pylint: disable=protected-access
 
-        cherrypy.tree.mount(RbdMirror(), '/api/test/rbdmirror')
-        cherrypy.tree.mount(Summary(), '/api/test/summary')
+        cls.setup_controllers([RbdMirror, Summary], '/api/test')
 
     @mock.patch('dashboard.controllers.rbd_mirroring.rbd')
     def test_default(self, rbd_mock):  # pylint: disable=W0613
index 645695a1227eb12eab24ebcd1fd7277978349383..f7b3ff08f85074e46ec6c01b0c1098fae2ed11af 100644 (file)
@@ -1,7 +1,5 @@
 from __future__ import absolute_import
 
-import cherrypy
-
 from .. import mgr
 from ..controllers.tcmu_iscsi import TcmuIscsi
 from .helper import ControllerTestCase
@@ -73,10 +71,10 @@ class TcmuIscsiControllerTest(ControllerTestCase):
         mgr.url_prefix = ''
         TcmuIscsi._cp_config['tools.authenticate.on'] = False  # pylint: disable=protected-access
 
-        cherrypy.tree.mount(TcmuIscsi(), "/api/test/tcmu")
+        cls.setup_controllers(TcmuIscsi, "/api/test")
 
     def test_list(self):
-        self._get('/api/test/tcmu')
+        self._get('/api/test/tcmuiscsi')
         self.assertStatus(200)
         self.assertJsonBody({
             'daemons': [
index ff79ccbeb0215e4acd31ca2e1bf3b0c5a53a4c28..7812f6a749fbb840bcbe88b89db543f45e7a0944 100644 (file)
@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
-import cherrypy
 from cherrypy.lib.sessions import RamSession
 from mock import patch
 
@@ -14,15 +13,15 @@ from ..tools import RESTController, ApiController
 class FooResource(RESTController):
     elems = []
 
-    def list(self, *vpath, **params):
+    def list(self):
         return FooResource.elems
 
-    def create(self, data, *args, **kwargs):
+    def create(self, data):
         FooResource.elems.append(data)
         return data
 
-    def get(self, key, *args, **kwargs):
-        return {'detail': (key, args)}
+    def get(self, key):
+        return {'detail': (key, [])}
 
     def delete(self, key):
         del FooResource.elems[int(key)]
@@ -35,10 +34,16 @@ class FooResource(RESTController):
         return dict(key=key, **data)
 
 
+@ApiController('foo/:key/:method')
+class FooResourceDetail(RESTController):
+    def list(self, key, method):
+        return {'detail': (key, [method])}
+
+
 @ApiController('fooargs')
 class FooArgs(RESTController):
     @RESTController.args_from_json
-    def set(self, code, name, opt1=None, opt2=None):
+    def set(self, code, name=None, opt1=None, opt2=None):
         return {'code': code, 'name': name, 'opt1': opt1, 'opt2': opt2}
 
 
@@ -52,7 +57,7 @@ class RESTControllerTest(ControllerTestCase):
 
     @classmethod
     def setup_server(cls):
-        cherrypy.tree.mount(Root())
+        cls.setup_controllers([FooResource, FooResourceDetail, FooArgs])
 
     def test_empty(self):
         self._delete("/foo")
@@ -84,11 +89,11 @@ class RESTControllerTest(ControllerTestCase):
 
     def test_not_implemented(self):
         self._put("/foo")
-        self.assertStatus(405)
+        self.assertStatus(404)
         body = self.jsonBody()
         self.assertIsInstance(body, dict)
-        assert body['detail'] == 'Method not implemented.'
-        assert '405' in body['status']
+        assert body['detail'] == "The path '/foo' was not found."
+        assert '404' in body['status']
         assert 'traceback' in body
 
     def test_args_from_json(self):
@@ -112,7 +117,7 @@ class RESTControllerTest(ControllerTestCase):
         self.assertJsonBody({'detail': ['1', ['detail']]})
 
         self._post('/foo/1/detail', 'post-data')
-        self.assertStatus(405)
+        self.assertStatus(404)
 
     def test_developer_page(self):
         self.getPage('/foo', headers=[('Accept', 'text/html')])
@@ -126,8 +131,4 @@ class RESTControllerTest(ControllerTestCase):
         self.getPage('/foo',
                      headers=[('Accept', 'text/html'), ('Content-Length', '0')],
                      method='put')
-        assert '<p>PUT' in self.body.decode('utf-8')
-        assert 'Exception' in self.body.decode('utf-8')
-        assert 'Content-Type: text/html' in self.body.decode('utf-8')
-        assert '<form action="/api/foo/" method="post">' in self.body.decode('utf-8')
-        assert '<input type="hidden" name="_method" value="post" />' in self.body.decode('utf-8')
+        self.assertStatus(404)
index d4f175d89257ed1eb28a292b13d98719eafaa28a..14c41197e7e12eac7c09c08332e77a0526556561 100644 (file)
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# pylint: disable=W0212
+# pylint: disable=W0212,too-many-lines
 from __future__ import absolute_import
 
 import collections
@@ -19,7 +19,7 @@ import cherrypy
 from six import add_metaclass
 
 from .settings import Settings
-from . import logger, mgr
+from . import logger
 
 
 def ApiController(path):
@@ -69,17 +69,85 @@ def load_controllers():
             # Controllers MUST be derived from the class BaseController.
             if inspect.isclass(cls) and issubclass(cls, BaseController) and \
                     hasattr(cls, '_cp_controller_'):
+                if cls._cp_path_.startswith(':'):
+                    # invalid _cp_path_ value
+                    logger.error("Invalid url prefix '%s' for controller '%s'",
+                                 cls._cp_path_, cls.__name__)
+                    continue
                 controllers.append(cls)
 
     return controllers
 
 
+def generate_controller_routes(ctrl_class, mapper, base_url):
+    inst = ctrl_class()
+    for methods, url_suffix, action, params in ctrl_class.endpoints():
+        if not url_suffix:
+            name = ctrl_class.__name__
+            url = "{}/{}".format(base_url, ctrl_class._cp_path_)
+        else:
+            name = "{}:{}".format(ctrl_class.__name__, url_suffix)
+            url = "{}/{}/{}".format(base_url, ctrl_class._cp_path_, url_suffix)
+
+        if params:
+            for param in params:
+                url = "{}/:{}".format(url, param)
+
+        conditions = dict(method=methods) if methods else None
+
+        logger.debug("Mapping [%s] to %s:%s restricted to %s",
+                     url, ctrl_class.__name__, action, methods)
+        mapper.connect(name, url, controller=inst, action=action,
+                       conditions=conditions)
+
+        # adding route with trailing slash
+        name += "/"
+        url += "/"
+        mapper.connect(name, url, controller=inst, action=action,
+                       conditions=conditions)
+
+
+def generate_routes(url_prefix):
+    mapper = cherrypy.dispatch.RoutesDispatcher()
+    ctrls = load_controllers()
+    for ctrl in ctrls:
+        generate_controller_routes(ctrl, mapper, "{}/api".format(url_prefix))
+
+    mapper.connect(ApiRoot.__name__, "{}/api".format(url_prefix),
+                   controller=ApiRoot("{}/api".format(url_prefix),
+                                      ctrls))
+    return mapper
+
+
 def json_error_page(status, message, traceback, version):
     cherrypy.response.headers['Content-Type'] = 'application/json'
     return json.dumps(dict(status=status, detail=message, traceback=traceback,
                            version=version))
 
 
+class ApiRoot(object):
+
+    _cp_config = {
+        'tools.sessions.on': True,
+        'tools.authenticate.on': True
+    }
+
+    def __init__(self, base_url, ctrls):
+        self.base_url = base_url
+        self.ctrls = ctrls
+
+    def __call__(self):
+        tpl = """API Endpoints:<br>
+        <ul>
+        {lis}
+        </ul>
+        """
+        endpoints = ['<li><a href="{}/{}">{}</a></li>'
+                     .format(self.base_url, ctrl._cp_path_, ctrl.__name__) for
+                     ctrl in self.ctrls]
+        return tpl.format(lis='\n'.join(endpoints))
+
+
 # pylint: disable=too-many-locals
 def browsable_api_view(meth):
     def wrapper(self, *vpath, **kwargs):
@@ -190,8 +258,7 @@ class BaseControllerMeta(type):
     def __new__(mcs, name, bases, dct):
         new_cls = type.__new__(mcs, name, bases, dct)
 
-        for a_name in new_cls.__dict__:
-            thing = new_cls.__dict__[a_name]
+        for a_name, thing in new_cls.__dict__.items():
             if isinstance(thing, (types.FunctionType, types.MethodType))\
                     and getattr(thing, 'exposed', False):
 
@@ -208,9 +275,189 @@ class BaseController(object):
     Base class for all controllers providing API endpoints.
     """
 
+    def __init__(self):
+        logger.info('Initializing controller: %s -> /api/%s',
+                    self.__class__.__name__, self._cp_path_)
+
+    @classmethod
+    def _parse_function_args(cls, func):
+        # pylint: disable=deprecated-method
+        if sys.version_info > (3, 0):  # pylint: disable=no-else-return
+            sig = inspect.signature(func)
+            cargs = [k for k, v in sig.parameters.items()
+                     if k != 'self' and v.default is inspect.Parameter.empty and
+                     (v.kind == inspect.Parameter.POSITIONAL_ONLY or
+                      v.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD)]
+        else:
+            args = inspect.getargspec(func)
+            nd = len(args.args) if not args.defaults else -len(args.defaults)
+            cargs = args.args[1:nd]
+
+        # filter out controller path params
+        for idx, step in enumerate(cls._cp_path_.split('/')):
+            if step[0] == ':':
+                param = step[1:]
+                if param not in cargs:
+                    raise Exception("function '{}' does not have the"
+                                    " positional argument '{}' in the {} "
+                                    "position".format(func, param, idx))
+                cargs.remove(param)
+        return cargs
+
+    @classmethod
+    def endpoints(cls):
+        result = []
+
+        def isfunction(m):
+            return inspect.isfunction(m) or inspect.ismethod(m)
+
+        for attr, val in inspect.getmembers(cls, predicate=isfunction):
+            if (hasattr(val, 'exposed') and val.exposed):
+                args = cls._parse_function_args(val)
+                suffix = attr
+                action = attr
+                if attr == '__call__':
+                    suffix = None
+                result.append(([], suffix, action, args))
+        return result
+
+
+class RESTController(BaseController):
+    """
+    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
+
+    """
+
+    _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),
+    }
+
+    @classmethod
+    def endpoints(cls):
+
+        def isfunction(m):
+            return inspect.isfunction(m) or inspect.ismethod(m)
+
+        result = []
+        for attr, val in inspect.getmembers(cls, predicate=isfunction):
+            if hasattr(val, 'exposed') and val.exposed and \
+                    attr != '_collection' and attr != '_element':
+                result.append(([], attr, attr, cls._parse_function_args(val)))
+
+        methods = []
+        for k, v in cls._method_mapping.items():
+            if not k[1] and hasattr(cls, v[0]):
+                methods.append(k[0])
+        if methods:
+            result.append((methods, None, '_collection', []))
+        methods = []
+        args = []
+        for k, v in cls._method_mapping.items():
+            if k[1] and hasattr(cls, v[0]):
+                methods.append(k[0])
+                if not args:
+                    args = cls._parse_function_args(getattr(cls, v[0]))
+        if methods:
+            result.append((methods, None, '_element', args))
+
+        return result
+
     @cherrypy.expose
-    def default(self, *_vpath, **_params):
-        raise cherrypy.NotFound()
+    def _collection(self, *vpath, **params):
+        return self._rest_request(False, *vpath, **params)
+
+    @cherrypy.expose
+    def _element(self, *vpath, **params):
+        return self._rest_request(True, *vpath, **params)
+
+    def _rest_request(self, is_element, *vpath, **params):
+        method_name, status_code = self._method_mapping[
+            (cherrypy.request.method, is_element)]
+        method = getattr(self, method_name, None)
+
+        if cherrypy.request.method not in ['GET', 'DELETE']:
+            method = RESTController._takes_json(method)
+
+        if cherrypy.request.method != 'DELETE':
+            method = RESTController._returns_json(method)
+
+        cherrypy.response.status = status_code
+
+        return method(*vpath, **params)
+
+    @staticmethod
+    def args_from_json(func):
+        func._args_from_json_ = True
+        return func
+
+    @staticmethod
+    def _function_args(func):
+        if sys.version_info > (3, 0):  # pylint: disable=no-else-return
+            return list(inspect.signature(func).parameters.keys())
+        else:
+            return inspect.getargspec(func).args[1:]  # pylint: disable=deprecated-method
+
+    # pylint: disable=W1505
+    @staticmethod
+    def _takes_json(func):
+        def inner(*args, **kwargs):
+            if cherrypy.request.headers.get('Content-Type',
+                                            '') == 'application/x-www-form-urlencoded':
+                if hasattr(func, '_args_from_json_'):  # pylint: disable=no-else-return
+                    return func(*args, **kwargs)
+                else:
+                    return func(kwargs)
+
+            content_length = int(cherrypy.request.headers['Content-Length'])
+            body = cherrypy.request.body.read(content_length)
+            if not body:
+                raise cherrypy.HTTPError(400, 'Empty body. Content-Length={}'
+                                         .format(content_length))
+            try:
+                data = json.loads(body.decode('utf-8'))
+            except Exception as e:
+                raise cherrypy.HTTPError(400, 'Failed to decode JSON: {}'
+                                         .format(str(e)))
+            if hasattr(func, '_args_from_json_'):
+                kwargs.update(data.items())
+                return func(*args, **kwargs)
+
+            return func(data, *args, **kwargs)
+        return inner
+
+    @staticmethod
+    def _returns_json(func):
+        def inner(*args, **kwargs):
+            cherrypy.response.headers['Content-Type'] = 'application/json'
+            ret = func(*args, **kwargs)
+            return json.dumps(ret).encode('utf8')
+        return inner
 
 
 class RequestLoggingTool(cherrypy.Tool):
@@ -405,145 +652,6 @@ class ViewCache(object):
         return wrapper
 
 
-class RESTController(BaseController):
-    """
-    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
-
-    """
-
-    def _not_implemented(self, is_sub_path):
-        methods = [method
-                   for ((method, _is_element), (meth, _))
-                   in self._method_mapping.items()
-                   if _is_element == is_sub_path is not None 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),
-    }
-
-    def _get_method(self, vpath):
-        is_sub_path = bool(len(vpath))
-        try:
-            method_name, status_code = self._method_mapping[
-                (cherrypy.request.method, is_sub_path)]
-        except KeyError:
-            self._not_implemented(is_sub_path)
-        method = getattr(self, method_name, None)
-        if not method:
-            self._not_implemented(is_sub_path)
-        return method, status_code
-
-    @cherrypy.expose
-    def default(self, *vpath, **params):
-        if cherrypy.request.path_info.startswith(
-                '{}/api/{}/default'.format(mgr.url_prefix, self._cp_path_)) or \
-                cherrypy.request.path_info.startswith('/{}/default'.format(self._cp_path_)):
-            # These two calls to default() are identical: `vpath` and
-            # params` are both empty:
-            # $ curl 'http://localhost/api/cp_path/'
-            # and
-            # $ curl 'http://localhost/api/cp_path/default'
-            # But we need to distinguish them. To fix this, we need
-            # to add the missing `default`
-            vpath = ['default'] + list(vpath)
-
-        method, status_code = self._get_method(vpath)
-
-        if cherrypy.request.method not in ['GET', 'DELETE']:
-            method = RESTController._takes_json(method)
-
-        if cherrypy.request.method != 'DELETE':
-            method = RESTController._returns_json(method)
-
-        cherrypy.response.status = status_code
-
-        return method(*vpath, **params)
-
-    @staticmethod
-    def args_from_json(func):
-        func._args_from_json_ = True
-        return func
-
-    @staticmethod
-    def _function_args(func):
-        if sys.version_info > (3, 0):  # pylint: disable=no-else-return
-            return list(inspect.signature(func).parameters.keys())
-        else:
-            return inspect.getargspec(func).args[1:]  # pylint: disable=deprecated-method
-
-    # pylint: disable=W1505
-    @staticmethod
-    def _takes_json(func):
-        def inner(*args, **kwargs):
-            if cherrypy.request.headers.get('Content-Type',
-                                            '') == 'application/x-www-form-urlencoded':
-                if hasattr(func, '_args_from_json_'):  # pylint: disable=no-else-return
-                    return func(*args, **kwargs)
-                else:
-                    return func(kwargs)
-
-            content_length = int(cherrypy.request.headers['Content-Length'])
-            body = cherrypy.request.body.read(content_length)
-            if not body:
-                raise cherrypy.HTTPError(400, 'Empty body. Content-Length={}'
-                                         .format(content_length))
-            try:
-                data = json.loads(body.decode('utf-8'))
-            except Exception as e:
-                raise cherrypy.HTTPError(400, 'Failed to decode JSON: {}'
-                                         .format(str(e)))
-            if hasattr(func, '_args_from_json_'):
-                kwargs.update(data.items())
-                return func(*args, **kwargs)
-
-            return func(data, *args, **kwargs)
-        return inner
-
-    @staticmethod
-    def _returns_json(func):
-        def inner(*args, **kwargs):
-            cherrypy.response.headers['Content-Type'] = 'application/json'
-            ret = func(*args, **kwargs)
-            return json.dumps(ret).encode('utf8')
-        return inner
-
-    @staticmethod
-    def split_vpath(vpath):
-        if not vpath:
-            return None, None
-        if len(vpath) == 1:
-            return vpath[0], None
-        return vpath[0], vpath[1]
-
-
 class Session(object):
     """
     This class contains all relevant settings related to cherrypy.session.