]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: move all controller infrastructure to controllers/__init__.py 21239/head
authorRicardo Dias <rdias@suse.com>
Wed, 4 Apr 2018 16:25:16 +0000 (17:25 +0100)
committerRicardo Dias <rdias@suse.com>
Wed, 4 Apr 2018 16:41:14 +0000 (17:41 +0100)
Signed-off-by: Ricardo Dias <rdias@suse.com>
20 files changed:
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/auth.py
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/controllers/cluster_configuration.py
src/pybind/mgr/dashboard/controllers/dashboard.py
src/pybind/mgr/dashboard/controllers/host.py
src/pybind/mgr/dashboard/controllers/monitor.py
src/pybind/mgr/dashboard/controllers/osd.py
src/pybind/mgr/dashboard/controllers/perf_counters.py
src/pybind/mgr/dashboard/controllers/pool.py
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/controllers/summary.py
src/pybind/mgr/dashboard/controllers/task.py
src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/tests/helper.py
src/pybind/mgr/dashboard/tests/test_tools.py
src/pybind/mgr/dashboard/tools.py

index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3eaa28f7133206ac0a52b29a702746c69e05790b 100644 (file)
@@ -0,0 +1,465 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=W0212
+from __future__ import absolute_import
+
+import collections
+from datetime import datetime, timedelta
+import fnmatch
+import importlib
+import inspect
+import json
+import os
+import pkgutil
+import sys
+import time
+import threading
+import types  # pylint: disable=import-error
+
+import cherrypy
+from six import add_metaclass
+
+from .. import logger
+from ..settings import Settings
+from ..tools import Session
+
+
+def ApiController(path):
+    def decorate(cls):
+        cls._cp_controller_ = True
+        cls._cp_path_ = path
+        config = {
+            'tools.sessions.on': True,
+            'tools.sessions.name': Session.NAME,
+            'tools.session_expire_at_browser_close.on': True
+        }
+        if not hasattr(cls, '_cp_config'):
+            cls._cp_config = {}
+        if 'tools.authenticate.on' not in cls._cp_config:
+            config['tools.authenticate.on'] = False
+        cls._cp_config.update(config)
+        return cls
+    return decorate
+
+
+def AuthRequired(enabled=True):
+    def decorate(cls):
+        if not hasattr(cls, '_cp_config'):
+            cls._cp_config = {
+                'tools.authenticate.on': enabled
+            }
+        else:
+            cls._cp_config['tools.authenticate.on'] = enabled
+        return cls
+    return decorate
+
+
+def load_controllers():
+    # setting sys.path properly when not running under the mgr
+    controllers_dir = os.path.dirname(os.path.realpath(__file__))
+    dashboard_dir = os.path.dirname(controllers_dir)
+    mgr_dir = os.path.dirname(dashboard_dir)
+    logger.debug("LC: controllers_dir=%s", controllers_dir)
+    logger.debug("LC: dashboard_dir=%s", dashboard_dir)
+    logger.debug("LC: mgr_dir=%s", mgr_dir)
+    if mgr_dir not in sys.path:
+        sys.path.append(mgr_dir)
+
+    controllers = []
+    mods = [mod for _, mod, _ in pkgutil.iter_modules([controllers_dir])]
+    logger.debug("LC: mods=%s", mods)
+    for mod_name in mods:
+        mod = importlib.import_module('.controllers.{}'.format(mod_name),
+                                      package='dashboard')
+        for _, cls in mod.__dict__.items():
+            # 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):
+        assert isinstance(self, BaseController)
+        if not Settings.ENABLE_BROWSABLE_API:
+            return meth(self, *vpath, **kwargs)
+        if 'text/html' not in cherrypy.request.headers.get('Accept', ''):
+            return meth(self, *vpath, **kwargs)
+        if '_method' in kwargs:
+            cherrypy.request.method = kwargs.pop('_method').upper()
+        if '_raw' in kwargs:
+            kwargs.pop('_raw')
+            return meth(self, *vpath, **kwargs)
+
+        template = """
+        <html>
+        <h1>Browsable API</h1>
+        {docstring}
+        <h2>Request</h2>
+        <p>{method} {breadcrump}</p>
+        <h2>Response</h2>
+        <p>Status: {status_code}<p>
+        <pre>{reponse_headers}</pre>
+        <form action="/api/{path}/{vpath}" method="get">
+        <input type="hidden" name="_raw" value="true" />
+        <button type="submit">GET raw data</button>
+        </form>
+        <h2>Data</h2>
+        <pre>{data}</pre>
+        {create_form}
+        <h2>Note</h2>
+        <p>Please note that this API is not an official Ceph REST API to be
+        used by third-party applications. It's primary purpose is to serve
+        the requirements of the Ceph Dashboard and is subject to change at
+        any time. Use at your own risk.</p>
+        """
+
+        create_form_template = """
+        <h2>Create Form</h2>
+        <form action="/api/{path}/{vpath}" method="post">
+        {fields}<br>
+        <input type="hidden" name="_method" value="post" />
+        <button type="submit">Create</button>
+        </form>
+        """
+
+        try:
+            data = meth(self, *vpath, **kwargs)
+        except Exception as e:  # pylint: disable=broad-except
+            except_template = """
+            <h2>Exception: {etype}: {tostr}</h2>
+            <pre>{trace}</pre>
+            Params: {kwargs}
+            """
+            import traceback
+            tb = sys.exc_info()[2]
+            cherrypy.response.headers['Content-Type'] = 'text/html'
+            data = except_template.format(
+                etype=e.__class__.__name__,
+                tostr=str(e),
+                trace='\n'.join(traceback.format_tb(tb)),
+                kwargs=kwargs
+            )
+
+        if cherrypy.response.headers['Content-Type'] == 'application/json':
+            data = json.dumps(json.loads(data), indent=2, sort_keys=True)
+
+        try:
+            create = getattr(self, 'create')
+            f_args = RESTController._function_args(create)
+            input_fields = ['{name}:<input type="text" name="{name}">'.format(name=name) for name in
+                            f_args]
+            create_form = create_form_template.format(
+                fields='<br>'.join(input_fields),
+                path=self._cp_path_,
+                vpath='/'.join(vpath)
+            )
+        except AttributeError:
+            create_form = ''
+
+        def mk_breadcrump(elems):
+            return '/'.join([
+                '<a href="/{}">{}</a>'.format('/'.join(elems[0:i+1]), e)
+                for i, e in enumerate(elems)
+            ])
+
+        cherrypy.response.headers['Content-Type'] = 'text/html'
+        return template.format(
+            docstring='<pre>{}</pre>'.format(self.__doc__) if self.__doc__ else '',
+            method=cherrypy.request.method,
+            path=self._cp_path_,
+            vpath='/'.join(vpath),
+            breadcrump=mk_breadcrump(['api', self._cp_path_] + list(vpath)),
+            status_code=cherrypy.response.status,
+            reponse_headers='\n'.join(
+                '{}: {}'.format(k, v) for k, v in cherrypy.response.headers.items()),
+            data=data,
+            create_form=create_form
+        )
+
+    wrapper.exposed = True
+    if hasattr(meth, '_cp_config'):
+        wrapper._cp_config = meth._cp_config
+    return wrapper
+
+
+class BaseControllerMeta(type):
+    def __new__(mcs, name, bases, dct):
+        new_cls = type.__new__(mcs, name, bases, dct)
+
+        for a_name, thing in new_cls.__dict__.items():
+            if isinstance(thing, (types.FunctionType, types.MethodType))\
+                    and getattr(thing, 'exposed', False):
+
+                # @cherrypy.tools.json_out() is incompatible with our browsable_api_view decorator.
+                cp_config = getattr(thing, '_cp_config', {})
+                if not cp_config.get('tools.json_out.on', False):
+                    setattr(new_cls, a_name, browsable_api_view(thing))
+        return new_cls
+
+
+@add_metaclass(BaseControllerMeta)
+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 _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
index 1e40d022e2cbbdda05f08a9d78bcd4a2f0567ca3..cd9d9237dd6850ad50adff0fc61188d11fe8725c 100644 (file)
@@ -6,8 +6,9 @@ import time
 import bcrypt
 import cherrypy
 
