166 lines
5.5 KiB
Python
166 lines
5.5 KiB
Python
import asyncio
|
|
from datetime import datetime, timezone
|
|
from typing import Dict
|
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
|
from services.runner import run_cmd
|
|
|
|
|
|
def container_uptime(started_at: str) -> str:
|
|
"""
|
|
started_at: 2026-02-06T21:14:33.123456789Z
|
|
"""
|
|
try:
|
|
start = datetime.fromisoformat(
|
|
started_at.replace("Z", "+00:00")
|
|
).astimezone(timezone.utc)
|
|
delta = datetime.now(timezone.utc) - start
|
|
days = delta.days
|
|
hours = delta.seconds // 3600
|
|
minutes = (delta.seconds % 3600) // 60
|
|
|
|
if days > 0:
|
|
return f"{days}d {hours}h"
|
|
if hours > 0:
|
|
return f"{hours}h {minutes}m"
|
|
return f"{minutes}m"
|
|
except Exception:
|
|
return "unknown"
|
|
|
|
|
|
async def build_docker_map(cfg) -> Dict[str, str]:
|
|
docker_cfg = cfg.get("docker", {})
|
|
|
|
result: Dict[str, str] = {}
|
|
|
|
# 1. autodiscovery
|
|
if docker_cfg.get("autodiscovery"):
|
|
rc, raw = await docker_cmd(["ps", "--format", "{{.Names}}"], timeout=20)
|
|
if rc == 0:
|
|
names = raw.splitlines()
|
|
patterns = docker_cfg.get("match", [])
|
|
for name in names:
|
|
if any(p in name for p in patterns):
|
|
result[name] = name
|
|
|
|
# 2. aliases override
|
|
aliases = docker_cfg.get("aliases", {})
|
|
for alias, real in aliases.items():
|
|
result[alias] = real
|
|
|
|
return result
|
|
|
|
|
|
async def discover_containers(cfg) -> Dict[str, str]:
|
|
"""
|
|
returns: alias -> real container name
|
|
"""
|
|
docker_cfg = cfg.get("docker", {})
|
|
result: Dict[str, str] = {}
|
|
|
|
# --- autodiscovery ---
|
|
if docker_cfg.get("autodiscovery"):
|
|
rc, raw = await docker_cmd(["ps", "--format", "{{.Names}}"], timeout=20)
|
|
|
|
if rc == 0:
|
|
found = raw.splitlines()
|
|
|
|
label = docker_cfg.get("label")
|
|
patterns = docker_cfg.get("match", [])
|
|
|
|
for name in found:
|
|
# label-based discovery
|
|
if label:
|
|
key, val = label.split("=", 1)
|
|
rc2, lbl = await docker_cmd(
|
|
["inspect", "-f", f"{{{{ index .Config.Labels \"{key}\" }}}}", name],
|
|
timeout=10
|
|
)
|
|
if rc2 == 0 and lbl.strip() == val:
|
|
result[name] = name
|
|
continue
|
|
|
|
# name-pattern discovery
|
|
if any(p in name for p in patterns):
|
|
result[name] = name
|
|
|
|
# --- manual aliases ALWAYS override ---
|
|
aliases = docker_cfg.get("aliases", {})
|
|
for alias, real in aliases.items():
|
|
result[alias] = real
|
|
|
|
return result
|
|
|
|
|
|
async def docker_cmd(args: list[str], timeout: int = 20):
|
|
rc, out = await run_cmd(["docker"] + args, timeout=timeout)
|
|
if rc == 0:
|
|
return rc, out
|
|
return await run_cmd(["sudo", "docker"] + args, timeout=timeout)
|
|
|
|
|
|
async def docker_watchdog(container_map, notify, bot, chat_id):
|
|
last = {}
|
|
while True:
|
|
if not last:
|
|
for alias, real in container_map.items():
|
|
rc, raw = await docker_cmd(
|
|
["inspect", "-f", "{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}n/a{{end}}", real],
|
|
timeout=10
|
|
)
|
|
if rc != 0:
|
|
last[alias] = ("error", "n/a")
|
|
continue
|
|
parts = raw.strip().split("|", 1)
|
|
status = parts[0] if parts else "unknown"
|
|
health = parts[1] if len(parts) > 1 else "n/a"
|
|
last[alias] = (status, health)
|
|
await asyncio.sleep(120)
|
|
continue
|
|
for alias, real in container_map.items():
|
|
rc, raw = await docker_cmd(
|
|
["inspect", "-f", "{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}n/a{{end}}", real],
|
|
timeout=10
|
|
)
|
|
if rc != 0:
|
|
status, health = "error", "n/a"
|
|
else:
|
|
parts = raw.strip().split("|", 1)
|
|
status = parts[0] if parts else "unknown"
|
|
health = parts[1] if len(parts) > 1 else "n/a"
|
|
|
|
if last.get(alias) != (status, health):
|
|
if status != "running":
|
|
kb = InlineKeyboardMarkup(
|
|
inline_keyboard=[[
|
|
InlineKeyboardButton(
|
|
text="🔄 Restart",
|
|
callback_data=f"wdrestart:{alias}"
|
|
)
|
|
]]
|
|
)
|
|
await bot.send_message(
|
|
chat_id,
|
|
f"🐳 {alias}: {status}",
|
|
reply_markup=kb,
|
|
)
|
|
elif health not in ("healthy", "n/a"):
|
|
await notify(
|
|
bot,
|
|
chat_id,
|
|
f"⚠️ {alias} health: {health}",
|
|
level="warn",
|
|
key=f"docker_health:{alias}",
|
|
category="docker",
|
|
)
|
|
else:
|
|
await notify(
|
|
bot,
|
|
chat_id,
|
|
f"🐳 {alias}: {status}",
|
|
level="info",
|
|
key=f"docker_status:{alias}:{status}",
|
|
category="docker",
|
|
)
|
|
last[alias] = (status, health)
|
|
await asyncio.sleep(120)
|