Grafana theming, dashboards, datasources management with basic auth
diff --git a/README.rst b/README.rst
index 26daa01..fcbdef8 100644
--- a/README.rst
+++ b/README.rst
@@ -51,13 +51,27 @@
grafana:
server:
enabled: true
- admin:
- user: admin
- password: passwd
dashboards:
enabled: true
path: /var/lib/grafana/dashboards
+Server with theme overrides
+
+.. code-block:: yaml
+
+ grafana:
+ server:
+ enabled: true
+ theme:
+ light:
+ css_override:
+ source: http://path.to.theme
+ source_hash: sha256=xyz
+ build: xyz
+ dark:
+ css_override:
+ source: salt://path.to.theme
+
Collector setup
---------------
@@ -74,7 +88,7 @@
Client setups
-------------
-Client enforced data sources
+Client with token based auth
.. code-block:: yaml
@@ -86,12 +100,43 @@
host: grafana.host
port: 3000
token: token
+
+Client with base auth
+
+.. code-block:: yaml
+
+ grafana:
+ client:
+ enabled: true
+ server:
+ protocol: https
+ host: grafana.host
+ port: 3000
+ user: admin
+ password: password
+
+Client enforcing graphite data source
+
+.. code-block:: yaml
+
+ grafana:
+ client:
+ enabled: true
datasource:
graphite:
type: graphite
host: mtr01.domain.com
protocol: https
port: 443
+
+Client enforcing elasticsearch data source
+
+.. code-block:: yaml
+
+ grafana:
+ client:
+ enabled: true
+ datasource:
elasticsearch:
type: elasticsearch
host: log01.domain.com
diff --git a/_states/grafana3_dashboard.py b/_states/grafana3_dashboard.py
new file mode 100644
index 0000000..0087b2c
--- /dev/null
+++ b/_states/grafana3_dashboard.py
@@ -0,0 +1,571 @@
+# -*- coding: utf-8 -*-
+'''
+Manage Grafana v3.0 Dashboards
+
+.. versionadded:: 2016.3.0
+
+.. code-block:: yaml
+
+ grafana:
+ grafana_timeout: 3
+ grafana_token: qwertyuiop
+ grafana_url: 'https://url.com'
+
+.. code-block:: yaml
+
+ Ensure minimum dashboard is managed:
+ grafana_dashboard.present:
+ - name: insightful-dashboard
+ - base_dashboards_from_pillar:
+ - default_dashboard
+ - base_rows_from_pillar:
+ - default_row
+ - base_panels_from_pillar:
+ - default_panel
+ - dashboard:
+ rows:
+ - title: Usage
+ panels:
+ - targets:
+ - target: alias(constantLine(50), 'max')
+ title: Imaginary
+ type: graph
+
+
+The behavior of this module is to create dashboards if they do not exist, to
+add rows if they do not exist in existing dashboards, and to update rows if
+they exist in dashboards. The module will not manage rows that are not defined,
+allowing users to manage their own custom rows.
+'''
+
+# Import Python libs
+from __future__ import absolute_import
+import copy
+import json
+import requests
+
+# Import Salt libs
+import salt.ext.six as six
+from salt.utils.dictdiffer import DictDiffer
+
+
+def __virtual__():
+ '''Only load if grafana v2.0 is configured.'''
+ return __salt__['config.get']('grafana_version', 1) == 3
+
+
+_DEFAULT_DASHBOARD_PILLAR = 'grafana_dashboards:default'
+_DEFAULT_PANEL_PILLAR = 'grafana_panels:default'
+_DEFAULT_ROW_PILLAR = 'grafana_rows:default'
+_PINNED_ROWS_PILLAR = 'grafana_pinned_rows'
+
+
+def present(name,
+ base_dashboards_from_pillar=None,
+ base_panels_from_pillar=None,
+ base_rows_from_pillar=None,
+ dashboard=None,
+ profile='grafana'):
+ '''
+ Ensure the grafana dashboard exists and is managed.
+
+ name
+ Name of the grafana dashboard.
+
+ base_dashboards_from_pillar
+ A pillar key that contains a list of dashboards to inherit from
+
+ base_panels_from_pillar
+ A pillar key that contains a list of panels to inherit from
+
+ base_rows_from_pillar
+ A pillar key that contains a list of rows to inherit from
+
+ dashboard
+ A dict that defines a dashboard that should be managed.
+
+ profile
+ A pillar key or dict that contains grafana information
+ '''
+ ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
+
+ base_dashboards_from_pillar = base_dashboards_from_pillar or []
+ base_panels_from_pillar = base_panels_from_pillar or []
+ base_rows_from_pillar = base_rows_from_pillar or []
+ dashboard = dashboard or {}
+
+ if isinstance(profile, six.string_types):
+ profile = __salt__['config.option'](profile)
+
+ # Add pillar keys for default configuration
+ base_dashboards_from_pillar = ([_DEFAULT_DASHBOARD_PILLAR] +
+ base_dashboards_from_pillar)
+ base_panels_from_pillar = ([_DEFAULT_PANEL_PILLAR] +
+ base_panels_from_pillar)
+ base_rows_from_pillar = [_DEFAULT_ROW_PILLAR] + base_rows_from_pillar
+
+ # Build out all dashboard fields
+ new_dashboard = _inherited_dashboard(
+ dashboard, base_dashboards_from_pillar, ret)
+ new_dashboard['title'] = name
+ rows = new_dashboard.get('rows', [])
+ for i, row in enumerate(rows):
+ rows[i] = _inherited_row(row, base_rows_from_pillar, ret)
+ for row in rows:
+ panels = row.get('panels', [])
+ for i, panel in enumerate(panels):
+ panels[i] = _inherited_panel(panel, base_panels_from_pillar, ret)
+ _auto_adjust_panel_spans(new_dashboard)
+ _ensure_panel_ids(new_dashboard)
+ _ensure_annotations(new_dashboard)
+
+ # Create dashboard if it does not exist
+ url = 'db/{0}'.format(name)
+ old_dashboard = _get(url, profile)
+ if not old_dashboard:
+ if __opts__['test']:
+ ret['result'] = None
+ ret['comment'] = 'Dashboard {0} is set to be created.'.format(name)
+ return ret
+
+ response = _update(new_dashboard, profile)
+ if response.get('status') == 'success':
+ ret['comment'] = 'Dashboard {0} created.'.format(name)
+ ret['changes']['new'] = 'Dashboard {0} created.'.format(name)
+ else:
+ ret['result'] = False
+ ret['comment'] = ("Failed to create dashboard {0}, "
+ "response={1}").format(name, response)
+ return ret
+
+ # Add unmanaged rows to the dashboard. They appear at the top if they are
+ # marked as pinned. They appear at the bottom otherwise.
+ managed_row_titles = [row.get('title')
+ for row in new_dashboard.get('rows', [])]
+ new_rows = new_dashboard.get('rows', [])
+ for old_row in old_dashboard.get('rows', []):
+ if old_row.get('title') not in managed_row_titles:
+ new_rows.append(copy.deepcopy(old_row))
+ _ensure_pinned_rows(new_dashboard)
+ _ensure_panel_ids(new_dashboard)
+
+ # Update dashboard if it differs
+ dashboard_diff = DictDiffer(_cleaned(new_dashboard),
+ _cleaned(old_dashboard))
+ updated_needed = (dashboard_diff.changed() or
+ dashboard_diff.added() or
+ dashboard_diff.removed())
+ if updated_needed:
+ if __opts__['test']:
+ ret['result'] = None
+ ret['comment'] = ('Dashboard {0} is set to be updated, '
+ 'changes={1}').format(
+ name,
+ json.dumps(
+ _dashboard_diff(
+ _cleaned(new_dashboard),
+ _cleaned(old_dashboard)
+ ),
+ indent=4
+ ))
+ return ret
+
+ response = _update(new_dashboard, profile)
+ if response.get('status') == 'success':
+ updated_dashboard = _get(url, profile)
+ dashboard_diff = DictDiffer(_cleaned(updated_dashboard),
+ _cleaned(old_dashboard))
+ ret['comment'] = 'Dashboard {0} updated.'.format(name)
+ ret['changes'] = _dashboard_diff(_cleaned(new_dashboard),
+ _cleaned(old_dashboard))
+ else:
+ ret['result'] = False
+ ret['comment'] = ("Failed to update dashboard {0}, "
+ "response={1}").format(name, response)
+ return ret
+
+ ret['comment'] = 'Dashboard present'
+ return ret
+
+
+def absent(name, profile='grafana'):
+ '''
+ Ensure the named grafana dashboard is absent.
+
+ name
+ Name of the grafana dashboard.
+
+ profile
+ A pillar key or dict that contains grafana information
+ '''
+ ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
+
+ if isinstance(profile, six.string_types):
+ profile = __salt__['config.option'](profile)
+
+ url = 'db/{0}'.format(name)
+ existing_dashboard = _get(url, profile)
+ if existing_dashboard:
+ if __opts__['test']:
+ ret['result'] = None
+ ret['comment'] = 'Dashboard {0} is set to be deleted.'.format(name)
+ return ret
+
+ _delete(url, profile)
+ ret['comment'] = 'Dashboard {0} deleted.'.format(name)
+ ret['changes']['new'] = 'Dashboard {0} deleted.'.format(name)
+ return ret
+
+ ret['comment'] = 'Dashboard absent'
+ return ret
+
+
+_IGNORED_DASHBOARD_FIELDS = [
+ 'id',
+ 'originalTitle',
+ 'version',
+]
+_IGNORED_ROW_FIELDS = []
+_IGNORED_PANEL_FIELDS = [
+ 'grid',
+ 'mode',
+ 'tooltip',
+]
+_IGNORED_TARGET_FIELDS = [
+ 'textEditor',
+]
+
+
+def _cleaned(_dashboard):
+ '''Return a copy without fields that can differ.'''
+ dashboard = copy.deepcopy(_dashboard)
+
+ for ignored_dashboard_field in _IGNORED_DASHBOARD_FIELDS:
+ dashboard.pop(ignored_dashboard_field, None)
+ for row in dashboard.get('rows', []):
+ for ignored_row_field in _IGNORED_ROW_FIELDS:
+ row.pop(ignored_row_field, None)
+ for i, panel in enumerate(row.get('panels', [])):
+ for ignored_panel_field in _IGNORED_PANEL_FIELDS:
+ panel.pop(ignored_panel_field, None)
+ for target in panel.get('targets', []):
+ for ignored_target_field in _IGNORED_TARGET_FIELDS:
+ target.pop(ignored_target_field, None)
+ row['panels'][i] = _stripped(panel)
+
+ return dashboard
+
+
+def _inherited_dashboard(dashboard, base_dashboards_from_pillar, ret):
+ '''Return a dashboard with properties from parents.'''
+ base_dashboards = []
+ for base_dashboard_from_pillar in base_dashboards_from_pillar:
+ base_dashboard = __salt__['pillar.get'](base_dashboard_from_pillar)
+ if base_dashboard:
+ base_dashboards.append(base_dashboard)
+ elif base_dashboard_from_pillar != _DEFAULT_DASHBOARD_PILLAR:
+ ret.setdefault('warnings', [])
+ warning_message = 'Cannot find dashboard pillar "{0}".'.format(
+ base_dashboard_from_pillar)
+ if warning_message not in ret['warnings']:
+ ret['warnings'].append(warning_message)
+ base_dashboards.append(dashboard)
+
+ result_dashboard = {}
+ tags = set()
+ for dashboard in base_dashboards:
+ tags.update(dashboard.get('tags', []))
+ result_dashboard.update(dashboard)
+ result_dashboard['tags'] = list(tags)
+ return result_dashboard
+
+
+def _inherited_row(row, base_rows_from_pillar, ret):
+ '''Return a row with properties from parents.'''
+ base_rows = []
+ for base_row_from_pillar in base_rows_from_pillar:
+ base_row = __salt__['pillar.get'](base_row_from_pillar)
+ if base_row:
+ base_rows.append(base_row)
+ elif base_row_from_pillar != _DEFAULT_ROW_PILLAR:
+ ret.setdefault('warnings', [])
+ warning_message = 'Cannot find row pillar "{0}".'.format(
+ base_row_from_pillar)
+ if warning_message not in ret['warnings']:
+ ret['warnings'].append(warning_message)
+ base_rows.append(row)
+
+ result_row = {}
+ for row in base_rows:
+ result_row.update(row)
+ return result_row
+
+
+def _inherited_panel(panel, base_panels_from_pillar, ret):
+ '''Return a panel with properties from parents.'''
+ base_panels = []
+ for base_panel_from_pillar in base_panels_from_pillar:
+ base_panel = __salt__['pillar.get'](base_panel_from_pillar)
+ if base_panel:
+ base_panels.append(base_panel)
+ elif base_panel_from_pillar != _DEFAULT_PANEL_PILLAR:
+ ret.setdefault('warnings', [])
+ warning_message = 'Cannot find panel pillar "{0}".'.format(
+ base_panel_from_pillar)
+ if warning_message not in ret['warnings']:
+ ret['warnings'].append(warning_message)
+ base_panels.append(panel)
+
+ result_panel = {}
+ for panel in base_panels:
+ result_panel.update(panel)
+ return result_panel
+
+
+_FULL_LEVEL_SPAN = 12
+_DEFAULT_PANEL_SPAN = 2.5
+
+
+def _auto_adjust_panel_spans(dashboard):
+ '''Adjust panel spans to take up the available width.
+
+ For each group of panels that would be laid out on the same level, scale up
+ the unspecified panel spans to fill up the level.
+ '''
+ for row in dashboard.get('rows', []):
+ levels = []
+ current_level = []
+ levels.append(current_level)
+ for panel in row.get('panels', []):
+ current_level_span = sum(panel.get('span', _DEFAULT_PANEL_SPAN)
+ for panel in current_level)
+ span = panel.get('span', _DEFAULT_PANEL_SPAN)
+ if current_level_span + span > _FULL_LEVEL_SPAN:
+ current_level = [panel]
+ levels.append(current_level)
+ else:
+ current_level.append(panel)
+
+ for level in levels:
+ specified_panels = [panel for panel in level if 'span' in panel]
+ unspecified_panels = [panel for panel in level
+ if 'span' not in panel]
+ if not unspecified_panels:
+ continue
+
+ specified_span = sum(panel['span'] for panel in specified_panels)
+ available_span = _FULL_LEVEL_SPAN - specified_span
+ auto_span = float(available_span) / len(unspecified_panels)
+ for panel in unspecified_panels:
+ panel['span'] = auto_span
+
+
+def _ensure_pinned_rows(dashboard):
+ '''Pin rows to the top of the dashboard.'''
+ pinned_row_titles = __salt__['pillar.get'](_PINNED_ROWS_PILLAR)
+ if not pinned_row_titles:
+ return
+
+ pinned_row_titles_lower = []
+ for title in pinned_row_titles:
+ pinned_row_titles_lower.append(title.lower())
+ rows = dashboard.get('rows', [])
+ pinned_rows = []
+ for i, row in enumerate(rows):
+ if row.get('title', '').lower() in pinned_row_titles_lower:
+ del rows[i]
+ pinned_rows.append(row)
+ rows = pinned_rows + rows
+
+
+def _ensure_panel_ids(dashboard):
+ '''Assign panels auto-incrementing IDs.'''
+ panel_id = 1
+ for row in dashboard.get('rows', []):
+ for panel in row.get('panels', []):
+ panel['id'] = panel_id
+ panel_id += 1
+
+
+def _ensure_annotations(dashboard):
+ '''Explode annotation_tags into annotations.'''
+ if 'annotation_tags' not in dashboard:
+ return
+ tags = dashboard['annotation_tags']
+ annotations = {
+ 'enable': True,
+ 'list': [],
+ }
+ for tag in tags:
+ annotations['list'].append({
+ 'datasource': "graphite",
+ 'enable': False,
+ 'iconColor': "#C0C6BE",
+ 'iconSize': 13,
+ 'lineColor': "rgba(255, 96, 96, 0.592157)",
+ 'name': tag,
+ 'showLine': True,
+ 'tags': tag,
+ })
+ del dashboard['annotation_tags']
+ dashboard['annotations'] = annotations
+
+
+def _get(url, profile):
+ '''Get a specific dashboard.'''
+ request_url = "{0}/api/dashboards/{1}".format(profile.get('grafana_url'),
+ url)
+ if profile.get('grafana_token', False):
+ response = requests.get(
+ request_url,
+ headers=_get_headers(profile),
+ timeout=profile.get('grafana_timeout', 3),
+ )
+ else:
+ response = requests.get(
+ request_url,
+ auth=_get_auth(profile),
+ timeout=profile.get('grafana_timeout', 3),
+ )
+ data = response.json()
+ if data.get('message') == 'Not found':
+ return None
+ if 'dashboard' not in data:
+ return None
+ return data['dashboard']
+
+
+def _delete(url, profile):
+ '''Delete a specific dashboard.'''
+ request_url = "{0}/api/dashboards/{1}".format(profile.get('grafana_url'),
+ url)
+ if profile.get('grafana_token', False):
+ response = requests.delete(
+ request_url,
+ headers=_get_headers(profile),
+ timeout=profile.get('grafana_timeout'),
+ )
+ else:
+ response = requests.delete(
+ request_url,
+ auth=_get_auth(profile),
+ timeout=profile.get('grafana_timeout'),
+ )
+ data = response.json()
+ return data
+
+
+def _update(dashboard, profile):
+ '''Update a specific dashboard.'''
+ payload = {
+ 'dashboard': dashboard,
+ 'overwrite': True
+ }
+ request_url = "{0}/api/dashboards/db".format(profile.get('grafana_url'))
+ if profile.get('grafana_token', False):
+ response = requests.post(
+ request_url,
+ headers=_get_headers(profile),
+ json=payload
+ )
+ else:
+ response = requests.post(
+ request_url,
+ auth=_get_auth(profile),
+ json=payload
+ )
+ return response.json()
+
+
+def _get_headers(profile):
+ return {
+ 'Accept': 'application/json',
+ 'Authorization': 'Bearer {0}'.format(profile['grafana_token'])
+ }
+
+
+def _get_auth(profile):
+ return requests.auth.HTTPBasicAuth(
+ profile['grafana_user'],
+ profile['grafana_password']
+ )
+
+
+def _dashboard_diff(_new_dashboard, _old_dashboard):
+ '''Return a dictionary of changes between dashboards.'''
+ diff = {}
+
+ # Dashboard diff
+ new_dashboard = copy.deepcopy(_new_dashboard)
+ old_dashboard = copy.deepcopy(_old_dashboard)
+ dashboard_diff = DictDiffer(new_dashboard, old_dashboard)
+ diff['dashboard'] = _stripped({
+ 'changed': list(dashboard_diff.changed()) or None,
+ 'added': list(dashboard_diff.added()) or None,
+ 'removed': list(dashboard_diff.removed()) or None,
+ })
+
+ # Row diff
+ new_rows = new_dashboard.get('rows', [])
+ old_rows = old_dashboard.get('rows', [])
+ new_rows_by_title = {}
+ old_rows_by_title = {}
+ for row in new_rows:
+ if 'title' in row:
+ new_rows_by_title[row['title']] = row
+ for row in old_rows:
+ if 'title' in row:
+ old_rows_by_title[row['title']] = row
+ rows_diff = DictDiffer(new_rows_by_title, old_rows_by_title)
+ diff['rows'] = _stripped({
+ 'added': list(rows_diff.added()) or None,
+ 'removed': list(rows_diff.removed()) or None,
+ })
+ for changed_row_title in rows_diff.changed():
+ old_row = old_rows_by_title[changed_row_title]
+ new_row = new_rows_by_title[changed_row_title]
+ row_diff = DictDiffer(new_row, old_row)
+ diff['rows'].setdefault('changed', {})
+ diff['rows']['changed'][changed_row_title] = _stripped({
+ 'changed': list(row_diff.changed()) or None,
+ 'added': list(row_diff.added()) or None,
+ 'removed': list(row_diff.removed()) or None,
+ })
+
+ # Panel diff
+ old_panels_by_id = {}
+ new_panels_by_id = {}
+ for row in old_dashboard.get('rows', []):
+ for panel in row.get('panels', []):
+ if 'id' in panel:
+ old_panels_by_id[panel['id']] = panel
+ for row in new_dashboard.get('rows', []):
+ for panel in row.get('panels', []):
+ if 'id' in panel:
+ new_panels_by_id[panel['id']] = panel
+ panels_diff = DictDiffer(new_panels_by_id, old_panels_by_id)
+ diff['panels'] = _stripped({
+ 'added': list(panels_diff.added()) or None,
+ 'removed': list(panels_diff.removed()) or None,
+ })
+ for changed_panel_id in panels_diff.changed():
+ old_panel = old_panels_by_id[changed_panel_id]
+ new_panel = new_panels_by_id[changed_panel_id]
+ panels_diff = DictDiffer(new_panel, old_panel)
+ diff['panels'].setdefault('changed', {})
+ diff['panels']['changed'][changed_panel_id] = _stripped({
+ 'changed': list(panels_diff.changed()) or None,
+ 'added': list(panels_diff.added()) or None,
+ 'removed': list(panels_diff.removed()) or None,
+ })
+
+ return diff
+
+
+def _stripped(d):
+ '''Strip falsey entries.'''
+ ret = {}
+ for k, v in six.iteritems(d):
+ if v:
+ ret[k] = v
+ return ret
\ No newline at end of file
diff --git a/_states/grafana3_datasource.py b/_states/grafana3_datasource.py
new file mode 100644
index 0000000..a66836b
--- /dev/null
+++ b/_states/grafana3_datasource.py
@@ -0,0 +1,263 @@
+# -*- coding: utf-8 -*-
+'''
+Manage Grafana v3.0 data sources
+
+.. versionadded:: 2016.3.0
+
+Token auth setup
+
+.. code-block:: yaml
+
+ grafana:
+ grafana_version: 3
+ grafana_timeout: 5
+ grafana_token: qwertyuiop
+ grafana_url: 'https://url.com'
+
+Basic auth setup
+
+.. code-block:: yaml
+
+ grafana:
+ grafana_version: 3
+ grafana_timeout: 5
+ grafana_user: grafana
+ grafana_password: qwertyuiop
+ grafana_url: 'https://url.com'
+
+.. code-block:: yaml
+
+ Ensure influxdb data source is present:
+ grafana_datasource.present:
+ - name: influxdb
+ - type: influxdb
+ - url: http://localhost:8086
+ - access: proxy
+ - basic_auth: true
+ - basic_auth_user: myuser
+ - basic_auth_password: mypass
+ - is_default: true
+'''
+from __future__ import absolute_import
+
+import requests
+
+from salt.ext.six import string_types
+
+
+def __virtual__():
+ '''Only load if grafana v3.0 is configured.'''
+ return __salt__['config.get']('grafana_version', 1) == 3
+
+
+def present(name,
+ type,
+ url,
+ access='proxy',
+ user='',
+ password='',
+ database='',
+ basic_auth=False,
+ basic_auth_user='',
+ basic_auth_password='',
+ is_default=False,
+ type_logo_url='public/app/plugins/datasource/graphite/img/graphite_logo.png',
+ with_credentials=False,
+ json_data=None,
+ profile='grafana'):
+ '''
+ Ensure that a data source is present.
+
+ name
+ Name of the data source.
+
+ type
+ Which type of data source it is ('graphite', 'influxdb' etc.).
+
+ url
+ The URL to the data source API.
+
+ user
+ Optional - user to authenticate with the data source
+
+ password
+ Optional - password to authenticate with the data source
+
+ basic_auth
+ Optional - set to True to use HTTP basic auth to authenticate with the
+ data source.
+
+ basic_auth_user
+ Optional - HTTP basic auth username.
+
+ basic_auth_password
+ Optional - HTTP basic auth password.
+
+ is_default
+ Default: False
+ '''
+ if isinstance(profile, string_types):
+ profile = __salt__['config.option'](profile)
+
+ ret = {'name': name, 'result': None, 'comment': None, 'changes': None}
+ datasource = _get_datasource(profile, name)
+ data = _get_json_data(name, type, url, access, user, password, database,
+ basic_auth, basic_auth_user, basic_auth_password, is_default, json_data)
+
+ if datasource:
+ if profile.get('grafana_token', False):
+ requests.put(
+ _get_url(profile, datasource['id']),
+ data,
+ headers=_get_headers(profile),
+ timeout=profile.get('grafana_timeout', 3),
+ )
+ else:
+ requests.put(
+ _get_url(profile, datasource['id']),
+ data,
+ auth=_get_auth(profile),
+ timeout=profile.get('grafana_timeout', 3),
+ )
+ ret['result'] = True
+ ret['changes'] = _diff(datasource, data)
+ if ret['changes']['new'] or ret['changes']['old']:
+ ret['comment'] = 'Data source {0} updated'.format(name)
+ else:
+ ret['changes'] = None
+ ret['comment'] = 'Data source {0} already up-to-date'.format(name)
+ else:
+ requests.post(
+ '{0}/api/datasources'.format(profile['grafana_url']),
+ data,
+ headers=_get_headers(profile),
+ timeout=profile.get('grafana_timeout', 3),
+ )
+ ret['result'] = True
+ ret['comment'] = 'New data source {0} added'.format(name)
+ ret['changes'] = data
+
+ return ret
+
+
+def absent(name, profile='grafana'):
+ '''
+ Ensure that a data source is present.
+
+ name
+ Name of the data source to remove.
+ '''
+ if isinstance(profile, string_types):
+ profile = __salt__['config.option'](profile)
+
+ ret = {'result': None, 'comment': None, 'changes': None}
+ datasource = _get_datasource(profile, name)
+
+ if not datasource:
+ ret['result'] = True
+ ret['comment'] = 'Data source {0} already absent'.format(name)
+ return ret
+
+ if profile.get('grafana_token', False):
+ requests.delete(
+ _get_url(profile, datasource['id']),
+ headers=_get_headers(profile),
+ timeout=profile.get('grafana_timeout', 3),
+ )
+ else:
+ requests.delete(
+ _get_url(profile, datasource['id']),
+ auth=_get_auth(profile),
+ timeout=profile.get('grafana_timeout', 3),
+ )
+
+ ret['result'] = True
+ ret['comment'] = 'Data source {0} was deleted'.format(name)
+
+ return ret
+
+
+def _get_url(profile, datasource_id):
+ return '{0}/api/datasources/{1}'.format(
+ profile['grafana_url'],
+ datasource_id
+ )
+
+
+def _get_datasource(profile, name):
+ if profile.get('grafana_token', False):
+ response = requests.get(
+ '{0}/api/datasources'.format(profile['grafana_url']),
+ headers=_get_headers(profile),
+ timeout=profile.get('grafana_timeout', 3),
+ )
+ else:
+ response = requests.get(
+ '{0}/api/datasources'.format(profile['grafana_url']),
+ auth=_get_auth(profile),
+ timeout=profile.get('grafana_timeout', 3),
+ )
+ data = response.json()
+ for datasource in data:
+ if datasource['name'] == name:
+ return datasource
+ return None
+
+
+def _get_headers(profile):
+ return {
+ 'Accept': 'application/json',
+ 'Authorization': 'Bearer {0}'.format(profile['grafana_token'])
+ }
+
+
+def _get_auth(profile):
+ return requests.auth.HTTPBasicAuth(
+ profile['grafana_user'],
+ profile['grafana_password']
+ )
+
+
+def _get_json_data(name,
+ type,
+ url,
+ access='proxy',
+ user='',
+ password='',
+ database='',
+ basic_auth=False,
+ basic_auth_user='',
+ basic_auth_password='',
+ is_default=False,
+ type_logo_url='public/app/plugins/datasource/graphite/img/graphite_logo.png',
+ with_credentials=False,
+ json_data=None):
+ return {
+ 'name': name,
+ 'type': type,
+ 'url': url,
+ 'access': access,
+ 'user': user,
+ 'password': password,
+ 'database': database,
+ 'basicAuth': basic_auth,
+ 'basicAuthUser': basic_auth_user,
+ 'basicAuthPassword': basic_auth_password,
+ 'isDefault': is_default,
+ 'typeLogoUrl': type_logo_url,
+ 'withCredentials': with_credentials,
+ 'jsonData': json_data,
+ }
+
+
+def _diff(old, new):
+ old_keys = old.keys()
+ old = old.copy()
+ new = new.copy()
+ for key in old_keys:
+ if key == 'id' or key == 'orgId':
+ del old[key]
+ elif old[key] == new[key]:
+ del old[key]
+ del new[key]
+ return {'old': old, 'new': new}
diff --git a/grafana/client.sls b/grafana/client.sls
index 804fa12..67a7c07 100644
--- a/grafana/client.sls
+++ b/grafana/client.sls
@@ -11,7 +11,7 @@
{%- for datasource_name, datasource in client.datasource.iteritems() %}
grafana_client_datasource_{{ datasource_name }}:
- grafana_datasource.present:
+ grafana3_datasource.present:
- name: {{ datasource_name }}
- type: {{ datasource.type }}
- url: http://{{ datasource.host }}:{{ datasource.get('port', 80) }}
@@ -62,11 +62,21 @@
{%- for dashboard_name, dashboard in final_dict.iteritems() %}
+{%- if dashboard.get('enabled', True) %}
+
grafana_client_dashboard_{{ dashboard_name }}:
- grafana_dashboard.present:
+ grafana3_dashboard.present:
- name: {{ dashboard_name }}
- dashboard: {{ dashboard }}
+{%- else %}
+
+grafana_client_dashboard_{{ dashboard_name }}:
+ grafana3_dashboard.absent:
+ - name: {{ dashboard_name }}
+
+{%- endif %}
+
{%- endfor %}
{%- endif %}
diff --git a/grafana/files/_grafana.conf b/grafana/files/_grafana.conf
index 105af23..1d64503 100644
--- a/grafana/files/_grafana.conf
+++ b/grafana/files/_grafana.conf
@@ -1,8 +1,13 @@
{%- from "grafana/map.jinja" import client with context %}
-grafana_version: 2
+grafana_version: {{ client.server.get('version', 3) }}
grafana:
grafana_timeout: 3
+ {%- if client.server.token is defined %}
grafana_token: {{ client.server.token }}
- grafana_url: 'http://{{ client.server.host }}:{{ client.server.get('port', 80) }}'
+ {%- else %}
+ grafana_user: {{ client.server.user }}
+ grafana_password: {{ client.server.password }}
+ {%- endif %}
+ grafana_url: '{{ client.server.get('protocol', 'http') }}://{{ client.server.host }}:{{ client.server.get('port', 80) }}'
diff --git a/grafana/map.jinja b/grafana/map.jinja
index 521add5..88ac842 100644
--- a/grafana/map.jinja
+++ b/grafana/map.jinja
@@ -1,5 +1,5 @@
-{%- load_yaml as base_defaults %}
+{%- load_yaml as server_defaults %}
Debian:
pkgs:
- grafana
@@ -17,13 +17,15 @@
allow_sign_up: False
allow_org_create: False
auto_assign_role: Viewer
+ dir:
+ static: /usr/share/grafana/public
dashboards:
enabled: false
{%- endload %}
-{%- set server = salt['grains.filter_by'](base_defaults, merge=salt['pillar.get']('grafana:server')) %}
+{%- set server = salt['grains.filter_by'](server_defaults, merge=salt['pillar.get']('grafana:server')) %}
-{%- load_yaml as base_defaults %}
+{%- load_yaml as client_defaults %}
Debian:
server:
host: 127.0.0.1
@@ -34,4 +36,12 @@
dashboard: {}
{%- endload %}
-{%- set client = salt['grains.filter_by'](base_defaults, merge=salt['pillar.get']('grafana:client')) %}
+{%- set client = salt['grains.filter_by'](client_defaults, merge=salt['pillar.get']('grafana:client')) %}
+
+{%- load_yaml as collector_defaults %}
+default:
+ storage:
+ engine: salt-mine
+{%- endload %}
+
+{%- set collector = salt['grains.filter_by'](collector_defaults, merge=salt['pillar.get']('grafana:collector')) %}
diff --git a/grafana/server.sls b/grafana/server.sls
index c3fc7ed..9e010d5 100644
--- a/grafana/server.sls
+++ b/grafana/server.sls
@@ -15,6 +15,7 @@
- pkg: grafana_packages
{%- if server.dashboards.enabled %}
+
grafana_copy_default_dashboards:
file.recurse:
- name: {{ server.dashboards.path }}
@@ -23,17 +24,42 @@
- group: grafana
- require:
- pkg: grafana_packages
+ - require_in:
+ - service: grafana_service
+
{%- endif %}
+{%- for theme_name, theme in server.get('theme', {}).iteritems() %}
+
+{%- if theme.css_override is defined %}
+
+grafana_{{ theme_name }}_css_override:
+ file.managed:
+ - names:
+ - {{ server.dir.static }}/css/grafana.{{ theme_name }}.min.css
+ {%- if theme.css_override.build is defined %}
+ - {{ server.dir.static }}/css/grafana.{{ theme_name }}.min.{{ theme.css_override.build }}.css
+ {%- endif %}
+ - source: {{ theme.css_override.source }}
+ {%- if theme.css_override.source_hash is defined %}
+ - source_hash: {{ theme.css_override.source_hash }}
+ {%- endif %}
+ - user: grafana
+ - group: grafana
+ - require:
+ - pkg: grafana_packages
+ - require_in:
+ - service: grafana_service
+
+{%- endif %}
+
+{%- endfor %}
+
grafana_service:
service.running:
- name: {{ server.service }}
- enable: true
- watch:
- file: /etc/grafana/grafana.ini
-{%- if server.dashboards.enabled %}
- - require:
- - file: grafana_copy_default_dashboards
-{%- endif %}
{%- endif %}