-from ..tools import ApiController, RESTController, Session
+from . import ApiController, RESTController
 from .. import logger, mgr
+from ..tools import Session
 
 
 @ApiController('auth')
index 39a5799acd7cd599e719e2dffdba19320232f4b5..f375a950da4a8fbc7c1f5f12a4713e2db389a458 100644 (file)
@@ -7,8 +7,9 @@ import cherrypy
 
 from ..services.ceph_service import CephService
 
+from . import ApiController, AuthRequired, BaseController
 from .. import mgr
-from ..tools import ApiController, AuthRequired, BaseController, ViewCache
+from ..tools import ViewCache
 
 
 @ApiController('cephfs')
index d02027b1c656819dd0cda6ade46b03c7f07ccc5d..fd8388d2b8765aca76588a8383d55bd872d36162 100644 (file)
@@ -4,7 +4,7 @@ from __future__ import absolute_import
 import cherrypy
 
 from .. import mgr
-from ..tools import ApiController, RESTController, AuthRequired
+from . import ApiController, RESTController, AuthRequired
 
 
 @ApiController('cluster_conf')
index 882f5072eb167ff89936389f39ab4655db43c227..cf6ec6bf87072efc0404cc1feac23dcb8bf6935f 100644 (file)
@@ -6,9 +6,10 @@ import json
 
 import cherrypy
 
