diff --git a/changelogs/grafana_5_dashboard_compatible.yaml b/changelogs/grafana_5_dashboard_compatible.yaml new file mode 100644 index 0000000000..b49527d711 --- /dev/null +++ b/changelogs/grafana_5_dashboard_compatible.yaml @@ -0,0 +1,3 @@ +--- +bugfixes: +- Grafana dashboard module compatible with grafana 5 (https://github.com/ansible/ansible/pull/41249) diff --git a/lib/ansible/modules/monitoring/grafana_dashboard.py b/lib/ansible/modules/monitoring/grafana_dashboard.py index c8f8941a07..f9346aa049 100644 --- a/lib/ansible/modules/monitoring/grafana_dashboard.py +++ b/lib/ansible/modules/monitoring/grafana_dashboard.py @@ -16,24 +16,30 @@ DOCUMENTATION = ''' --- 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 @@ options: 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 @@ options: - 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 +- 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_api_key: XXXXXXXXXXXX - state: export - slug: foo - 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) + headers = grafana_headers(module, data) - if data.get('slug'): - slug = data['slug'] - elif 'meta' in payload and 'slug' in payload['meta']: - slug = payload['meta']['slug'] + 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 info['status'] == 200: - result['msg'] = "Dashboard %s deleted" % data['slug'] - result['changed'] = True - result['slug'] = data['slug'] + if grafana_version < 5: + r, info = fetch_url(module, '%s/api/dashboards/db/%s' % (data['grafana_url'], uid), headers=headers, method='DELETE') else: - raise GrafanaAPIException('Unable to update the dashboard %s : %s' % (data['slug'], info)) + 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" % uid + result['changed'] = True + result['uid'] = uid + else: + 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