diff --git a/config.example.yaml b/config.example.yaml index 0c7976b..d10b819 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -28,4 +28,6 @@ docker: tg-admin-bot: "tg-admin-bot" # Explicit list used by legacy modules containers: - tg-admin-bot: "tg-admin-bot" + tg-admin-bot: + name: "tg-admin-bot" + url: "http://127.0.0.1:8080/health" diff --git a/handlers/help.py b/handlers/help.py index 272b803..2443b17 100644 --- a/handlers/help.py +++ b/handlers/help.py @@ -17,7 +17,7 @@ async def help_cmd(msg: Message): "🐳 Docker — управление контейнерами\n" "📦 Backup — restic бэкапы\n" "🧉 Artifacts — критичные образы (Clonezilla, NAND)\n" - "⚙️ System — диски, безопасность, reboot\n\n" + "⚙️ System — диски, безопасность, URL, reboot\n\n" "Inline-кнопки используются для выбора контейнеров.", reply_markup=menu_kb, parse_mode="Markdown", diff --git a/handlers/system.py b/handlers/system.py index c62eefe..b15eb21 100644 --- a/handlers/system.py +++ b/handlers/system.py @@ -4,6 +4,9 @@ from app import dp from auth import is_admin_msg from keyboards import system_kb from system_checks import security, disks +from app import cfg +from services.http_checks import get_url_checks, check_url +import asyncio @dp.message(F.text == "💽 Disks") @@ -16,3 +19,34 @@ async def sd(msg: Message): async def sec(msg: Message): if is_admin_msg(msg): await msg.answer(security(), reply_markup=system_kb) + + +@dp.message(F.text == "🌐 URLs") +async def urls(msg: Message): + if not is_admin_msg(msg): + return + + checks = list(get_url_checks(cfg)) + if not checks: + await msg.answer("⚠️ Нет URL для проверки", reply_markup=system_kb) + return + + await msg.answer("⏳ Проверяю URL…", reply_markup=system_kb) + + async def worker(): + tasks = [asyncio.to_thread(check_url, url) for _, url in checks] + results = await asyncio.gather(*tasks) + + lines = ["🌐 URLs\n"] + for (alias, url), (ok, status, ms, err) in zip(checks, results): + if ok: + lines.append(f"🟢 {alias}: {status} ({ms}ms)") + elif status is not None: + lines.append(f"🔴 {alias}: {status} ({ms}ms)") + else: + reason = err or "error" + lines.append(f"🔴 {alias}: {reason} ({ms}ms)") + + await msg.answer("\n".join(lines), reply_markup=system_kb) + + asyncio.create_task(worker()) diff --git a/keyboards.py b/keyboards.py index fa65393..c3c9132 100644 --- a/keyboards.py +++ b/keyboards.py @@ -49,6 +49,7 @@ artifacts_kb = ReplyKeyboardMarkup( system_kb = ReplyKeyboardMarkup( keyboard=[ [KeyboardButton(text="💽 Disks"), KeyboardButton(text="🔐 Security")], + [KeyboardButton(text="🌐 URLs")], [KeyboardButton(text="🔄 Reboot")], [KeyboardButton(text="⬅️ Назад")], ], diff --git a/services/http_checks.py b/services/http_checks.py new file mode 100644 index 0000000..59972b8 --- /dev/null +++ b/services/http_checks.py @@ -0,0 +1,38 @@ +import time +from typing import Iterable, Tuple +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + + +def get_url_checks(cfg) -> Iterable[Tuple[str, str]]: + docker_cfg = cfg.get("docker", {}) + containers = docker_cfg.get("containers", {}) + for alias, value in containers.items(): + if isinstance(value, str): + continue + if isinstance(value, dict): + url = value.get("url") + if url: + yield alias, url + + +def check_url(url: str, timeout: int = 5) -> Tuple[bool, int | None, int | None, str | None]: + start = time.time() + req = Request(url, headers={"User-Agent": "tg-admin-bot"}) + try: + with urlopen(req, timeout=timeout) as resp: + status = int(resp.status) + except HTTPError as e: + status = int(e.code) + elapsed_ms = int((time.time() - start) * 1000) + return False, status, elapsed_ms, None + except URLError as e: + elapsed_ms = int((time.time() - start) * 1000) + return False, None, elapsed_ms, str(e.reason) + except Exception as e: + elapsed_ms = int((time.time() - start) * 1000) + return False, None, elapsed_ms, str(e) + + elapsed_ms = int((time.time() - start) * 1000) + ok = status < 400 + return ok, status, elapsed_ms, None