+from . import ApiController, AuthRequired, BaseController
 from .. import mgr
 from ..services.ceph_service import CephService
-from ..tools import ApiController, AuthRequired, BaseController, NotificationQueue
+from ..tools import NotificationQueue
 
 
 LOG_BUFFER_SIZE = 30
index 8bef07185ae9932762fc7c5ac245fa01ee5d6704..f93d943b66485bf9df27227900ab3ab91a40df52 100644 (file)
@@ -1,8 +1,8 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
+from . import ApiController, AuthRequired, RESTController
 from .. import mgr
-from ..tools import ApiController, AuthRequired, RESTController
 
 
 @ApiController('host')
index 3a57f55b558df57689a4228a140cbe2eabd08700..d429e7c303cdd4121a071c2ecbb53c92707698ad 100644 (file)
@@ -5,8 +5,8 @@ import json
 
 import cherrypy
 
+from . import ApiController, AuthRequired, BaseController
 from .. import mgr
-from ..tools import ApiController, AuthRequired, BaseController
 
 
 @ApiController('monitor')
index f777f083457e5ec5c51e402186901f4bd31d699c..f4a4e68c22a9f2503a59b5c98409498ce09a765e 100644 (file)
@@ -5,8 +5,8 @@ import json
 
 from mgr_module import CommandResult
 
+from . import ApiController, AuthRequired, RESTController
 from .. import logger, mgr
-from ..tools import ApiController, AuthRequired, RESTController
 
 
 @ApiController('osd')
index 8813fcb5624f1b5dd53827473893096f208e5a63..af0631a0a6659795176a2120caf4e2300e6de66a 100644 (file)
@@ -1,8 +1,8 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
+from . import ApiController, AuthRequired, RESTController
 from .. import mgr
-from ..tools import ApiController, AuthRequired, RESTController
 
 
 class PerfCounter(RESTController):
index 9a5176f646e5df9bc5165f97b0a97057e20e4404..a9231ada23f1fbb9b02d263d7550d89b4f20c93a 100644 (file)
@@ -3,9 +3,9 @@ from __future__ import absolute_import
 
 import cherrypy
 
