]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Add Grafana proxy
authorPatrick Nawracay <pnawracay@suse.com>
Thu, 19 Apr 2018 07:29:41 +0000 (09:29 +0200)
committerPatrick Nawracay <pnawracay@suse.com>
Wed, 6 Jun 2018 09:11:27 +0000 (11:11 +0200)
Add the ability to connect the dashboard with a Grafana instance to
display graphs from Grafana incorporated into the dashboard.

Possible configuration options:

$ ceph dashboard set-grafana-api-host <host> # Default: 'localhost'
$ ceph dashboard set-grafana-api-port <port> # Default: 3000
$ ceph dashboard set-grafana-api-scheme <scheme> # Default: 'http'

The configuration can be checked using a GET request to the API endpoint
`/api/grafana/status`. It returns a JSON response in the following
format:

    {
"available": False,
"message": "<error message>",
    }

The message is not returned if Grafana is available.

The Grafana proxy is reachable through
`http(s)://<host>/api/grafana/proxy`.

Signed-off-by: Patrick Nawracay <pnawracay@suse.com>
doc/mgr/dashboard.rst
src/pybind/mgr/dashboard/controllers/grafana.py [new file with mode: 0644]
src/pybind/mgr/dashboard/settings.py
src/pybind/mgr/dashboard/tests/test_grafana.py [new file with mode: 0644]

index fc028038fde7ac470eab6524a4df01ece281fe79..c5238c0b0f74e88bd3fd6f8c64f5e419aa638f34 100644 (file)
@@ -232,6 +232,60 @@ exist and you may find yourself in the situation that you have to use them::
   $ ceph dashboard set-rgw-api-admin-resource <admin_resource>
   $ ceph dashboard set-rgw-api-user-id <user_id>
 
+Enabling the Embedding of Grafana Dashboards
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Grafana and Prometheus are likely going to be bundled and installed by some
+orchestration tools along Ceph in the near future, but currently, you will have
+to install and configure both manually. After you have installed Prometheus and
+Grafana on your preferred hosts, proceed with the following steps::
+
+ 1. Enable the Ceph Exporter which comes as Ceph Manager module by running::
+
+    $ ceph mgr module enable prometheus
+
+    More details can be found on the `documentation
+    <http://docs.ceph.com/docs/master/mgr/prometheus/>`_ of the prometheus
+    module.
+
+ 2. Add the corresponding scrape configuration to Prometheus. This may look
+    like::
+
+        global:
+          scrape_interval: 5s
+
+        scrape_configs:
+          - job_name: 'prometheus'
+            static_configs:
+              - targets: ['localhost:9090']
+          - job_name: 'ceph'
+            static_configs:
+              - targets: ['localhost:9283']
+          - job_name: 'node-exporter'
+            static_configs:
+              - targets: ['localhost:9100']
+
+ 3. Add Prometheus as data source to Grafana
+
+ 4. Install the `vonage-status-panel` plugin using::
+
+        grafana-cli plugins install vonage-status-panel
+
+ 4. Add the Dashboards to Grafana by importing them
+
+ 5. Configure Grafana in `/etc/grafana/grafana.ini` to adapt the URLs to the
+    Ceph Dashboard properly::
+
+        root_url = http://localhost:3000/api/grafana/proxy
+
+After you have configured Grafana and Prometheus, you will need to tell the
+Ceph Manager Dashboard where it can access Grafana and what the credentials are
+to do so. This can be done by using the following commands::
+
+  $ ceph dashboard set-grafana-api-url <url>  # default: 'http://localhost:3000'
+  $ ceph dashboard set-grafana-api-username <username> # default: 'admin'
+  $ ceph dashboard set-grafana-api-password <password> # default: 'admin'
+
 Accessing the dashboard
 ^^^^^^^^^^^^^^^^^^^^^^^
 
