Files
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

152 lines
5.8 KiB
YAML

---
# Verifies the critical services listed per host in the inventory
# (critical_services) and optionally starts any that are stopped. Services
# found not running are sent as one Telegram message per run when Telegram
# credentials are present.
#
# Extra vars (optional):
# restart_stopped - default false; when true, start services found stopped
# telegram_bot_token / TELEGRAM_BOT_TOKEN - extra var or environment variable
# telegram_chat_id / TELEGRAM_CHAT_ID - extra var or environment variable
- name: Check Host Availability
hosts: ubuntu:rocky
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: Service Check
hosts: reachable_hosts
become: true
gather_facts: false
vars:
restart_stopped: false
tasks:
- name: Gather service facts
ansible.builtin.service_facts:
- name: Evaluate critical services
ansible.builtin.set_fact:
service_states: >-
{{ service_states | default([])
+ [{'name': item, 'state': (ansible_facts.services[item ~ '.service'].state | default('not-found'))}] }}
loop: "{{ critical_services | default([]) }}"
- name: Print report
ansible.builtin.debug:
msg: |
======================================================
HOST: {{ inventory_hostname }} ({{ ansible_host }})
======================================================
{% if service_states | default([]) | length == 0 %}
no critical services defined in inventory
{% else %}
{% for svc in service_states %}
{{ '%-24s' | format(svc.name) }} {{ 'RUNNING' if svc.state == 'running' else ('NOT FOUND' if svc.state == 'not-found' else svc.state | upper) }}
{% endfor %}
{% endif %}
======================================================
- name: Warn on services that are not running
ansible.builtin.debug:
msg: "WARNING: {{ item.name }} is {{ 'not installed' if item.state == 'not-found' else item.state }} on {{ inventory_hostname }}"
loop: "{{ service_states | default([]) }}"
loop_control:
label: "{{ item.name }}"
when: item.state != 'running'
- name: Start stopped services
ansible.builtin.service:
name: "{{ item.name }}"
state: started
loop: "{{ service_states | default([]) }}"
loop_control:
label: "{{ item.name }}"
when:
- restart_stopped | bool
- item.state not in ['running', 'not-found']
- name: Telegram alert
hosts: localhost
connection: local
gather_facts: false
vars:
restart_stopped: false
# 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 }}"
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 svc in hostvars[h].service_states | default([]) if svc.state != 'running' %}
{% set note = ' - start attempted' if (restart_stopped | bool and svc.state != 'not-found') else '' %}
SERVICE {{ h }}: {{ svc.name }} {{ 'not installed' if svc.state == 'not-found' else svc.state }}{{ note }}
{% endfor %}
{% endfor %}
- name: Nothing to alert
ansible.builtin.debug:
msg: "All critical services running - 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 service 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