579 lines
26 KiB
Python
579 lines
26 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
MikroTik IP Monitor — читает адреса из /ip firewall address-list,
|
|
пингует параллельно, шлёт уведомления в Telegram и MAX (VK Teams).
|
|
|
|
Возможности:
|
|
- Параллельный пинг всех хостов
|
|
- Повторные уведомления о даунтайме
|
|
- Время даунтайма при восстановлении
|
|
- Ежедневный отчёт
|
|
- Команды /status, /help через Telegram и MAX
|
|
- Веб-дашборд на порту 8080
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
import logging
|
|
import asyncio
|
|
import threading
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
|
|
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.getenv("TELEGRAM_BOT_TOKEN", "")
|
|
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
|
TELEGRAM_API_URL = os.getenv("TELEGRAM_API_URL", "https://api.telegram.org")
|
|
|
|
MAX_BOT_TOKEN = os.getenv("MAX_BOT_TOKEN", "")
|
|
MAX_CHAT_ID = os.getenv("MAX_CHAT_ID", "")
|
|
MAX_API_URL = os.getenv("MAX_API_URL", "https://platform-api.max.ru")
|
|
|
|
ADDRESS_LIST_NAME = os.environ["ADDRESS_LIST_NAME"]
|
|
OBJECT_NAME = os.getenv("OBJECT_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"))
|
|
REPEAT_NOTIFY_MIN = int(os.getenv("REPEAT_NOTIFY_MIN", "30"))
|
|
DAILY_REPORT_TIME = os.getenv("DAILY_REPORT_TIME", "09:00")
|
|
WEB_PORT = int(os.getenv("WEB_PORT", "8080"))
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Логирование
|
|
# ──────────────────────────────────────────────
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
log = logging.getLogger(__name__)
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Хелпер: префикс объекта для уведомлений
|
|
# ──────────────────────────────────────────────
|
|
def obj_prefix() -> str:
|
|
return f"🏢 <b>{OBJECT_NAME}</b>\n" if OBJECT_NAME else ""
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Глобальное состояние
|
|
# ──────────────────────────────────────────────
|
|
hosts_state: dict[str, dict] = {}
|
|
router_identity: str = MIKROTIK_HOST
|
|
last_check_time: Optional[datetime] = None
|
|
daily_report_sent_date: Optional[str] = None
|
|
_tg_bot_username: str = ""
|
|
tg_last_update_id: int = 0
|
|
max_last_marker: int = 0
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Уведомления
|
|
# ──────────────────────────────────────────────
|
|
async def send_telegram(message: str) -> None:
|
|
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
|
|
return
|
|
url = f"{TELEGRAM_API_URL}/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, data=payload)
|
|
resp.raise_for_status()
|
|
log.info("Telegram: отправлено")
|
|
except httpx.HTTPStatusError as exc:
|
|
log.error("Telegram: %s — %s", exc.response.status_code, exc.response.text)
|
|
except Exception as exc:
|
|
log.error("Telegram: %s", exc)
|
|
|
|
|
|
async def send_max(message: str) -> None:
|
|
if not MAX_BOT_TOKEN or not MAX_CHAT_ID:
|
|
return
|
|
headers = {"Authorization": MAX_BOT_TOKEN, "Content-Type": "application/json"}
|
|
payload = {"text": message, "format": "html"}
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
try:
|
|
resp = await client.post(
|
|
f"{MAX_API_URL}/messages",
|
|
headers=headers, params={"chat_id": MAX_CHAT_ID}, json=payload,
|
|
)
|
|
resp.raise_for_status()
|
|
log.info("MAX: отправлено")
|
|
except Exception as exc:
|
|
log.error("MAX: %s", exc)
|
|
|
|
|
|
def notify(message: str) -> None:
|
|
async def _all():
|
|
await asyncio.gather(send_telegram(message), send_max(message))
|
|
asyncio.run(_all())
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# MikroTik утилиты
|
|
# ──────────────────────────────────────────────
|
|
def decode(val) -> str:
|
|
if isinstance(val, bytes):
|
|
try:
|
|
return val.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
return val.decode("windows-1251", errors="replace")
|
|
return str(val) if val else ""
|
|
|
|
|
|
def fetch_address_list(api) -> list[tuple[str, str]]:
|
|
ip_list = []
|
|
for entry in api.get_binary_resource("/ip/firewall/address-list").get():
|
|
if decode(entry.get("list", b"")) != ADDRESS_LIST_NAME:
|
|
continue
|
|
if decode(entry.get("disabled", b"false")) == "true":
|
|
continue
|
|
address = decode(entry.get("address", b"")).strip()
|
|
if "/" in address:
|
|
address = address.split("/")[0]
|
|
comment = decode(entry.get("comment", b"")).strip()
|
|
if address:
|
|
ip_list.append((address, comment))
|
|
return ip_list
|
|
|
|
|
|
def fetch_identity(api) -> str:
|
|
try:
|
|
result = api.get_binary_resource("/system/identity").get()
|
|
return decode(result[0].get("name", MIKROTIK_HOST.encode())) if result else MIKROTIK_HOST
|
|
except Exception:
|
|
return MIKROTIK_HOST
|
|
|
|
|
|
def parse_rtt(val) -> Optional[float]:
|
|
if isinstance(val, bytes):
|
|
val = val.decode()
|
|
val = val.strip()
|
|
if not val:
|
|
return None
|
|
ms, us = 0.0, 0.0
|
|
if "ms" in val:
|
|
parts = val.split("ms")
|
|
try:
|
|
ms = float(parts[0])
|
|
except ValueError:
|
|
pass
|
|
if len(parts) > 1 and "us" in parts[1]:
|
|
try:
|
|
us = float(parts[1].replace("us", ""))
|
|
except ValueError:
|
|
pass
|
|
elif "us" in val:
|
|
try:
|
|
us = float(val.replace("us", ""))
|
|
except ValueError:
|
|
pass
|
|
total = ms + us / 1000.0
|
|
return total if total > 0 else None
|
|
|
|
|
|
def mikrotik_ping(api, address: str) -> Optional[float]:
|
|
try:
|
|
results = api.get_binary_resource("/").call(
|
|
"ping",
|
|
{"address": address.encode(), "count": str(PING_COUNT).encode(), "interval": b"0.2"},
|
|
)
|
|
rtts = [r for r in (parse_rtt(row.get("avg-rtt", "")) for row in results) if r is not None]
|
|
return rtts[-1] if rtts else None
|
|
except Exception as exc:
|
|
log.warning("Пинг %s: %s", address, exc)
|
|
return None
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Параллельный пинг
|
|
# ──────────────────────────────────────────────
|
|
async def ping_all_async(api, ip_list: list[tuple[str, str]]) -> dict[str, Optional[float]]:
|
|
loop = asyncio.get_event_loop()
|
|
tasks = {ip: loop.run_in_executor(None, mikrotik_ping, api, ip) for ip, _ in ip_list}
|
|
return {ip: await task for ip, task in tasks.items()}
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Форматирование
|
|
# ──────────────────────────────────────────────
|
|
def fmt_duration(td: timedelta) -> str:
|
|
total = int(td.total_seconds())
|
|
if total < 60:
|
|
return f"{total} сек"
|
|
elif total < 3600:
|
|
return f"{total // 60} мин"
|
|
else:
|
|
h, m = divmod(total // 60, 60)
|
|
return f"{h} ч {m} мин" if m else f"{h} ч"
|
|
|
|
|
|
def fmt_label(ip: str) -> str:
|
|
comment = hosts_state.get(ip, {}).get("comment", "")
|
|
return f"{ip} ({comment})" if comment else ip
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Статус-отчёт
|
|
# ──────────────────────────────────────────────
|
|
def build_status_report(title: str) -> str:
|
|
now = datetime.now()
|
|
prefix = f"🏢 <b>{OBJECT_NAME}</b>\n" if OBJECT_NAME else ""
|
|
lines = [f"{prefix}<b>{title}</b>", f"🔧 Роутер: <b>{router_identity}</b>", ""]
|
|
down = [ip for ip, s in hosts_state.items() if s["status"] == "down"]
|
|
up = [ip for ip, s in hosts_state.items() if s["status"] == "up"]
|
|
|
|
if down:
|
|
lines.append(f"🔴 <b>Недоступны ({len(down)}):</b>")
|
|
for ip in down:
|
|
ds = hosts_state[ip].get("down_since")
|
|
dur = fmt_duration(now - ds) if ds else "?"
|
|
lines.append(f" • <code>{fmt_label(ip)}</code> — {dur}")
|
|
lines.append("")
|
|
if up:
|
|
lines.append(f"✅ <b>Доступны ({len(up)}):</b>")
|
|
for ip in up:
|
|
rtt = hosts_state[ip].get("rtt")
|
|
lines.append(f" • <code>{fmt_label(ip)}</code>" + (f" {rtt:.1f} ms" if rtt else ""))
|
|
|
|
lines += ["", f"⏱ {now.strftime('%Y-%m-%d %H:%M:%S')}"]
|
|
return "\n".join(lines)
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Команды ботов
|
|
# ──────────────────────────────────────────────
|
|
async def process_telegram_commands() -> None:
|
|
global tg_last_update_id, _tg_bot_username
|
|
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
|
|
return
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
try:
|
|
resp = await client.get(
|
|
f"{TELEGRAM_API_URL}/bot{TELEGRAM_BOT_TOKEN}/getUpdates",
|
|
params={"timeout": 1, "offset": tg_last_update_id + 1},
|
|
)
|
|
data = resp.json()
|
|
for update in data.get("result", []):
|
|
tg_last_update_id = update["update_id"]
|
|
text = update.get("message", {}).get("text", "").strip().lower().split("@")[0]
|
|
if text == "/status":
|
|
await send_telegram(build_status_report("📊 Текущий статус"))
|
|
elif text == "/help":
|
|
await send_telegram(
|
|
"🤖 <b>Доступные команды:</b>\n"
|
|
"/status — статус всех хостов\n"
|
|
"/help — это сообщение"
|
|
)
|
|
except Exception as exc:
|
|
log.debug("TG polling: %s", exc)
|
|
|
|
|
|
async def process_max_commands() -> None:
|
|
global max_last_marker
|
|
if not MAX_BOT_TOKEN or not MAX_CHAT_ID:
|
|
return
|
|
params = {"access_token": MAX_BOT_TOKEN}
|
|
if max_last_marker:
|
|
params["marker"] = max_last_marker
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
try:
|
|
resp = await client.get(f"{MAX_API_URL}/updates", params=params)
|
|
data = resp.json()
|
|
max_last_marker = data.get("marker", max_last_marker)
|
|
for update in data.get("updates", []):
|
|
if update.get("update_type") != "message_created":
|
|
continue
|
|
text = update.get("message", {}).get("body", {}).get("text", "").strip().lower()
|
|
if text == "/status":
|
|
await send_max(build_status_report("📊 Текущий статус"))
|
|
elif text == "/help":
|
|
await send_max(
|
|
"🤖 <b>Доступные команды:</b>\n"
|
|
"/status — статус всех хостов\n"
|
|
"/help — это сообщение"
|
|
)
|
|
except Exception as exc:
|
|
log.debug("MAX polling: %s", exc)
|
|
|
|
|
|
async def process_bot_commands() -> None:
|
|
await asyncio.gather(process_telegram_commands(), process_max_commands())
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Ежедневный отчёт
|
|
# ──────────────────────────────────────────────
|
|
def check_daily_report() -> None:
|
|
global daily_report_sent_date
|
|
now = datetime.now()
|
|
today = now.strftime("%Y-%m-%d")
|
|
if now.strftime("%H:%M") == DAILY_REPORT_TIME and daily_report_sent_date != today:
|
|
daily_report_sent_date = today
|
|
notify(build_status_report(f"📅 Ежедневный отчёт — {today}"))
|
|
log.info("Ежедневный отчёт отправлен")
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Веб-дашборд
|
|
# ──────────────────────────────────────────────
|
|
class DashboardHandler(BaseHTTPRequestHandler):
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
def do_GET(self):
|
|
if self.path not in ("/", "/status"):
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
return
|
|
|
|
now = datetime.now()
|
|
rows = ""
|
|
for ip, state in hosts_state.items():
|
|
comment = state.get("comment", "")
|
|
status = state.get("status", "unknown")
|
|
rtt = state.get("rtt")
|
|
ds = state.get("down_since")
|
|
ls = state.get("last_seen")
|
|
|
|
if status == "up":
|
|
badge = '<span class="badge up">● UP</span>'
|
|
rtt_cell = f"{rtt:.1f} ms" if rtt else "—"
|
|
dur_cell = "—"
|
|
elif status == "down":
|
|
badge = '<span class="badge down">● DOWN</span>'
|
|
rtt_cell = "—"
|
|
dur_cell = fmt_duration(now - ds) if ds else "—"
|
|
else:
|
|
badge = '<span class="badge unknown">● ?</span>'
|
|
rtt_cell = dur_cell = "—"
|
|
|
|
rows += f"""<tr class="{status}">
|
|
<td><code>{ip}</code></td><td>{comment}</td><td>{badge}</td>
|
|
<td>{rtt_cell}</td><td>{dur_cell}</td>
|
|
<td>{ls.strftime('%H:%M:%S') if ls else '—'}</td></tr>"""
|
|
|
|
total = len(hosts_state)
|
|
up_count = sum(1 for s in hosts_state.values() if s.get("status") == "up")
|
|
down_count= sum(1 for s in hosts_state.values() if s.get("status") == "down")
|
|
last_str = last_check_time.strftime("%Y-%m-%d %H:%M:%S") if last_check_time else "—"
|
|
|
|
html = f"""<!DOCTYPE html><html lang="ru"><head>
|
|
<meta charset="UTF-8"><meta http-equiv="refresh" content="{CHECK_INTERVAL}">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>MikroTik Monitor — {router_identity}</title>
|
|
<style>
|
|
*{{box-sizing:border-box;margin:0;padding:0}}
|
|
body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0f1117;color:#e0e0e0;padding:24px}}
|
|
h1{{font-size:1.4rem;color:#fff;margin-bottom:4px}}
|
|
.sub{{color:#888;font-size:.85rem;margin-bottom:24px}}
|
|
.cards{{display:flex;gap:16px;margin-bottom:24px;flex-wrap:wrap}}
|
|
.card{{background:#1a1d27;border-radius:10px;padding:16px 24px;min-width:130px}}
|
|
.card .num{{font-size:2rem;font-weight:700}}.card .lbl{{font-size:.8rem;color:#888;margin-top:2px}}
|
|
.green{{color:#4caf50}}.red{{color:#f44336}}.gray{{color:#aaa}}
|
|
table{{width:100%;border-collapse:collapse;background:#1a1d27;border-radius:10px;overflow:hidden}}
|
|
th{{background:#22263a;color:#aaa;font-size:.78rem;text-transform:uppercase;letter-spacing:.05em;padding:10px 14px;text-align:left}}
|
|
td{{padding:10px 14px;font-size:.9rem;border-top:1px solid #22263a}}
|
|
tr.down td{{background:rgba(244,67,54,.07)}}
|
|
code{{background:#22263a;padding:2px 6px;border-radius:4px;font-size:.85rem}}
|
|
.badge{{padding:3px 10px;border-radius:20px;font-size:.78rem;font-weight:600}}
|
|
.badge.up{{background:rgba(76,175,80,.15);color:#4caf50}}
|
|
.badge.down{{background:rgba(244,67,54,.15);color:#f44336}}
|
|
.badge.unknown{{background:rgba(158,158,158,.15);color:#9e9e9e}}
|
|
.footer{{margin-top:16px;font-size:.78rem;color:#555}}
|
|
</style></head><body>
|
|
<h1>🔍 MikroTik Monitor</h1>
|
|
<div class="sub">Роутер: <b>{router_identity}</b> ({MIKROTIK_HOST}) |
|
|
List: <b>{ADDRESS_LIST_NAME}</b> | Проверка: {last_str}</div>
|
|
<div class="cards">
|
|
<div class="card"><div class="num gray">{total}</div><div class="lbl">Всего</div></div>
|
|
<div class="card"><div class="num green">{up_count}</div><div class="lbl">Доступны</div></div>
|
|
<div class="card"><div class="num red">{down_count}</div><div class="lbl">Недоступны</div></div>
|
|
</div>
|
|
<table><thead><tr>
|
|
<th>IP</th><th>Имя</th><th>Статус</th><th>RTT</th><th>Даунтайм</th><th>Последний ответ</th>
|
|
</tr></thead><tbody>{rows}</tbody></table>
|
|
<div class="footer">Автообновление каждые {CHECK_INTERVAL} сек</div>
|
|
</body></html>"""
|
|
|
|
body = html.encode("utf-8")
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
|
|
def start_web_server() -> None:
|
|
server = HTTPServer(("0.0.0.0", WEB_PORT), DashboardHandler)
|
|
log.info("Веб-дашборд: http://0.0.0.0:%d", WEB_PORT)
|
|
server.serve_forever()
|
|
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Основной цикл
|
|
# ──────────────────────────────────────────────
|
|
def main() -> None:
|
|
global router_identity, last_check_time
|
|
|
|
log.info("=== MikroTik Monitor запущен ===")
|
|
log.info("Address-list: '%s' Интервал: %d сек", ADDRESS_LIST_NAME, CHECK_INTERVAL)
|
|
|
|
threading.Thread(target=start_web_server, daemon=True).start()
|
|
|
|
startup_notified = False
|
|
mikrotik_error_notified = False # флаг: уже уведомили об ошибке подключения
|
|
|
|
while True:
|
|
pool = None
|
|
try:
|
|
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")
|
|
|
|
# Если до этого была ошибка — уведомить о восстановлении
|
|
if mikrotik_error_notified:
|
|
mikrotik_error_notified = False
|
|
notify(
|
|
f"{obj_prefix()}"
|
|
f"✅ <b>Подключение к MikroTik восстановлено</b>\n"
|
|
f"🔧 Роутер: <b>{router_identity}</b> (<code>{MIKROTIK_HOST}</code>)"
|
|
)
|
|
|
|
ip_list = fetch_address_list(api)
|
|
|
|
if not startup_notified:
|
|
router_identity = fetch_identity(api)
|
|
ip_lines = "\n".join(
|
|
f" • <code>{ip}</code> ({comment})" if comment else f" • <code>{ip}</code>"
|
|
for ip, comment in ip_list
|
|
) or " (список пуст)"
|
|
notify(
|
|
f"{obj_prefix()}"
|
|
f"🟢 <b>MikroTik Monitor запущен</b>\n\n"
|
|
f"🔧 Роутер: <b>{router_identity}</b> (<code>{MIKROTIK_HOST}</code>)\n"
|
|
f"📋 Address-list: <code>{ADDRESS_LIST_NAME}</code>\n\n"
|
|
f"<b>Мониторинг адресов:</b>\n{ip_lines}\n\n"
|
|
f"⏱ Интервал: {CHECK_INTERVAL} сек"
|
|
)
|
|
startup_notified = True
|
|
|
|
if not ip_list:
|
|
log.warning("Address-list '%s' пуст!", ADDRESS_LIST_NAME)
|
|
else:
|
|
log.info("Адресов: %d", len(ip_list))
|
|
|
|
now = datetime.now()
|
|
for ip, comment in ip_list:
|
|
if ip not in hosts_state:
|
|
hosts_state[ip] = {
|
|
"comment": comment, "status": "unknown",
|
|
"rtt": None, "down_since": None,
|
|
"last_seen": None, "last_notify": None,
|
|
}
|
|
else:
|
|
hosts_state[ip]["comment"] = comment
|
|
|
|
# Параллельный пинг
|
|
ping_results = asyncio.run(ping_all_async(api, ip_list))
|
|
last_check_time = datetime.now()
|
|
now = last_check_time
|
|
|
|
for ip, comment in ip_list:
|
|
rtt = ping_results.get(ip)
|
|
state = hosts_state[ip]
|
|
label = fmt_label(ip)
|
|
ts = now.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
if rtt is not None:
|
|
log.info("✅ %-34s %.1f ms", label, rtt)
|
|
state["rtt"] = rtt
|
|
state["last_seen"] = now
|
|
|
|
if state["status"] == "down":
|
|
ds = state.get("down_since")
|
|
dur = fmt_duration(now - ds) if ds else "?"
|
|
state.update({"status": "up", "down_since": None, "last_notify": None})
|
|
notify(
|
|
f"{obj_prefix()}"
|
|
f"✅ <b>Хост восстановился</b>\n"
|
|
f"IP: <code>{ip}</code>" + (f" ({comment})" if comment else "") + "\n"
|
|
f"RTT: {rtt:.1f} ms\n"
|
|
f"Даунтайм: {dur}\n"
|
|
f"Время: {ts}"
|
|
)
|
|
else:
|
|
state["status"] = "up"
|
|
|
|
else:
|
|
log.warning("❌ %-34s нет ответа", label)
|
|
state["rtt"] = None
|
|
|
|
if state["status"] != "down":
|
|
state.update({"status": "down", "down_since": now, "last_notify": now})
|
|
notify(
|
|
f"{obj_prefix()}"
|
|
f"🔴 <b>Хост недоступен!</b>\n"
|
|
f"IP: <code>{ip}</code>" + (f" ({comment})" if comment else "") + "\n"
|
|
f"Список: <code>{ADDRESS_LIST_NAME}</code>\n"
|
|
f"Время: {ts}"
|
|
)
|
|
elif REPEAT_NOTIFY_MIN > 0 and state.get("last_notify"):
|
|
mins = (now - state["last_notify"]).total_seconds() / 60
|
|
if mins >= REPEAT_NOTIFY_MIN:
|
|
ds = state.get("down_since")
|
|
dur = fmt_duration(now - ds) if ds else "?"
|
|
state["last_notify"] = now
|
|
notify(
|
|
f"{obj_prefix()}"
|
|
f"🔴 <b>Хост всё ещё недоступен!</b>\n"
|
|
f"IP: <code>{ip}</code>" + (f" ({comment})" if comment else "") + "\n"
|
|
f"Недоступен уже: {dur}\n"
|
|
f"Время: {ts}"
|
|
)
|
|
|
|
asyncio.run(process_bot_commands())
|
|
check_daily_report()
|
|
|
|
except Exception as exc:
|
|
log.error("Ошибка: %s", exc)
|
|
if not mikrotik_error_notified:
|
|
mikrotik_error_notified = True
|
|
notify(
|
|
f"{obj_prefix()}"
|
|
f"⚠️ <b>Ошибка подключения к MikroTik</b>\n"
|
|
f"🔧 Роутер: <b>{router_identity}</b> (<code>{MIKROTIK_HOST}</code>)\n"
|
|
f"<code>{exc}</code>"
|
|
)
|
|
finally:
|
|
if pool:
|
|
try:
|
|
pool.disconnect()
|
|
except Exception:
|
|
pass
|
|
|
|
log.info("Следующая проверка через %d сек …\n", CHECK_INTERVAL)
|
|
time.sleep(CHECK_INTERVAL)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|