+from . import ApiController, RESTController, AuthRequired
 from .. import mgr
 from ..services.ceph_service import CephService
-from ..tools import ApiController, RESTController, AuthRequired
 
 
 @ApiController('pool')
index 585d2087405e805a9ff8c59cbb41ae0c65187987..5303fb4bfe830dea67a1a06f7b32e92cb5b93d31 100644 (file)
@@ -5,8 +5,9 @@ import math
 import cherrypy
 import rbd
 
+from . import ApiController, AuthRequired, RESTController
 from .. import mgr
-from ..tools import ApiController, AuthRequired, RESTController, ViewCache
+from ..tools import ViewCache
 
 
 @ApiController('rbd')
index 118183160ea082f0a5c94c86902444ce361b5913..8fb1ec56577d702372b99110748630ea6d464bfb 100644 (file)
@@ -9,9 +9,10 @@ from functools import partial
 import cherrypy
 import rbd
 
+from . import ApiController, AuthRequired, BaseController
 from .. import logger, mgr
 from ..services.ceph_service import CephService
-from ..tools import ApiController, AuthRequired, BaseController, ViewCache
+from ..tools import ViewCache
 
 
 @ViewCache()
index 6d31fd35769ae70bd7f9d65ec24153bdb64c9a13..d0e60aa5946c29cb70d74bc905ddf2f6060db94e 100644 (file)
@@ -3,9 +3,9 @@ from __future__ import absolute_import
 
 import json
 
+from . import ApiController, RESTController, AuthRequired
 from .. import logger
 from ..services.ceph_service import CephService
-from ..tools import ApiController, RESTController, AuthRequired
 
 
 @ApiController('rgw')
index 002f2b71c18eec6cec4dd4477dff5ad1c94b3681..c2b7c4f601543c86e51ba108d31f297bab81a3cd 100644 (file)
@@ -5,10 +5,11 @@ import json
 
 import cherrypy
 
+from . import AuthRequired, ApiController, BaseController
 from .. import logger, mgr
 from ..controllers.rbd_mirroring import get_daemons_and_pools
-from ..tools import AuthRequired, ApiController, BaseController, TaskManager
 from ..services.ceph_service import CephService
+from ..tools import TaskManager
 
 
 @ApiController('summary')
index 4ec9204b6e104389bc4492bb6457f2c4e933d2d0..b986b172aec4c0a7d138bae7483868b59b0d9e1d 100644 (file)
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
-from ..tools import ApiController, AuthRequired, RESTController, TaskManager
+from . import ApiController, AuthRequired, RESTController
+from ..tools import TaskManager
 
 
 @ApiController('task')
index 1838b16c2c8791d96c5b1b94b6a62597c9c40a07..2b32a9a69aa1d2734e9468dd7617126dcd01ba47 100644 (file)
@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
+from . import ApiController, AuthRequired, RESTController
 from .. import mgr
 from ..services.ceph_service import CephService
-from ..tools import ApiController, AuthRequired, RESTController
 
 SERVICE_TYPE = 'tcmu-runner'
 
index 9221d8a4288e63d29b2872823199c047b47ca889..5bd0007af31d515d021cffc10a81be4b30fcc936 100644 (file)
@@ -26,9 +26,10 @@ if 'COVERAGE_ENABLED' in os.environ:
 
 # pylint: disable=wrong-import-position
 from . import logger, mgr
+from .controllers import generate_routes, json_error_page
 from .controllers.auth import Auth
-from .tools import generate_routes, json_error_page, SessionExpireAtBrowserCloseTool, \
-                   NotificationQueue, RequestLoggingTool, TaskManager
+from .tools import SessionExpireAtBrowserCloseTool, NotificationQueue, \
+                   RequestLoggingTool, TaskManager
 from .settings import options_command_list, handle_option_command
 
 
