From 6be7fad541b179a7e07a46cb277d49cf3849523e Mon Sep 17 00:00:00 2001
From: Sergey
Date: Sat, 16 May 2026 15:08:53 +0700
Subject: [PATCH] feat: initial commit
---
.env.example | 26 ++++++
.gitignore | 22 +++++
Dockerfile | 19 +++++
PUSH.md | 77 +++++++++++++++++
README.md | 132 +++++++++++++++++++++++++++++
monitor.py | 228 +++++++++++++++++++++++++++++++++++++++++++++++++++
stack.yml | 71 ++++++++++++++++
7 files changed, 575 insertions(+)
create mode 100644 .env.example
create mode 100644 .gitignore
create mode 100644 Dockerfile
create mode 100644 PUSH.md
create mode 100644 README.md
create mode 100644 monitor.py
create mode 100644 stack.yml
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..fe39057
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,26 @@
+# Скопируйте этот файл в .env и заполните значения
+# cp .env.example .env
+#
+# ⚠️ Файл .env добавлен в .gitignore — не коммитьте его!
+
+# ─── MikroTik ─────────────────────────────────────────────────────────────────
+MIKROTIK_HOST=192.168.88.1
+MIKROTIK_PORT=8728
+MIKROTIK_USER=monitor
+MIKROTIK_PASSWORD=your_password
+
+# ─── Telegram ─────────────────────────────────────────────────────────────────
+TELEGRAM_BOT_TOKEN=123456789:AABBccDDeeFFggHH
+TELEGRAM_CHAT_ID=-1001234567890
+
+# Имя address-list из /ip firewall address-list на MikroTik
+ADDRESS_LIST_NAME=monitor
+
+# ─── Параметры мониторинга ────────────────────────────────────────────────────
+CHECK_INTERVAL=60
+PING_COUNT=3
+PING_TIMEOUT_MS=1000
+FAILURE_THRESHOLD=2
+
+# ─── Временная зона ───────────────────────────────────────────────────────────
+TZ=Europe/Moscow
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d57104e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+# Секреты — никогда не коммитить!
+.env
+
+# Python
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+.Python
+*.egg-info/
+dist/
+build/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..f3c8c00
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,19 @@
+FROM python:3.12-slim
+
+LABEL description="MikroTik IP Monitor with Telegram alerts"
+
+WORKDIR /app
+
+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
+
+# Копируем скрипт из репозитория
+COPY monitor.py .
+
+ENV TZ=Europe/Moscow
+CMD ["python", "-u", "/app/monitor.py"]
diff --git a/PUSH.md b/PUSH.md
new file mode 100644
index 0000000..bfcd29f
--- /dev/null
+++ b/PUSH.md
@@ -0,0 +1,77 @@
+# Публикация в Gitea — пошаговая инструкция
+
+## 1. Создайте репозиторий в Gitea
+
+1. Откройте Gitea → **+** → **New Repository**
+2. Имя: `mikrotik-monitor`
+3. Видимость: Private (рекомендуется)
+4. **Не инициализировать** репозиторий (без README, .gitignore)
+5. Нажмите **Create Repository**
+
+---
+
+## 2. Инициализируйте и запушьте
+
+Скопируйте папку репозитория на свой компьютер, затем:
+
+```bash
+cd mikrotik-monitor-repo
+
+# Инициализация
+git init
+git checkout -b main
+
+# Первый коммит
+git add .
+git commit -m "feat: initial MikroTik monitor setup"
+
+# Подключение к Gitea (замените URL на свой)
+git remote add origin https://gitea.example.com/yourname/mikrotik-monitor.git
+
+# Пуш
+git push -u origin main
+```
+
+---
+
+## 3. Настройте SSH (опционально, но удобнее)
+
+```bash
+# Сгенерируйте ключ (если ещё нет)
+ssh-keygen -t ed25519 -C "your@email.com"
+
+# Скопируйте публичный ключ
+cat ~/.ssh/id_ed25519.pub
+```
+
+Добавьте ключ в Gitea: **Settings → SSH / GPG Keys → Add Key**
+
+Затем используйте SSH-URL:
+```bash
+git remote set-url origin git@gitea.example.com:yourname/mikrotik-monitor.git
+```
+
+---
+
+## 4. Дальнейшая работа
+
+```bash
+# Внести изменения и запушить
+git add .
+git commit -m "fix: описание изменения"
+git push
+
+# Посмотреть статус
+git status
+git log --oneline
+```
+
+---
+
+## ⚠️ Важно: секреты в git
+
+Файл `.env` добавлен в `.gitignore` и **никогда не попадёт в репозиторий**.
+Для хранения секретов (пароли, токены) используйте:
+
+- Переменные окружения в Portainer (раздел **Environment variables** при создании стека)
+- [Gitea Secrets](https://docs.gitea.com/usage/secrets) — если настроите CI/CD
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9760e8b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,132 @@
+# 🔍 MikroTik Monitor
+
+Мониторинг IP-адресов через MikroTik API с уведомлениями в Telegram.
+Деплой через Portainer Stack — без лишних зависимостей.
+
+---
+
+## Как это работает
+
+```
+[Docker Container]
+ │
+ ├─→ Подключается к MikroTik по API (порт 8728)
+ │ └─→ Выполняет /ping для каждого IP из списка
+ │
+ └─→ Если хост не отвечает N раз подряд → Telegram уведомление
+```
+
+**Логика уведомлений:**
+- 🔴 `DOWN` — хост не ответил `FAILURE_THRESHOLD` раз подряд
+- ✅ `UP` — хост восстановился после даунтайма
+- ⚠️ `ERROR` — не удалось подключиться к MikroTik
+
+---
+
+## Структура репозитория
+
+```
+.
+├── Dockerfile # Образ с встроенным скриптом
+├── monitor.py # Основной скрипт мониторинга
+├── stack.yml # Portainer Stack
+├── .env.example # Шаблон переменных окружения
+└── README.md
+```
+
+---
+
+## Быстрый старт
+
+### 1. Настройте MikroTik
+
+```routeros
+/ip service enable api
+
+/user group add name=monitor policy=api,read,test
+/user add name=monitor password=StrongPass group=monitor
+```
+
+### 2. Создайте Telegram-бота
+
+1. [@BotFather](https://t.me/BotFather) → `/newbot` → скопируйте токен
+2. Добавьте бота в чат, напишите любое сообщение
+3. Получите `chat_id`:
+ ```
+ https://api.telegram.org/bot/getUpdates
+ ```
+
+### 3. Соберите Docker-образ на сервере
+
+```bash
+git clone https://gitea.example.com/yourname/mikrotik-monitor.git
+cd mikrotik-monitor
+docker build -t mikrotik-monitor:latest .
+```
+
+### 4. Задеплойте через Portainer
+
+1. **Stacks** → **Add Stack**
+2. Имя: `mikrotik-monitor`
+3. **Web editor** → вставьте содержимое `stack.yml`
+4. Заполните переменные окружения
+5. **Deploy the stack**
+
+---
+
+## Переменные окружения
+
+| Переменная | Обязательная | По умолчанию | Описание |
+|---|:---:|---|---|
+| `MIKROTIK_HOST` | ✅ | — | IP или hostname MikroTik |
+| `MIKROTIK_PORT` | | `8728` | API-порт (8729 для SSL) |
+| `MIKROTIK_USER` | ✅ | — | Логин |
+| `MIKROTIK_PASSWORD` | ✅ | — | Пароль |
+| `TELEGRAM_BOT_TOKEN` | ✅ | — | Токен бота от @BotFather |
+| `TELEGRAM_CHAT_ID` | ✅ | — | ID чата/группы/канала |
+| `IP_LIST` | ✅ | — | Адреса через запятую |
+| `CHECK_INTERVAL` | | `60` | Интервал проверок (сек) |
+| `PING_COUNT` | | `3` | Кол-во пинг-пакетов |
+| `PING_TIMEOUT_MS` | | `1000` | Таймаут пинга (мс) |
+| `FAILURE_THRESHOLD` | | `2` | Провалов подряд → DOWN |
+| `TZ` | | `Europe/Moscow` | Временная зона |
+
+### Рекомендуемый способ: переменные Portainer
+
+В `stack.yml` используйте синтаксис `${VAR}`:
+```yaml
+environment:
+ - MIKROTIK_PASSWORD=${MIKROTIK_PASSWORD}
+ - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
+```
+Значения добавляйте в раздел **Environment variables** в UI Portainer — они хранятся зашифрованно.
+
+---
+
+## Обновление
+
+```bash
+# Пересобрать образ
+cd mikrotik-monitor
+git pull
+docker build -t mikrotik-monitor:latest .
+```
+
+Затем в Portainer: **Stacks → mikrotik-monitor → Update the stack**.
+
+---
+
+## Просмотр логов
+
+```bash
+docker logs -f mikrotik-monitor
+```
+
+Пример вывода:
+```
+2024-01-15 14:30:00 [INFO] === MikroTik Monitor запущен ===
+2024-01-15 14:30:00 [INFO] Хосты: ['192.168.1.1', '192.168.1.10']
+2024-01-15 14:30:01 [INFO] Подключено к MikroTik
+2024-01-15 14:30:02 [INFO] ✅ 192.168.1.1 avg-rtt=0.8 ms
+2024-01-15 14:30:03 [WARNING] ❌ 192.168.1.10 не отвечает (1/2)
+```
diff --git a/monitor.py b/monitor.py
new file mode 100644
index 0000000..53d6982
--- /dev/null
+++ b/monitor.py
@@ -0,0 +1,228 @@
+#!/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 если хост недоступен.
+ 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
+
+ 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()
diff --git a/stack.yml b/stack.yml
new file mode 100644
index 0000000..d2d6b60
--- /dev/null
+++ b/stack.yml
@@ -0,0 +1,71 @@
+version: "3.9"
+
+# ═══════════════════════════════════════════════════════════════════
+# MikroTik Monitor — Portainer Stack
+#
+# Перед деплоем:
+# 1. Соберите образ на сервере (см. README) и запомните его имя
+# 2. Заполните все переменные окружения ниже
+# 3. В Portainer: Stacks → Add Stack → вставьте этот файл → Deploy
+# ═══════════════════════════════════════════════════════════════════
+
+services:
+ mikrotik-monitor:
+ # ── Образ ──────────────────────────────────────────────────────
+ # Соберите локально: docker build -t mikrotik-monitor:latest .
+ # Или укажите адрес своего реестра:
+ # registry.example.com/mikrotik-monitor:latest
+ image: mikrotik-monitor:latest
+
+ container_name: mikrotik-monitor
+ restart: unless-stopped
+ hostname: mikrotik-monitor
+
+ # ── Переменные окружения ────────────────────────────────────────
+ environment:
+ # MikroTik
+ - MIKROTIK_HOST=192.168.88.1 # ← IP вашего роутера
+ - MIKROTIK_PORT=8728 # API-порт (8728 plain / 8729 SSL)
+ - MIKROTIK_USER=monitor # ← логин
+ - MIKROTIK_PASSWORD=your_password # ← пароль
+
+ # Telegram
+ - TELEGRAM_BOT_TOKEN=123456:AABBcc # ← токен от @BotFather
+ - TELEGRAM_CHAT_ID=-1001234567890 # ← ID чата/группы
+
+ # Имя address-list из /ip firewall address-list на MikroTik
+ - ADDRESS_LIST_NAME=monitor
+
+ # Параметры мониторинга
+ - CHECK_INTERVAL=60 # интервал между проверками (сек)
+ - PING_COUNT=3 # кол-во пинг-пакетов
+ - PING_TIMEOUT_MS=1000 # таймаут пинга (мс)
+ - FAILURE_THRESHOLD=2 # провалов подряд → уведомление DOWN
+
+ # Временная зона
+ - TZ=Europe/Moscow
+
+ # ── Тома ───────────────────────────────────────────────────────
+ volumes:
+ - /etc/localtime:/etc/localtime:ro
+
+ # ── Логирование ─────────────────────────────────────────────────
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
+ # ── Healthcheck ─────────────────────────────────────────────────
+ # Контейнер помечается healthy, если процесс monitor.py жив
+ healthcheck:
+ test: ["CMD-SHELL", "pgrep -f monitor.py || exit 1"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 10s
+
+ # ── Метки для Portainer ─────────────────────────────────────────
+ labels:
+ - "com.docker.compose.project=mikrotik-monitor"
+ - "description=MikroTik IP monitoring with Telegram alerts"