Repository: ansible/ansible

Issue:
grafana_dashboard fails with Grafana v5.0
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
grafana_dashboard

##### ANSIBLE VERSION
```
ansible 2.5.0
  config file = /Users/remirey/dev/grtgas/avignon/ansible.cfg
  configured module search path = [u'/Users/remirey/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /Users/remirey/.virtualenvs/avignon/lib/python2.7/site-packages/ansible
  executable location = /Users/remirey/.virtualenvs/avignon/bin/ansible
  python version = 2.7.13 (default, Jul 18 2017, 09:17:00) [GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)]
```

##### CONFIGURATION
```
DEFAULT_GATHERING(/Users/remirey/dev/grtgas/avignon/ansible.cfg) = explicit
DEFAULT_HASH_BEHAVIOUR(/Users/remirey/dev/grtgas/avignon/ansible.cfg) = replace
DEFAULT_MANAGED_STR(/Users/remirey/dev/grtgas/avignon/ansible.cfg) = Please do not change this file directly since it is managed by Ansible and will be overwritten
DEFAULT_VAULT_PASSWORD_FILE(/Users/remirey/dev/grtgas/avignon/ansible.cfg) = /Users/remirey/dev/grtgas/avignon/ansible_vault_password
HOST_KEY_CHECKING(/Users/remirey/dev/grtgas/avignon/ansible.cfg) = False
RETRY_FILES_ENABLED(/Users/remirey/dev/grtgas/avignon/ansible.cfg) = False
```

##### OS / ENVIRONMENT
N/A

##### SUMMARY

When executing a dashboard creation to test idempotency, the second execution fails with the following error:

```
The full traceback is:
  File "/var/folders/nx/br96pdj13gd30xmz81_fpsw00000gn/T/ansible_NgOhbA/ansible_module_grafana_dashboard.py", line 307, in main
    result = grafana_create_dashboard(module, module.params)
  File "/var/folders/nx/br96pdj13gd30xmz81_fpsw00000gn/T/ansible_NgOhbA/ansible_module_grafana_dashboard.py", line 215, in grafana_create_dashboard
    raise GrafanaAPIException('Unable to create the new dashboard %s : %s - %s.' % (slug, info['status'], info))

failed: [localhost] (item={u'path': u'aws_ecs.json', u'slug': u'aws_ecs'}) => {
    "changed": false,
    "invocation": {
        "module_args": {
            "grafana_api_key": null,
            "grafana_password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "grafana_url": "<some_url>",
            "grafana_user": "<some_user>",
            "message": "Updated by ansible",
            "org_id": 1,
            "overwrite": true,
            "path": "files/grafana/dashboards/aws_ecs.json",
            "slug": "aws_ecs",
            "state": "present",
            "validate_certs": true
        }
    },
    "item": {
        "path": "aws_ecs.json",
        "slug": "aws_ecs"
    },
    "msg": "error : Unable to create the new dashboard aws_ecs : 412 - {'body': '{\"message\":\"The dashboard has been changed by someone else\",\"status\":\"version-mismatch\"}', 'date': 'Thu, 03 May 2018 09:37:05 GMT', 'content-length': '88', 'status': 412, 'url': '<some_url>/api/dashboards/db', Expires=Thu, 10 May 2018 09:37:05 GMT; Path=/', 'content-type': 'application/json', 'connection': 'close', 'msg': 'HTTP Error 412: Precondition Failed'}."
```

##### STEPS TO REPRODUCE

```
    - name: Import the Grafana dashboards
      grafana_dashboard:
        grafana_url: <some_url>
        grafana_user: "{{ grafana_admin_user }}"
        grafana_password: "{{ grafana_admin_password }}"
        state: present
        message: Updated by ansible
        overwrite: yes
        path: "files/grafana/dashboards/{{ item.path }}"
        slug: "{{ item.slug }}"
      with_items:
        - path: aws_ecs.json
          slug: aws_ecs
```

The meta section was added manually in the dashboard json to have the import work in the first place.

##### EXPECTED RESULTS
Idempotency works and second execution marks the task "ok" and not "changed".

##### ACTUAL RESULTS
Exception

