feat: initial MikroTik monitor setup
This commit is contained in:
+194
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MikroTik IP Monitor — пингует адреса через MikroTik API и шлёт уведомления в Telegram.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import routeros_api
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Настройки из переменных окружения
|
||||
# ──────────────────────────────────────────────
|
||||
MIKROTIK_HOST = os.environ["MIKROTIK_HOST"]
|
||||
MIKROTIK_PORT = int(os.getenv("MIKROTIK_PORT", "8728"))
|
||||
MIKROTIK_USER = os.environ["MIKROTIK_USER"]
|
||||
MIKROTIK_PASSWORD = os.environ["MIKROTIK_PASSWORD"]
|
||||
|
||||
TELEGRAM_BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
|
||||
TELEGRAM_CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
|
||||
|
||||
# Список IP-адресов для мониторинга (через запятую в .env или задать напрямую)
|
||||
IP_LIST = [ip.strip() for ip in os.environ["IP_LIST"].split(",") if ip.strip()]
|
||||
|
||||
CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", "60")) # секунды между проверками
|
||||
PING_COUNT = int(os.getenv("PING_COUNT", "3")) # количество пингов
|
||||
PING_TIMEOUT_MS = int(os.getenv("PING_TIMEOUT_MS", "1000")) # таймаут пинга (мс)
|
||||
|
||||
# Сколько раз подряд хост должен не ответить, чтобы считаться DOWN
|
||||
FAILURE_THRESHOLD = int(os.getenv("FAILURE_THRESHOLD", "2"))
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Логирование
|
||||
# ──────────────────────────────────────────────
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Telegram
|
||||
# ──────────────────────────────────────────────
|
||||
async def send_telegram(message: str) -> None:
|
||||
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
|
||||
payload = {
|
||||
"chat_id": TELEGRAM_CHAT_ID,
|
||||
"text": message,
|
||||
"parse_mode": "HTML",
|
||||
}
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
try:
|
||||
resp = await client.post(url, json=payload)
|
||||
resp.raise_for_status()
|
||||
log.info("Telegram: сообщение отправлено")
|
||||
except Exception as exc:
|
||||
log.error("Telegram: ошибка отправки — %s", exc)
|
||||
|
||||
|
||||
def notify(message: str) -> None:
|
||||
asyncio.run(send_telegram(message))
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# MikroTik: пинг через API
|
||||
# ──────────────────────────────────────────────
|
||||
def mikrotik_ping(connection, address: str) -> Optional[float]:
|
||||
"""
|
||||
Запускает ping на MikroTik. Возвращает средний avg-rtt (мс) или None если недоступен.
|
||||
"""
|
||||
try:
|
||||
api = connection.get_api()
|
||||
ping_resource = api.get_resource("/ping")
|
||||
results = ping_resource.call(
|
||||
"",
|
||||
{
|
||||
"address": address,
|
||||
"count": str(PING_COUNT),
|
||||
"timeout": str(PING_TIMEOUT_MS),
|
||||
},
|
||||
)
|
||||
|
||||
# Ищем строку с итогами (packet-loss и avg-rtt)
|
||||
for row in results:
|
||||
if "avg-rtt" in row and row.get("avg-rtt"):
|
||||
avg_rtt_str = row["avg-rtt"].replace("ms", "").strip()
|
||||
try:
|
||||
return float(avg_rtt_str)
|
||||
except ValueError:
|
||||
pass
|
||||
if row.get("packet-loss") == "100":
|
||||
return None
|
||||
|
||||
return None
|
||||
except Exception as exc:
|
||||
log.warning("Ошибка пинга %s: %s", address, exc)
|
||||
return None
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Основной цикл
|
||||
# ──────────────────────────────────────────────
|
||||
def main() -> None:
|
||||
log.info("=== MikroTik Monitor запущен ===")
|
||||
log.info("Хосты для проверки: %s", IP_LIST)
|
||||
log.info("Интервал проверки: %d сек", CHECK_INTERVAL)
|
||||
|
||||
# Состояние: счётчик последовательных ошибок и флаг «уже DOWN»
|
||||
failure_count: dict[str, int] = {ip: 0 for ip in IP_LIST}
|
||||
host_down: dict[str, bool] = {ip: False for ip in IP_LIST}
|
||||
|
||||
notify(
|
||||
f"🟢 <b>MikroTik Monitor запущен</b>\n"
|
||||
f"Хосты: <code>{', '.join(IP_LIST)}</code>\n"
|
||||
f"Интервал: {CHECK_INTERVAL} сек"
|
||||
)
|
||||
|
||||
while True:
|
||||
connection = None
|
||||
try:
|
||||
log.info("Подключение к MikroTik %s:%d …", MIKROTIK_HOST, MIKROTIK_PORT)
|
||||
connection = routeros_api.RouterOsApiPool(
|
||||
MIKROTIK_HOST,
|
||||
username=MIKROTIK_USER,
|
||||
password=MIKROTIK_PASSWORD,
|
||||
port=MIKROTIK_PORT,
|
||||
plaintext_login=True,
|
||||
)
|
||||
connection.get_api() # проверяем соединение
|
||||
log.info("Подключено к MikroTik")
|
||||
|
||||
for ip in IP_LIST:
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
rtt = mikrotik_ping(connection, ip)
|
||||
|
||||
if rtt is not None:
|
||||
log.info("✅ %s avg-rtt=%.1f ms", ip, rtt)
|
||||
if host_down[ip]:
|
||||
# Хост восстановился
|
||||
host_down[ip] = False
|
||||
failure_count[ip] = 0
|
||||
notify(
|
||||
f"✅ <b>Хост восстановился</b>\n"
|
||||
f"IP: <code>{ip}</code>\n"
|
||||
f"RTT: {rtt:.1f} ms\n"
|
||||
f"Время: {now}"
|
||||
)
|
||||
else:
|
||||
failure_count[ip] = 0
|
||||
else:
|
||||
failure_count[ip] += 1
|
||||
log.warning(
|
||||
"❌ %s не отвечает (попытка %d/%d)",
|
||||
ip, failure_count[ip], FAILURE_THRESHOLD,
|
||||
)
|
||||
|
||||
if failure_count[ip] >= FAILURE_THRESHOLD and not host_down[ip]:
|
||||
host_down[ip] = True
|
||||
notify(
|
||||
f"🔴 <b>Хост недоступен!</b>\n"
|
||||
f"IP: <code>{ip}</code>\n"
|
||||
f"Не отвечает {failure_count[ip]} проверки подряд\n"
|
||||
f"Время: {now}"
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
log.error("Ошибка подключения к MikroTik: %s", exc)
|
||||
notify(
|
||||
f"⚠️ <b>Ошибка подключения к MikroTik</b>\n"
|
||||
f"<code>{exc}</code>"
|
||||
)
|
||||
finally:
|
||||
if connection:
|
||||
try:
|
||||
connection.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
log.info("Следующая проверка через %d сек …\n", CHECK_INTERVAL)
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user