#!/usr/bin/env python3 """ MikroTik IP Monitor — читает адреса из /ip firewall address-list, пингует их через MikroTik API и шлёт уведомления в Telegram. """ import os import time import logging import asyncio from datetime import datetime from typing import Optional import routeros_api import httpx # ────────────────────────────────────────────── # Настройки из переменных окружения # ────────────────────────────────────────────── 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"] # Имя address-list в MikroTik (/ip firewall address-list) ADDRESS_LIST_NAME = os.environ["ADDRESS_LIST_NAME"] 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")) 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: получить список IP из address-list # ────────────────────────────────────────────── def fetch_address_list(api) -> list[str]: """ Читает /ip firewall address-list и возвращает список IP из записей с именем ADDRESS_LIST_NAME (пропускает отключённые). """ resource = api.get_resource("/ip/firewall/address-list") entries = resource.get(list=ADDRESS_LIST_NAME) ip_list = [] for entry in entries: # Пропускаем отключённые записи if entry.get("disabled", "false") == "true": log.debug("Пропущена (disabled): %s", entry.get("address")) continue address = entry.get("address", "").strip() # Убираем префикс /32 если есть — пингуем чистый IP if "/" in address: address = address.split("/")[0] if address: ip_list.append(address) return ip_list # ────────────────────────────────────────────── # MikroTik: пинг через API # ────────────────────────────────────────────── def mikrotik_ping(api, address: str) -> Optional[float]: """ Возвращает avg-rtt (мс) или None если хост недоступен. <<<<<<< HEAD RouterOS API: /ping — это команда, вызывается через get_binary_resource. """ try: binary_resource = api.get_binary_resource("/") results = binary_resource.call( "ping", { "address": address.encode(), "count": str(PING_COUNT).encode(), "interval": b"0.2", }, ) rtts = [] for row in results: # Каждая строка — один пинг-ответ # "time" присутствует только если ответ получен time_val = row.get(b"time", b"").decode().replace("ms", "").strip() if time_val: try: rtts.append(float(time_val)) except ValueError: pass if rtts: return sum(rtts) / len(rtts) return None ======= """ try: ping_resource = api.get_resource("/ping") results = ping_resource.call( "", { "address": address, "count": str(PING_COUNT), "timeout": str(PING_TIMEOUT_MS), }, ) for row in results: if "avg-rtt" in row and row.get("avg-rtt"): try: return float(row["avg-rtt"].replace("ms", "").strip()) except ValueError: pass if row.get("packet-loss") == "100": return None return None >>>>>>> 994b95f3e9aa67ddf108ce11904cd69f3d736ab8 except Exception as exc: log.warning("Ошибка пинга %s: %s", address, exc) return None # ────────────────────────────────────────────── # Основной цикл # ────────────────────────────────────────────── def main() -> None: log.info("=== MikroTik Monitor запущен ===") log.info("Address-list: '%s'", ADDRESS_LIST_NAME) log.info("Интервал: %d сек", CHECK_INTERVAL) # Состояние хостов: счётчик ошибок и флаг DOWN # Обновляется динамически при каждом чтении address-list failure_count: dict[str, int] = {} host_down: dict[str, bool] = {} notify( f"🟢 MikroTik Monitor запущен\n" f"Address-list: {ADDRESS_LIST_NAME}\n" f"Интервал: {CHECK_INTERVAL} сек" ) while True: pool = None try: log.info("Подключение к MikroTik %s:%d …", MIKROTIK_HOST, MIKROTIK_PORT) pool = routeros_api.RouterOsApiPool( MIKROTIK_HOST, username=MIKROTIK_USER, password=MIKROTIK_PASSWORD, port=MIKROTIK_PORT, plaintext_login=True, ) api = pool.get_api() log.info("Подключено к MikroTik") # Читаем актуальный список адресов с MikroTik ip_list = fetch_address_list(api) if not ip_list: log.warning("Address-list '%s' пуст или не найден!", ADDRESS_LIST_NAME) else: log.info("Адресов для проверки: %d → %s", len(ip_list), ip_list) # Добавляем новые хосты в state (без сброса существующих) for ip in ip_list: if ip not in failure_count: failure_count[ip] = 0 host_down[ip] = False # Пингуем каждый адрес for ip in ip_list: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rtt = mikrotik_ping(api, ip) if rtt is not None: log.info("✅ %-18s avg-rtt=%.1f ms", ip, rtt) if host_down[ip]: host_down[ip] = False failure_count[ip] = 0 notify( f"✅ Хост восстановился\n" f"IP: {ip}\n" f"RTT: {rtt:.1f} ms\n" f"Время: {now}" ) else: failure_count[ip] = 0 else: failure_count[ip] += 1 log.warning( "❌ %-18s не отвечает (%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"🔴 Хост недоступен!\n" f"IP: {ip}\n" f"Список: {ADDRESS_LIST_NAME}\n" f"Не отвечает {failure_count[ip]} проверки подряд\n" f"Время: {now}" ) except Exception as exc: log.error("Ошибка: %s", exc) notify(f"⚠️ Ошибка подключения к MikroTik\n{exc}") finally: if pool: try: pool.disconnect() except Exception: pass log.info("Следующая проверка через %d сек …\n", CHECK_INTERVAL) time.sleep(CHECK_INTERVAL) if __name__ == "__main__": main()