```
The full traceback is:
  File "/var/folders/nx/br96pdj13gd30xmz81_fpsw00000gn/T/ansible_NgOhbA/ansible_module_grafana_dashboard.py", line 307, in main
    result = grafana_create_dashboard(module, module.params)
  File "/var/folders/nx/br96pdj13gd30xmz81_fpsw00000gn/T/ansible_NgOhbA/ansible_module_grafana_dashboard.py", line 215, in grafana_create_dashboard
    raise GrafanaAPIException('Unable to create the new dashboard %s : %s - %s.' % (slug, info['status'], info))
```



Discussion:
Files identified in the description:
* [lib/ansible/modules/monitoring/grafana_dashboard.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/monitoring/grafana_dashboard.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @tsalle
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I dug a bit in the module code, and the module use the slug to retrieve the dashboard:

```
def grafana_dashboard_exists(module, grafana_url, slug, headers):
    dashboard_exists = False
    dashboard = {}
    r, info = fetch_url(module, '%s/api/dashboards/db/%s' % (grafana_url, slug), headers=headers, method='GET')
    if info['status'] == 200:
        dashboard_exists = True
        try:
            dashboard = json.loads(r.read())
        except Exception as e:
            raise GrafanaMalformedJson(e)
    elif info['status'] == 404:
        dashboard_exists = False
    else:
        raise GrafanaAPIException('Unable to get dashboard %s : %s' % (slug, info))

    return dashboard_exists, dashboard
```

In Grafana v5, the "Get dashboard by slug" is deprecated, and I could see that the dashboard json does not contain the slug even if it was present in the imported json used by the ansible module.
It leads to the dashboard retrieval to get a 404, and the dashboard is not found.

It may be related to the final error, but I didn't go further yet.
Hi,

Same problem here and I confirm that in Grafana release number `>= 5.0` the method `Get dashboard by slug` had been deprecated. The method `Get dashboard by uid resource` is recommended instead.

See link for more information : http://docs.grafana.org/http_api/dashboard/#get-dashboard-by-slug

Do you know any workaround to implement to continue to use this useful module ?

##### GRAFANA VERSION
```
5.0.4-1
```
##### ANSIBLE VERSION
```
ansible 2.5.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/${USER}/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible-2.5.0-py2.7.egg/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Nov  6 2016, 00:28:07) [GCC 4.8.5 20150623 (Red Hat 4.8.5-11)]
```
##### COMPONENT NAME
grafana_dashboard
##### OS / ENVIRONMENT
Running Ansible from : `CentOS Linux release 7.3.1611 (Core)`
OS managing by Ansible : `CentOS Linux release 7.3.1611 (Core)`

Thanks !
I guess we need a patch for this. I didn't find time to start working on it yet.

cc @seuf by the way.
I ran into this as well. Unfortunately I lack the knowledge to fix this myself, I'll work around it statically. Thanks for the work @rrey 
Ok, I will take a look this week..
@seuf I took some time to check how a fix could be made. I guess that we should have both methods available (slug and uuid) to allow the module to work with early versions of Grafana and more recent ones.

I saw that there are some code duplication in the module (for the header management for instance) and that authentication is not taking advantage of the mechanisms available in fetch_url helper.

Maybe it is an occasion to do a bit of refactoring ? 
I can try to push something this weekend if you are interested ?

Resolving patch:
diff --git a/lib/ansible/modules/monitoring/grafana_dashboard.py b/lib/ansible/modules/monitoring/grafana_dashboard.py
--- a/lib/ansible/modules/monitoring/grafana_dashboard.py
+++ b/lib/ansible/modules/monitoring/grafana_dashboard.py
@@ -16,24 +16,30 @@
 ---
 module: grafana_dashboard
 author:
-  - Thierry Sallé (@tsalle)
+  - Thierry Sallé (@seuf)
 version_added: "2.5"
 short_description: Manage Grafana dashboards
 description:
   - Create, update, delete, export Grafana dashboards via API.
 options:
-  grafana_url:
+  url:
     description:
       - The Grafana URL.
     required: true
-  grafana_user:
+    aliases: [ grafana_url ]
+    version_added: 2.7
+  url_username:
     description:
       - The Grafana API user.
     default: admin
-  grafana_password:
+    aliases: [ grafana_user ]
+    version_added: 2.7
+  url_password:
     description:
       - The Grafana API password.
     default: admin
+    aliases: [ grafana_password ]
+    version_added: 2.7
   grafana_api_key:
     description:
       - The Grafana API key.
@@ -51,10 +57,15 @@
     default: present
   slug:
     description:
+      - Deprecated since Grafana 5. Use grafana dashboard uid instead.
       - slug of the dashboard. It's the friendly url name of the dashboard.
       - When C(state) is C(present), this parameter can override the slug in the meta section of the json file.
       - If you want to import a json dashboard exported directly from the interface (not from the api),
         you have to specify the slug parameter because there is no meta section in the exported json.
+  uid:
+    version_added: 2.7
+    description:
+      - uid of the dasboard to export when C(state) is C(export) or C(absent).
   path:
     description:
       - The path to the json file containing the Grafana dashboard to import or export.
@@ -73,42 +84,63 @@
       - This should only be used on personally controlled sites using self-signed certificates.
     type: bool
     default: 'yes'
+  client_cert:
+    description:
+      - PEM formatted certificate chain file to be used for SSL client authentication.
+      - This file can also include the key as well, and if the key is included, client_key is not required
+    version_added: 2.7
+  client_key:
+    description:
+      - PEM formatted file that contains your private key to be used for SSL client
+      - authentication. If client_cert contains both the certificate and key, this option is not required
+    version_added: 2.7
+  use_proxy:
+    description:
+      - Boolean of whether or not to use proxy.
+    default: 'yes'
+    type: bool
+    version_added: 2.7
 '''
 
 EXAMPLES = '''
-- name: Import Grafana dashboard foo
-  grafana_dashboard:
-    grafana_url: http://grafana.company.com
-    grafana_api_key: XXXXXXXXXXXX
-    state: present
-    message: Updated by ansible
-    overwrite: yes
-    path: /path/to/dashboards/foo.json
-
-- name: Export dashboard
-  grafana_dashboard:
-    grafana_url: http://grafana.company.com
-    grafana_api_key: XXXXXXXXXXXX
-    state: export
-    slug: foo
-    path: /path/to/dashboards/foo.json
+- hosts: localhost
+  connection: local
+  tasks:
+    - name: Import Grafana dashboard foo
+      grafana_dashboard:
+        grafana_url: http://grafana.company.com
+        grafana_api_key: "{{ grafana_api_key }}"
+        state: present
+        message: Updated by ansible
+        overwrite: yes
+        path: /path/to/dashboards/foo.json
+
+    - name: Export dashboard
+      grafana_dashboard:
+        grafana_url: http://grafana.company.com
+        grafana_user: "admin"
+        grafana_password: "{{ grafana_password }}"
+        org_id: 1
+        state: export
+        uid: "000000653"
+        path: "/path/to/dashboards/000000653.json"
 '''
 
 RETURN = '''
 ---
-slug:
-  description: slug of the created / deleted / exported dashboard.
+uid:
+  description: uid or slug of the created / deleted / exported dashboard.
   returned: success
   type: string
-  sample: foo
+  sample: 000000063
 '''
 
 import json
-import base64
+import string
 
 from ansible.module_utils.basic import AnsibleModule
-from ansible.module_utils.urls import fetch_url
-from ansible.module_utils._text import to_bytes
+from ansible.module_utils.urls import fetch_url, url_argument_spec
+from ansible.module_utils._text import to_native
 
 __metaclass__ = type
 
@@ -125,26 +157,62 @@ class GrafanaExportException(Exception):
     pass
 
 
+class GrafanaDeleteException(Exception):
+    pass
+
+
 def grafana_switch_organisation(module, grafana_url, org_id, headers):
     r, info = fetch_url(module, '%s/api/user/using/%s' % (grafana_url, org_id), headers=headers, method='POST')
     if info['status'] != 200:
         raise GrafanaAPIException('Unable to switch to organization %s : %s' % (org_id, info))
 
 
-def grafana_dashboard_exists(module, grafana_url, slug, headers):
+def grafana_headers(module, data):
+    headers = {'content-type': 'application/json; charset=utf8'}
+    if 'grafana_api_key' in data and data['grafana_api_key']:
+        headers['Authorization'] = "Bearer %s" % data['grafana_api_key']
+    else:
+        module.params['force_basic_auth'] = True
+        grafana_switch_organisation(module, data['grafana_url'], data['org_id'], headers)
+
+    return headers
+
+
+def get_grafana_version(module, grafana_url, headers):
+    grafana_version = None
+    r, info = fetch_url(module, '%s/api/frontend/settings' % grafana_url, headers=headers, method='GET')
+    if info['status'] == 200:
+        try:
+            settings = json.loads(r.read())
+            grafana_version = string.split(settings['buildInfo']['version'], '.')[0]
+        except Exception as e:
+            raise GrafanaAPIException(e)
+    else:
+        raise GrafanaAPIException('Unable to get grafana version : %s' % info)
+
+    return int(grafana_version)
+
+
+def grafana_dashboard_exists(module, grafana_url, uid, headers):
     dashboard_exists = False
     dashboard = {}
-    r, info = fetch_url(module, '%s/api/dashboards/db/%s' % (grafana_url, slug), headers=headers, method='GET')
+
+    grafana_version = get_grafana_version(module, grafana_url, headers)
+
+    if grafana_version >= 5:
+        r, info = fetch_url(module, '%s/api/dashboards/uid/%s' % (grafana_url, uid), headers=headers, method='GET')
+    else:
+        r, info = fetch_url(module, '%s/api/dashboards/db/%s' % (grafana_url, uid), headers=headers, method='GET')
     if info['status'] == 200:
         dashboard_exists = True
         try:
             dashboard = json.loads(r.read())
         except Exception as e:
-            raise GrafanaMalformedJson(e)
+            raise GrafanaAPIException(e)
     elif info['status'] == 404:
         dashboard_exists = False
     else:
-        raise GrafanaAPIException('Unable to get dashboard %s : %s' % (slug, info))
+        raise GrafanaAPIException('Unable to get dashboard %s : %s' % (uid, info))
 
     return dashboard_exists, dashboard
 
@@ -156,60 +224,75 @@ def grafana_create_dashboard(module, data):
         with open(data['path'], 'r') as json_file:
             payload = json.load(json_file)
     except Exception as e:
-        raise GrafanaMalformedJson("Can't load json file %s" % str(e))
+        raise GrafanaAPIException("Can't load json file %s" % to_native(e))
 
     # define http header
-    headers = {'content-type': 'application/json; charset=utf8'}
-    if 'grafana_api_key' in data and data['grafana_api_key']:
-        headers['Authorization'] = "Bearer %s" % data['grafana_api_key']
-    else:
-        auth = base64.b64encode(to_bytes('%s:%s' % (data['grafana_user'], data['grafana_password'])).replace('\n', ''))
-        headers['Authorization'] = 'Basic %s' % auth
-        grafana_switch_organisation(module, data['grafana_url'], data['org_id'], headers)
-
-    if data.get('slug'):
-        slug = data['slug']
-    elif 'meta' in payload and 'slug' in payload['meta']:
-        slug = payload['meta']['slug']
+    headers = grafana_headers(module, data)
+
+    grafana_version = get_grafana_version(module, data['grafana_url'], headers)
+    if grafana_version < 5:
+        if data.get('slug'):
+            uid = data['slug']
+        elif 'meta' in payload and 'slug' in payload['meta']:
+            uid = payload['meta']['slug']
+        else:
+            raise GrafanaMalformedJson('No slug found in json. Needed with grafana < 5')
     else:
-        raise GrafanaMalformedJson('No slug found in json')
+        if data.get('uid'):
+            uid = data['uid']
+        elif 'uid' in payload['dashboard']:
+            uid = payload['dashboard']['uid']
+        else:
+            uid = None
 
     # test if dashboard already exists
-    dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], slug, headers=headers)
+    dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], uid, headers=headers)
 
     result = {}
     if dashboard_exists is True:
         if dashboard == payload:
             # unchanged
-            result['slug'] = data['slug']
-            result['msg'] = "Dashboard %s unchanged." % data['slug']
+            result['uid'] = uid
+            result['msg'] = "Dashboard %s unchanged." % uid
             result['changed'] = False
         else:
             # update
-            if 'overwrite' in data and data['overwrite']:
+            if 'overwrite' in data and data['overwrite'] == 'yes':
                 payload['overwrite'] = True
             if 'message' in data and data['message']:
                 payload['message'] = data['message']
 
             r, info = fetch_url(module, '%s/api/dashboards/db' % data['grafana_url'], data=json.dumps(payload), headers=headers, method='POST')
             if info['status'] == 200:
-                result['slug'] = slug
-                result['msg'] = "Dashboard %s updated" % slug
+                if grafana_version >= 5:
+                    try:
+                        dashboard = json.loads(r.read())
+                        uid = dashboard['uid']
+                    except Exception as e:
+                        raise GrafanaAPIException(e)
+                result['uid'] = uid
+                result['msg'] = "Dashboard %s updated" % uid
                 result['changed'] = True
             else:
                 body = json.loads(info['body'])
-                raise GrafanaAPIException('Unable to update the dashboard %s : %s' % (slug, body['message']))
+                raise GrafanaAPIException('Unable to update the dashboard %s : %s' % (uid, body['message']))
     else:
         # create
         if 'dashboard' not in payload:
             payload = {'dashboard': payload}
         r, info = fetch_url(module, '%s/api/dashboards/db' % data['grafana_url'], data=json.dumps(payload), headers=headers, method='POST')
         if info['status'] == 200:
-            result['msg'] = "Dashboard %s created" % slug
+            result['msg'] = "Dashboard %s created" % uid
             result['changed'] = True
-            result['slug'] = slug
+            if grafana_version >= 5:
+                try:
+                    dashboard = json.loads(r.read())
+                    uid = dashboard['uid']
+                except Exception as e:
+                    raise GrafanaAPIException(e)
+            result['uid'] = uid
         else:
-            raise GrafanaAPIException('Unable to create the new dashboard %s : %s - %s.' % (slug, info['status'], info))
+            raise GrafanaAPIException('Unable to create the new dashboard %s : %s - %s.' % (uid, info['status'], info))
 
     return result
 
@@ -217,32 +300,41 @@ def grafana_create_dashboard(module, data):
 def grafana_delete_dashboard(module, data):
 
     # define http headers
-    headers = {'content-type': 'application/json'}
-    if 'grafana_api_key' in data and data['grafana_api_key']:
-        headers['Authorization'] = "Bearer %s" % data['grafana_api_key']
+    headers = grafana_headers(module, data)
+
+    grafana_version = get_grafana_version(module, data['grafana_url'], headers)
+    if grafana_version < 5:
+        if data.get('slug'):
+            uid = data['slug']
+        else:
+            raise GrafanaMalformedJson('No slug parameter. Needed with grafana < 5')
     else:
-        auth = base64.b64encode(to_bytes('%s:%s' % (data['grafana_user'], data['grafana_password'])).replace('\n', ''))
-        headers['Authorization'] = 'Basic %s' % auth
-        grafana_switch_organisation(module, data['grafana_url'], data['org_id'], headers)
+        if data.get('uid'):
+            uid = data['uid']
+        else:
+            raise GrafanaDeleteException('No uid specified %s')
 
     # test if dashboard already exists
-    dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], data['slug'], headers=headers)
+    dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], uid, headers=headers)
 
     result = {}
     if dashboard_exists is True:
         # delete
-        r, info = fetch_url(module, '%s/api/dashboards/db/%s' % (data['grafana_url'], data['slug']), headers=headers, method='DELETE')
+        if grafana_version < 5:
+            r, info = fetch_url(module, '%s/api/dashboards/db/%s' % (data['grafana_url'], uid), headers=headers, method='DELETE')
+        else:
+            r, info = fetch_url(module, '%s/api/dashboards/uid/%s' % (data['grafana_url'], uid), headers=headers, method='DELETE')
         if info['status'] == 200:
-            result['msg'] = "Dashboard %s deleted" % data['slug']
+            result['msg'] = "Dashboard %s deleted" % uid
             result['changed'] = True
-            result['slug'] = data['slug']
+            result['uid'] = uid
         else:
-            raise GrafanaAPIException('Unable to update the dashboard %s : %s' % (data['slug'], info))
+            raise GrafanaAPIException('Unable to update the dashboard %s : %s' % (uid, info))
     else:
         # dashboard does not exist, do nothing
-        result = {'msg': "Dashboard %s does not exist." % data['slug'],
+        result = {'msg': "Dashboard %s does not exist." % uid,
                   'changed': False,
-                  'slug': data['slug']}
+                  'uid': uid}
 
     return result
 
@@ -250,53 +342,65 @@ def grafana_delete_dashboard(module, data):
 def grafana_export_dashboard(module, data):
 
     # define http headers
-    headers = {'content-type': 'application/json'}
-    if 'grafana_api_key' in data and data['grafana_api_key']:
-        headers['Authorization'] = "Bearer %s" % data['grafana_api_key']
+    headers = grafana_headers(module, data)
+
+    grafana_version = get_grafana_version(module, data['grafana_url'], headers)
+    if grafana_version < 5:
+        if data.get('slug'):
+            uid = data['slug']
+        else:
+            raise GrafanaMalformedJson('No slug parameter. Needed with grafana < 5')
     else:
-        auth = base64.b64encode(to_bytes('%s:%s' % (data['grafana_user'], data['grafana_password'])).replace('\n', ''))
-        headers['Authorization'] = 'Basic %s' % auth
-        grafana_switch_organisation(module, data['grafana_url'], data['org_id'], headers)
+        if data.get('uid'):
+            uid = data['uid']
+        else:
+            raise GrafanaExportException('No uid specified')
 
     # test if dashboard already exists
-    dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], data['slug'], headers=headers)
+    dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], uid, headers=headers)
 
     if dashboard_exists is True:
         try:
             with open(data['path'], 'w') as f:
                 f.write(json.dumps(dashboard))
         except Exception as e:
-            raise GrafanaExportException("Can't write json file : %s" % str(e))
-        result = {'msg': "Dashboard %s exported to %s" % (data['slug'], data['path']),
-                  'slug': data['slug'],
+            raise GrafanaExportException("Can't write json file : %s" % to_native(e))
+        result = {'msg': "Dashboard %s exported to %s" % (uid, data['path']),
+                  'uid': uid,
                   'changed': True}
     else:
-        result = {'msg': "Dashboard %s does not exist." % data['slug'],
-                  'slug': data['slug'],
+        result = {'msg': "Dashboard %s does not exist." % uid,
+                  'uid': uid,
                   'changed': False}
 
     return result
 
 
 def main():
+    # use the predefined argument spec for url
+    argument_spec = url_argument_spec()
+    # remove unnecessary arguments
+    del argument_spec['force']
+    del argument_spec['force_basic_auth']
+    del argument_spec['http_agent']
+    argument_spec.update(
+        state=dict(choices=['present', 'absent', 'export'], default='present'),
+        url=dict(aliases=['grafana_url'], required=True),
+        url_username=dict(aliases=['grafana_user'], default='admin'),
+        url_password=dict(aliases=['grafana_password'], default='admin', no_log=True),
+        grafana_api_key=dict(type='str', no_log=True),
+        org_id=dict(default=1, type='int'),
+        uid=dict(type='str'),
+        slug=dict(type='str'),
+        path=dict(type='str'),
+        overwrite=dict(type='bool', default=False),
+        message=dict(type='str'),
+    )
     module = AnsibleModule(
-        argument_spec=dict(
-            state=dict(choices=['present', 'absent', 'export'],
-                       default='present'),
-            grafana_url=dict(required=True),
-            grafana_user=dict(default='admin'),
-            grafana_password=dict(default='admin', no_log=True),
-            grafana_api_key=dict(type='str', no_log=True),
-            org_id=dict(default=1, type='int'),
-            slug=dict(type='str'),
-            path=dict(type='str'),
-            overwrite=dict(type='bool', default=False),
-            message=dict(type='str'),
-            validate_certs=dict(type='bool', default=True)
-        ),
+        argument_spec=argument_spec,
         supports_check_mode=False,
-        required_together=[['grafana_user', 'grafana_password', 'org_id']],
-        mutually_exclusive=[['grafana_user', 'grafana_api_key']],
+        required_together=[['url_username', 'url_password', 'org_id']],
+        mutually_exclusive=[['grafana_user', 'grafana_api_key'], ['uid', 'slug']],
     )
 
     try:
@@ -309,7 +413,7 @@ def main():
     except GrafanaAPIException as e:
         module.fail_json(
             failed=True,
-            msg="error : %s" % e
+            msg="error : %s" % to_native(e)
         )
         return
     except GrafanaMalformedJson as e:
@@ -318,10 +422,16 @@ def main():
             msg="error : json file does not contain a meta section with a slug parameter, or you did'nt specify the slug parameter"
         )
         return
+    except GrafanaDeleteException as e:
+        module.fail_json(
+            failed=True,
+            msg="error : Can't delete dashboard : %s" % to_native(e)
+        )
+        return
     except GrafanaExportException as e:
         module.fail_json(
             failed=True,
-            msg="error : json file cannot be written : %s" % str(e)
+            msg="error : Can't export dashboard : %s" % to_native(e)
         )
         return
 


Test patch: