commit e3238bebf14a6cbe677cca6bc1c0639179a67352 Author: vikkernes Date: Sun Aug 2 21:46:32 2026 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..530f0d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +venv/ +__pycache__/ +*.pyc +bot.log +http:/ diff --git a/backup/bot.py.bak b/backup/bot.py.bak new file mode 100644 index 0000000..6a30844 --- /dev/null +++ b/backup/bot.py.bak @@ -0,0 +1,148 @@ +import asyncio +import logging +from datetime import datetime +from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes + +from config import BOT_TOKEN, ADMIN_ID, HOSTS_LIST +from monitor import collect_metrics + +# Настройка логирования +logging.basicConfig(level=logging.INFO) + +# Кеш для хранения последних собранных данных (чтобы не дергать SSH каждый раз при нажатии кнопки) +CACHE = {} + +def format_metrics(data): + """Формирует красивое сообщение из метрик""" + if "error" in data: + return f"❌ Ошибка подключения к {data['host']}:\n{data['error']}" + + m = data['metrics'] + host = data['host'] + ip = data['ip'] + + # Блок CPU + cpu_bar = "█" * int(m['cpu'] // 5) + "░" * (20 - int(m['cpu'] // 5)) + text = f"🖥️ *{host} ({ip})*\n" + text += f"🕒 *Uptime:* {m.get('uptime', 'N/A')}\n" + text += f"📊 *Load Avg:* {m.get('load_1', '0')}, {m.get('load_5', '0')}, {m.get('load_15', '0')}\n\n" + + text += f"🔥 *CPU:* {m['cpu']:.1f}%\n`{cpu_bar}`\n" + + mem_bar = "█" * int(m['mem_percent'] // 5) + "░" * (20 - int(m['mem_percent'] // 5)) + text += f"🧠 *RAM:* {m['mem_used_mb']} MB ({m['mem_percent']}%)\n`{mem_bar}`\n" + + # Сеть (за час) + text += f"🌐 *Network (per hour):* 📥 {m['net_rx_mb']} MB | 📤 {m['net_tx_mb']} MB\n" + + # Диски + text += "\n💾 *Disks (top 3 by %util):*\n" + for disk in m.get('disks', []): + text += f" └─ {disk['name']}: util {disk['util']:.1f}% (r: {disk['rps']:.1f}/s, w: {disk['wps']:.1f}/s)\n" + + # Температуры + if m.get('temps'): + text += "\n🌡️ *Temperatures:*\n" + for i, temp in enumerate(m['temps']): + text += f" └─ Sensor {i+1}: {temp:.1f}°C\n" + + # Топ процессов + text += "\n🏆 *Top 5 CPU:*\n```\n" + text += m.get('top_cpu', 'N/A') + "\n```" + text += "🏆 *Top 5 RAM:*\n```\n" + text += m.get('top_mem', 'N/A') + "\n```" + + # Логи и безопасность + text += "\n⚠️ *Recent Errors (journalctl -p 3):*\n```\n" + text += m.get('logs_errors', 'N/A')[:300] + "\n```" + + text += "\n🛡️ *Fail2ban Status:*\n```\n" + text += m.get('fail2ban_status', 'N/A') + "\n```" + + text += "\n🚫 *Fail2ban Logs (last bans):*\n```\n" + text += m.get('fail2ban_logs', 'N/A') + "\n```" + + text += f"\n⏱️ *Обновлено:* {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + return text + + # OOM проверка + text += "\n💀 *Out of Memory Events:*\n```\n" + text += m.get('oom', 'Нет данных') + "\n```" + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик команды /status""" + if update.effective_user.id != ADMIN_ID: + await update.message.reply_text("⛔ Доступ запрещен. Ты не мой хозяин.") + return + + # Создаем клавиатуру с хостами + keyboard = [] + for host in HOSTS_LIST: + keyboard.append([InlineKeyboardButton(host['name'], callback_data=f"host_{host['ip']}")]) + keyboard.append([InlineKeyboardButton("🔄 Все хосты", callback_data="host_all")]) + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "🔍 Выбери хост для мониторинга или нажми 'Все хосты':", + reply_markup=reply_markup + ) + +async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка нажатий на кнопки""" + query = update.callback_query + await query.answer() + + if update.effective_user.id != ADMIN_ID: + await query.edit_message_text("⛔ Недостаточно прав.") + return + + data = query.data + if data == "host_all": + await query.edit_message_text("⏳ Собираю данные со всех хостов...") + full_report = "" + for host in HOSTS_LIST: + result = collect_metrics(host) + report = format_metrics(result) + full_report += report + "\n\n" + "="*30 + "\n\n" + # Telegram имеет лимит 4096 символов, разобьем если что + await query.edit_message_text(full_report[:4000]) + elif data.startswith("host_"): + ip = data.replace("host_", "") + host = next((h for h in HOSTS_LIST if h['ip'] == ip), None) + if not host: + await query.edit_message_text("Хост не найден") + return + await query.edit_message_text(f"⏳ Собираю данные с {host['name']}...") + result = collect_metrics(host) + report = format_metrics(result) + await query.edit_message_text(report, parse_mode="Markdown") + +async def scheduled_job(context: ContextTypes.DEFAULT_TYPE): + """Функция, вызываемая раз в час""" + bot: Bot = context.bot + for host in HOSTS_LIST: + result = collect_metrics(host) + report = format_metrics(result) + try: + await bot.send_message(chat_id=ADMIN_ID, text=report, parse_mode="Markdown") + except Exception as e: + await bot.send_message(chat_id=ADMIN_ID, text=f"Ошибка отправки для {host['name']}: {e}") + +def main(): + app = Application.builder().token(BOT_TOKEN).build() + + # Регистрируем команды + app.add_handler(CommandHandler("status", status_command)) + app.add_handler(CallbackQueryHandler(button_callback)) + + # Настройка шедулера (раз в час) + job_queue = app.job_queue + # Запускаем сразу при старте, потом каждый час + job_queue.run_repeating(scheduled_job, interval=3600, first=10) # first=10 (через 10 сек после запуска) + + print("🤖 Бот запущен и готов к работе!") + app.run_polling() + +if __name__ == "__main__": + main() diff --git a/backup/config.py b/backup/config.py new file mode 100644 index 0000000..7b62781 --- /dev/null +++ b/backup/config.py @@ -0,0 +1,22 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +BOT_TOKEN = os.getenv("BOT_TOKEN") +ADMIN_ID = int(os.getenv("ADMIN_ID")) + +# Парсим хосты +HOSTS_RAW = os.getenv("HOSTS", "") +HOSTS_LIST = [] +if HOSTS_RAW: + for item in HOSTS_RAW.split(";"): + if item.strip(): + name, ip, port, user, key_path = item.strip().split(",") + HOSTS_LIST.append({ + "name": name, + "ip": ip, + "port": int(port), + "user": user, + "key_path": key_path + }) diff --git a/backup/monitor.py.bak b/backup/monitor.py.bak new file mode 100644 index 0000000..16a51db --- /dev/null +++ b/backup/monitor.py.bak @@ -0,0 +1,183 @@ +import paramiko +import time +import re +from collections import defaultdict + +# Глобальный словарь для хранения предыдущих значений RX/TX (для расчета за час) +# Ключ: (host_ip, interface) -> {'rx': int, 'tx': int, 'time': timestamp} +prev_net_stats = {} + +def get_ssh_client(host_config): + """Создает SSH подключение""" + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + client.connect( + hostname=host_config['ip'], + port=host_config['port'], + username=host_config['user'], + key_filename=host_config['key_path'], + timeout=10 + ) + return client + except Exception as e: + return f"SSH Error: {str(e)}" + +def exec_cmd(client, command): + """Выполняет команду и возвращает вывод""" + try: + stdin, stdout, stderr = client.exec_command(command, timeout=10) + return stdout.read().decode('utf-8').strip() + except Exception as e: + return f"CMD Error: {str(e)}" + +def collect_metrics(host_config): + """Сбор всех метрик с одного хоста""" + client = get_ssh_client(host_config) + if isinstance(client, str): + return {"error": client} + + result = { + "host": host_config['name'], + "ip": host_config['ip'], + "metrics": {} + } + + # 1. CPU загрузка (%) + cpu_output = exec_cmd(client, "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1") + try: + result["metrics"]["cpu"] = float(cpu_output.replace(',', '.')) + except: + result["metrics"]["cpu"] = 0.0 + + # 2. ОЗУ (используется в MB и %) + mem_output = exec_cmd(client, "free -m | grep Mem") + parts = re.split(r'\s+', mem_output) + if len(parts) >= 3: + total_mem = int(parts[1]) + used_mem = int(parts[2]) + result["metrics"]["mem_used_mb"] = used_mem + result["metrics"]["mem_percent"] = round((used_mem / total_mem) * 100, 1) + else: + result["metrics"]["mem_used_mb"] = 0 + result["metrics"]["mem_percent"] = 0 + + # 3. Uptime и Load Average + uptime_output = exec_cmd(client, "uptime") + # Парсим load average (1,5,15 мин) + load_avg = re.search(r'load average:\s+([\d.]+),\s+([\d.]+),\s+([\d.]+)', uptime_output) + if load_avg: + result["metrics"]["load_1"] = load_avg.group(1) + result["metrics"]["load_5"] = load_avg.group(2) + result["metrics"]["load_15"] = load_avg.group(3) + # Парсим uptime (дни/часы) + uptime_seconds = exec_cmd(client, "cat /proc/uptime | awk '{print $1}'") + try: + uptime_seconds = float(uptime_seconds.split('.')[0]) + days = uptime_seconds // 86400 + hours = (uptime_seconds % 86400) // 3600 + minutes = (uptime_seconds % 3600) // 60 + result["metrics"]["uptime"] = f"{int(days)}d {int(hours)}h {int(minutes)}m" + except: + result["metrics"]["uptime"] = "N/A" + + # 4. Сеть (трафик за последний час) + # Сначала собираем текущие счетчики по всем интерфейсам (кроме lo) + net_output = exec_cmd(client, "cat /proc/net/dev | tail -n +3 | grep -v lo") + current_net = {} + for line in net_output.split('\n'): + if not line.strip(): + continue + parts = re.split(r'\s+', line.strip()) + iface = parts[0].replace(':', '') + rx_bytes = int(parts[1]) + tx_bytes = int(parts[9]) + current_net[iface] = {'rx': rx_bytes, 'tx': tx_bytes} + + # Расчет за час (используем глобальный кеш) + global prev_net_stats + host_key = host_config['ip'] + total_rx_hour = 0 + total_tx_hour = 0 + + if host_key in prev_net_stats: + old_data = prev_net_stats[host_key] + for iface, vals in current_net.items(): + if iface in old_data: + # Считаем разницу. Если перезагрузили сервер - счетчики сбросятся, это ок. + delta_rx = vals['rx'] - old_data[iface]['rx'] + delta_tx = vals['tx'] - old_data[iface]['tx'] + if delta_rx > 0: + total_rx_hour += delta_rx + if delta_tx > 0: + total_tx_hour += delta_tx + + # Переводим в Мегабайты (для удобства) + result["metrics"]["net_rx_mb"] = round(total_rx_hour / (1024 * 1024), 2) + result["metrics"]["net_tx_mb"] = round(total_tx_hour / (1024 * 1024), 2) + + # Сохраняем текущие значения для следующего замера + prev_net_stats[host_key] = current_net + + # 5. Диски (iostat %util) + iostat_output = exec_cmd(client, "iostat -x -k 1 2 | grep -E 'sd[a-z]|vd[a-z]|nvme' | tail -n +3") + disk_stats = [] + # Собираем все диски, показываем топ по нагрузке + for line in iostat_output.split('\n'): + if not line.strip(): + continue + parts = re.split(r'\s+', line.strip()) + if len(parts) >= 14: + disk_name = parts[0] + try: + util = float(parts[-1].replace(',', '.')) + rps = float(parts[3].replace(',', '.')) # read/s + wps = float(parts[4].replace(',', '.')) # write/s + disk_stats.append({ + "name": disk_name, + "util": util, + "rps": rps, + "wps": wps + }) + except: + pass + + # Сортируем по утилизации и берем топ-3 самых нагруженных + disk_stats.sort(key=lambda x: x['util'], reverse=True) + result["metrics"]["disks"] = disk_stats[:3] + + # 6. Температуры (если есть sensors) + temp_output = exec_cmd(client, "sensors -u 2>/dev/null | grep -E 'temp[0-9]+_input|Core' | head -5") + temps = [] + if temp_output and "Error" not in temp_output: + # Парсим цифры + for line in temp_output.split('\n'): + if '_input:' in line: + val = re.search(r'([\d.]+)', line) + if val: + temps.append(float(val.group(1))) + result["metrics"]["temps"] = temps + + # 7. Топ-5 процессов по CPU и RAM + top_cpu = exec_cmd(client, "ps aux --sort=-%cpu | head -6 | tail -5") + top_mem = exec_cmd(client, "ps aux --sort=-%mem | head -6 | tail -5") + result["metrics"]["top_cpu"] = top_cpu + result["metrics"]["top_mem"] = top_mem + + # 8. Логи (ошибки, fail2ban) + # Проверяем системный журнал на панику/ошибки за последнюю минуту (чтобы не спамить) + last_logs = exec_cmd(client, "journalctl -p 3 -xn 5 --no-pager 2>/dev/null || dmesg | tail -5") + # Fail2ban статус + fail2ban_status = exec_cmd(client, "fail2ban-client status 2>/dev/null | grep 'Number of failed'") + if not fail2ban_status: + fail2ban_status = "Fail2ban not installed or inactive" + + # Кто пытался прорваться (последние записи из логов) + fail2ban_logs = exec_cmd(client, "tail -10 /var/log/fail2ban.log 2>/dev/null | grep -i banned || echo 'No recent bans'") + + result["metrics"]["logs_errors"] = last_logs + result["metrics"]["fail2ban_status"] = fail2ban_status + result["metrics"]["fail2ban_logs"] = fail2ban_logs + + client.close() + return result diff --git a/backup/monitor.py.bak_1 b/backup/monitor.py.bak_1 new file mode 100644 index 0000000..3147036 --- /dev/null +++ b/backup/monitor.py.bak_1 @@ -0,0 +1,217 @@ +import paramiko +import time +import re +from collections import defaultdict +import logging + +logger = logging.getLogger(__name__) + +# Глобальный словарь для хранения предыдущих значений RX/TX (для расчета за час) +prev_net_stats = {} + +def get_ssh_client(host_config): + """Создает SSH подключение с детальным логированием""" + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + logger.info(f"🔌 Подключаюсь к {host_config['ip']}:{host_config['port']} как {host_config['user']}") + client.connect( + hostname=host_config['ip'], + port=host_config['port'], + username=host_config['user'], + key_filename=host_config['key_path'], + timeout=10 + ) + logger.info(f"✅ Подключено к {host_config['ip']}") + return client + except paramiko.AuthenticationException: + error_msg = f"❌ Ошибка аутентификации для {host_config['ip']}! Проверь логин или ключ." + logger.error(error_msg) + return error_msg + except paramiko.SSHException as e: + error_msg = f"❌ SSH ошибка для {host_config['ip']}: {str(e)}" + logger.error(error_msg) + return error_msg + except Exception as e: + error_msg = f"❌ Неизвестная ошибка для {host_config['ip']}: {str(e)}" + logger.error(error_msg) + return error_msg + +def exec_cmd(client, command): + """Выполняет команду и возвращает вывод""" + try: + stdin, stdout, stderr = client.exec_command(command, timeout=10) + return stdout.read().decode('utf-8').strip() + except Exception as e: + return f"CMD Error: {str(e)}" + +def collect_metrics(host_config): + """Сбор всех метрик с одного хоста""" + client = get_ssh_client(host_config) + if isinstance(client, str): + return {"error": client, "host": host_config['name'], "ip": host_config['ip']} + + result = { + "host": host_config['name'], + "ip": host_config['ip'], + "metrics": {} + } + + # 1. CPU загрузка (%) + cpu_output = exec_cmd(client, "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1") + try: + result["metrics"]["cpu"] = float(cpu_output.replace(',', '.')) + except: + result["metrics"]["cpu"] = 0.0 + + # 2. ОЗУ (используется в MB и %) + mem_output = exec_cmd(client, "free -m | grep Mem") + parts = re.split(r'\s+', mem_output) + if len(parts) >= 3: + total_mem = int(parts[1]) + used_mem = int(parts[2]) + result["metrics"]["mem_used_mb"] = used_mem + result["metrics"]["mem_percent"] = round((used_mem / total_mem) * 100, 1) + else: + result["metrics"]["mem_used_mb"] = 0 + result["metrics"]["mem_percent"] = 0 + + # 3. Uptime и Load Average + uptime_output = exec_cmd(client, "uptime") + load_avg = re.search(r'load average:\s+([\d.]+),\s+([\d.]+),\s+([\d.]+)', uptime_output) + if load_avg: + result["metrics"]["load_1"] = load_avg.group(1) + result["metrics"]["load_5"] = load_avg.group(2) + result["metrics"]["load_15"] = load_avg.group(3) + + uptime_seconds = exec_cmd(client, "cat /proc/uptime | awk '{print $1}'") + try: + uptime_seconds = float(uptime_seconds.split('.')[0]) + days = uptime_seconds // 86400 + hours = (uptime_seconds % 86400) // 3600 + minutes = (uptime_seconds % 3600) // 60 + result["metrics"]["uptime"] = f"{int(days)}d {int(hours)}h {int(minutes)}m" + except: + result["metrics"]["uptime"] = "N/A" + + # 4. Сеть (трафик за последний час) + net_output = exec_cmd(client, "cat /proc/net/dev | tail -n +3 | grep -v lo") + current_net = {} + for line in net_output.split('\n'): + if not line.strip(): + continue + parts = re.split(r'\s+', line.strip()) + iface = parts[0].replace(':', '') + rx_bytes = int(parts[1]) + tx_bytes = int(parts[9]) + current_net[iface] = {'rx': rx_bytes, 'tx': tx_bytes} + + global prev_net_stats + host_key = host_config['ip'] + total_rx_hour = 0 + total_tx_hour = 0 + + if host_key in prev_net_stats: + old_data = prev_net_stats[host_key] + for iface, vals in current_net.items(): + if iface in old_data: + delta_rx = vals['rx'] - old_data[iface]['rx'] + delta_tx = vals['tx'] - old_data[iface]['tx'] + if delta_rx > 0: + total_rx_hour += delta_rx + if delta_tx > 0: + total_tx_hour += delta_tx + + result["metrics"]["net_rx_mb"] = round(total_rx_hour / (1024 * 1024), 2) + result["metrics"]["net_tx_mb"] = round(total_tx_hour / (1024 * 1024), 2) + + prev_net_stats[host_key] = current_net + + # 5. Диски (iostat %util) + iostat_output = exec_cmd(client, "iostat -x -k 1 2 | grep -E 'sd[a-z]|vd[a-z]|nvme' | tail -n +3") + disk_stats = [] + for line in iostat_output.split('\n'): + if not line.strip(): + continue + parts = re.split(r'\s+', line.strip()) + if len(parts) >= 14: + disk_name = parts[0] + try: + util = float(parts[-1].replace(',', '.')) + rps = float(parts[3].replace(',', '.')) + wps = float(parts[4].replace(',', '.')) + disk_stats.append({ + "name": disk_name, + "util": util, + "rps": rps, + "wps": wps + }) + except: + pass + + disk_stats.sort(key=lambda x: x['util'], reverse=True) + result["metrics"]["disks"] = disk_stats[:3] + + # 6. Температуры + temp_output = exec_cmd(client, "sensors -u 2>/dev/null | grep -E 'temp[0-9]+_input|Core' | head -5") + temps = [] + if temp_output and "Error" not in temp_output: + for line in temp_output.split('\n'): + if '_input:' in line: + val = re.search(r'([\d.]+)', line) + if val: + temps.append(float(val.group(1))) + result["metrics"]["temps"] = temps + + # 7. Топ-5 процессов по CPU и RAM + top_cpu = exec_cmd(client, "ps aux --sort=-%cpu | head -6 | tail -5") + top_mem = exec_cmd(client, "ps aux --sort=-%mem | head -6 | tail -5") + result["metrics"]["top_cpu"] = top_cpu + result["metrics"]["top_mem"] = top_mem + + # 8. Логи (ошибки) + last_logs = exec_cmd(client, "journalctl -p 3 -xn 5 --no-pager 2>/dev/null || dmesg | tail -5") + result["metrics"]["logs_errors"] = last_logs + + # 9. Fail2ban (УЛУЧШЕННАЯ ВЕРСИЯ) + # Проверяем статус + fail2ban_status = exec_cmd(client, "fail2ban-client status 2>/dev/null") + if not fail2ban_status or "not found" in fail2ban_status: + # Проверяем, запущен ли процесс + ps_fail2ban = exec_cmd(client, "ps aux | grep -v grep | grep fail2ban-server") + if ps_fail2ban: + fail2ban_status = "✅ Fail2ban запущен (проверь джейлы командой fail2ban-client status)" + else: + fail2ban_status = "❌ Fail2ban не установлен или не запущен" + else: + # Парсим статус для красивого вывода + lines = fail2ban_status.split('\n') + jails_info = [] + for line in lines: + if '|- Number of jail:' in line or '`- Number of jail:' in line: + jails_info.append(line.strip()) + if 'Currently banned:' in line: + jails_info.append(line.strip()) + if jails_info: + fail2ban_status = "✅ Fail2ban активен\n" + "\n".join(jails_info) + + # Логи банов + fail2ban_logs = exec_cmd(client, "tail -20 /var/log/fail2ban.log 2>/dev/null | grep -E 'BAN|UNBAN' | tail -5 || echo 'Нет записей о банах'") + + # Активные баны + active_bans = exec_cmd(client, "fail2ban-client status 2>/dev/null | grep -A 5 'Currently banned:'") + if active_bans and "Currently banned:" in active_bans: + fail2ban_logs += "\n\n🔒 Активные баны:\n" + active_bans + + result["metrics"]["fail2ban_status"] = fail2ban_status + result["metrics"]["fail2ban_logs"] = fail2ban_logs + + # 10. Проверка на OOM + oom_check = exec_cmd(client, "dmesg | grep -i 'out of memory' | tail -3 || echo 'Нет OOM-событий'") + if oom_check and "Нет OOM-событий" not in oom_check: + result["metrics"]["oom"] = oom_check + else: + result["metrics"]["oom"] = "✅ Нет OOM-событий" + + client.close() + return result diff --git a/backup/monitor.py.bak_new b/backup/monitor.py.bak_new new file mode 100644 index 0000000..de1383e --- /dev/null +++ b/backup/monitor.py.bak_new @@ -0,0 +1,305 @@ +import paramiko +import time +import re +from collections import defaultdict +import logging + +logger = logging.getLogger(__name__) + +# Глобальный словарь для хранения предыдущих значений RX/TX +prev_net_stats = {} + +def get_ssh_client(host_config): + """Создает SSH подключение с детальным логированием""" + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + logger.info(f"🔌 Подключаюсь к {host_config['ip']}:{host_config['port']} как {host_config['user']}") + client.connect( + hostname=host_config['ip'], + port=host_config['port'], + username=host_config['user'], + key_filename=host_config['key_path'], + timeout=10 + ) + logger.info(f"✅ Подключено к {host_config['ip']}") + return client + except paramiko.AuthenticationException: + error_msg = f"❌ Ошибка аутентификации для {host_config['ip']}! Проверь логин или ключ." + logger.error(error_msg) + return error_msg + except paramiko.SSHException as e: + error_msg = f"❌ SSH ошибка для {host_config['ip']}: {str(e)}" + logger.error(error_msg) + return error_msg + except Exception as e: + error_msg = f"❌ Неизвестная ошибка для {host_config['ip']}: {str(e)}" + logger.error(error_msg) + return error_msg + +def exec_cmd(client, command): + """Выполняет команду и возвращает вывод""" + try: + stdin, stdout, stderr = client.exec_command(command, timeout=10) + return stdout.read().decode('utf-8').strip() + except Exception as e: + return f"CMD Error: {str(e)}" + +def detect_host_type(client): + """Определяет тип хоста: physical, vm, lxc, container""" + # Проверка на LXC + lxc_check = exec_cmd(client, "cat /proc/1/environ 2>/dev/null | tr '\\0' '\\n' | grep -i lxc || echo ''") + if lxc_check: + return "lxc" + + # Проверка на контейнер (Docker и др) + container_check = exec_cmd(client, "cat /proc/1/environ 2>/dev/null | tr '\\0' '\\n' | grep -i container || echo ''") + if container_check: + return "container" + + # Проверка на виртуальную машину + virt_check = exec_cmd(client, "systemd-detect-virt 2>/dev/null || echo 'none'") + if virt_check and virt_check not in ['none', '']: + return "vm" + + # Проверка через dmidecode (если есть) + virt_check2 = exec_cmd(client, "dmidecode -s system-manufacturer 2>/dev/null || echo 'Unknown'") + if virt_check2: + virt_vendors = ['vmware', 'virtualbox', 'kvm', 'qemu', 'xen', 'microsoft', 'parallels'] + if any(vendor in virt_check2.lower() for vendor in virt_vendors): + return "vm" + + # Если ничего не найдено - считаем физическим железом + return "physical" + +def check_alerts(metrics, host_type): + """Проверяет критические метрики и возвращает список алертов""" + alerts = [] + + # CPU алерт (>80%) + cpu = metrics.get('cpu', 0) + if cpu > 80: + alerts.append(f"🔴 ВЫСОКАЯ ЗАГРУЗКА CPU: {cpu:.1f}%") + elif cpu > 60: + alerts.append(f"🟡 ПОВЫШЕННАЯ ЗАГРУЗКА CPU: {cpu:.1f}%") + + # RAM алерт (>85%) + mem_percent = metrics.get('mem_percent', 0) + if mem_percent > 85: + alerts.append(f"🔴 НЕХВАТКА ОЗУ: {mem_percent:.1f}% (использовано {metrics.get('mem_used_mb', 0)} MB)") + elif mem_percent > 70: + alerts.append(f"🟡 ПОВЫШЕННОЕ ПОТРЕБЛЕНИЕ ОЗУ: {mem_percent:.1f}%") + + # Диск алерт (>80%) + disk_usage = metrics.get('disk_usage', 0) + if disk_usage > 80: + alerts.append(f"🔴 МАЛО МЕСТА НА ДИСКЕ: {disk_usage:.1f}%") + elif disk_usage > 70: + alerts.append(f"🟡 ЗАКАНЧИВАЕТСЯ МЕСТО НА ДИСКЕ: {disk_usage:.1f}%") + + # OOM алерт + oom = metrics.get('oom', '') + if oom and "Нет OOM" not in oom: + alerts.append(f"💀 OOM KILLER СРАБОТАЛ! Проверь логи!") + + # Load Average алерт (если > количество ядер) + load_1 = float(metrics.get('load_1', 0)) + if load_1 > 4: + alerts.append(f"🔴 ВЫСОКИЙ LOAD AVERAGE: {load_1}") + elif load_1 > 2: + alerts.append(f"🟡 ПОВЫШЕННЫЙ LOAD AVERAGE: {load_1}") + + return alerts + +def collect_metrics(host_config): + """Сбор всех метрик с одного хоста""" + client = get_ssh_client(host_config) + if isinstance(client, str): + return {"error": client, "host": host_config['name'], "ip": host_config['ip']} + + result = { + "host": host_config['name'], + "ip": host_config['ip'], + "metrics": {} + } + + # 1. Определяем тип хоста + host_type = detect_host_type(client) + result["host_type"] = host_type + result["metrics"]["host_type"] = host_type + + # 2. CPU загрузка (%) + cpu_output = exec_cmd(client, "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1") + try: + result["metrics"]["cpu"] = float(cpu_output.replace(',', '.')) + except: + result["metrics"]["cpu"] = 0.0 + + # 3. ОЗУ (используется в MB и %) + mem_output = exec_cmd(client, "free -m | grep Mem") + parts = re.split(r'\s+', mem_output) + if len(parts) >= 3: + total_mem = int(parts[1]) + used_mem = int(parts[2]) + result["metrics"]["mem_used_mb"] = used_mem + result["metrics"]["mem_percent"] = round((used_mem / total_mem) * 100, 1) + result["metrics"]["mem_total_mb"] = total_mem + else: + result["metrics"]["mem_used_mb"] = 0 + result["metrics"]["mem_percent"] = 0 + result["metrics"]["mem_total_mb"] = 0 + + # 4. Uptime и Load Average + uptime_output = exec_cmd(client, "uptime") + load_avg = re.search(r'load average:\s+([\d.]+),\s+([\d.]+),\s+([\d.]+)', uptime_output) + if load_avg: + result["metrics"]["load_1"] = load_avg.group(1) + result["metrics"]["load_5"] = load_avg.group(2) + result["metrics"]["load_15"] = load_avg.group(3) + + uptime_seconds = exec_cmd(client, "cat /proc/uptime | awk '{print $1}'") + try: + uptime_seconds = float(uptime_seconds.split('.')[0]) + days = uptime_seconds // 86400 + hours = (uptime_seconds % 86400) // 3600 + minutes = (uptime_seconds % 3600) // 60 + result["metrics"]["uptime"] = f"{int(days)}d {int(hours)}h {int(minutes)}m" + except: + result["metrics"]["uptime"] = "N/A" + + # 5. Сеть (трафик за последний час) + net_output = exec_cmd(client, "cat /proc/net/dev | tail -n +3 | grep -v lo") + current_net = {} + for line in net_output.split('\n'): + if not line.strip(): + continue + parts = re.split(r'\s+', line.strip()) + iface = parts[0].replace(':', '') + rx_bytes = int(parts[1]) + tx_bytes = int(parts[9]) + current_net[iface] = {'rx': rx_bytes, 'tx': tx_bytes} + + global prev_net_stats + host_key = host_config['ip'] + total_rx_hour = 0 + total_tx_hour = 0 + + if host_key in prev_net_stats: + old_data = prev_net_stats[host_key] + for iface, vals in current_net.items(): + if iface in old_data: + delta_rx = vals['rx'] - old_data[iface]['rx'] + delta_tx = vals['tx'] - old_data[iface]['tx'] + if delta_rx > 0: + total_rx_hour += delta_rx + if delta_tx > 0: + total_tx_hour += delta_tx + + result["metrics"]["net_rx_mb"] = round(total_rx_hour / (1024 * 1024), 2) + result["metrics"]["net_tx_mb"] = round(total_tx_hour / (1024 * 1024), 2) + + prev_net_stats[host_key] = current_net + + # 6. Диски (iostat %util) + iostat_output = exec_cmd(client, "iostat -x -k 1 2 | grep -E 'sd[a-z]|vd[a-z]|nvme' | tail -n +3") + disk_stats = [] + for line in iostat_output.split('\n'): + if not line.strip(): + continue + parts = re.split(r'\s+', line.strip()) + if len(parts) >= 14: + disk_name = parts[0] + try: + util = float(parts[-1].replace(',', '.')) + rps = float(parts[3].replace(',', '.')) + wps = float(parts[4].replace(',', '.')) + disk_stats.append({ + "name": disk_name, + "util": util, + "rps": rps, + "wps": wps + }) + except: + pass + + disk_stats.sort(key=lambda x: x['util'], reverse=True) + result["metrics"]["disks"] = disk_stats[:3] + + # 7. Свободное место на диске (%) + disk_usage_output = exec_cmd(client, "df -h / | tail -1 | awk '{print $5}' | sed 's/%//'") + try: + result["metrics"]["disk_usage"] = float(disk_usage_output.replace(',', '.')) + except: + result["metrics"]["disk_usage"] = 0 + + # 8. Температуры (ТОЛЬКО ДЛЯ ФИЗИЧЕСКИХ ХОСТОВ!) + temps = [] + if host_type == "physical": + temp_output = exec_cmd(client, "sensors -u 2>/dev/null | grep -E 'temp[0-9]+_input|Core' | head -5") + if temp_output and "Error" not in temp_output and "command not found" not in temp_output: + for line in temp_output.split('\n'): + if '_input:' in line: + val = re.search(r'([\d.]+)', line) + if val: + temps.append(float(val.group(1))) + else: + # Если sensors не установлен - пробуем через /sys/class/thermal + temp_sys = exec_cmd(client, "cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | head -3") + if temp_sys and "No such file" not in temp_sys: + for line in temp_sys.split('\n'): + try: + temps.append(float(line) / 1000) + except: + pass + result["metrics"]["temps"] = temps + + # 9. Топ-5 процессов по CPU и RAM + top_cpu = exec_cmd(client, "ps aux --sort=-%cpu | head -6 | tail -5") + top_mem = exec_cmd(client, "ps aux --sort=-%mem | head -6 | tail -5") + result["metrics"]["top_cpu"] = top_cpu + result["metrics"]["top_mem"] = top_mem + + # 10. Логи (ошибки) + last_logs = exec_cmd(client, "journalctl -p 3 -xn 5 --no-pager 2>/dev/null || dmesg | tail -5") + result["metrics"]["logs_errors"] = last_logs + + # 11. Fail2ban + fail2ban_status = exec_cmd(client, "fail2ban-client status 2>/dev/null") + if not fail2ban_status or "not found" in fail2ban_status: + ps_fail2ban = exec_cmd(client, "ps aux | grep -v grep | grep fail2ban-server") + if ps_fail2ban: + fail2ban_status = "✅ Fail2ban запущен" + else: + fail2ban_status = "❌ Fail2ban не установлен или не запущен" + else: + lines = fail2ban_status.split('\n') + jails_info = [] + for line in lines: + if '|- Number of jail:' in line or '`- Number of jail:' in line: + jails_info.append(line.strip()) + if 'Currently banned:' in line: + jails_info.append(line.strip()) + if jails_info: + fail2ban_status = "✅ Fail2ban активен\n" + "\n".join(jails_info) + + fail2ban_logs = exec_cmd(client, "tail -20 /var/log/fail2ban.log 2>/dev/null | grep -E 'BAN|UNBAN' | tail -5 || echo 'Нет записей о банах'") + + active_bans = exec_cmd(client, "fail2ban-client status 2>/dev/null | grep -A 5 'Currently banned:'") + if active_bans and "Currently banned:" in active_bans: + fail2ban_logs += "\n\n🔒 Активные баны:\n" + active_bans + + result["metrics"]["fail2ban_status"] = fail2ban_status + result["metrics"]["fail2ban_logs"] = fail2ban_logs + + # 12. OOM + oom_check = exec_cmd(client, "dmesg | grep -i 'out of memory' | tail -3 || echo 'Нет OOM-событий'") + if oom_check and "Нет OOM-событий" not in oom_check: + result["metrics"]["oom"] = oom_check + else: + result["metrics"]["oom"] = "✅ Нет OOM-событий" + + # 13. Проверка алертов + result["metrics"]["alerts"] = check_alerts(result["metrics"], host_type) + + client.close() + return result diff --git a/backup/tg-bot-mon.py b/backup/tg-bot-mon.py new file mode 100644 index 0000000..de1383e --- /dev/null +++ b/backup/tg-bot-mon.py @@ -0,0 +1,305 @@ +import paramiko +import time +import re +from collections import defaultdict +import logging + +logger = logging.getLogger(__name__) + +# Глобальный словарь для хранения предыдущих значений RX/TX +prev_net_stats = {} + +def get_ssh_client(host_config): + """Создает SSH подключение с детальным логированием""" + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + logger.info(f"🔌 Подключаюсь к {host_config['ip']}:{host_config['port']} как {host_config['user']}") + client.connect( + hostname=host_config['ip'], + port=host_config['port'], + username=host_config['user'], + key_filename=host_config['key_path'], + timeout=10 + ) + logger.info(f"✅ Подключено к {host_config['ip']}") + return client + except paramiko.AuthenticationException: + error_msg = f"❌ Ошибка аутентификации для {host_config['ip']}! Проверь логин или ключ." + logger.error(error_msg) + return error_msg + except paramiko.SSHException as e: + error_msg = f"❌ SSH ошибка для {host_config['ip']}: {str(e)}" + logger.error(error_msg) + return error_msg + except Exception as e: + error_msg = f"❌ Неизвестная ошибка для {host_config['ip']}: {str(e)}" + logger.error(error_msg) + return error_msg + +def exec_cmd(client, command): + """Выполняет команду и возвращает вывод""" + try: + stdin, stdout, stderr = client.exec_command(command, timeout=10) + return stdout.read().decode('utf-8').strip() + except Exception as e: + return f"CMD Error: {str(e)}" + +def detect_host_type(client): + """Определяет тип хоста: physical, vm, lxc, container""" + # Проверка на LXC + lxc_check = exec_cmd(client, "cat /proc/1/environ 2>/dev/null | tr '\\0' '\\n' | grep -i lxc || echo ''") + if lxc_check: + return "lxc" + + # Проверка на контейнер (Docker и др) + container_check = exec_cmd(client, "cat /proc/1/environ 2>/dev/null | tr '\\0' '\\n' | grep -i container || echo ''") + if container_check: + return "container" + + # Проверка на виртуальную машину + virt_check = exec_cmd(client, "systemd-detect-virt 2>/dev/null || echo 'none'") + if virt_check and virt_check not in ['none', '']: + return "vm" + + # Проверка через dmidecode (если есть) + virt_check2 = exec_cmd(client, "dmidecode -s system-manufacturer 2>/dev/null || echo 'Unknown'") + if virt_check2: + virt_vendors = ['vmware', 'virtualbox', 'kvm', 'qemu', 'xen', 'microsoft', 'parallels'] + if any(vendor in virt_check2.lower() for vendor in virt_vendors): + return "vm" + + # Если ничего не найдено - считаем физическим железом + return "physical" + +def check_alerts(metrics, host_type): + """Проверяет критические метрики и возвращает список алертов""" + alerts = [] + + # CPU алерт (>80%) + cpu = metrics.get('cpu', 0) + if cpu > 80: + alerts.append(f"🔴 ВЫСОКАЯ ЗАГРУЗКА CPU: {cpu:.1f}%") + elif cpu > 60: + alerts.append(f"🟡 ПОВЫШЕННАЯ ЗАГРУЗКА CPU: {cpu:.1f}%") + + # RAM алерт (>85%) + mem_percent = metrics.get('mem_percent', 0) + if mem_percent > 85: + alerts.append(f"🔴 НЕХВАТКА ОЗУ: {mem_percent:.1f}% (использовано {metrics.get('mem_used_mb', 0)} MB)") + elif mem_percent > 70: + alerts.append(f"🟡 ПОВЫШЕННОЕ ПОТРЕБЛЕНИЕ ОЗУ: {mem_percent:.1f}%") + + # Диск алерт (>80%) + disk_usage = metrics.get('disk_usage', 0) + if disk_usage > 80: + alerts.append(f"🔴 МАЛО МЕСТА НА ДИСКЕ: {disk_usage:.1f}%") + elif disk_usage > 70: + alerts.append(f"🟡 ЗАКАНЧИВАЕТСЯ МЕСТО НА ДИСКЕ: {disk_usage:.1f}%") + + # OOM алерт + oom = metrics.get('oom', '') + if oom and "Нет OOM" not in oom: + alerts.append(f"💀 OOM KILLER СРАБОТАЛ! Проверь логи!") + + # Load Average алерт (если > количество ядер) + load_1 = float(metrics.get('load_1', 0)) + if load_1 > 4: + alerts.append(f"🔴 ВЫСОКИЙ LOAD AVERAGE: {load_1}") + elif load_1 > 2: + alerts.append(f"🟡 ПОВЫШЕННЫЙ LOAD AVERAGE: {load_1}") + + return alerts + +def collect_metrics(host_config): + """Сбор всех метрик с одного хоста""" + client = get_ssh_client(host_config) + if isinstance(client, str): + return {"error": client, "host": host_config['name'], "ip": host_config['ip']} + + result = { + "host": host_config['name'], + "ip": host_config['ip'], + "metrics": {} + } + + # 1. Определяем тип хоста + host_type = detect_host_type(client) + result["host_type"] = host_type + result["metrics"]["host_type"] = host_type + + # 2. CPU загрузка (%) + cpu_output = exec_cmd(client, "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1") + try: + result["metrics"]["cpu"] = float(cpu_output.replace(',', '.')) + except: + result["metrics"]["cpu"] = 0.0 + + # 3. ОЗУ (используется в MB и %) + mem_output = exec_cmd(client, "free -m | grep Mem") + parts = re.split(r'\s+', mem_output) + if len(parts) >= 3: + total_mem = int(parts[1]) + used_mem = int(parts[2]) + result["metrics"]["mem_used_mb"] = used_mem + result["metrics"]["mem_percent"] = round((used_mem / total_mem) * 100, 1) + result["metrics"]["mem_total_mb"] = total_mem + else: + result["metrics"]["mem_used_mb"] = 0 + result["metrics"]["mem_percent"] = 0 + result["metrics"]["mem_total_mb"] = 0 + + # 4. Uptime и Load Average + uptime_output = exec_cmd(client, "uptime") + load_avg = re.search(r'load average:\s+([\d.]+),\s+([\d.]+),\s+([\d.]+)', uptime_output) + if load_avg: + result["metrics"]["load_1"] = load_avg.group(1) + result["metrics"]["load_5"] = load_avg.group(2) + result["metrics"]["load_15"] = load_avg.group(3) + + uptime_seconds = exec_cmd(client, "cat /proc/uptime | awk '{print $1}'") + try: + uptime_seconds = float(uptime_seconds.split('.')[0]) + days = uptime_seconds // 86400 + hours = (uptime_seconds % 86400) // 3600 + minutes = (uptime_seconds % 3600) // 60 + result["metrics"]["uptime"] = f"{int(days)}d {int(hours)}h {int(minutes)}m" + except: + result["metrics"]["uptime"] = "N/A" + + # 5. Сеть (трафик за последний час) + net_output = exec_cmd(client, "cat /proc/net/dev | tail -n +3 | grep -v lo") + current_net = {} + for line in net_output.split('\n'): + if not line.strip(): + continue + parts = re.split(r'\s+', line.strip()) + iface = parts[0].replace(':', '') + rx_bytes = int(parts[1]) + tx_bytes = int(parts[9]) + current_net[iface] = {'rx': rx_bytes, 'tx': tx_bytes} + + global prev_net_stats + host_key = host_config['ip'] + total_rx_hour = 0 + total_tx_hour = 0 + + if host_key in prev_net_stats: + old_data = prev_net_stats[host_key] + for iface, vals in current_net.items(): + if iface in old_data: + delta_rx = vals['rx'] - old_data[iface]['rx'] + delta_tx = vals['tx'] - old_data[iface]['tx'] + if delta_rx > 0: + total_rx_hour += delta_rx + if delta_tx > 0: + total_tx_hour += delta_tx + + result["metrics"]["net_rx_mb"] = round(total_rx_hour / (1024 * 1024), 2) + result["metrics"]["net_tx_mb"] = round(total_tx_hour / (1024 * 1024), 2) + + prev_net_stats[host_key] = current_net + + # 6. Диски (iostat %util) + iostat_output = exec_cmd(client, "iostat -x -k 1 2 | grep -E 'sd[a-z]|vd[a-z]|nvme' | tail -n +3") + disk_stats = [] + for line in iostat_output.split('\n'): + if not line.strip(): + continue + parts = re.split(r'\s+', line.strip()) + if len(parts) >= 14: + disk_name = parts[0] + try: + util = float(parts[-1].replace(',', '.')) + rps = float(parts[3].replace(',', '.')) + wps = float(parts[4].replace(',', '.')) + disk_stats.append({ + "name": disk_name, + "util": util, + "rps": rps, + "wps": wps + }) + except: + pass + + disk_stats.sort(key=lambda x: x['util'], reverse=True) + result["metrics"]["disks"] = disk_stats[:3] + + # 7. Свободное место на диске (%) + disk_usage_output = exec_cmd(client, "df -h / | tail -1 | awk '{print $5}' | sed 's/%//'") + try: + result["metrics"]["disk_usage"] = float(disk_usage_output.replace(',', '.')) + except: + result["metrics"]["disk_usage"] = 0 + + # 8. Температуры (ТОЛЬКО ДЛЯ ФИЗИЧЕСКИХ ХОСТОВ!) + temps = [] + if host_type == "physical": + temp_output = exec_cmd(client, "sensors -u 2>/dev/null | grep -E 'temp[0-9]+_input|Core' | head -5") + if temp_output and "Error" not in temp_output and "command not found" not in temp_output: + for line in temp_output.split('\n'): + if '_input:' in line: + val = re.search(r'([\d.]+)', line) + if val: + temps.append(float(val.group(1))) + else: + # Если sensors не установлен - пробуем через /sys/class/thermal + temp_sys = exec_cmd(client, "cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | head -3") + if temp_sys and "No such file" not in temp_sys: + for line in temp_sys.split('\n'): + try: + temps.append(float(line) / 1000) + except: + pass + result["metrics"]["temps"] = temps + + # 9. Топ-5 процессов по CPU и RAM + top_cpu = exec_cmd(client, "ps aux --sort=-%cpu | head -6 | tail -5") + top_mem = exec_cmd(client, "ps aux --sort=-%mem | head -6 | tail -5") + result["metrics"]["top_cpu"] = top_cpu + result["metrics"]["top_mem"] = top_mem + + # 10. Логи (ошибки) + last_logs = exec_cmd(client, "journalctl -p 3 -xn 5 --no-pager 2>/dev/null || dmesg | tail -5") + result["metrics"]["logs_errors"] = last_logs + + # 11. Fail2ban + fail2ban_status = exec_cmd(client, "fail2ban-client status 2>/dev/null") + if not fail2ban_status or "not found" in fail2ban_status: + ps_fail2ban = exec_cmd(client, "ps aux | grep -v grep | grep fail2ban-server") + if ps_fail2ban: + fail2ban_status = "✅ Fail2ban запущен" + else: + fail2ban_status = "❌ Fail2ban не установлен или не запущен" + else: + lines = fail2ban_status.split('\n') + jails_info = [] + for line in lines: + if '|- Number of jail:' in line or '`- Number of jail:' in line: + jails_info.append(line.strip()) + if 'Currently banned:' in line: + jails_info.append(line.strip()) + if jails_info: + fail2ban_status = "✅ Fail2ban активен\n" + "\n".join(jails_info) + + fail2ban_logs = exec_cmd(client, "tail -20 /var/log/fail2ban.log 2>/dev/null | grep -E 'BAN|UNBAN' | tail -5 || echo 'Нет записей о банах'") + + active_bans = exec_cmd(client, "fail2ban-client status 2>/dev/null | grep -A 5 'Currently banned:'") + if active_bans and "Currently banned:" in active_bans: + fail2ban_logs += "\n\n🔒 Активные баны:\n" + active_bans + + result["metrics"]["fail2ban_status"] = fail2ban_status + result["metrics"]["fail2ban_logs"] = fail2ban_logs + + # 12. OOM + oom_check = exec_cmd(client, "dmesg | grep -i 'out of memory' | tail -3 || echo 'Нет OOM-событий'") + if oom_check and "Нет OOM-событий" not in oom_check: + result["metrics"]["oom"] = oom_check + else: + result["metrics"]["oom"] = "✅ Нет OOM-событий" + + # 13. Проверка алертов + result["metrics"]["alerts"] = check_alerts(result["metrics"], host_type) + + client.close() + return result diff --git a/backup/tg-bot-mon.py_new b/backup/tg-bot-mon.py_new new file mode 100644 index 0000000..880775e --- /dev/null +++ b/backup/tg-bot-mon.py_new @@ -0,0 +1,237 @@ +import asyncio +import logging +from datetime import datetime +from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes + +from config import BOT_TOKEN, ADMIN_ID, HOSTS_LIST +from monitor import collect_metrics + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler("bot.log"), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# Кеш для хранения последних собранных данных +CACHE = {} + +def format_metrics(data): + """Формирует красивое сообщение из метрик""" + if "error" in data: + return f"❌ Ошибка подключения к {data['host']}:\n{data['error']}" + + m = data['metrics'] + host = data['host'] + ip = data['ip'] + host_type = data.get('host_type', 'unknown') + + # Эмодзи для типа хоста + type_emoji = { + 'physical': '🖥️', + 'vm': '🖥️', + 'lxc': '📦', + 'container': '🐳' + }.get(host_type, '🖥️') + + type_label = { + 'physical': 'Железо', + 'vm': 'ВМ', + 'lxc': 'LXC', + 'container': 'Контейнер' + }.get(host_type, 'Неизвестно') + + text = f"{type_emoji} *{host} ({ip})* - `{type_label}`\n" + text += f"🕒 *Uptime:* {m.get('uptime', 'N/A')}\n" + text += f"📊 *Load Avg:* {m.get('load_1', '0')}, {m.get('load_5', '0')}, {m.get('load_15', '0')}\n\n" + + # Алерты (если есть) + alerts = m.get('alerts', []) + if alerts: + text += "🚨 *АЛЕРТЫ!*\n" + for alert in alerts: + text += f" {alert}\n" + text += "\n" + + # CPU + cpu_bar = "█" * int(m['cpu'] // 5) + "░" * (20 - int(m['cpu'] // 5)) + cpu_color = "🔴" if m['cpu'] > 80 else "🟡" if m['cpu'] > 60 else "🟢" + text += f"{cpu_color} *CPU:* {m['cpu']:.1f}%\n`{cpu_bar}`\n" + + # RAM + mem_bar = "█" * int(m['mem_percent'] // 5) + "░" * (20 - int(m['mem_percent'] // 5)) + mem_color = "🔴" if m['mem_percent'] > 85 else "🟡" if m['mem_percent'] > 70 else "🟢" + text += f"{mem_color} *RAM:* {m['mem_used_mb']} MB / {m.get('mem_total_mb', '?')} MB ({m['mem_percent']}%)\n`{mem_bar}`\n" + + # Диск (свободное место) + disk_color = "🔴" if m.get('disk_usage', 0) > 80 else "🟡" if m.get('disk_usage', 0) > 70 else "🟢" + text += f"{disk_color} *Disk (/):* {m.get('disk_usage', 0):.1f}% used\n" + + # Сеть + text += f"🌐 *Network (per hour):* 📥 {m['net_rx_mb']} MB | 📤 {m['net_tx_mb']} MB\n" + + # Диски iostat + text += "\n💾 *Disks (top 3 by %util):*\n" + for disk in m.get('disks', []): + text += f" └─ {disk['name']}: util {disk['util']:.1f}% (r: {disk['rps']:.1f}/s, w: {disk['wps']:.1f}/s)\n" + + # Температуры (только для железа) + if host_type == "physical" and m.get('temps'): + text += "\n🌡️ *Temperatures:*\n" + for i, temp in enumerate(m['temps']): + temp_color = "🔴" if temp > 75 else "🟡" if temp > 60 else "🟢" + text += f" {temp_color} Sensor {i+1}: {temp:.1f}°C\n" + elif host_type != "physical": + text += "\n🌡️ *Temperatures:* Не требуется (ВМ/LXC)\n" + + # Топ процессов + text += "\n🏆 *Top 5 CPU:*\n```\n" + text += m.get('top_cpu', 'N/A') + "\n```" + text += "🏆 *Top 5 RAM:*\n```\n" + text += m.get('top_mem', 'N/A') + "\n```" + + # Логи и безопасность + text += "\n⚠️ *Recent Errors (journalctl -p 3):*\n```\n" + text += m.get('logs_errors', 'N/A')[:300] + "\n```" + + # OOM + text += "\n💀 *Out of Memory Events:*\n```\n" + text += m.get('oom', 'Нет данных') + "\n```" + + # Fail2ban + text += "\n🛡️ *Fail2ban Status:*\n```\n" + text += m.get('fail2ban_status', 'N/A') + "\n```" + + text += "\n🚫 *Fail2ban Logs (last bans):*\n```\n" + text += m.get('fail2ban_logs', 'N/A') + "\n```" + + text += f"\n⏱️ *Обновлено:* {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + return text + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик команды /status""" + if update.effective_user.id != ADMIN_ID: + await update.message.reply_text("⛔ Доступ запрещен. Ты не мой хозяин.") + return + + # Создаем клавиатуру с хостами + keyboard = [] + for host in HOSTS_LIST: + keyboard.append([InlineKeyboardButton(host['name'], callback_data=f"host_{host['ip']}")]) + keyboard.append([InlineKeyboardButton("🔄 Все хосты", callback_data="host_all")]) + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "🔍 Выбери хост для мониторинга или нажми 'Все хосты':", + reply_markup=reply_markup + ) + +async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка нажатий на кнопки""" + query = update.callback_query + await query.answer() + + if update.effective_user.id != ADMIN_ID: + await query.edit_message_text("⛔ Недостаточно прав.") + return + + data = query.data + if data == "host_all": + await query.edit_message_text("⏳ Собираю данные со всех хостов...") + full_report = "" + for host in HOSTS_LIST: + result = collect_metrics(host) + report = format_metrics(result) + full_report += report + "\n\n" + "="*30 + "\n\n" + # Обрезаем, если слишком длинное + if len(full_report) > 4000: + full_report = full_report[:4000] + "\n\n... (обрезано)" + await query.edit_message_text(full_report, parse_mode="Markdown") + elif data.startswith("host_"): + ip = data.replace("host_", "") + host = next((h for h in HOSTS_LIST if h['ip'] == ip), None) + if not host: + await query.edit_message_text("Хост не найден") + return + await query.edit_message_text(f"⏳ Собираю данные с {host['name']}...") + result = collect_metrics(host) + report = format_metrics(result) + await query.edit_message_text(report, parse_mode="Markdown") + +async def scheduled_job(context: ContextTypes.DEFAULT_TYPE): + """Функция, вызываемая раз в час""" + bot: Bot = context.bot + logger.info("🔄 Запущен плановый сбор метрик (каждый час)") + + for host in HOSTS_LIST: + try: + logger.info(f"📊 Собираю данные с {host['name']}") + result = collect_metrics(host) + report = format_metrics(result) + await bot.send_message(chat_id=ADMIN_ID, text=report, parse_mode="Markdown") + logger.info(f"✅ Отправлен отчет для {host['name']}") + except Exception as e: + error_msg = f"❌ Ошибка при сборе/отправке для {host['name']}: {e}" + logger.error(error_msg) + await bot.send_message(chat_id=ADMIN_ID, text=error_msg) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Команда /start - проверка, что бот жив""" + if update.effective_user.id != ADMIN_ID: + await update.message.reply_text("Привет! Я бот для мониторинга серверов, но ты не мой хозяин.") + return + + await update.message.reply_text( + "🤖 *Бот мониторинга запущен!*\n\n" + "📌 Используй команду `/status`, чтобы получить актуальные метрики с серверов.\n" + "⏰ Каждый час я буду присылать автоматический отчет.\n\n" + f"📋 Настроено хостов: {len(HOSTS_LIST)}", + parse_mode="Markdown" + ) + +def main(): + """Главная функция запуска бота""" + logger.info("🚀 Запуск бота...") + logger.info(f"📋 Загружено хостов: {len(HOSTS_LIST)}") + + # Проверяем хосты при старте + if not HOSTS_LIST: + logger.error("❌ Нет хостов в конфигурации! Проверь .env файл!") + return + + for host in HOSTS_LIST: + logger.info(f" - {host['name']} ({host['ip']}:{host['port']})") + + app = Application.builder().token(BOT_TOKEN).build() + + # Регистрируем команды + app.add_handler(CommandHandler("start", start_command)) + app.add_handler(CommandHandler("status", status_command)) + app.add_handler(CallbackQueryHandler(button_callback)) + + # Настройка шедулера (раз в час) + job_queue = app.job_queue + if job_queue: + # Запускаем через 10 секунд после старта, потом каждый час + job_queue.run_repeating(scheduled_job, interval=3600, first=10) + logger.info("⏰ Шедулер настроен: отчеты будут приходить каждый час") + else: + logger.warning("⚠️ JobQueue не доступна! Автоматические отчеты не будут работать.") + logger.warning("⚠️ Установи: pip install 'python-telegram-bot[job-queue]'") + + logger.info("🤖 Бот запущен и готов к работе! Напиши /start в Telegram") + + try: + app.run_polling() + except KeyboardInterrupt: + logger.info("🛑 Бот остановлен пользователем") + except Exception as e: + logger.error(f"❌ Ошибка при работе бота: {e}") + +if __name__ == "__main__": + main() diff --git a/config.py b/config.py new file mode 100644 index 0000000..5ed9672 --- /dev/null +++ b/config.py @@ -0,0 +1,44 @@ +import os +import logging +from dotenv import load_dotenv + +load_dotenv() + +logger = logging.getLogger(__name__) + +BOT_TOKEN = os.getenv("BOT_TOKEN") +ADMIN_ID = int(os.getenv("ADMIN_ID", 0)) + +if not BOT_TOKEN: + logger.error("❌ BOT_TOKEN не найден в .env!") +if ADMIN_ID == 0: + logger.warning("⚠️ ADMIN_ID не установлен или равен 0!") + +# Парсим хосты +HOSTS_RAW = os.getenv("HOSTS", "") +HOSTS_LIST = [] + +if HOSTS_RAW: + for item in HOSTS_RAW.split(";"): + if item.strip(): + try: + parts = item.strip().split(",") + if len(parts) != 5: + logger.warning(f"⚠️ Неверный формат хоста: {item}. Ожидается: имя,ip,порт,логин,путь_к_ключу") + continue + name, ip, port, user, key_path = parts + # Проверяем существование ключа + if not os.path.exists(key_path): + logger.warning(f"⚠️ Файл ключа {key_path} не существует для хоста {name}!") + HOSTS_LIST.append({ + "name": name, + "ip": ip, + "port": int(port), + "user": user, + "key_path": key_path + }) + logger.info(f"✅ Добавлен хост: {name} ({ip})") + except ValueError as e: + logger.error(f"❌ Ошибка парсинга хоста {item}: {e}") +else: + logger.error("❌ HOSTS не задан в .env!") diff --git a/monitor.py b/monitor.py new file mode 100644 index 0000000..6e9d26f --- /dev/null +++ b/monitor.py @@ -0,0 +1,333 @@ +import paramiko +import time +import re +import os +from collections import defaultdict +import logging + +logger = logging.getLogger(__name__) + +# Глобальный словарь для хранения предыдущих значений RX/TX +prev_net_stats = {} + +def get_ssh_client(host_config): + """Создает SSH подключение через ssh-agent или ключ""" + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + logger.info(f"🔌 Подключаюсь к {host_config['ip']}:{host_config['port']} как {host_config['user']}") + + # Сначала пробуем ssh-agent + try: + from paramiko.agent import Agent + agent = Agent() + if agent.get_keys(): + logger.info(f"🔑 Использую ssh-agent для {host_config['ip']}") + client.connect( + hostname=host_config['ip'], + port=host_config['port'], + username=host_config['user'], + timeout=10, + allow_agent=True, + look_for_keys=True + ) + logger.info(f"✅ Подключено к {host_config['ip']} через ssh-agent") + return client + except Exception as e: + logger.debug(f"ssh-agent не доступен: {e}") + + # Если ssh-agent не работает, пробуем через ключ + if os.path.exists(host_config['key_path']): + logger.info(f"🔑 Использую ключ: {host_config['key_path']}") + client.connect( + hostname=host_config['ip'], + port=host_config['port'], + username=host_config['user'], + key_filename=host_config['key_path'], + timeout=10 + ) + logger.info(f"✅ Подключено к {host_config['ip']} по ключу") + return client + else: + return f"❌ Файл ключа не найден: {host_config['key_path']}" + + except paramiko.AuthenticationException: + error_msg = f"❌ Ошибка аутентификации для {host_config['ip']}! Проверь логин, ключ или ssh-agent." + logger.error(error_msg) + return error_msg + except paramiko.SSHException as e: + error_msg = f"❌ SSH ошибка для {host_config['ip']}: {str(e)}" + logger.error(error_msg) + return error_msg + except Exception as e: + error_msg = f"❌ Неизвестная ошибка для {host_config['ip']}: {str(e)}" + logger.error(error_msg) + return error_msg + +def exec_cmd(client, command): + """Выполняет команду и возвращает вывод""" + try: + stdin, stdout, stderr = client.exec_command(command, timeout=10) + return stdout.read().decode('utf-8').strip() + except Exception as e: + return f"CMD Error: {str(e)}" + +def detect_host_type(client): + """Определяет тип хоста: physical, vm, lxc, container""" + # Проверка на LXC + lxc_check = exec_cmd(client, "cat /proc/1/environ 2>/dev/null | tr '\\0' '\\n' | grep -i lxc || echo ''") + if lxc_check: + return "lxc" + + # Проверка на контейнер (Docker и др) + container_check = exec_cmd(client, "cat /proc/1/environ 2>/dev/null | tr '\\0' '\\n' | grep -i container || echo ''") + if container_check: + return "container" + + # Проверка на виртуальную машину + virt_check = exec_cmd(client, "systemd-detect-virt 2>/dev/null || echo 'none'") + if virt_check and virt_check not in ['none', '']: + return "vm" + + # Проверка через dmidecode (если есть) + virt_check2 = exec_cmd(client, "dmidecode -s system-manufacturer 2>/dev/null || echo 'Unknown'") + if virt_check2: + virt_vendors = ['vmware', 'virtualbox', 'kvm', 'qemu', 'xen', 'microsoft', 'parallels'] + if any(vendor in virt_check2.lower() for vendor in virt_vendors): + return "vm" + + # Если ничего не найдено - считаем физическим железом + return "physical" + +def check_alerts(metrics, host_type): + """Проверяет критические метрики и возвращает список алертов""" + alerts = [] + + # CPU алерт (>80%) + cpu = metrics.get('cpu', 0) + if cpu > 80: + alerts.append(f"🔴 ВЫСОКАЯ ЗАГРУЗКА CPU: {cpu:.1f}%") + elif cpu > 60: + alerts.append(f"🟡 ПОВЫШЕННАЯ ЗАГРУЗКА CPU: {cpu:.1f}%") + + # RAM алерт (>85%) + mem_percent = metrics.get('mem_percent', 0) + if mem_percent > 85: + alerts.append(f"🔴 НЕХВАТКА ОЗУ: {mem_percent:.1f}% (использовано {metrics.get('mem_used_mb', 0)} MB)") + elif mem_percent > 70: + alerts.append(f"🟡 ПОВЫШЕННОЕ ПОТРЕБЛЕНИЕ ОЗУ: {mem_percent:.1f}%") + + # Диск алерт (>80%) + disk_usage = metrics.get('disk_usage', 0) + if disk_usage > 80: + alerts.append(f"🔴 МАЛО МЕСТА НА ДИСКЕ: {disk_usage:.1f}%") + elif disk_usage > 70: + alerts.append(f"🟡 ЗАКАНЧИВАЕТСЯ МЕСТО НА ДИСКЕ: {disk_usage:.1f}%") + + # OOM алерт + oom = metrics.get('oom', '') + if oom and "Нет OOM" not in oom: + alerts.append(f"💀 OOM KILLER СРАБОТАЛ! Проверь логи!") + + # Load Average алерт (если > количество ядер) + load_1 = float(metrics.get('load_1', 0)) + if load_1 > 4: + alerts.append(f"🔴 ВЫСОКИЙ LOAD AVERAGE: {load_1}") + elif load_1 > 2: + alerts.append(f"🟡 ПОВЫШЕННЫЙ LOAD AVERAGE: {load_1}") + + return alerts + +def collect_metrics(host_config): + """Сбор всех метрик с одного хоста""" + client = get_ssh_client(host_config) + if isinstance(client, str): + return {"error": client, "host": host_config['name'], "ip": host_config['ip']} + + result = { + "host": host_config['name'], + "ip": host_config['ip'], + "metrics": {} + } + + # 1. Определяем тип хоста + host_type = detect_host_type(client) + result["host_type"] = host_type + result["metrics"]["host_type"] = host_type + + # 2. CPU загрузка (%) + cpu_output = exec_cmd(client, "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1") + try: + result["metrics"]["cpu"] = float(cpu_output.replace(',', '.')) + except: + result["metrics"]["cpu"] = 0.0 + + # 3. ОЗУ (используется в MB и %) + mem_output = exec_cmd(client, "free -m | grep Mem") + parts = re.split(r'\s+', mem_output) + if len(parts) >= 3: + total_mem = int(parts[1]) + used_mem = int(parts[2]) + result["metrics"]["mem_used_mb"] = used_mem + result["metrics"]["mem_percent"] = round((used_mem / total_mem) * 100, 1) + result["metrics"]["mem_total_mb"] = total_mem + else: + result["metrics"]["mem_used_mb"] = 0 + result["metrics"]["mem_percent"] = 0 + result["metrics"]["mem_total_mb"] = 0 + + # 4. Uptime и Load Average + uptime_output = exec_cmd(client, "uptime") + load_avg = re.search(r'load average:\s+([\d.]+),\s+([\d.]+),\s+([\d.]+)', uptime_output) + if load_avg: + result["metrics"]["load_1"] = load_avg.group(1) + result["metrics"]["load_5"] = load_avg.group(2) + result["metrics"]["load_15"] = load_avg.group(3) + + uptime_seconds = exec_cmd(client, "cat /proc/uptime | awk '{print $1}'") + try: + uptime_seconds = float(uptime_seconds.split('.')[0]) + days = uptime_seconds // 86400 + hours = (uptime_seconds % 86400) // 3600 + minutes = (uptime_seconds % 3600) // 60 + result["metrics"]["uptime"] = f"{int(days)}d {int(hours)}h {int(minutes)}m" + except: + result["metrics"]["uptime"] = "N/A" + + # 5. Сеть (трафик за последний час) + net_output = exec_cmd(client, "cat /proc/net/dev | tail -n +3 | grep -v lo") + current_net = {} + for line in net_output.split('\n'): + if not line.strip(): + continue + parts = re.split(r'\s+', line.strip()) + iface = parts[0].replace(':', '') + rx_bytes = int(parts[1]) + tx_bytes = int(parts[9]) + current_net[iface] = {'rx': rx_bytes, 'tx': tx_bytes} + + global prev_net_stats + host_key = host_config['ip'] + total_rx_hour = 0 + total_tx_hour = 0 + + if host_key in prev_net_stats: + old_data = prev_net_stats[host_key] + for iface, vals in current_net.items(): + if iface in old_data: + delta_rx = vals['rx'] - old_data[iface]['rx'] + delta_tx = vals['tx'] - old_data[iface]['tx'] + if delta_rx > 0: + total_rx_hour += delta_rx + if delta_tx > 0: + total_tx_hour += delta_tx + + result["metrics"]["net_rx_mb"] = round(total_rx_hour / (1024 * 1024), 2) + result["metrics"]["net_tx_mb"] = round(total_tx_hour / (1024 * 1024), 2) + + prev_net_stats[host_key] = current_net + + # 6. Диски (iostat %util) + iostat_output = exec_cmd(client, "iostat -x -k 1 2 | grep -E 'sd[a-z]|vd[a-z]|nvme' | tail -n +3") + disk_stats = [] + for line in iostat_output.split('\n'): + if not line.strip(): + continue + parts = re.split(r'\s+', line.strip()) + if len(parts) >= 14: + disk_name = parts[0] + try: + util = float(parts[-1].replace(',', '.')) + rps = float(parts[3].replace(',', '.')) + wps = float(parts[4].replace(',', '.')) + disk_stats.append({ + "name": disk_name, + "util": util, + "rps": rps, + "wps": wps + }) + except: + pass + + disk_stats.sort(key=lambda x: x['util'], reverse=True) + result["metrics"]["disks"] = disk_stats[:3] + + # 7. Свободное место на диске (%) + disk_usage_output = exec_cmd(client, "df -h / | tail -1 | awk '{print $5}' | sed 's/%//'") + try: + result["metrics"]["disk_usage"] = float(disk_usage_output.replace(',', '.')) + except: + result["metrics"]["disk_usage"] = 0 + + # 8. Температуры (ТОЛЬКО ДЛЯ ФИЗИЧЕСКИХ ХОСТОВ!) + temps = [] + if host_type == "physical": + temp_output = exec_cmd(client, "sensors -u 2>/dev/null | grep -E 'temp[0-9]+_input|Core' | head -5") + if temp_output and "Error" not in temp_output and "command not found" not in temp_output: + for line in temp_output.split('\n'): + if '_input:' in line: + val = re.search(r'([\d.]+)', line) + if val: + temps.append(float(val.group(1))) + else: + # Если sensors не установлен - пробуем через /sys/class/thermal + temp_sys = exec_cmd(client, "cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | head -3") + if temp_sys and "No such file" not in temp_sys: + for line in temp_sys.split('\n'): + try: + temps.append(float(line) / 1000) + except: + pass + result["metrics"]["temps"] = temps + + # 9. Топ-5 процессов по CPU и RAM + top_cpu = exec_cmd(client, "ps aux --sort=-%cpu | head -6 | tail -5") + top_mem = exec_cmd(client, "ps aux --sort=-%mem | head -6 | tail -5") + result["metrics"]["top_cpu"] = top_cpu + result["metrics"]["top_mem"] = top_mem + + # 10. Логи (ошибки) + last_logs = exec_cmd(client, "journalctl -p 3 -xn 5 --no-pager 2>/dev/null || dmesg | tail -5") + result["metrics"]["logs_errors"] = last_logs + + # 11. Fail2ban + fail2ban_status = exec_cmd(client, "fail2ban-client status 2>/dev/null") + if not fail2ban_status or "not found" in fail2ban_status: + ps_fail2ban = exec_cmd(client, "ps aux | grep -v grep | grep fail2ban-server") + if ps_fail2ban: + fail2ban_status = "✅ Fail2ban запущен" + else: + fail2ban_status = "❌ Fail2ban не установлен или не запущен" + else: + lines = fail2ban_status.split('\n') + jails_info = [] + for line in lines: + if '|- Number of jail:' in line or '`- Number of jail:' in line: + jails_info.append(line.strip()) + if 'Currently banned:' in line: + jails_info.append(line.strip()) + if jails_info: + fail2ban_status = "✅ Fail2ban активен\n" + "\n".join(jails_info) + + fail2ban_logs = exec_cmd(client, "tail -20 /var/log/fail2ban.log 2>/dev/null | grep -E 'BAN|UNBAN' | tail -5 || echo 'Нет записей о банах'") + + active_bans = exec_cmd(client, "fail2ban-client status 2>/dev/null | grep -A 5 'Currently banned:'") + if active_bans and "Currently banned:" in active_bans: + fail2ban_logs += "\n\n🔒 Активные баны:\n" + active_bans + + result["metrics"]["fail2ban_status"] = fail2ban_status + result["metrics"]["fail2ban_logs"] = fail2ban_logs + + # 12. OOM + oom_check = exec_cmd(client, "dmesg | grep -i 'out of memory' | tail -3 || echo 'Нет OOM-событий'") + if oom_check and "Нет OOM-событий" not in oom_check: + result["metrics"]["oom"] = oom_check + else: + result["metrics"]["oom"] = "✅ Нет OOM-событий" + + # 13. Проверка алертов + result["metrics"]["alerts"] = check_alerts(result["metrics"], host_type) + + client.close() + return result diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..95b1b5a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +python-telegram-bot==20.7 +python-dotenv==1.0.0 +paramiko==3.4.0 diff --git a/test_ssh.py b/test_ssh.py new file mode 100644 index 0000000..fc9a0b2 --- /dev/null +++ b/test_ssh.py @@ -0,0 +1,36 @@ +import paramiko +import os +from config import HOSTS_LIST + +for host in HOSTS_LIST: + print(f"\n🔍 Проверяю {host['name']} ({host['ip']})") + print(f" Пользователь: {host['user']}") + print(f" Ключ: {host['key_path']}") + + # Проверяем существование ключа + if not os.path.exists(host['key_path']): + print(f" ❌ Файл ключа НЕ СУЩЕСТВУЕТ: {host['key_path']}") + continue + + # Пробуем подключиться + try: + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect( + hostname=host['ip'], + port=host['port'], + username=host['user'], + key_filename=host['key_path'], + timeout=5 + ) + print(" ✅ Подключено успешно!") + + # Проверяем тип хоста + stdin, stdout, stderr = client.exec_command("uname -a") + print(f" Система: {stdout.read().decode().strip()}") + + client.close() + except paramiko.AuthenticationException: + print(" ❌ Ошибка аутентификации! Неверный логин или ключ") + except Exception as e: + print(f" ❌ Ошибка: {e}") diff --git a/tg-bot-mon.py b/tg-bot-mon.py new file mode 100644 index 0000000..7d8f998 --- /dev/null +++ b/tg-bot-mon.py @@ -0,0 +1,246 @@ +import asyncio +import logging +from datetime import datetime +from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes + +from config import BOT_TOKEN, ADMIN_ID, HOSTS_LIST +from monitor import collect_metrics + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler("bot.log"), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# Кеш для хранения последних собранных данных +CACHE = {} + +def format_metrics(data): + """Формирует красивое сообщение из метрик""" + if "error" in data: + return f"❌ Ошибка подключения к {data['host']}:\n{data['error']}" + + m = data['metrics'] + host = data['host'] + ip = data['ip'] + host_type = data.get('host_type', 'unknown') + + # Эмодзи для типа хоста + type_emoji = { + 'physical': '🖥️', + 'vm': '🖥️', + 'lxc': '📦', + 'container': '🐳' + }.get(host_type, '🖥️') + + type_label = { + 'physical': 'Железо', + 'vm': 'ВМ', + 'lxc': 'LXC', + 'container': 'Контейнер' + }.get(host_type, 'Неизвестно') + + text = f"{type_emoji} *{host} ({ip})* - `{type_label}`\n" + text += f"🕒 *Uptime:* {m.get('uptime', 'N/A')}\n" + text += f"📊 *Load Avg:* {m.get('load_1', '0')}, {m.get('load_5', '0')}, {m.get('load_15', '0')}\n\n" + + # Алерты (если есть) + alerts = m.get('alerts', []) + if alerts: + text += "🚨 *АЛЕРТЫ!*\n" + for alert in alerts: + text += f" {alert}\n" + text += "\n" + + # CPU + cpu_bar = "█" * int(m['cpu'] // 5) + "░" * (20 - int(m['cpu'] // 5)) + cpu_color = "🔴" if m['cpu'] > 80 else "🟡" if m['cpu'] > 60 else "🟢" + text += f"{cpu_color} *CPU:* {m['cpu']:.1f}%\n`{cpu_bar}`\n" + + # RAM + mem_bar = "█" * int(m['mem_percent'] // 5) + "░" * (20 - int(m['mem_percent'] // 5)) + mem_color = "🔴" if m['mem_percent'] > 85 else "🟡" if m['mem_percent'] > 70 else "🟢" + text += f"{mem_color} *RAM:* {m['mem_used_mb']} MB / {m.get('mem_total_mb', '?')} MB ({m['mem_percent']}%)\n`{mem_bar}`\n" + + # Диск (свободное место) + disk_color = "🔴" if m.get('disk_usage', 0) > 80 else "🟡" if m.get('disk_usage', 0) > 70 else "🟢" + text += f"{disk_color} *Disk (/):* {m.get('disk_usage', 0):.1f}% used\n" + + # Сеть + text += f"🌐 *Network (per hour):* 📥 {m['net_rx_mb']} MB | 📤 {m['net_tx_mb']} MB\n" + + # Диски iostat + text += "\n💾 *Disks (top 3 by %util):*\n" + for disk in m.get('disks', []): + text += f" └─ {disk['name']}: util {disk['util']:.1f}% (r: {disk['rps']:.1f}/s, w: {disk['wps']:.1f}/s)\n" + + # Температуры (только для железа) + if host_type == "physical" and m.get('temps'): + text += "\n🌡️ *Temperatures:*\n" + for i, temp in enumerate(m['temps']): + temp_color = "🔴" if temp > 75 else "🟡" if temp > 60 else "🟢" + text += f" {temp_color} Sensor {i+1}: {temp:.1f}°C\n" + elif host_type != "physical": + text += "\n🌡️ *Temperatures:* Не требуется (ВМ/LXC)\n" + + # Топ процессов + text += "🏆 *Top 5 CPU:*\n```\n" + top_cpu = m.get('top_cpu', 'N/A') + # Экранируем опасные символы для Markdown + top_cpu = top_cpu.replace('_', '\\_').replace('*', '\\*').replace('`', '\\`') + text += top_cpu + "\n```" + text += "🏆 *Top 5 RAM:*\n```\n" + top_mem = m.get('top_mem', 'N/A') + top_mem = top_mem.replace('_', '\\_').replace('*', '\\*').replace('`', '\\`') + text += top_mem + "\n```" + # text += "\n🏆 *Top 5 CPU:*\n```\n" + # text += m.get('top_cpu', 'N/A') + "\n```" + # text += "🏆 *Top 5 RAM:*\n```\n" + # text += m.get('top_mem', 'N/A') + "\n```" + + # Логи и безопасность + text += "\n⚠️ *Recent Errors (journalctl -p 3):*\n```\n" + text += m.get('logs_errors', 'N/A')[:300] + "\n```" + + # OOM + text += "\n💀 *Out of Memory Events:*\n```\n" + text += m.get('oom', 'Нет данных') + "\n```" + + # Fail2ban + text += "\n🛡️ *Fail2ban Status:*\n```\n" + text += m.get('fail2ban_status', 'N/A') + "\n```" + + text += "\n🚫 *Fail2ban Logs (last bans):*\n```\n" + text += m.get('fail2ban_logs', 'N/A') + "\n```" + + text += f"\n⏱️ *Обновлено:* {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + return text + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик команды /status""" + if update.effective_user.id != ADMIN_ID: + await update.message.reply_text("⛔ Доступ запрещен. Ты не мой хозяин.") + return + + # Создаем клавиатуру с хостами + keyboard = [] + for host in HOSTS_LIST: + keyboard.append([InlineKeyboardButton(host['name'], callback_data=f"host_{host['ip']}")]) + keyboard.append([InlineKeyboardButton("🔄 Все хосты", callback_data="host_all")]) + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "🔍 Выбери хост для мониторинга или нажми 'Все хосты':", + reply_markup=reply_markup + ) + +async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка нажатий на кнопки""" + query = update.callback_query + await query.answer() + + if update.effective_user.id != ADMIN_ID: + await query.edit_message_text("⛔ Недостаточно прав.") + return + + data = query.data + if data == "host_all": + await query.edit_message_text("⏳ Собираю данные со всех хостов...") + full_report = "" + for host in HOSTS_LIST: + result = collect_metrics(host) + report = format_metrics(result) + full_report += report + "\n\n" + "="*30 + "\n\n" + # Обрезаем, если слишком длинное + if len(full_report) > 4000: + full_report = full_report[:4000] + "\n\n... (обрезано)" + await query.edit_message_text(full_report, parse_mode="Markdown") + elif data.startswith("host_"): + ip = data.replace("host_", "") + host = next((h for h in HOSTS_LIST if h['ip'] == ip), None) + if not host: + await query.edit_message_text("Хост не найден") + return + await query.edit_message_text(f"⏳ Собираю данные с {host['name']}...") + result = collect_metrics(host) + report = format_metrics(result) + await query.edit_message_text(report, parse_mode="Markdown") + +async def scheduled_job(context: ContextTypes.DEFAULT_TYPE): + """Функция, вызываемая раз в час""" + bot: Bot = context.bot + logger.info("🔄 Запущен плановый сбор метрик (каждый час)") + + for host in HOSTS_LIST: + try: + logger.info(f"📊 Собираю данные с {host['name']}") + result = collect_metrics(host) + report = format_metrics(result) + await bot.send_message(chat_id=ADMIN_ID, text=report, parse_mode="Markdown") + logger.info(f"✅ Отправлен отчет для {host['name']}") + except Exception as e: + error_msg = f"❌ Ошибка при сборе/отправке для {host['name']}: {e}" + logger.error(error_msg) + await bot.send_message(chat_id=ADMIN_ID, text=error_msg) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Команда /start - проверка, что бот жив""" + if update.effective_user.id != ADMIN_ID: + await update.message.reply_text("Привет! Я бот для мониторинга серверов, но ты не мой хозяин.") + return + + await update.message.reply_text( + "🤖 *Бот мониторинга запущен!*\n\n" + "📌 Используй команду `/status`, чтобы получить актуальные метрики с серверов.\n" + "⏰ Каждый час я буду присылать автоматический отчет.\n\n" + f"📋 Настроено хостов: {len(HOSTS_LIST)}", + parse_mode="Markdown" + ) + +def main(): + """Главная функция запуска бота""" + logger.info("🚀 Запуск бота...") + logger.info(f"📋 Загружено хостов: {len(HOSTS_LIST)}") + + # Проверяем хосты при старте + if not HOSTS_LIST: + logger.error("❌ Нет хостов в конфигурации! Проверь .env файл!") + return + + for host in HOSTS_LIST: + logger.info(f" - {host['name']} ({host['ip']}:{host['port']})") + + app = Application.builder().token(BOT_TOKEN).build() + + # Регистрируем команды + app.add_handler(CommandHandler("start", start_command)) + app.add_handler(CommandHandler("status", status_command)) + app.add_handler(CallbackQueryHandler(button_callback)) + + # Настройка шедулера (раз в час) + job_queue = app.job_queue + if job_queue: + # Запускаем через 10 секунд после старта, потом каждый час + job_queue.run_repeating(scheduled_job, interval=3600, first=10) + logger.info("⏰ Шедулер настроен: отчеты будут приходить каждый час") + else: + logger.warning("⚠️ JobQueue не доступна! Автоматические отчеты не будут работать.") + logger.warning("⚠️ Установи: pip install 'python-telegram-bot[job-queue]'") + + logger.info("🤖 Бот запущен и готов к работе! Напиши /start в Telegram") + + try: + app.run_polling() + except KeyboardInterrupt: + logger.info("🛑 Бот остановлен пользователем") + except Exception as e: + logger.error(f"❌ Ошибка при работе бота: {e}") + +if __name__ == "__main__": + main()