From: Patrick Nawracay Date: Thu, 19 Apr 2018 07:29:41 +0000 (+0200) Subject: mgr/dashboard: Add Grafana proxy X-Git-Tag: v14.0.1~1173^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=30b5d72d2d53561072e7ed141dc7e87a956d0dd4;p=ceph-ci.git mgr/dashboard: Add Grafana proxy 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 # Default: 'localhost' $ ceph dashboard set-grafana-api-port # Default: 3000 $ ceph dashboard set-grafana-api-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": "", } The message is not returned if Grafana is available. The Grafana proxy is reachable through `http(s):///api/grafana/proxy`. Signed-off-by: Patrick Nawracay --- diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index fc028038fde..c5238c0b0f7 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -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 $ ceph dashboard set-rgw-api-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 + `_ 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 # default: 'http://localhost:3000' + $ ceph dashboard set-grafana-api-username # default: 'admin' + $ ceph dashboard set-grafana-api-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 index 00000000000..37f2568d1d2 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/grafana.py @@ -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 diff --git a/src/pybind/mgr/dashboard/settings.py b/src/pybind/mgr/dashboard/settings.py index 3875f268254..3ece55b798d 100644 --- a/src/pybind/mgr/dashboard/settings.py +++ b/src/pybind/mgr/dashboard/settings.py @@ -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 index 00000000000..6f3a08f6840 --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/test_grafana.py @@ -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')