index 4557c528c5a0e2c8203dcce208921419da2087ab..a503c45006733dcff1be34175aa80a9c602ea1e1 100644 (file)
@@ -8,8 +8,8 @@ import cherrypy
 from cherrypy.test import helper
 
 from ..controllers.auth import Auth
-from ..tools import json_error_page, SessionExpireAtBrowserCloseTool, \
-                    generate_controller_routes
+from ..controllers import json_error_page, generate_controller_routes
+from ..tools import SessionExpireAtBrowserCloseTool
 
 
 class ControllerTestCase(helper.CPWebCase):
index 7812f6a749fbb840bcbe88b89db543f45e7a0944..da74f69d44e0771ea7996a1c84f02eff4e28fce1 100644 (file)
@@ -5,7 +5,7 @@ from cherrypy.lib.sessions import RamSession
 from mock import patch
 
 from .helper import ControllerTestCase
-from ..tools import RESTController, ApiController
+from ..controllers import RESTController, ApiController
 
 
 # pylint: disable=W0613
index 14c41197e7e12eac7c09c08332e77a0526556561..6bc094ef496c7b9c933fe308b0000efbd1bf07c8 100644 (file)
 # -*- coding: utf-8 -*-
-# pylint: disable=W0212,too-many-lines
+# pylint: disable=W0212
 from __future__ import absolute_import
 
 import collections
 from datetime import datetime, timedelta
 import fnmatch
-import importlib
-import inspect
-import json
-import os
-import pkgutil
-import sys
 import time
 import threading
-import types  # pylint: disable=import-error
 
 import cherrypy
-from six import add_metaclass
 
-from .settings import Settings
 from . import logger
 
 
