--- # 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