diff --git a/playbooks/statistica.yml b/playbooks/statistica.yml index 4325052..d4028de 100644 --- a/playbooks/statistica.yml +++ b/playbooks/statistica.yml @@ -1,65 +1,291 @@ --- -- name: Update AdGuard VPN exclusions +- name: Collect Debian system metrics and send to Telegram (with optional proxy) hosts: all - gather_facts: no + gather_facts: yes + become: true + vars: - exclusions_url: "https://raw.githubusercontent.com/lib4u/amnezia-tunneling-ru/main/amnezia.json" telegram_token: "8820292312:AAECFO1AEWOJTh30zNuhyjC5C81o_qJHQcM" telegram_chat_id: "407968638" + # Прокси: socks5h://user:pass@host:port — оставь пустым, если не нужен + proxy: "" + # Пакеты, нужные для метрик (Debian) + metrics_packages: + - curl + - bc + - procps + - sysstat + - lm-sensors + - smartmontools + tasks: - - name: Download current exclusion list - ansible.builtin.uri: - url: "{{ exclusions_url }}" - return_content: yes - register: exclusions_raw + # ---------- 0. PREFLIGHT: ставим зависимости на Debian ---------- + - name: Ensure required packages are installed (Debian/Ubuntu) + ansible.builtin.apt: + name: "{{ metrics_packages }}" + state: present + update_cache: yes + cache_valid_time: 3600 + when: ansible_os_family == "Debian" + register: apt_result - - name: Extract domains from JSON (amnezia.json format) - ansible.builtin.set_fact: - domains: "{{ exclusions_raw.content | from_json | json_query('domains') }}" - when: exclusions_raw.content | from_json | type_debug == 'dict' - ignore_errors: yes - - - name: Extract domains from plain text (fallback) - ansible.builtin.set_fact: - domains: "{{ exclusions_raw.content | split('\n') | reject('match', '^#') | reject('match', '^$') | list }}" - when: domains is not defined - - - name: Count domains + - name: Warn if host is not Debian family ansible.builtin.debug: - msg: "Found {{ domains | length }} domains to exclude" + msg: "WARNING: {{ inventory_hostname }} is {{ ansible_os_family }}, не Debian — часть метрик может быть недоступна" + when: ansible_os_family != "Debian" - - name: Clear existing exclusions - ansible.builtin.command: - cmd: adguardvpn-cli site-exclusions clear - changed_when: true + # ---------- 1. Uptime ---------- + - name: Gather uptime + ansible.builtin.command: uptime -p + register: uptime_output + changed_when: false - - name: Set exclusion mode to GENERAL - ansible.builtin.command: - cmd: adguardvpn-cli site-exclusions mode general - changed_when: true + # ---------- 2. Load average ---------- + - name: Get load average (1, 5, 15 min) + ansible.builtin.command: uptime + register: load_output + changed_when: false - - name: Add each domain as exclusion - ansible.builtin.command: - cmd: "adguardvpn-cli site-exclusions add {{ item }}" - loop: "{{ domains }}" - when: domains | length > 0 - loop_control: - pause: 0.1 - changed_when: true + - name: Extract load averages + ansible.builtin.set_fact: + load1: "{{ (load_output.stdout.split('load average:')[1].split(',')[0] | trim) if 'load average:' in load_output.stdout else 'N/A' }}" + 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' }}" - - name: Restart VPN to apply changes - ansible.builtin.command: - cmd: "adguardvpn-cli disconnect && adguardvpn-cli connect" - changed_when: true + # ---------- 3. 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 + vmstat 1 2 | tail -1 | awk '{print $15}' + else + echo "" + fi + register: cpu_idle_vmstat + changed_when: false + failed_when: false - - name: Send Telegram notification via API - ansible.builtin.uri: - url: "https://api.telegram.org/bot{{ telegram_token }}/sendMessage" - method: POST - body_format: json - body: - chat_id: "{{ telegram_chat_id }}" - text: "✅ Список исключений обновлён. Добавлено {{ domains | length }} доменов." - disable_web_page_preview: yes + - name: Get CPU idle via mpstat (fallback) + ansible.builtin.shell: | + if command -v mpstat >/dev/null 2>&1; then + mpstat 1 1 | awk '/Average/ {print $NF}' + else + echo "" + fi + register: cpu_idle_mpstat + changed_when: false + failed_when: false + when: cpu_idle_vmstat.stdout == "" + + - name: Get CPU idle via top (fallback) + ansible.builtin.shell: | + top -bn2 | grep "Cpu(s)" | tail -1 | sed 's/.*, *\([0-9.]*\)%* id.*/\1/' + register: cpu_idle_top + changed_when: false + failed_when: false + when: cpu_idle_vmstat.stdout == "" and cpu_idle_mpstat.stdout == "" + + - name: Get CPU idle via /proc/stat (last resort) + 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) + sleep 1 + curr_idle=$(awk '/^cpu / {print $5}' /proc/stat) + curr_total=$(awk '/^cpu / {print $2+$3+$4+$5+$6+$7+$8}' /proc/stat) + diff_idle=$((curr_idle - prev_idle)) + diff_total=$((curr_total - prev_total)) + awk -v i="$diff_idle" -v t="$diff_total" 'BEGIN {printf "%.1f", (i*100)/t}' + register: cpu_idle_proc + changed_when: false + failed_when: false + when: cpu_idle_vmstat.stdout == "" and cpu_idle_mpstat.stdout == "" and cpu_idle_top.stdout == "" + + - name: Set final idle with validation + ansible.builtin.set_fact: + cpu_idle_final: >- + {%- set idle = (cpu_idle_vmstat.stdout | float) if cpu_idle_vmstat.stdout != "" else + (cpu_idle_mpstat.stdout | float) if cpu_idle_mpstat.stdout != "" else + (cpu_idle_top.stdout | float) if cpu_idle_top.stdout != "" else + (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 + ansible.builtin.shell: | + free -h | awk '/Mem:/ {printf "Занято: %s, Всего: %s, Доступно: %s", $3, $2, $6}' + register: memory_usage_human + changed_when: false + + # ---------- 5. Диск ---------- + - 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 ---------- + - name: Get CPU frequency + ansible.builtin.shell: | + if [ -f /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq ]; then + awk '{printf "%.2f GHz", $1/1000000}' /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq + elif command -v lscpu >/dev/null 2>&1; then + lscpu | grep "MHz" | awk '{print $3}' | head -1 | awk '{printf "%.2f GHz", $1/1000}' + else + echo "" + fi + register: cpu_freq + changed_when: false + failed_when: false + + - name: Set CPU frequency text + ansible.builtin.set_fact: + cpu_freq_text: "{{ cpu_freq.stdout if cpu_freq.stdout != '' else '' }}" + + # ---------- 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 ---------- + - 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}' + register: cpu_temp + changed_when: false + failed_when: false + + - name: Fallback to sensors if thermal zone not available + 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 + changed_when: false + failed_when: false + when: cpu_temp.stdout == "" + + - name: Set final CPU temperature + 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 + 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 + ansible.builtin.shell: | + smartctl -A /dev/{{ disk_device.stdout }} 2>/dev/null \ + | grep -i "Temperature" | awk '{print $10}' \ + | sed 's/°C//g' | awk '{printf "%.1f°C", $1}' + register: disk_temp + changed_when: false + failed_when: false + when: smartctl_check.rc == 0 and disk_device.stdout != "" + + - name: Set disk temperature variables (safe) + ansible.builtin.set_fact: + disk_temp_text: "{{ (disk_temp | default({})).stdout | default('') }}" + disk_device_name: "{{ (disk_device | default({})).stdout | default('') }}" + + # ---------- 10. Расчёт процентов ---------- + - name: Calculate CPU usage % + ansible.builtin.set_fact: + cpu_usage: "{{ 100 - (cpu_idle_final | float) }}" + + - name: Extract disk used percentage + 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) + 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 }}" + + - name: Create bar strings + 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)) }}" + + # ---------- 12. Индикаторы ---------- + - name: Set health indicators + 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 %}" + disk_health: "{% if (disk_used_percent | float) < 70 %}🟢{% elif (disk_used_percent | float) < 85 %}🟡{% else %}🔴{% endif %}" + + # ---------- 13. Формирование сообщения ---------- + - name: Build 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 }}
+ + # ---------- 14. Отправка в Telegram ---------- + - name: Send to Telegram via proxy + ansible.builtin.shell: | + set -e + proxy_opt="" + if [ -n "{{ proxy }}" ]; then + proxy_opt="--socks5-hostname {{ proxy }}" + fi + response=$(curl -s -w "\n%{http_code}" -X POST $proxy_opt \ + -H "Content-Type: application/json" \ + -d '{"chat_id": "{{ telegram_chat_id }}", "text": {{ message_html | to_json }}, "parse_mode": "HTML"}' \ + "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" + exit 0 + fi + echo "ERROR: Telegram send failed" >&2 + exit 1 + changed_when: false + register: tg_response + failed_when: tg_response.rc != 0 delegate_to: localhost - when: telegram_token is defined and telegram_chat_id is defined \ No newline at end of file + become: false \ No newline at end of file