Skip to content
This repository was archived by the owner on May 31, 2023. It is now read-only.

Commit 2d1ed55

Browse files
authored
Refactor dashboard provisioning (#134)
* synchronize dashboards by default; group tasks in blocks * remove names from blocks * remove quotes * do not move dashboards around * simplify priviledge escalation * do not install rsync in test runs * move dashboard lists manipulation to separate task
1 parent 423ae4f commit 2d1ed55

File tree

4 files changed

+132
-151
lines changed

4 files changed

+132
-151
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Provision and manage [grafana](https://github.com/grafana/grafana) - platform fo
1515
- Ansible >= 2.5
1616
- libselinux-python on deployer host (only when deployer machine has SELinux)
1717
- grafana >= 5.1 (for older grafana versions use this role in version 0.10.1 or earlier)
18-
- rsync if you plan to use grafana provisioning
18+
- jmespath on deployer machine. If you are using Ansible from a Python virtualenv, install *jmespath* to the same virtualenv via pip.
1919

2020
## Role Variables
2121

@@ -24,7 +24,7 @@ All variables which can be overridden are stored in [defaults/main.yml](defaults
2424
| Name | Default Value | Description |
2525
| -------------- | ------------- | -----------------------------------|
2626
| `grafana_use_provisioning` | true | Use Grafana provisioning capalibity when possible (**grafana_version=latest will assume >= 5.0**). |
27-
| `grafana_provisioning_synced` | false | Ensure no previously provisioned dashboards are kept if not referenced anymore. |
27+
| `grafana_provisioning_synced` | true | Ensure no previously provisioned dashboards are kept if not referenced anymore. |
2828
| `grafana_system_user` | grafana | Grafana server system user |
2929
| `grafana_system_group` | grafana | Grafana server system group |
3030
| `grafana_version` | latest | Grafana package version |

defaults/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ grafana_yum_repo_template: etc/yum.repos.d/grafana.repo.j2
66
grafana_use_provisioning: true
77

88
# Should the provisioning be kept synced. If true, previous provisioned objects will be removed if not referenced anymore.
9-
grafana_provisioning_synced: false
9+
grafana_provisioning_synced: true
1010

1111
grafana_instance: "{{ ansible_fqdn | default(ansible_host) | default(inventory_hostname) }}"
1212

molecule/default/prepare.yml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,4 @@
22
- name: Prepare
33
hosts: all
44
gather_facts: false
5-
tasks:
6-
- name: Install rsync for grafana dashboard provisioning
7-
package:
8-
name: ["rsync"]
9-
register: task_result
10-
until: task_result is success
11-
retries: 10
12-
delay: 2
5+
tasks: []

tasks/dashboards.yml

Lines changed: 128 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,94 @@
11
---
2-
- name: Empty local grafana dashboard directory
3-
become: false
4-
file:
5-
path: /tmp/dashboards
6-
state: absent
2+
- become: false
73
delegate_to: localhost
84
run_once: true
9-
check_mode: false
10-
changed_when: false
11-
when: grafana_use_provisioning and grafana_provisioning_synced
5+
block:
6+
- name: Create local grafana dashboard directory
7+
tempfile:
8+
state: directory
9+
register: _tmp_dashboards
10+
changed_when: false
11+
check_mode: false
1212

13-
- name: Create local grafana dashboard directories
14-
become: false
15-
file:
16-
path: /tmp/dashboards
17-
state: directory
18-
mode: 0755
19-
delegate_to: localhost
20-
run_once: true
21-
check_mode: false
22-
changed_when: false
23-
24-
# - name: download grafana dashboard from grafana.net to local folder
25-
# become: false
26-
# get_url:
27-
# url: "https://grafana.com/api/dashboards/{{ item.dashboard_id }}/revisions/{{ item.revision_id }}/download"
28-
# dest: "/tmp/dashboards/{{ item.dashboard_id }}.json"
29-
# register: _download_dashboards
30-
# until: _download_dashboards is succeeded
31-
# retries: 5
32-
# delay: 2
33-
# delegate_to: localhost
34-
# run_once: true
35-
# changed_when: false
36-
# with_items: "{{ grafana_dashboards }}"
37-
# when: grafana_dashboards | length > 0
13+
# Use curl to solve issue #77
14+
- name: download grafana dashboard from grafana.net to local directory
15+
command: >
16+
curl --fail --compressed
17+
https://grafana.com/api/dashboards/{{ item.dashboard_id }}/revisions/{{ item.revision_id }}/download
18+
-o {{ _tmp_dashboards.path }}/{{ item.dashboard_id }}.json
19+
args:
20+
creates: "{{ _tmp_dashboards.path }}/{{ item.dashboard_id }}.json"
21+
warn: false
22+
register: _download_dashboards
23+
until: _download_dashboards is succeeded
24+
retries: 5
25+
delay: 2
26+
with_items: "{{ grafana_dashboards }}"
27+
when: grafana_dashboards | length > 0
28+
changed_when: false
29+
check_mode: false
30+
tags:
31+
- skip_ansible_lint
3832

39-
# Use curl to solve issue #77
40-
- name: download grafana dashboard from grafana.net to local directory
41-
become: false
42-
command: "curl --fail --compressed https://grafana.com/api/dashboards/{{ item.dashboard_id }}/revisions/{{ item.revision_id }}/download -o /tmp/dashboards/{{ item.dashboard_id }}.json" # noqa 204
43-
args:
44-
creates: "/tmp/dashboards/{{ item.dashboard_id }}.json"
45-
warn: false
46-
register: _download_dashboards
47-
until: _download_dashboards is succeeded
48-
retries: 5
49-
delay: 2
50-
delegate_to: localhost
51-
run_once: true
52-
with_items: "{{ grafana_dashboards }}"
53-
when: grafana_dashboards | length > 0
54-
tags:
55-
- skip_ansible_lint
56-
check_mode: false
33+
# As noted in [1] an exported dashboard replaces the exporter's datasource
34+
# name with a representative name, something like 'DS_GRAPHITE'. The name
35+
# is different for each datasource plugin, but always begins with 'DS_'.
36+
# In the rest of the data, the same name is used, but captured in braces,
37+
# for example: '${DS_GRAPHITE}'.
38+
#
39+
# [1] http://docs.grafana.org/reference/export_import/#import-sharing-with-grafana-2-x-or-3-0
40+
#
41+
# The data structure looks (massively abbreviated) something like:
42+
#
43+
# "name": "DS_GRAPHITE",
44+
# "datasource": "${DS_GRAPHITE}",
45+
#
46+
# If we import the downloaded dashboard verbatim, it will not automatically
47+
# be connected to the data source like we want it. The Grafana UI expects
48+
# us to do the final connection by hand, which we do not want to do.
49+
# So, in the below task we ensure that we replace instances of this string
50+
# with the data source name we want.
51+
# To make sure that we're not being too greedy with the regex replacement
52+
# of the data source to use for each dashboard that's uploaded, we make the
53+
# regex match very specific by using the following:
54+
#
55+
# 1. Literal boundaries for " on either side of the match.
56+
# 2. Non-capturing optional group matches for the ${} bits which may, or
57+
# or may not, be there..
58+
# 3. A case-sensitive literal match for DS .
59+
# 4. A one-or-more case-sensitive match for the part that follows the
60+
# underscore, with only A-Z, 0-9 and - or _ allowed.
61+
#
62+
# This regex can be tested and understood better by looking at the
63+
# matches and non-matches in https://regex101.com/r/f4Gkvg/6
5764

58-
# As noted in [1] an exported dashboard replaces the exporter's datasource
59-
# name with a representative name, something like 'DS_GRAPHITE'. The name
60-
# is different for each datasource plugin, but always begins with 'DS_'.
61-
# In the rest of the data, the same name is used, but captured in braces,
62-
# for example: '${DS_GRAPHITE}'.
63-
#
64-
# [1] http://docs.grafana.org/reference/export_import/#import-sharing-with-grafana-2-x-or-3-0
65-
#
66-
# The data structure looks (massively abbreviated) something like:
67-
#
68-
# "name": "DS_GRAPHITE",
69-
# "datasource": "${DS_GRAPHITE}",
70-
#
71-
# If we import the downloaded dashboard verbatim, it will not automatically
72-
# be connected to the data source like we want it. The Grafana UI expects
73-
# us to do the final connection by hand, which we do not want to do.
74-
# So, in the below task we ensure that we replace instances of this string
75-
# with the data source name we want.
76-
# To make sure that we're not being too greedy with the regex replacement
77-
# of the data source to use for each dashboard that's uploaded, we make the
78-
# regex match very specific by using the following:
79-
#
80-
# 1. Literal boundaries for " on either side of the match.
81-
# 2. Non-capturing optional group matches for the ${} bits which may, or
82-
# or may not, be there..
83-
# 3. A case-sensitive literal match for DS .
84-
# 4. A one-or-more case-sensitive match for the part that follows the
85-
# underscore, with only A-Z, 0-9 and - or _ allowed.
86-
#
87-
# This regex can be tested and understood better by looking at the
88-
# matches and non-matches in https://regex101.com/r/f4Gkvg/6
65+
- name: Set the correct data source name in the dashboard
66+
replace:
67+
dest: "{{ _tmp_dashboards.path }}/{{ item.dashboard_id }}.json"
68+
regexp: '"(?:\${)?DS_[A-Z0-9_-]+(?:})?"'
69+
replace: '"{{ item.datasource }}"'
70+
changed_when: false
71+
with_items: "{{ grafana_dashboards }}"
72+
when: grafana_dashboards | length > 0
8973

90-
- name: Set the correct data source name in the dashboard
91-
become: false
92-
replace:
93-
dest: "/tmp/dashboards/{{ item.dashboard_id }}.json"
94-
regexp: '"(?:\${)?DS_[A-Z0-9_-]+(?:})?"'
95-
replace: '"{{ item.datasource }}"'
96-
delegate_to: localhost
97-
run_once: true
98-
changed_when: false
99-
with_items: "{{ grafana_dashboards }}"
100-
when: grafana_dashboards | length > 0
101-
102-
- name: copy local grafana dashboards
103-
become: false
104-
copy:
105-
src: "{{ item }}"
106-
dest: "/tmp/dashboards/{{ item | basename }}"
107-
with_fileglob:
108-
- "{{ grafana_dashboards_dir }}/*.json"
109-
delegate_to: localhost
110-
run_once: true
111-
changed_when: false
112-
113-
- name: import grafana dashboards through API
74+
- name: Import grafana dashboards through API
11475
uri:
11576
url: "{{ grafana_api_url }}/api/dashboards/db"
11677
user: "{{ grafana_security.admin_user }}"
11778
password: "{{ grafana_security.admin_password }}"
11879
force_basic_auth: true
11980
method: POST
12081
body_format: json
121-
body: '{ "dashboard": {{ lookup("file", item) }}, "overwrite": true, "message": "Updated by ansible" }'
82+
body: >
83+
{
84+
"dashboard": {{ lookup("file", item) }},
85+
"overwrite": true,
86+
"message": "Updated by ansible"
87+
}
12288
no_log: true
12389
with_fileglob:
124-
- "/tmp/dashboards/*"
90+
- "{{ _tmp_dashboards.path }}/*"
91+
- "{{ grafana_dashboards_dir }}/*.json"
12592
when: not grafana_use_provisioning
12693

12794
# TODO: uncomment this when ansible 2.7 will be min supported version
@@ -138,36 +105,57 @@
138105
# with_fileglob:
139106
# - "/tmp/dashboards/*"
140107

141-
- name: Create/Update dashboards file (provisioning)
142-
become: true
143-
copy:
144-
dest: "/etc/grafana/provisioning/dashboards/ansible.yml"
145-
content: |
146-
apiVersion: 1
147-
providers:
148-
- name: 'default'
149-
orgId: 1
150-
folder: ''
151-
type: file
152-
options:
153-
path: /var/lib/grafana/dashboards
154-
backup: false
155-
owner: root
156-
group: grafana
157-
mode: 0640
158-
notify: restart grafana
159-
when: grafana_use_provisioning
108+
- when: grafana_use_provisioning
109+
block:
110+
- name: Create/Update dashboards file (provisioning)
111+
become: true
112+
copy:
113+
dest: "/etc/grafana/provisioning/dashboards/ansible.yml"
114+
content: |
115+
apiVersion: 1
116+
providers:
117+
- name: 'default'
118+
orgId: 1
119+
folder: ''
120+
type: file
121+
options:
122+
path: /var/lib/grafana/dashboards
123+
backup: false
124+
owner: root
125+
group: grafana
126+
mode: 0640
127+
notify: restart grafana
128+
129+
- name: Register previously copied dashboards
130+
find:
131+
paths: "/var/lib/grafana/dashboards"
132+
hidden: true
133+
patterns:
134+
- "*.json"
135+
register: _dashboards_present
136+
when: grafana_provisioning_synced
137+
138+
- name: Import grafana dashboards
139+
become: true
140+
copy:
141+
src: "{{ item }}"
142+
dest: "/var/lib/grafana/dashboards/{{ item | basename }}"
143+
with_fileglob:
144+
- "{{ _tmp_dashboards.path }}/*"
145+
- "{{ grafana_dashboards_dir }}/*.json"
146+
register: _dashboards_copied
147+
notify: "provisioned dashboards changed"
148+
149+
- name: Get dashboard lists
150+
set_fact:
151+
_dashboards_present_list: "{{ _dashboards_present | json_query('files[*].path') | default([]) }}"
152+
_dashboards_copied_list: "{{ _dashboards_copied | json_query('results[*].dest') | default([]) }}"
153+
when: grafana_provisioning_synced
160154

161-
- name: Import grafana dashboards through provisioning
162-
become: true
163-
synchronize:
164-
src: "/tmp/dashboards/"
165-
dest: "/var/lib/grafana/dashboards"
166-
archive: false
167-
checksum: true
168-
recursive: true
169-
delete: "{{ grafana_provisioning_synced }}"
170-
rsync_opts:
171-
- "--no-motd"
172-
when: grafana_use_provisioning
173-
notify: "provisioned dashboards changed"
155+
- name: Remove dashbards not present on deployer machine (synchronize)
156+
become: true
157+
file:
158+
path: "{{ item }}"
159+
state: absent
160+
with_items: "{{ _dashboards_present_list | difference( _dashboards_copied_list ) }}"
161+
when: grafana_provisioning_synced

0 commit comments

Comments
 (0)