diff --git a/src/pybind/mgr/dashboard/controllers/grafana.py b/src/pybind/mgr/dashboard/controllers/grafana.py
new file mode 100644 (file)
index 0000000..37f2568
--- /dev/null
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import cherrypy
+import requests
+from six.moves.urllib.parse import urlparse  # pylint: disable=import-error
+
+from . import ApiController, BaseController, AuthRequired, Proxy, Endpoint
+from .. import logger
+from ..settings import Settings
+
+
+class GrafanaRestClient(object):
+    _instance = None
+
+    @staticmethod
+    def _raise_for_validation(url, user, password):
+        msg = 'No {} found or misconfigured, please consult the ' \
+              'documentation about how to configure Grafana for the dashboard.'
+
+        o = urlparse(url)
+        if not (o.netloc and o.scheme):
+            raise LookupError(msg.format('URL'))
+
+        if not all((user, password)):
+            raise LookupError(msg.format('credentials'))
+
+    def __init__(self, url, username, password):
+        """
+        :type url: str
+        :type username: str
+        :type password: str
+        """
+        self._raise_for_validation(url, username, password)
+
+        self._url = url.rstrip('/')
+        self._user = username
+        self._password = password
+
+    @classmethod
+    def instance(cls):
+        """
+        This method shall be used by default to create an instance and will use
+        the settings to retrieve the required credentials.
+
+        :rtype: GrafanaRestClient
+        """
+        if not cls._instance:
+            url = Settings.GRAFANA_API_URL
+            user = Settings.GRAFANA_API_USERNAME
+            password = Settings.GRAFANA_API_PASSWORD
+
+            cls._instance = GrafanaRestClient(url, user, password)
+
+        return cls._instance
+
+    def proxy_request(self, method, path, params, data):
+        url = '{}/{}'.format(self._url, path.lstrip('/'))
+
+        # Forwards some headers
+        headers = {k: v for k, v in cherrypy.request.headers.items()
+                   if k.lower() in ('content-type', 'accept')}
+
+        response = requests.request(
+            method,
+            url,
+            params=params,
+            data=data,
+            headers=headers,
+            auth=(self._user, self._password))
+        logger.debug("proxying method=%s path=%s params=%s data=%s", method,
+                     path, params, data)
+
+        return response
+
+    def is_service_online(self):
+        try:
+            response = self.instance().proxy_request('GET', '/', None, None)
+            response.raise_for_status()
+        except Exception as e:  # pylint: disable=broad-except
+            logger.error(e)
+            return False, str(e)
+
+        return True, ''
+
+
+@ApiController('/grafana')
+@AuthRequired()
+class Grafana(BaseController):
+
+    @Endpoint()
+    def status(self):
+        grafana = GrafanaRestClient.instance()
+        available, msg = grafana.is_service_online()
+        response = {'available': available}
+        if msg:
+            response['message'] = msg
+
+        return response
+
+
+@ApiController('/grafana/proxy')
+@AuthRequired()
+class GrafanaProxy(BaseController):
+    @Proxy()
+    def __call__(self, path, **params):
+        grafana = GrafanaRestClient.instance()
+        method = cherrypy.request.method
+
+        data = None
+        if cherrypy.request.body.length:
+            data = cherrypy.request.body.read()
+
+        response = grafana.proxy_request(method, path, params, data)
+
+        cherrypy.response.headers['Content-Type'] = response.headers[
+            'Content-Type']
+
+        return response.content
index 3875f268254a9d374d36b7ea152c8c74c345abe0..3ece55b798d39b6dc6df7de3196247ac6986a77f 100644 (file)
@@ -29,6 +29,11 @@ class Options(object):
     RGW_API_SCHEME = ('http', str)
     RGW_API_USER_ID = ('', str)
 
+    # Grafana settings
+    GRAFANA_API_URL = ('http://localhost:3000', str)
+    GRAFANA_API_USERNAME = ('admin', str)
+    GRAFANA_API_PASSWORD = ('admin', str)
+
     @staticmethod
     def has_default_value(name):
         return getattr(Settings, name, None) is None or \
diff --git a/src/pybind/mgr/dashboard/tests/test_grafana.py b/src/pybind/mgr/dashboard/tests/test_grafana.py
new file mode 100644 (file)
index 0000000..6f3a08f
--- /dev/null
@@ -0,0 +1,53 @@
+from unittest import TestCase
+
+import cherrypy
+import six
+from .. import mgr
+from ..controllers import BaseController, Controller, Proxy
+from ..controllers.grafana import GrafanaProxy, GrafanaRestClient
+
+from .helper import ControllerTestCase
+
+
+class Grafana(TestCase):
+    def test_missing_credentials(self):
+        with six.assertRaisesRegex(self, LookupError, r'^No credentials.*'):
+            GrafanaRestClient(
+                url='http://localhost:3000', username='', password='admin')
+
+        with six.assertRaisesRegex(self, LookupError, r'^No URL.*'):
+            GrafanaRestClient(
+                url='//localhost:3000', username='admin', password='admin')
+
+
+@Controller('/grafana/mocked')
+class GrafanaMockInstance(BaseController):
+    @Proxy()
+    def __call__(self, path, **params):
+        cherrypy.response.headers['foo'] = 'bar'
+        return 'Static Content at path {}'.format(path)
+
+
+class GrafanaControllerTestCase(ControllerTestCase):
+    @classmethod
+    def setup_server(cls):
+        settings = {
+            'GRAFANA_API_URL':
+                'http://localhost:{}/grafana/mocked/'.format(54583),
+            'GRAFANA_API_USERNAME': 'admin',
+            'GRAFANA_API_PASSWORD': 'admin',
+        }
+        mgr.get_config.side_effect = settings.get
+        GrafanaProxy._cp_config['tools.authenticate.on'] = False  # pylint: disable=protected-access
+
+        cls.setup_controllers([GrafanaProxy, GrafanaMockInstance])
+
+    def test_grafana_proxy(self):
+        self._get('/grafana/mocked/foo')
+        self.assertStatus(200)
+        self.assertBody('Static Content at path foo')
+
+        # Test the proxy
+        self._get('/api/grafana/proxy/bar')
+        self.assertStatus(200)
+        self.assertBody('Static Content at path bar')