from aiogram import F from aiogram.types import Message from app import dp, cfg from auth import is_admin_msg from keyboards import docker_kb, docker_inline_kb from services.docker import container_uptime, docker_cmd from services.incidents import log_incident from state import DOCKER_MAP, LOG_FILTER_PENDING import time import json async def cmd_docker_status(msg: Message): try: if not DOCKER_MAP: await msg.answer( "⚠️ DOCKER_MAP пуст.\n" "Контейнеры не обнаружены.", reply_markup=docker_kb, ) return lines = ["🐳 Docker containers\n"] for alias, real in DOCKER_MAP.items(): rc, raw = await docker_cmd( ["inspect", "-f", "{{.State.Status}}|{{.State.StartedAt}}", real], timeout=10, ) if rc != 0: lines.append(f"🔴 {alias}: inspect error") continue raw = raw.strip() if "|" not in raw: lines.append(f"🟡 {alias}: invalid inspect output") continue status, started = raw.split("|", 1) up = container_uptime(started) icon = "🟢" if status == "running" else "🔴" lines.append(f"{icon} {alias}: {status} ({up})") await msg.answer("\n".join(lines), reply_markup=docker_kb) log_incident(cfg, f"docker_status by {msg.from_user.id}", category="docker") except Exception as e: # ⬅️ КРИТИЧЕСКИ ВАЖНО await msg.answer( "❌ Docker status crashed:\n" f"```{type(e).__name__}: {e}```", reply_markup=docker_kb, parse_mode="Markdown", ) @dp.message(F.text == "🔄 Restart") async def dr(msg: Message): if is_admin_msg(msg): await msg.answer( "🔄 Выберите контейнер для рестарта:", reply_markup=docker_inline_kb("restart") ) @dp.message(F.text == "📜 Logs") async def dl(msg: Message): if is_admin_msg(msg): await msg.answer( "📜 Выберите контейнер для логов:", reply_markup=docker_inline_kb("logs") ) @dp.message(F.text == "🐳 Status") async def ds(msg: Message): if is_admin_msg(msg): await cmd_docker_status(msg) @dp.message(F.text == "/docker_status") async def ds_cmd(msg: Message): if is_admin_msg(msg): await cmd_docker_status(msg) @dp.message(F.text, F.func(lambda m: (m.text or "").split()[0] == "/docker_health")) async def docker_health(msg: Message): if not is_admin_msg(msg): return parts = msg.text.split() if len(parts) < 2: await msg.answer("Usage: /docker_health ") return alias = parts[1] real = DOCKER_MAP.get(alias) if not real: await msg.answer(f"⚠️ Unknown container: {alias}", reply_markup=docker_kb) return rc, out = await docker_cmd(["inspect", "-f", "{{json .State.Health}}", real], timeout=10) if rc != 0 or not out.strip(): await msg.answer(f"⚠️ Failed to get health for {alias}", reply_markup=docker_kb) return try: data = json.loads(out) except json.JSONDecodeError: await msg.answer(f"⚠️ Invalid health JSON for {alias}", reply_markup=docker_kb) return status = data.get("Status", "n/a") fail = data.get("FailingStreak", "n/a") logs = data.get("Log") or [] lines = [f"🐳 {alias} health", f"Status: {status}", f"Failing streak: {fail}"] if logs: lines.append("Recent logs:") for entry in logs[-5:]: if not isinstance(entry, dict): continue ts = entry.get("Start") or entry.get("End") or "" exitc = entry.get("ExitCode", "") out_line = entry.get("Output", "").strip() lines.append(f"- {ts} rc={exitc} {out_line}") await msg.answer("\n".join(lines), reply_markup=docker_kb) log_incident(cfg, f"docker_health alias={alias} by {msg.from_user.id}", category="docker") @dp.message(F.text == "/docker_health_summary") async def docker_health_summary(msg: Message): if not is_admin_msg(msg): return if not DOCKER_MAP: await msg.answer("⚠️ DOCKER_MAP пуст", reply_markup=docker_kb) return problems = [] total = len(DOCKER_MAP) for alias, real in DOCKER_MAP.items(): rc, out = await docker_cmd(["inspect", "-f", "{{json .State}}", real], timeout=10) if rc != 0: problems.append(f"{alias}: inspect error") continue try: state = json.loads(out) except Exception: problems.append(f"{alias}: bad JSON") continue status = state.get("Status", "n/a") health = (state.get("Health") or {}).get("Status", "n/a") if status != "running" or health not in ("healthy", "none"): problems.append(f"{alias}: {status}/{health}") ok = total - len(problems) lines = [f"🐳 Docker health: 🟢 {ok}/{total} healthy, 🔴 {len(problems)} issues"] if problems: lines.append("Problems:") lines.extend([f"- {p}" for p in problems]) await msg.answer("\n".join(lines), reply_markup=docker_kb) @dp.message(F.text == "📈 Stats") async def dstats(msg: Message): if not is_admin_msg(msg): return if not DOCKER_MAP: await msg.answer( "⚠️ DOCKER_MAP пуст.\n" "Контейнеры не обнаружены.", reply_markup=docker_kb, ) return names = list(DOCKER_MAP.values()) fmt = "{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}" rc, out = await docker_cmd(["stats", "--no-stream", "--format", fmt] + names) if rc != 0: await msg.answer(out, reply_markup=docker_kb) return lines = [line.strip() for line in out.splitlines() if line.strip()] if not lines: await msg.answer("📈 Stats\n\n(no data)", reply_markup=docker_kb) return alias_by_name = {v: k for k, v in DOCKER_MAP.items()} rows = [] for line in lines: parts = line.split("|") if len(parts) != 5: continue name, cpu, mem, net, blk = [p.strip() for p in parts] display = alias_by_name.get(name, name) try: cpu_val = float(cpu.strip("%")) except ValueError: cpu_val = 0.0 rows.append((cpu_val, display, cpu, mem, net, blk)) if not rows: await msg.answer("📈 Stats\n\n(no data)", reply_markup=docker_kb) return rows.sort(key=lambda r: r[0], reverse=True) header = f"{'NAME':<18} {'CPU':>6} {'MEM':>18} {'NET':>16} {'IO':>16}" formatted = [header] for _cpu_val, name, cpu, mem, net, blk in rows: formatted.append(f"{name[:18]:<18} {cpu:>6} {mem:>18} {net:>16} {blk:>16}") body = "\n".join(formatted) await msg.answer( f"📈 **Docker stats**\n```\n{body}\n```", reply_markup=docker_kb, parse_mode="Markdown", ) @dp.message(F.text, F.func(lambda msg: msg.from_user and msg.from_user.id in LOG_FILTER_PENDING)) async def log_filter_input(msg: Message): if not is_admin_msg(msg): return pending = LOG_FILTER_PENDING.pop(msg.from_user.id, None) if not pending: return alias = pending["alias"] real = DOCKER_MAP.get(alias) if not real: await msg.answer("⚠️ Container not found", reply_markup=docker_kb) return needle = (msg.text or "").strip() if not needle: await msg.answer("⚠️ Empty filter text", reply_markup=docker_kb) return since_ts = str(int(time.time() - int(pending.get("since_sec", 1800)))) rc, out = await docker_cmd(["logs", "--since", since_ts, "--tail", "400", real]) if rc != 0: await msg.answer(out, reply_markup=docker_kb) return if not out.strip(): await msg.answer("⚠️ Нет логов за выбранный период", reply_markup=docker_kb) return lines = [line for line in out.splitlines() if needle.lower() in line.lower()] filtered = "\n".join(lines) if lines else "(no matches)" await msg.answer( f"📜 **Logs filter: {alias}**\n```{filtered}```", parse_mode="Markdown", reply_markup=docker_kb, )