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()