diff --git a/lib/ansible/modules/monitoring/grafana_dashboard.py b/lib/ansible/modules/monitoring/grafana_dashboard.py index 2ce0e940ed8..a97b7317848 100644 --- a/lib/ansible/modules/monitoring/grafana_dashboard.py +++ b/lib/ansible/modules/monitoring/grafana_dashboard.py @@ -49,6 +49,11 @@ options: - The Grafana Organisation ID where the dashboard will be imported / exported. - Not used when I(grafana_api_key) is set, because the grafana_api_key only belongs to one organisation.. default: 1 + folder: + description: + - The Grafana folder where this dashboard will be imported to. + default: General + version_added: '2.10' state: description: - State of the dashboard. @@ -139,6 +144,7 @@ import json from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.urls import fetch_url, url_argument_spec +from ansible.module_utils.six.moves.urllib.parse import urlencode from ansible.module_utils._text import to_native from ansible.module_utils._text import to_text @@ -195,16 +201,40 @@ def get_grafana_version(module, grafana_url, headers): return int(grafana_version) +def grafana_folder_exists(module, grafana_url, folder_name, headers): + # the 'General' folder is a special case, it's ID is always '0' + if folder_name == 'General': + return True, 0 + + try: + r, info = fetch_url(module, '%s/api/folders' % grafana_url, headers=headers, method='GET') + + if info['status'] != 200: + raise GrafanaAPIException("Unable to query Grafana API for folders (name: %s): %d" % (folder_name, info['status'])) + + folders = json.loads(r.read()) + + for folder in folders: + if folder['title'] == folder_name: + return True, folder['id'] + except Exception as e: + raise GrafanaAPIException(e) + + return False, 0 + + def grafana_dashboard_exists(module, grafana_url, uid, headers): dashboard_exists = False dashboard = {} 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') + uri = '%s/api/dashboards/uid/%s' % (grafana_url, uid) else: - r, info = fetch_url(module, '%s/api/dashboards/db/%s' % (grafana_url, uid), headers=headers, method='GET') + uri = '%s/api/dashboards/db/%s' % (grafana_url, uid) + + r, info = fetch_url(module, uri, headers=headers, method='GET') + if info['status'] == 200: dashboard_exists = True try: @@ -219,6 +249,52 @@ def grafana_dashboard_exists(module, grafana_url, uid, headers): return dashboard_exists, dashboard +def grafana_dashboard_search(module, grafana_url, folder_id, title, headers): + + # search by title + uri = '%s/api/search?%s' % (grafana_url, urlencode({ + 'folderIds': folder_id, + 'query': title, + 'type': 'dash-db' + })) + r, info = fetch_url(module, uri, headers=headers, method='GET') + + if info['status'] == 200: + try: + dashboards = json.loads(r.read()) + for d in dashboards: + if d['title'] == title: + return grafana_dashboard_exists(module, grafana_url, d['uid'], headers) + except Exception as e: + raise GrafanaAPIException(e) + else: + raise GrafanaAPIException('Unable to search dashboard %s : %s' % (title, info)) + + return False, None + + +# for comparison, we sometimes need to ignore a few keys +def grafana_dashboard_changed(payload, dashboard): + # you don't need to set the version, but '0' is incremented to '1' by Grafana's API + if payload['dashboard']['version'] == 0: + del(payload['dashboard']['version']) + del(dashboard['dashboard']['version']) + + # the meta key is not part of the 'payload' ever + if 'meta' in dashboard: + del(dashboard['meta']) + + # new dashboards don't require an id attribute (or, it can be 'null'), Grafana's API will generate it + if payload['dashboard']['id'] is None: + del(dashboard['dashboard']['id']) + del(payload['dashboard']['id']) + + if payload == dashboard: + return True + + return False + + def grafana_create_dashboard(module, data): # define data payload for grafana API @@ -251,24 +327,37 @@ def grafana_create_dashboard(module, data): else: uid = None - # test if dashboard already exists - dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], uid, headers=headers) - result = {} - if dashboard_exists is True: - if dashboard == payload: - # unchanged + + # test if the folder exists + if grafana_version >= 5: + folder_exists, folder_id = grafana_folder_exists(module, data['grafana_url'], data['folder'], headers) + if folder_exists is False: + result['msg'] = "Dashboard folder '%s' does not exist." % data['folder'] result['uid'] = uid - result['msg'] = "Dashboard %s unchanged." % uid result['changed'] = False - else: + return result + + payload['folderId'] = folder_id + + # test if dashboard already exists + if uid: + dashboard_exists, dashboard = grafana_dashboard_exists( + module, data['grafana_url'], uid, headers=headers) + else: + dashboard_exists, dashboard = grafana_dashboard_search( + module, data['grafana_url'], folder_id, payload['dashboard']['title'], headers=headers) + + if dashboard_exists is True: + if grafana_dashboard_changed(payload, dashboard): # update if 'overwrite' in data and data['overwrite']: 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') + r, info = fetch_url(module, '%s/api/dashboards/db' % data['grafana_url'], + data=json.dumps(payload), headers=headers, method='POST') if info['status'] == 200: if grafana_version >= 5: try: @@ -277,16 +366,26 @@ def grafana_create_dashboard(module, data): except Exception as e: raise GrafanaAPIException(e) result['uid'] = uid - result['msg'] = "Dashboard %s updated" % uid + result['msg'] = "Dashboard %s updated" % payload['dashboard']['title'] result['changed'] = True else: body = json.loads(info['body']) - raise GrafanaAPIException('Unable to update the dashboard %s : %s' % (uid, body['message'])) + raise GrafanaAPIException('Unable to update the dashboard %s : %s (HTTP: %d)' % + (uid, body['message'], info['status'])) + else: + # unchanged + result['uid'] = uid + result['msg'] = "Dashboard %s unchanged." % payload['dashboard']['title'] + result['changed'] = False else: # create - r, info = fetch_url(module, '%s/api/dashboards/db' % data['grafana_url'], data=json.dumps(payload), headers=headers, method='POST') + if folder_exists is True: + payload['folderId'] = folder_id + + 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" % uid + result['msg'] = "Dashboard %s created" % payload['dashboard']['title'] result['changed'] = True if grafana_version >= 5: try: @@ -296,7 +395,8 @@ def grafana_create_dashboard(module, data): raise GrafanaAPIException(e) result['uid'] = uid else: - raise GrafanaAPIException('Unable to create the new dashboard %s : %s - %s.' % (uid, info['status'], info)) + raise GrafanaAPIException('Unable to create the new dashboard %s : %s - %s.' % + (payload['dashboard']['title'], info['status'], info)) return result @@ -394,6 +494,7 @@ def main(): 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'), + folder=dict(type='str', default='General'), uid=dict(type='str'), slug=dict(type='str'), path=dict(type='str'), @@ -423,7 +524,7 @@ def main(): except GrafanaMalformedJson as e: module.fail_json( failed=True, - msg="error : json file does not contain a meta section with a slug parameter, or you did not specify the slug parameter" + msg="error : %s" % to_native(e) ) return except GrafanaDeleteException as e: