diff --git a/.env.example b/.env.example
index c3b6558..fe39057 100644
--- a/.env.example
+++ b/.env.example
@@ -13,8 +13,8 @@ MIKROTIK_PASSWORD=your_password
TELEGRAM_BOT_TOKEN=123456789:AABBccDDeeFFggHH
TELEGRAM_CHAT_ID=-1001234567890
-# ─── Список адресов (через запятую) ──────────────────────────────────────────
-IP_LIST=192.168.1.1,192.168.1.10,192.168.1.20
+# Имя address-list из /ip firewall address-list на MikroTik
+ADDRESS_LIST_NAME=monitor
# ─── Параметры мониторинга ────────────────────────────────────────────────────
CHECK_INTERVAL=60
diff --git a/Dockerfile b/Dockerfile
index 8012a70..f3c8c00 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,166 +8,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
tzdata \
&& rm -rf /var/lib/apt/lists/*
-# Зависимости
RUN pip install --no-cache-dir \
routeros-api==0.17.0 \
- httpx==0.27.0 \
- python-dotenv==1.0.1
+ httpx==0.27.0
-# Скрипт встроен прямо в Dockerfile — не нужны дополнительные файлы
-RUN cat > /app/monitor.py << 'PYEOF'
-#!/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
-
-# ── Настройки ──────────────────────────────────────────────────────────────
-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_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"))
-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 ping ──────────────────────────────────────────────────────────
-def mikrotik_ping(connection, address: str) -> Optional[float]:
- 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)},
- )
- 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
- 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)
-
- 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"🟢 MikroTik Monitor запущен\n"
- f"Хосты: {', '.join(IP_LIST)}\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"✅ Хост восстановился\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("❌ %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"🔴 Хост недоступен!\n"
- f"IP: {ip}\n"
- f"Не отвечает {failure_count[ip]} проверки подряд\n"
- f"Время: {now}"
- )
-
- except Exception as exc:
- log.error("Ошибка подключения к MikroTik: %s", exc)
- notify(f"⚠️ Ошибка подключения к MikroTik\n{exc}")
- finally:
- if connection:
- try:
- connection.disconnect()
- except Exception:
- pass
-
- log.info("Следующая проверка через %d сек …\n", CHECK_INTERVAL)
- time.sleep(CHECK_INTERVAL)
-
-
-if __name__ == "__main__":
- main()
-PYEOF
+# Копируем скрипт из репозитория
+COPY monitor.py .
ENV TZ=Europe/Moscow
CMD ["python", "-u", "/app/monitor.py"]
diff --git a/monitor.py b/monitor.py
index a691d3d..c3eb7cd 100644
--- a/monitor.py
+++ b/monitor.py
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""
-MikroTik IP Monitor — пингует адреса через MikroTik API и шлёт уведомления в Telegram.
+MikroTik IP Monitor — читает адреса из /ip firewall address-list,
+пингует их через MikroTik API и шлёт уведомления в Telegram.
"""
import os
@@ -12,9 +13,6 @@ from typing import Optional
import routeros_api
import httpx
-from dotenv import load_dotenv
-
-load_dotenv()
# ──────────────────────────────────────────────
# Настройки из переменных окружения
@@ -27,14 +25,12 @@ 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()]
+# Имя 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")) # таймаут пинга (мс)
-
-# Сколько раз подряд хост должен не ответить, чтобы считаться DOWN
+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"))
# ──────────────────────────────────────────────
@@ -53,11 +49,7 @@ log = logging.getLogger(__name__)
# ──────────────────────────────────────────────
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",
- }
+ 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)
@@ -71,15 +63,41 @@ 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(connection, address: str) -> Optional[float]:
+def mikrotik_ping(api, address: str) -> Optional[float]:
"""
- Запускает ping на MikroTik. Возвращает средний avg-rtt (мс) или None если недоступен.
+ Возвращает avg-rtt (мс) или None если хост недоступен.
"""
try:
- api = connection.get_api()
ping_resource = api.get_resource("/ping")
results = ping_resource.call(
"",
@@ -89,18 +107,14 @@ def mikrotik_ping(connection, address: str) -> Optional[float]:
"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)
+ return float(row["avg-rtt"].replace("ms", "").strip())
except ValueError:
pass
if row.get("packet-loss") == "100":
return None
-
return None
except Exception as exc:
log.warning("Ошибка пинга %s: %s", address, exc)
@@ -112,41 +126,56 @@ def mikrotik_ping(connection, address: str) -> Optional[float]:
# ──────────────────────────────────────────────
def main() -> None:
log.info("=== MikroTik Monitor запущен ===")
- log.info("Хосты для проверки: %s", IP_LIST)
- log.info("Интервал проверки: %d сек", CHECK_INTERVAL)
+ log.info("Address-list: '%s'", ADDRESS_LIST_NAME)
+ 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}
+ # Состояние хостов: счётчик ошибок и флаг DOWN
+ # Обновляется динамически при каждом чтении address-list
+ failure_count: dict[str, int] = {}
+ host_down: dict[str, bool] = {}
notify(
f"🟢 MikroTik Monitor запущен\n"
- f"Хосты: {', '.join(IP_LIST)}\n"
+ f"Address-list: {ADDRESS_LIST_NAME}\n"
f"Интервал: {CHECK_INTERVAL} сек"
)
while True:
- connection = None
+ pool = None
try:
log.info("Подключение к MikroTik %s:%d …", MIKROTIK_HOST, MIKROTIK_PORT)
- connection = routeros_api.RouterOsApiPool(
+ pool = routeros_api.RouterOsApiPool(
MIKROTIK_HOST,
username=MIKROTIK_USER,
password=MIKROTIK_PASSWORD,
port=MIKROTIK_PORT,
plaintext_login=True,
)
- connection.get_api() # проверяем соединение
+ api = pool.get_api()
log.info("Подключено к MikroTik")
- for ip in IP_LIST:
+ # Читаем актуальный список адресов с 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(connection, ip)
+ rtt = mikrotik_ping(api, ip)
if rtt is not None:
- log.info("✅ %s avg-rtt=%.1f ms", ip, rtt)
+ log.info("✅ %-18s avg-rtt=%.1f ms", ip, rtt)
if host_down[ip]:
- # Хост восстановился
host_down[ip] = False
failure_count[ip] = 0
notify(
@@ -160,29 +189,26 @@ def main() -> None:
else:
failure_count[ip] += 1
log.warning(
- "❌ %s не отвечает (попытка %d/%d)",
+ "❌ %-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("Ошибка подключения к MikroTik: %s", exc)
- notify(
- f"⚠️ Ошибка подключения к MikroTik\n"
- f"{exc}"
- )
+ log.error("Ошибка: %s", exc)
+ notify(f"⚠️ Ошибка подключения к MikroTik\n{exc}")
finally:
- if connection:
+ if pool:
try:
- connection.disconnect()
+ pool.disconnect()
except Exception:
pass
diff --git a/stack.yml b/stack.yml
index 6f86c0b..d2d6b60 100644
--- a/stack.yml
+++ b/stack.yml
@@ -33,8 +33,8 @@ services:
- TELEGRAM_BOT_TOKEN=123456:AABBcc # ← токен от @BotFather
- TELEGRAM_CHAT_ID=-1001234567890 # ← ID чата/группы
- # Список IP через запятую
- - IP_LIST=192.168.1.1,192.168.1.10,192.168.1.20
+ # Имя address-list из /ip firewall address-list на MikroTik
+ - ADDRESS_LIST_NAME=monitor
# Параметры мониторинга
- CHECK_INTERVAL=60 # интервал между проверками (сек)