149 lines
6.1 KiB
Python
149 lines
6.1 KiB
Python
|
|
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()
|