Grafana theming, dashboards, datasources management with basic auth
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}