From de8391f980fa32ff80f3c120e46e8f1dc14bd7c8 Mon Sep 17 00:00:00 2001 From: administrator Date: Sun, 11 Oct 2026 22:03:29 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20playbooks/statistica.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- playbooks/statistica.yml | 432 +++++++++++++++++++++++++++++++-------- 1 file changed, 344 insertions(+), 88 deletions(-) diff --git a/playbooks/statistica.yml b/playbooks/statistica.yml index a7a9726..3fe3e43 100644 --- a/playbooks/statistica.yml +++ b/playbooks/statistica.yml @@ -1,5 +1,5 @@ --- -- name: Collect Debian system metrics and send to Telegram (via SOCKS5 proxy) +- name: Weekly Debian server report (with trends) → Telegram via SOCKS5 hosts: all gather_facts: yes become: true @@ -7,10 +7,8 @@ vars: telegram_token: "8820292312:AAECFO1AEWOJTh30zNuhyjC5C81o_qJHQcM" telegram_chat_id: "407968638" - # SOCKS5-прокси на control node (машинке, откуда запускается Ansible) telegram_proxy: "socks5h://administrator:60NthRegT@192.168.0.218:1080" - # Пакеты для метрик (Debian) metrics_packages: - curl - bc @@ -18,10 +16,16 @@ - sysstat - lm-sensors - smartmontools + - gawk + - coreutils + + snapshot_dir: "/var/lib/ansible-metrics" tasks: - # ---------- 0. PREFLIGHT: зависимости на Debian ---------- - - name: Ensure required packages are installed (Debian/Ubuntu) + # ================================================================== + # 0. PREFLIGHT + # ================================================================== + - name: Ensure required packages are installed (Debian) ansible.builtin.apt: name: "{{ metrics_packages }}" state: present @@ -29,19 +33,54 @@ cache_valid_time: 3600 when: ansible_os_family == "Debian" + - name: Enable and start sysstat (needed for sar) + ansible.builtin.service: + name: sysstat + state: started + enabled: yes + when: ansible_os_family == "Debian" + failed_when: false + - name: Warn if host is not Debian family ansible.builtin.debug: msg: "WARNING: {{ inventory_hostname }} is {{ ansible_os_family }}, не Debian — часть метрик может быть недоступна" when: ansible_os_family != "Debian" - # ---------- 1. Uptime ---------- + # ================================================================== + # 1. Читаем снапшот прошлой недели (на control node) + # ================================================================== + - name: Ensure snapshot dir exists on control node + ansible.builtin.file: + path: "{{ snapshot_dir }}" + state: directory + mode: "0755" + delegate_to: localhost + become: false + run_once: true + + - name: Read previous week snapshot + ansible.builtin.slurp: + src: "{{ snapshot_dir }}/{{ inventory_hostname }}.json" + register: prev_snapshot_raw + failed_when: false + delegate_to: localhost + become: false + + - name: Parse previous snapshot (or empty dict on first run) + ansible.builtin.set_fact: + prev: "{{ (prev_snapshot_raw.content | b64decode | from_json) if prev_snapshot_raw.content is defined else {} }}" + delegate_to: localhost + become: false + + # ================================================================== + # 2. БАЗОВЫЕ МЕТРИКИ + # ================================================================== - name: Gather uptime ansible.builtin.command: uptime -p register: uptime_output changed_when: false - # ---------- 2. Load average ---------- - - name: Get load average (1, 5, 15 min) + - name: Get load average ansible.builtin.command: uptime register: load_output changed_when: false @@ -52,7 +91,7 @@ load5: "{{ (load_output.stdout.split('load average:')[1].split(',')[1] | trim) if 'load average:' in load_output.stdout else 'N/A' }}" load15: "{{ (load_output.stdout.split('load average:')[1].split(',')[2] | trim) if 'load average:' in load_output.stdout else 'N/A' }}" - # ---------- 3. CPU idle (vmstat -> mpstat -> top -> /proc/stat) ---------- + # CPU idle (vmstat -> mpstat -> top -> /proc/stat) - name: Get CPU idle via vmstat ansible.builtin.shell: | if command -v vmstat >/dev/null 2>&1; then @@ -64,7 +103,7 @@ changed_when: false failed_when: false - - name: Get CPU idle via mpstat (fallback) + - name: Get CPU idle via mpstat ansible.builtin.shell: | if command -v mpstat >/dev/null 2>&1; then mpstat 1 1 | awk '/Average/ {print $NF}' @@ -76,7 +115,7 @@ failed_when: false when: cpu_idle_vmstat.stdout == "" - - name: Get CPU idle via top (fallback) + - name: Get CPU idle via top ansible.builtin.shell: | top -bn2 | grep "Cpu(s)" | tail -1 | sed 's/.*, *\([0-9.]*\)%* id.*/\1/' register: cpu_idle_top @@ -84,7 +123,7 @@ failed_when: false when: cpu_idle_vmstat.stdout == "" and cpu_idle_mpstat.stdout == "" - - name: Get CPU idle via /proc/stat (last resort) + - name: Get CPU idle via /proc/stat ansible.builtin.shell: | prev_idle=$(awk '/^cpu / {print $5}' /proc/stat) prev_total=$(awk '/^cpu / {print $2+$3+$4+$5+$6+$7+$8}' /proc/stat) @@ -108,21 +147,27 @@ (cpu_idle_proc.stdout | float) if cpu_idle_proc.stdout != "" else 90 -%} {%- if idle > 100 or idle < 0 -%}{{ 90 }}{%- else -%}{{ idle }}{%- endif -%} - # ---------- 4. Память ---------- - - name: Get memory usage in human-readable + # Память + - name: Get memory usage human ansible.builtin.shell: | free -h | awk '/Mem:/ {printf "Занято: %s, Всего: %s, Доступно: %s", $3, $2, $6}' register: memory_usage_human changed_when: false - # ---------- 5. Диск ---------- + - name: Get memory percentage + ansible.builtin.shell: | + free -m | awk 'NR==2{printf "%.0f", $3/$2*100}' + register: mem_percent + changed_when: false + + # Диск - name: Get disk usage for root ansible.builtin.shell: | df -h / | awk 'NR==2{printf "Занято: %s, Всего: %s, Свободно: %s, Использовано: %s", $3, $2, $4, $5}' register: disk_usage changed_when: false - # ---------- 6. Частота CPU ---------- + # Частота CPU - name: Get CPU frequency ansible.builtin.shell: | if [ -f /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq ]; then @@ -138,20 +183,9 @@ - name: Set CPU frequency text ansible.builtin.set_fact: - cpu_freq_text: "{{ cpu_freq.stdout if cpu_freq.stdout != '' else '' }}" + cpu_freq_text: "{{ cpu_freq.stdout | default('') }}" - # ---------- 7. Топ-5 процессов ---------- - - name: Get top 5 processes by memory - ansible.builtin.shell: | - ps aux --sort=-%mem | head -6 | tail -5 | awk '{print $11 " (" $4 "%)"}' - register: top_procs - changed_when: false - - - name: Join top processes into a single string - ansible.builtin.set_fact: - top_procs_joined: "{{ top_procs.stdout_lines | join('\n') }}" - - # ---------- 8. Температура CPU ---------- + # Температура CPU - name: Get CPU temperature from thermal zone ansible.builtin.shell: | cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null | awk '{printf "%.1f°C", $1/1000}' @@ -159,7 +193,7 @@ changed_when: false failed_when: false - - name: Fallback to sensors if thermal zone not available + - name: Fallback to sensors ansible.builtin.shell: | sensors 2>/dev/null | grep -i "Package id 0" | awk '{print $4}' | sed 's/+//g' | sed 's/°C//g' | awk '{printf "%.1f°C", $1}' register: cpu_temp_sensors @@ -171,21 +205,21 @@ ansible.builtin.set_fact: cpu_temp_final: "{{ cpu_temp.stdout if cpu_temp.stdout != '' else (cpu_temp_sensors.stdout | default('')) }}" - # ---------- 9. Температура диска ---------- + # Температура и здоровье диска - name: Check if smartctl exists ansible.builtin.command: which smartctl register: smartctl_check changed_when: false failed_when: false - - name: Get list of block devices + - name: Get first block device ansible.builtin.shell: | lsblk -dn -o NAME | grep -E '^(sd|hd|nvme|vd|xvd)' | head -1 register: disk_device changed_when: false failed_when: false - - name: Get disk temperature for first found disk + - name: Get disk temperature ansible.builtin.shell: | smartctl -A /dev/{{ disk_device.stdout }} 2>/dev/null \ | grep -i "Temperature" | awk '{print $10}' \ @@ -195,75 +229,299 @@ failed_when: false when: smartctl_check.rc == 0 and disk_device.stdout != "" - - name: Set disk temperature variables (safe) + - name: Get disk SMART health + ansible.builtin.shell: | + smartctl -H /dev/{{ disk_device.stdout }} 2>/dev/null \ + | grep -i "SMART overall-health" | awk -F: '{print $2}' | xargs + register: smart_health + changed_when: false + failed_when: false + when: smartctl_check.rc == 0 and disk_device.stdout != "" + + - name: Get reallocated sectors + ansible.builtin.shell: | + smartctl -A /dev/{{ disk_device.stdout }} 2>/dev/null \ + | awk '/Reallocated_Sector_Ct/ {print $NF}' | head -1 + register: reallocated + changed_when: false + failed_when: false + when: smartctl_check.rc == 0 and disk_device.stdout != "" + + - name: Set disk-related facts (safe) ansible.builtin.set_fact: disk_temp_text: "{{ (disk_temp | default({})).stdout | default('') }}" disk_device_name: "{{ (disk_device | default({})).stdout | default('') }}" + smart_health_text: "{{ (smart_health | default({})).stdout | default('N/A') }}" + reallocated_text: "{{ (reallocated | default({})).stdout | default('0') }}" - # ---------- 10. Расчёт процентов ---------- + # ================================================================== + # 3. НЕДЕЛЬНЫЕ АГРЕГАТЫ через sar + # ================================================================== + - name: Weekly CPU avg/peak from sar + ansible.builtin.shell: | + { + for i in $(seq 0 6); do + d=$(date -d "$i days ago" +%d) + f="/var/log/sysstat/sa$d" + [ -f "$f" ] && sar -u -f "$f" 2>/dev/null + done + } | awk '/^[0-9]/ { + cpu=100-$NF + sum+=cpu; n++ + if (cpu>max) max=cpu + } END { + if (n>0) printf "%.1f %.1f %d", sum/n, max, n; + else print "N/A N/A 0" + }' + register: cpu_week + changed_when: false + failed_when: false + + - name: Weekly memory avg/peak from sar + ansible.builtin.shell: | + { + for i in $(seq 0 6); do + d=$(date -d "$i days ago" +%d) + f="/var/log/sysstat/sa$d" + [ -f "$f" ] && sar -r -f "$f" 2>/dev/null + done + } | awk '/^[0-9]/ { + used=$4+$5 + if (used>max) max=used + } END { + if (max>0) printf "%.1f", max; + else print "N/A" + }' + register: mem_week_peak + changed_when: false + failed_when: false + + - name: Parse weekly CPU values + ansible.builtin.set_fact: + cpu_week_avg: "{{ (cpu_week.stdout.split()[0]) if cpu_week.stdout.split() | length >= 1 else 'N/A' }}" + cpu_week_peak: "{{ (cpu_week.stdout.split()[1]) if cpu_week.stdout.split() | length >= 2 else 'N/A' }}" + cpu_week_samples: "{{ (cpu_week.stdout.split()[2]) if cpu_week.stdout.split() | length >= 3 else '0' }}" + + # ================================================================== + # 4. СОСТОЯНИЕ СИСТЕМЫ: обновления, reboot, сервисы, безопасность + # ================================================================== + - name: Count upgradable packages + ansible.builtin.shell: | + apt list --upgradable 2>/dev/null | tail -n +2 | grep -c upgradable || true + register: upgradable_count + changed_when: false + failed_when: false + + - name: Count security updates + ansible.builtin.shell: | + apt list --upgradable 2>/dev/null | grep -ci security || true + register: security_count + changed_when: false + failed_when: false + + - name: Check if reboot required + ansible.builtin.stat: + path: /var/run/reboot-required + register: reboot_required + + - name: Count failed systemd units + ansible.builtin.shell: | + systemctl --failed --no-legend 2>/dev/null | wc -l + register: failed_units + changed_when: false + failed_when: false + + - name: Count reboots in last 7 days + ansible.builtin.shell: | + last -x reboot --since "7 days ago" 2>/dev/null | grep -c "^reboot" || true + register: reboot_count + changed_when: false + failed_when: false + + - name: Count failed SSH logins in last 7 days + ansible.builtin.shell: | + journalctl --since "7 days ago" -u ssh --no-pager 2>/dev/null \ + | grep -c "Failed password" || true + register: failed_ssh + changed_when: false + failed_when: false + + - name: Count OOM kills in last 7 days + ansible.builtin.shell: | + journalctl --since "7 days ago" -k --no-pager 2>/dev/null \ + | grep -ci "out of memory" || true + register: oom_kills + changed_when: false + failed_when: false + + - name: Count I/O errors in last 7 days + ansible.builtin.shell: | + journalctl --since "7 days ago" -k --no-pager 2>/dev/null \ + | grep -ci "I/O error" || true + register: io_errors + changed_when: false + failed_when: false + + # Топ-5 процессов по памяти (мгновенно) + по суммарному CPU-времени за неделю + - name: Top 5 by memory now + ansible.builtin.shell: | + ps aux --sort=-%mem | head -6 | tail -5 | awk '{print $11 " (" $4 "%)"}' + register: top_procs + changed_when: false + + - name: Top 5 by cumulative CPU time (long-running) + ansible.builtin.shell: | + ps -eo etimes,comm --sort=-etimes | awk 'NR>1 && $1>604800 {print $2 " (uptime " int($1/86400) "d)"}' | head -5 + register: top_long + changed_when: false + failed_when: false + + - name: Join top lists + ansible.builtin.set_fact: + top_procs_joined: "{{ top_procs.stdout_lines | join('\n') }}" + top_long_joined: "{{ top_long.stdout_lines | join('\n') if top_long.stdout_lines | length > 0 else '(нет долгоживущих процессов > 7 дней)' }}" + + # ================================================================== + # 5. РАСЧЁТЫ, ДЕЛЬТЫ, HEALTH-SCORE + # ================================================================== - name: Calculate CPU usage % ansible.builtin.set_fact: cpu_usage: "{{ 100 - (cpu_idle_final | float) }}" - - name: Extract disk used percentage + - name: Extract disk used % ansible.builtin.set_fact: disk_used_percent: "{{ disk_usage.stdout.split('Использовано:')[1].strip().replace('%', '') | float }}" - - name: Extract memory percentage - ansible.builtin.shell: | - free -m | awk 'NR==2{printf "%.0f", $3/$2*100}' - register: mem_percent - changed_when: false - - # ---------- 11. Прогресс-бары ---------- - - name: Build progress bars (20 chars) + # Δ-тренды относительно прошлой недели + - name: Compute deltas vs previous week ansible.builtin.set_fact: - bar_length: 20 - cpu_bar: "{{ ((cpu_usage | float / 5) | round(0) | int) if (cpu_usage | float) < 100 else 20 }}" - mem_bar: "{{ ((mem_percent.stdout | float / 5) | round(0) | int) if (mem_percent.stdout | float) < 100 else 20 }}" - disk_bar: "{{ ((disk_used_percent | float / 5) | round(0) | int) if (disk_used_percent | float) < 100 else 20 }}" + disk_delta: "{{ ((disk_used_percent | float) - (prev.disk_used_pct | float)) | round(1) }}" + mem_delta: "{{ ((mem_percent.stdout | float) - (prev.mem_pct | float)) | round(1) }}" + when: + - prev.disk_used_pct is defined + - prev.mem_pct is defined - - name: Create bar strings + - name: Format delta strings with arrows ansible.builtin.set_fact: - cpu_progress: "{{ '█' * (cpu_bar | int) + '░' * (bar_length - (cpu_bar | int)) }}" - mem_progress: "{{ '█' * (mem_bar | int) + '░' * (bar_length - (mem_bar | int)) }}" - disk_progress: "{{ '█' * (disk_bar | int) + '░' * (bar_length - (disk_bar | int)) }}" + disk_delta_str: >- + {%- if disk_delta is defined -%} + {%- set d = disk_delta | float -%} + {%- if d > 1 %} (+{{ d }}% ⬆️) + {%- elif d < -1 %} ({{ d }}% ⬇️) + {%- else %} (≈ без изменений){%- endif -%} + {%- else -%} (нет данных за прошлую неделю){%- endif -%} + mem_delta_str: >- + {%- if mem_delta is defined -%} + {%- set d = mem_delta | float -%} + {%- if d > 3 %} (+{{ d }}% ⬆️) + {%- elif d < -3 %} ({{ d }}% ⬇️) + {%- else %} (≈ без изменений){%- endif -%} + {%- else -%} (нет данных){%- endif -%} - # ---------- 12. Индикаторы ---------- - - name: Set health indicators + # Health indicators по компонентам + - name: Set component health ansible.builtin.set_fact: cpu_health: "{% if (cpu_usage | float) < 50 %}🟢{% elif (cpu_usage | float) < 80 %}🟡{% else %}🔴{% endif %}" - mem_health: "{% if (mem_percent.stdout | float) < 50 %}🟢{% elif (mem_percent.stdout | float) < 80 %}🟡{% else %}🔴{% endif %}" + mem_health: "{% if (mem_percent.stdout | float) < 60 %}🟢{% elif (mem_percent.stdout | float) < 85 %}🟡{% else %}🔴{% endif %}" disk_health: "{% if (disk_used_percent | float) < 70 %}🟢{% elif (disk_used_percent | float) < 85 %}🟡{% else %}🔴{% endif %}" - # ---------- 13. Формирование сообщения ---------- - - name: Build message (HTML) + # Общий health-score: считаем количество warning-условий + - name: Compute overall health score + ansible.builtin.set_fact: + warnings_list: >- + {{ + ( + (['Диск > 80%'] if (disk_used_percent | float) > 80 else []) + + (['Диск вырос > 5% за неделю'] if (disk_delta is defined and (disk_delta | float) > 5) else []) + + (['Требуется перезагрузка'] if reboot_required.stat.exists else []) + + (['Security-обновления не установлены'] if (security_count.stdout | int) > 0 else []) + + (['Failed systemd units: ' ~ failed_units.stdout] if (failed_units.stdout | int) > 0 else []) + + (['OOM-kills за неделю'] if (oom_kills.stdout | int) > 0 else []) + + (['I/O errors в ядре'] if (io_errors.stdout | int) > 0 else []) + + (['SMART не PASSED'] if (smart_health_text != 'PASSED' and smart_health_text != 'N/A' and smart_health_text != '') else []) + ) | list + }} + + - name: Compute overall status emoji + ansible.builtin.set_fact: + overall_status: >- + {%- set n = warnings_list | length -%} + {{ '🔴 Требует внимания' if n >= 3 else ('🟡 Есть предупреждения' if n > 0 else '🟢 Всё в порядке') }} + + # ================================================================== + # 6. ФОРМИРОВАНИЕ СООБЩЕНИЯ + # ================================================================== + - name: Build weekly report message (HTML) ansible.builtin.set_fact: message_html: | - 📊 Системные метрики ({{ ansible_date_time.date }} {{ ansible_date_time.time }}) - 🖥️ Хост: {{ ansible_hostname }} ({{ ansible_default_ipv4.address | default('N/A') }}) - ⏱️ Время работы: {{ uptime_output.stdout }} - 📈 Нагрузка (1,5,15 мин): {{ load1 | default('N/A') }} , {{ load5 | default('N/A') }} , {{ load15 | default('N/A') }} - ─────────────── - 💻 CPU: {{ (cpu_usage | float) | round(1) }}% {{ cpu_health }}{% if cpu_freq_text != '' %} ({{ cpu_freq_text }}){% endif %} - {{ cpu_progress }} - ─────────────── - 🧠 Память: {{ mem_percent.stdout }}% {{ mem_health }} ({{ memory_usage_human.stdout }}) - {{ mem_progress }} - ─────────────── - 💾 Диск /: {{ (disk_used_percent | float) | round(1) }}% {{ disk_health }} ({{ disk_usage.stdout.split('Использовано:')[0].strip() }}) - {{ disk_progress }} - ─────────────── - 🌡️ Температура CPU: {{ cpu_temp_final | default('недоступно') }} - {% if disk_temp_text != "" %} - 💽 Температура диска ({{ disk_device_name }}): {{ disk_temp_text }} - {% endif %} - ─────────────── - 🔥 Топ-5 процессов по памяти: -
{{ top_procs_joined }}
+ 📅 Еженедельный отчёт: {{ ansible_hostname }} + Статус: {{ overall_status }} {% if warnings_list | length > 0 %}({{ warnings_list | length }} ⚠️){% endif %} + {{ ansible_date_time.date }} {{ ansible_date_time.time }} · {{ ansible_default_ipv4.address | default('N/A') }} - # ---------- 14. Проверка доступности Telegram через прокси ---------- - - name: Check Telegram API reachability via SOCKS5 proxy (from control node) + {% if warnings_list | length > 0 %} + ⚠️ Предупреждения: + {% for w in warnings_list %}• {{ w }} + {% endfor %} + {% endif %} + + ─────────────── + 📊 Средние и пиковые за неделю + 💻 CPU: avg {{ cpu_week_avg }}% · peak {{ cpu_week_peak }}% ({{ cpu_week_samples }} замеров) + 🧠 RAM: peak {{ mem_week_peak }}% использовано + ─────────────── + 📈 Текущее состояние + ⏱️ Аптайм: {{ uptime_output.stdout }} + 📉 Load avg (1,5,15): {{ load1 }} , {{ load5 }} , {{ load15 }} + + 💻 CPU сейчас: {{ (cpu_usage | float) | round(1) }}% {{ cpu_health }}{% if cpu_freq_text != '' %} ({{ cpu_freq_text }}){% endif %} + 🧠 RAM сейчас: {{ mem_percent.stdout }}% {{ mem_health }} ({{ memory_usage_human.stdout }}){{ mem_delta_str }} + 💾 Диск /: {{ (disk_used_percent | float) | round(1) }}% {{ disk_health }} ({{ disk_usage.stdout.split('Использовано:')[0].strip() }}){{ disk_delta_str }} + ─────────────── + 🌡️ Температуры + CPU: {{ cpu_temp_final | default('недоступно') }} + {% if disk_temp_text != "" %}Диск {{ disk_device_name }}: {{ disk_temp_text }} · SMART: {{ smart_health_text }} · realloc: {{ reallocated_text }}{% endif %} + ─────────────── + 🛠️ Обслуживание + 📦 Обновлений: {{ upgradable_count.stdout }} (🔴 security: {{ security_count.stdout }}) + ♻️ Требуется reboot: {{ 'ДА ⚠️' if reboot_required.stat.exists else 'нет' }} + 🔁 Перезагрузок за неделю: {{ reboot_count.stdout }} + 🚨 Failed units: {{ failed_units.stdout }} + ─────────────── + 🔐 Безопасность и ошибки за неделю + 🔓 Неудачных SSH-логинов: {{ failed_ssh.stdout }} + 💥 OOM-kills: {{ oom_kills.stdout }} + 🗄️ I/O errors: {{ io_errors.stdout }} + ─────────────── + 🔥 Топ-5 процессов по памяти (сейчас): +
{{ top_procs_joined }}
+ 🕰️ Топ-5 долгоживущих процессов (> 7 дней): +
{{ top_long_joined }}
+ + # ================================================================== + # 7. СОХРАНЕНИЕ СНАПШОТА ДЛЯ СЛЕДУЮЩЕЙ НЕДЕЛИ + # ================================================================== + - name: Save snapshot for next week + ansible.builtin.copy: + content: | + {{ + { + 'ts': ansible_date_time.iso8601, + 'disk_used_pct': disk_used_percent | float, + 'mem_pct': mem_percent.stdout | float, + 'cpu_now': cpu_usage | float, + 'cpu_week_avg': cpu_week_avg, + 'cpu_week_peak': cpu_week_peak + } | to_nice_json + }} + dest: "{{ snapshot_dir }}/{{ inventory_hostname }}.json" + mode: "0644" + delegate_to: localhost + become: false + + # ================================================================== + # 8. ОТПРАВКА В TELEGRAM через SOCKS5 (с control node) + # ================================================================== + - name: Check Telegram API reachability via SOCKS5 proxy ansible.builtin.shell: | curl -s -o /dev/null -w "%{http_code}" --max-time 15 \ --proxy "{{ telegram_proxy }}" \ @@ -274,33 +532,31 @@ delegate_to: localhost become: false - - name: Fail early if Telegram API is unreachable via proxy + - name: Fail if Telegram API unreachable ansible.builtin.fail: msg: > Telegram API недоступен через прокси {{ telegram_proxy }}. - HTTP-код: {{ tg_check.stdout }}. Проверь прокси, учётку и сеть. + HTTP-код: {{ tg_check.stdout }}. when: tg_check.stdout != "200" delegate_to: localhost become: false - # ---------- 15. Отправка в Telegram через SOCKS5-прокси ---------- - - name: Send to Telegram via SOCKS5 proxy (from control node) + - name: Send report to Telegram ansible.builtin.shell: | set -e response=$(curl -s -w "\n%{http_code}" -X POST \ --proxy "{{ telegram_proxy }}" \ -H "Content-Type: application/json" \ - -d '{"chat_id": "{{ telegram_chat_id }}", "text": {{ message_html | to_json }}, "parse_mode": "HTML"}' \ + -d '{"chat_id": "{{ telegram_chat_id }}", "text": {{ message_html | to_json }}, "parse_mode": "HTML", "disable_web_page_preview": true}' \ "https://api.telegram.org/bot{{ telegram_token }}/sendMessage" 2>&1) http_code=$(echo "$response" | tail -n1) body=$(echo "$response" | sed '$d') echo "HTTP code: $http_code" - echo "Response body: $body" if [ "$http_code" -eq 200 ] && echo "$body" | grep -q '"ok":true'; then - echo "Message sent successfully" + echo "OK" exit 0 fi - echo "ERROR: Telegram send failed" >&2 + echo "ERROR: $body" >&2 exit 1 changed_when: false register: tg_response