Files
ansible/playbooks/health_check.yml
T
Harwinder Sekhon 7ad388ba50 Telegram alerts: accept credentials from extra vars or environment, report which is missing
Semaphore variable groups can pass values as extra vars or as environment
variables, and secrets can be either type. Read telegram_bot_token,
TELEGRAM_BOT_TOKEN (extra var) or TELEGRAM_BOT_TOKEN (env), same for the
chat id, and say in the "not configured" message which of the two is
missing so a wrong box or a detached variable group is visible in the log.

Claude-Session: https://claude.ai/code/session_01XEizHawueRXUcxQELRZ8kq
2026-09-06 23:15:11 -04:00

149 lines
5.9 KiB
YAML

---
# Disk and RAM report for every reachable host. Filesystems above
# disk_warn_pct and hosts that did not answer are printed as warnings and,
# when Telegram credentials are present, sent as one Telegram message per run.
#
# Extra vars (optional):
# disk_warn_pct - default 80
# telegram_bot_token / TELEGRAM_BOT_TOKEN - extra var or environment variable
# telegram_chat_id / TELEGRAM_CHAT_ID - extra var or environment variable
# Without both Telegram values the playbook only prints the report.
- name: Check Host Availability
hosts: all
gather_facts: false
ignore_unreachable: true
tasks:
- name: Ping hosts to check availability
ansible.builtin.ping:
register: ping_result
ignore_errors: true
ignore_unreachable: true
- name: Add reachable hosts to group
ansible.builtin.group_by:
key: reachable_hosts
when: ping_result is reachable and ping_result is succeeded
- name: Report offline hosts
ansible.builtin.debug:
msg: "{{ inventory_hostname }} - OFFLINE"
when: ping_result is unreachable or ping_result is failed
delegate_to: localhost
- name: Disk and RAM Usage Report
hosts: reachable_hosts
become: true
gather_facts: true
vars:
disk_warn_pct: 80
tasks:
- name: Get disk usage (excluding snap, overlay, tmpfs)
ansible.builtin.shell: df -h --output=target,size,used,avail,pcent -x tmpfs -x devtmpfs -x squashfs -x overlay -x efivarfs | tail -n +2
register: disk_usage
changed_when: false
- name: Calculate memory usage
ansible.builtin.set_fact:
mem_total_mb: "{{ ansible_memtotal_mb }}"
mem_used_mb: "{{ ansible_memory_mb.real.used }}"
mem_pct: "{{ ((ansible_memory_mb.real.used / ansible_memtotal_mb) * 100) | round(1) }}"
swap_total_mb: "{{ ansible_swaptotal_mb }}"
swap_used_mb: "{{ ansible_swaptotal_mb - ansible_swapfree_mb }}"
- name: Print report
ansible.builtin.debug:
msg: |
======================================================
HOST: {{ inventory_hostname }} ({{ ansible_host }})
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
======================================================
RAM: {{ mem_used_mb }}MB / {{ mem_total_mb }}MB ({{ mem_pct }}%)
SWAP: {{ swap_used_mb }}MB / {{ swap_total_mb }}MB
------------------------------------------------------
DISK:
{{ disk_usage.stdout }}
======================================================
- name: Collect filesystems above {{ disk_warn_pct }}%
ansible.builtin.set_fact:
disk_alerts: "{{ disk_alerts | default([]) + [item.split()[0] ~ ' ' ~ item.split()[-1]] }}"
loop: "{{ disk_usage.stdout_lines }}"
when: (item.split()[-1] | replace('%', '') | int) > disk_warn_pct | int
- name: Warn on high disk usage
ansible.builtin.debug:
msg: "WARNING: {{ item }} used on {{ inventory_hostname }} (threshold {{ disk_warn_pct }}%)"
loop: "{{ disk_alerts | default([]) }}"
- name: Telegram alert
hosts: localhost
connection: local
gather_facts: false
vars:
disk_warn_pct: 80
# Accepted from a Semaphore variable group as extra vars (telegram_bot_token
# or TELEGRAM_BOT_TOKEN) or as environment variables (TELEGRAM_BOT_TOKEN).
telegram_bot_token: "{{ TELEGRAM_BOT_TOKEN | default(lookup('env', 'TELEGRAM_BOT_TOKEN'), true) | string }}"
telegram_chat_id: "{{ TELEGRAM_CHAT_ID | default(lookup('env', 'TELEGRAM_CHAT_ID'), true) | string }}"
telegram_configured: "{{ telegram_bot_token | string | length > 0 and telegram_chat_id | string | length > 0 }}"
offline_hosts: "{{ groups['all'] | difference(groups['reachable_hosts'] | default([])) }}"
tasks:
- name: Build alert text
ansible.builtin.set_fact:
alert_text: "{{ alert_body | default('', true) | trim }}" # an empty template renders as None
vars:
alert_body: |-
{% for h in groups['reachable_hosts'] | default([]) %}
{% for fs in hostvars[h].disk_alerts | default([]) %}
DISK {{ h }}: {{ fs }} used (threshold {{ disk_warn_pct }}%)
{% endfor %}
{% endfor %}
{% for h in offline_hosts %}
OFFLINE {{ h }} did not answer
{% endfor %}
- name: Nothing to alert
ansible.builtin.debug:
msg: "Every host reachable and all filesystems at or below {{ disk_warn_pct }}% - no alert"
when: alert_text | length == 0
- name: Print alerts when Telegram is not configured
ansible.builtin.debug:
msg: |-
ALERTS (Telegram not configured - bot token {{ 'set' if telegram_bot_token | string | length > 0 else 'MISSING' }},
chat id {{ 'set' if telegram_chat_id | string | length > 0 else 'MISSING' }};
pass TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID through the variable group attached to this template)
{{ alert_text }}
when:
- alert_text | length > 0
- not telegram_configured
- name: Send Telegram alert
ansible.builtin.uri:
url: "https://api.telegram.org/bot{{ telegram_bot_token }}/sendMessage"
method: POST
body_format: json
body:
chat_id: "{{ telegram_chat_id }}"
text: "Homelab health check {{ now().strftime('%Y-%m-%d %H:%M') }}\n{{ alert_text }}"
status_code: [200]
register: telegram_result
failed_when: false
no_log: true # the bot token is part of the URL
when:
- alert_text | length > 0
- telegram_configured
- name: Telegram delivery status
ansible.builtin.assert:
that: telegram_result.status == 200
fail_msg: "Telegram API returned {{ telegram_result.status }}: {{ telegram_result.json.description | default('no response') }}"
success_msg: "Telegram alert sent ({{ alert_text.split('\n') | length }} line(s))"
when: telegram_result is not skipped