--- - name: "Download grafana.net dashboards" become: false delegate_to: localhost run_once: true when: "grafana_dashboards | length > 0" block: - name: "Create local grafana dashboard directory" ansible.builtin.tempfile: state: directory register: __tmp_dashboards changed_when: false - name: "Download grafana dashboard from grafana.net to local directory" ansible.builtin.get_url: url: "https://grafana.com/api/dashboards/{{ item.dashboard_id }}/revisions/{{ item.revision_id }}/download" dest: "{{ __tmp_dashboards.path }}/{{ item.dashboard_id }}.json" mode: "0644" register: __download_dashboards until: "__download_dashboards is succeeded" retries: 5 delay: 2 changed_when: false loop: "{{ grafana_dashboards }}" # As noted in [1] an exported dashboard replaces the exporter's datasource # name with a representative name, something like 'DS_GRAPHITE'. The name # is different for each datasource plugin, but always begins with 'DS_'. # In the rest of the data, the same name is used, but captured in braces, # for example: '${DS_GRAPHITE}'. # # [1] http://docs.grafana.org/reference/export_import/#import-sharing-with-grafana-2-x-or-3-0 # # The data structure looks (massively abbreviated) something like: # # "name": "DS_GRAPHITE", # "datasource": "${DS_GRAPHITE}", # # If we import the downloaded dashboard verbatim, it will not automatically # be connected to the data source like we want it. The Grafana UI expects # us to do the final connection by hand, which we do not want to do. # So, in the below task we ensure that we replace instances of this string # with the data source name we want. # To make sure that we're not being too greedy with the regex replacement # of the data source to use for each dashboard that's uploaded, we make the # regex match very specific by using the following: # # 1. Literal boundaries for " on either side of the match. # 2. Non-capturing optional group matches for the ${} bits which may, or # or may not, be there.. # 3. A case-sensitive literal match for DS . # 4. A one-or-more case-sensitive match for the part that follows the # underscore, with only A-Z, 0-9 and - or _ allowed. # # This regex can be tested and understood better by looking at the # matches and non-matches in https://regex101.com/r/f4Gkvg/6 - name: "Set the correct data source name in the dashboard" ansible.builtin.replace: dest: "{{ __tmp_dashboards.path }}/{{ item.dashboard_id }}.json" regexp: '"(?:\${)?DS_[A-Z0-9_-]+(?:})?"' replace: '"{{ item.datasource }}"' changed_when: false loop: "{{ grafana_dashboards }}" - name: "Import grafana dashboards via api" community.grafana.grafana_dashboard: grafana_url: "{{ grafana_api_url }}" grafana_user: "{{ grafana_security.admin_user }}" grafana_password: "{{ grafana_security.admin_password }}" path: "{{ item }}" message: "Updated by ansible role {{ ansible_role_name }}" state: present overwrite: true no_log: "{{ 'false' if lookup('env', 'CI') else 'true' }}" with_fileglob: - "{{ __tmp_dashboards.path }}/*" - "{{ grafana_dashboards_dir }}/*.json" when: "not grafana_use_provisioning" - name: "Import grafana dashboards through provisioning" when: grafana_use_provisioning block: - name: "Create/Update dashboards file (provisioning)" ansible.builtin.copy: dest: "/etc/grafana/provisioning/dashboards/ansible.yml" content: | apiVersion: 1 providers: - name: 'default' orgId: 1 folder: '' type: file options: path: "{{ grafana_data_dir }}/dashboards" backup: false owner: root group: grafana mode: "0640" become: true notify: restart_grafana - name: "Register previously copied dashboards" ansible.builtin.find: paths: "{{ grafana_data_dir }}/dashboards" hidden: true patterns: - "*.json" register: __dashboards_present when: grafana_provisioning_synced - name: "Import grafana dashboards" ansible.builtin.copy: src: "{{ item }}" dest: "{{ grafana_data_dir }}/dashboards/{{ item | basename }}" mode: "0640" with_fileglob: - "{{ __tmp_dashboards.path }}/*" - "{{ grafana_dashboards_dir }}/*.json" become: true register: __dashboards_copied notify: "provisioned dashboards changed" - name: "Remove dashboards not present on deployer machine (synchronize)" ansible.builtin.file: path: "{{ item }}" state: absent loop: "{{ __dashboards_present_list | difference(__dashboards_copied_list) }}" become: true when: grafana_provisioning_synced vars: __dashboards_present_list: "{{ __dashboards_present | json_query('files[*].path') | default([]) }}" __dashboards_copied_list: "{{ __dashboards_copied | json_query('results[*].dest') | default([]) }}"