7ad388ba50
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
149 lines
5.9 KiB
YAML
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
|