/
/
/
This repo is destined for my server automations and setup.
1---
2# ============================================================================
3# Image Update Monitor - read-only, scheduled nightly in Semaphore
4# ============================================================================
5#
6# Checks every pin in group_vars/all/images.yml for newer upstream tags of
7# the same variant (scripts/check_image_updates.py), compares the pins with
8# the containers actually running (drift), and publishes the result to Home
9# Assistant as retained MQTT sensors:
10#
11# sensor.homelab_image_updates main images with a newer tag;
12# attributes = the full report
13# sensor.homelab_image_drift containers not running their pin
14# sensor.homelab_image_updates_checked time of the last check
15#
16# Nothing is updated. A bump is a manual edit of images.yml + a deploy.
17# The run fails only if the check itself could not be completed.
18#
19# USAGE:
20# ansible-playbook check-image-updates.yml
21# ============================================================================
22
23- name: Collect the images that are running
24 hosts: docker_servers
25 become: true
26 gather_facts: false
27 ignore_unreachable: true
28
29 tasks:
30 - name: List running containers (with labels, to know their compose project)
31 community.docker.docker_host_info:
32 containers: true
33 verbose_output: true
34 register: _running
35
36- name: Check for newer tags and report
37 hosts: localhost
38 connection: local
39 gather_facts: false
40
41 vars:
42 report_ha_url: "{{ vault_runner.homeassistant_url | default('') | regex_replace('/$', '') }}"
43 report_ha_token: "{{ vault_runner.homeassistant_token | default('') }}"
44 report_device:
45 identifiers: ["homelab_images"]
46 name: "Homelab Images"
47 manufacturer: "server_automation"
48 model: "check-image-updates"
49 # Evaluated by Home Assistant, so Ansible must not render them
50 report_sensors:
51 - key: updates
52 name: "Updates available"
53 icon: "mdi:package-up"
54 value: !unsafe "{{ value_json.updates }}"
55 attributes: true
56 - key: drift
57 name: "Drift"
58 icon: "mdi:source-branch-sync"
59 value: !unsafe "{{ value_json.drift }}"
60 - key: updates_checked
61 name: "Last check"
62 device_class: timestamp
63 value: !unsafe "{{ value_json.checked }}"
64
65 tasks:
66 - name: Create a work directory
67 ansible.builtin.tempfile:
68 state: directory
69 prefix: image-updates-
70 register: _workdir
71
72 - name: Remember the work directory
73 ansible.builtin.set_fact:
74 _work: "{{ _workdir.path }}"
75
76 - name: Write the running images per host
77 ansible.builtin.copy:
78 dest: "{{ _work }}/running.json"
79 mode: '0600'
80 content: >-
81 {%- set out = {} -%}
82 {%- for h in groups['docker_servers'] if hostvars[h]._running.containers is defined -%}
83 {%- set imgs = [] -%}
84 {%- for c in hostvars[h]._running.containers -%}
85 {%- set project = (c.Labels | default({}, true)).get('com.docker.compose.project', '') -%}
86 {%- if project not in images_unmanaged_projects -%}
87 {%- set _ = imgs.append(c.Image) -%}
88 {%- endif -%}
89 {%- endfor -%}
90 {%- set _ = out.update({h: imgs}) -%}
91 {%- endfor -%}
92 {{ out | to_json }}
93
94 - name: Check the registries
95 ansible.builtin.command:
96 argv:
97 - "{{ ansible_playbook_python }}"
98 - "{{ playbook_dir }}/scripts/check_image_updates.py"
99 - --images
100 - "{{ playbook_dir }}/group_vars/all/images.yml"
101 - --running
102 - "{{ _work }}/running.json"
103 - --out
104 - "{{ _work }}/result.json"
105 register: _check
106 changed_when: false
107
108 - name: Read the result
109 ansible.builtin.slurp:
110 src: "{{ _work }}/result.json"
111 register: _result_raw
112
113 - name: Remove the work directory
114 ansible.builtin.file:
115 path: "{{ _work }}"
116 state: absent
117
118 - name: Prepare the report
119 ansible.builtin.set_fact:
120 _result: "{{ _result_raw.content | b64decode | from_json }}"
121 _unreachable: >-
122 {%- set out = [] -%}
123 {%- for h in groups['docker_servers'] if hostvars[h]._running.containers is not defined -%}
124 {%- set _ = out.append(h) -%}
125 {%- endfor -%}
126 {{ out }}
127
128 - name: Show the report
129 ansible.builtin.debug:
130 msg: |
131 {{ _check.stdout }}
132 Main images with updates:
133 {% for r in _result.updates %}
134 - {{ r.service }}/{{ r.key }}: {{ r.current }} -> {{ r.latest }} ({{ r.kind }}){{ ', same major: ' ~ r.latest_same_major if r.latest_same_major else '' }}
135 {% endfor %}
136 Aux images with updates: {{ _result.aux_updates | map(attribute='key') | join(', ') or 'none' }}
137 Drift: {% for d in _result.drift %}{{ d.host }}: {{ d.running }} (pinned {{ d.pinned | join('/') }}); {% else %}none{% endfor %}
138
139 Not checked: {{ _result.unchecked | map(attribute='key') | join(', ') or 'none' }}
140 Registry errors: {{ _result.errors | length }}
141 Hosts not reached for the drift check: {{ _unreachable | join(', ') or 'none' }}
142
143 - name: Publish the sensor discovery configs
144 ansible.builtin.uri:
145 url: "{{ report_ha_url }}/api/services/mqtt/publish"
146 method: POST
147 headers:
148 Authorization: "Bearer {{ report_ha_token }}"
149 body_format: json
150 body:
151 topic: "homeassistant/sensor/homelab_images/{{ item.key }}/config"
152 retain: true
153 payload: >-
154 {{ ({
155 'name': item.name,
156 'unique_id': 'homelab_image_' ~ item.key,
157 'object_id': 'homelab_image_' ~ item.key,
158 'state_topic': 'homelab/images/state',
159 'value_template': item.value,
160 'icon': item.icon | default('mdi:docker'),
161 'device': report_device
162 } | combine({'device_class': item.device_class} if item.device_class is defined else {})
163 | combine({'json_attributes_topic': 'homelab/images/attributes'} if item.attributes | default(false) else {})
164 ) | to_json }}
165 status_code: [200]
166 loop: "{{ report_sensors }}"
167 loop_control:
168 label: "{{ item.key }}"
169 when: report_ha_url | length > 0 and report_ha_token | length > 0
170 no_log: true # the request carries the HA token
171
172 - name: Publish the state and the report
173 ansible.builtin.uri:
174 url: "{{ report_ha_url }}/api/services/mqtt/publish"
175 method: POST
176 headers:
177 Authorization: "Bearer {{ report_ha_token }}"
178 body_format: json
179 body:
180 topic: "homelab/images/{{ item.topic }}"
181 retain: true
182 payload: "{{ item.payload | to_json }}"
183 status_code: [200]
184 loop:
185 - topic: state
186 payload:
187 updates: "{{ _result.updates | length }}"
188 drift: "{{ _result.drift | length }}"
189 checked: "{{ _result.checked }}"
190 - topic: attributes
191 payload: "{{ _result | combine({'unreachable_hosts': _unreachable}) }}"
192 loop_control:
193 label: "{{ item.topic }}"
194 when: report_ha_url | length > 0 and report_ha_token | length > 0
195 no_log: true
196