-def ApiController(path):
-    def decorate(cls):
-        cls._cp_controller_ = True
-        cls._cp_path_ = path
-        config = {
-            'tools.sessions.on': True,
-            'tools.sessions.name': Session.NAME,
-            'tools.session_expire_at_browser_close.on': True
-        }
-        if not hasattr(cls, '_cp_config'):
-            cls._cp_config = {}
-        if 'tools.authenticate.on' not in cls._cp_config:
-            config['tools.authenticate.on'] = False
-        cls._cp_config.update(config)
-        return cls
-    return decorate
-
-
-def AuthRequired(enabled=True):
-    def decorate(cls):
-        if not hasattr(cls, '_cp_config'):
-            cls._cp_config = {
-                'tools.authenticate.on': enabled
-            }
-        else:
-            cls._cp_config['tools.authenticate.on'] = enabled
-        return cls
-    return decorate
-
-
-def load_controllers():
-    # setting sys.path properly when not running under the mgr
-    dashboard_dir = os.path.dirname(os.path.realpath(__file__))
-    mgr_dir = os.path.dirname(dashboard_dir)
-    if mgr_dir not in sys.path:
-        sys.path.append(mgr_dir)
-
-    controllers = []
-    ctrls_path = '{}/controllers'.format(dashboard_dir)
-    mods = [mod for _, mod, _ in pkgutil.iter_modules([ctrls_path])]
-    for mod_name in mods:
-        mod = importlib.import_module('.controllers.{}'.format(mod_name),
-                                      package='dashboard')
-        for _, cls in mod.__dict__.items():
-            # 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):
-        assert isinstance(self, BaseController)
-        if not Settings.ENABLE_BROWSABLE_API:
-            return meth(self, *vpath, **kwargs)
-        if 'text/html' not in cherrypy.request.headers.get('Accept', ''):
-            return meth(self, *vpath, **kwargs)
-        if '_method' in kwargs:
-            cherrypy.request.method = kwargs.pop('_method').upper()
-        if '_raw' in kwargs:
-            kwargs.pop('_raw')
-            return meth(self, *vpath, **kwargs)
-
-        template = """
-        <html>
-        <h1>Browsable API</h1>
-        {docstring}
-        <h2>Request</h2>
-        <p>{method} {breadcrump}</p>
-        <h2>Response</h2>
-        <p>Status: {status_code}<p>
-        <pre>{reponse_headers}</pre>
-        <form action="/api/{path}/{vpath}" method="get">
-        <input type="hidden" name="_raw" value="true" />
-        <button type="submit">GET raw data</button>
-        </form>
-        <h2>Data</h2>
-        <pre>{data}</pre>
-        {create_form}
-        <h2>Note</h2>
-        <p>Please note that this API is not an official Ceph REST API to be
-        used by third-party applications. It's primary purpose is to serve
-        the requirements of the Ceph Dashboard and is subject to change at
-        any time. Use at your own risk.</p>
-        """
-
-        create_form_template = """
-        <h2>Create Form</h2>
-        <form action="/api/{path}/{vpath}" method="post">
-        {fields}<br>
-        <input type="hidden" name="_method" value="post" />
-        <button type="submit">Create</button>
-        </form>
-        """
-
-        try:
-            data = meth(self, *vpath, **kwargs)
-        except Exception as e:  # pylint: disable=broad-except
-            except_template = """
-            <h2>Exception: {etype}: {tostr}</h2>
-            <pre>{trace}</pre>
-            Params: {kwargs}
-            """
-            import traceback
-            tb = sys.exc_info()[2]
-            cherrypy.response.headers['Content-Type'] = 'text/html'
-            data = except_template.format(
-                etype=e.__class__.__name__,
-                tostr=str(e),
-                trace='\n'.join(traceback.format_tb(tb)),
-                kwargs=kwargs
-            )
-
-        if cherrypy.response.headers['Content-Type'] == 'application/json':
-            data = json.dumps(json.loads(data), indent=2, sort_keys=True)
-
-        try:
-            create = getattr(self, 'create')
-            f_args = RESTController._function_args(create)
-            input_fields = ['{name}:<input type="text" name="{name}">'.format(name=name) for name in
-                            f_args]
-            create_form = create_form_template.format(
-                fields='<br>'.join(input_fields),
-                path=self._cp_path_,
-                vpath='/'.join(vpath)
-            )
-        except AttributeError:
-            create_form = ''
-
-        def mk_breadcrump(elems):
-            return '/'.join([
-                '<a href="/{}">{}</a>'.format('/'.join(elems[0:i+1]), e)
-                for i, e in enumerate(elems)
-            ])
-
-        cherrypy.response.headers['Content-Type'] = 'text/html'
-        return template.format(
-            docstring='<pre>{}</pre>'.format(self.__doc__) if self.__doc__ else '',
-            method=cherrypy.request.method,
-            path=self._cp_path_,
-            vpath='/'.join(vpath),
-            breadcrump=mk_breadcrump(['api', self._cp_path_] + list(vpath)),
-            status_code=cherrypy.response.status,
-            reponse_headers='\n'.join(
-                '{}: {}'.format(k, v) for k, v in cherrypy.response.headers.items()),
-            data=data,
-            create_form=create_form
-        )
-
-    wrapper.exposed = True
-    if hasattr(meth, '_cp_config'):
-        wrapper._cp_config = meth._cp_config
-    return wrapper
-
-
-class BaseControllerMeta(type):
-    def __new__(mcs, name, bases, dct):
-        new_cls = type.__new__(mcs, name, bases, dct)
-
-        for a_name, thing in new_cls.__dict__.items():
-            if isinstance(thing, (types.FunctionType, types.MethodType))\
-                    and getattr(thing, 'exposed', False):
-
-                # @cherrypy.tools.json_out() is incompatible with our browsable_api_view decorator.
-                cp_config = getattr(thing, '_cp_config', {})
-                if not cp_config.get('tools.json_out.on', False):
-                    setattr(new_cls, a_name, browsable_api_view(thing))
-        return new_cls
-
-
-@add_metaclass(BaseControllerMeta)
-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 _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):
     def __init__(self):
         cherrypy.Tool.__init__(self, 'before_handler', self.request_begin,