1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
|
---
- name: Prepare YAML exporter updates
ansible.builtin.set_fact:
_prometheus_target_yaml_updates: >-
{%- set ns = namespace(updates=[]) -%}
{%- set exporters = prometheus_target_exporter +
([] if prometheus_target_skip_default_exporters else prometheus_target_default_exporters) -%}
{%- for exporter in exporters -%}
{%- set defaults = prometheus_target_exporter_defaults[exporter.id] | default({})
if exporter.id is defined else {} -%}
{%- set path =
(exporter.path_prefix if exporter.path_prefix is defined else
(defaults.path_prefix | default(prometheus_target_exporter_target_prefix))) ~
((exporter.path if exporter.path is defined else defaults.path) | mandatory) -%}
{%- set host = (exporter.host if exporter.host is defined else defaults.host) | mandatory -%}
{%- set labels = (defaults.labels | default({})) | combine(exporter.labels | default({})) -%}
{%- set target_state = exporter.state if exporter.state is defined else
(defaults.state | default('present')) -%}
{%- set _ = ns.updates.append(
{'path': path, 'host': host, 'labels': labels, 'state': target_state}) -%}
{%- endfor -%}
{{ ns.updates }}
- name: Validate YAML exporter states
ansible.builtin.assert:
that: _yaml_update.state in ["present", "absent"]
fail_msg: "Exporter state must be 'present' or 'absent', got '{{ _yaml_update.state }}'"
loop: "{{ _prometheus_target_yaml_updates }}"
loop_control:
loop_var: _yaml_update
label: "{{ _yaml_update.path }}"
# Every play host prepares its values above. Applying all updates from one host
# prevents parallel Ansible forks from overwriting each other's YAML changes.
- name: Aggregate YAML exporter updates
ansible.builtin.set_fact:
_prometheus_target_yaml_updates_all: >-
{%- set ns = namespace(updates=[]) -%}
{%- for target_host in ansible_play_batch -%}
{%- for update in hostvars[target_host]._prometheus_target_yaml_updates -%}
{%- set _ = ns.updates.append(update) -%}
{%- endfor -%}
{%- endfor -%}
{{ ns.updates }}
run_once: true
- name: Collect YAML target paths
ansible.builtin.set_fact:
_prometheus_target_yaml_paths: >-
{{ _prometheus_target_yaml_updates_all | map(attribute='path') | unique | list }}
run_once: true
- name: Read YAML target files
ansible.builtin.slurp:
src: "{{ _yaml_path }}"
delegate_to: "{{ prometheus_target_host }}"
loop: "{{ _prometheus_target_yaml_paths }}"
loop_control:
loop_var: _yaml_path
register: _yaml_target_files
failed_when: false
run_once: true
# Build each destination completely in memory and write it once. Besides
# avoiding lost updates, this keeps updates to different files independent.
- name: Build updated YAML target files
ansible.builtin.set_fact:
_prometheus_target_yaml_files: >-
{%- set output = namespace(files=[]) -%}
{%- for file in _yaml_target_files.results -%}
{%- set file_updates = _prometheus_target_yaml_updates_all |
selectattr('path', 'equalto', file._yaml_path) | list -%}
{%- set present_updates = file_updates |
selectattr('state', 'equalto', 'present') | list -%}
{%- set state = namespace(groups=(file.content | b64decode | from_yaml)
if file.content is defined else []) -%}
{%- set state.groups = state.groups if state.groups is not none else [] -%}
{%- for update in file_updates -%}
{%- set next = namespace(found=false, groups=[]) -%}
{%- for group in state.groups -%}
{%- set group_labels = group.labels | default({}) -%}
{%- if update.state == 'absent' -%}
{%- set filtered = group.targets | reject('equalto', update.host) | list -%}
{%- if filtered | length > 0 -%}
{%- set replacement = {'targets': filtered} -%}
{%- if group_labels | length > 0 -%}
{%- set _ = replacement.update({'labels': group_labels}) -%}
{%- endif -%}
{%- set _ = next.groups.append(replacement) -%}
{%- endif -%}
{%- elif group_labels == update.labels -%}
{%- if update.host not in group.targets -%}
{%- set _ = group.targets.append(update.host) -%}
{%- endif -%}
{%- set next.found = true -%}
{%- set _ = next.groups.append(group) -%}
{%- else -%}
{%- set filtered = group.targets | reject('equalto', update.host) | list -%}
{%- if filtered | length > 0 -%}
{%- set replacement = {'targets': filtered} -%}
{%- if group_labels | length > 0 -%}
{%- set _ = replacement.update({'labels': group_labels}) -%}
{%- endif -%}
{%- set _ = next.groups.append(replacement) -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{%- if update.state == 'present' and not next.found -%}
{%- set new_group = {'targets': [update.host]} -%}
{%- if update.labels | length > 0 -%}
{%- set _ = new_group.update({'labels': update.labels}) -%}
{%- endif -%}
{%- set _ = next.groups.append(new_group) -%}
{%- endif -%}
{%- set state.groups = next.groups -%}
{%- endfor -%}
{%- if file.content is defined or present_updates | length > 0 -%}
{%- set _ = output.files.append({'path': file._yaml_path, 'groups': state.groups}) -%}
{%- endif -%}
{%- endfor -%}
{{ output.files }}
run_once: true
- name: Write updated YAML target files
ansible.builtin.copy:
content: "{{ _yaml_file.groups | to_nice_yaml(indent=2, width=1337) }}"
dest: "{{ _yaml_file.path }}"
mode: "0644"
delegate_to: "{{ prometheus_target_host }}"
become: true
loop: "{{ _prometheus_target_yaml_files }}"
loop_control:
loop_var: _yaml_file
label: "{{ _yaml_file.path }}"
register: _yaml_results
run_once: true
- name: Track aggregated YAML changes on each host
ansible.builtin.set_fact:
changed: >-
{{ hostvars[ansible_play_batch | first]._yaml_results.results |
selectattr('changed') | list | length > 0 }}
|