232 lines
9.9 KiB
Python
Executable File
232 lines
9.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""작은도시 인프라 현황 — PC / NAS / 공유기 / 에이전트 / 도메인 5탭"""
|
|
import json, subprocess, re
|
|
from pathlib import Path
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
KST = timezone(timedelta(hours=9))
|
|
STATUS_FILE = Path(__file__).parent / "health.json"
|
|
|
|
# ===== 기본 체크 함수 =====
|
|
def http_check(url, timeout=5):
|
|
try:
|
|
r = subprocess.run(
|
|
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}:%{time_total}",
|
|
url, "--max-time", str(timeout)],
|
|
capture_output=True, text=True, timeout=timeout+2)
|
|
parts = r.stdout.strip().split(":")
|
|
code = parts[0] if parts else "000"
|
|
return {"status": "online" if code.startswith(("2","3")) else "error",
|
|
"detail": f"HTTP {code}",
|
|
"latency_ms": round(float(parts[1])*1000,0) if len(parts)>1 and parts[1] else None}
|
|
except: return {"status": "offline", "detail": "timeout", "latency_ms": None}
|
|
|
|
def ssh_check(host, timeout=8):
|
|
try:
|
|
r = subprocess.run(
|
|
["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no",
|
|
host, "echo alive"],
|
|
capture_output=True, text=True, timeout=timeout)
|
|
alive = r.returncode == 0 and "alive" in r.stdout
|
|
return {"status": "online" if alive else "offline",
|
|
"detail": "alive" if alive else r.stderr.strip()[:80]}
|
|
except: return {"status": "offline", "detail": "timeout"}
|
|
|
|
def tailscale_ping(host, timeout=8):
|
|
try:
|
|
r = subprocess.run(
|
|
["tailscale", "ping", "--c", "3", host],
|
|
capture_output=True, text=True, timeout=timeout)
|
|
out = r.stdout + r.stderr
|
|
alive = "pong" in out or "is local" in out
|
|
return {"status": "online" if alive else "offline",
|
|
"detail": "" if alive else "no pong"}
|
|
except: return {"status": "offline", "detail": "timeout"}
|
|
|
|
def proc_check(name_pattern, engine_name="?"):
|
|
result = {"status": "offline", "version": "?", "uptime": "?", "proc_count": 0,
|
|
"engine": engine_name, "detail": "not running", "mem": None}
|
|
try:
|
|
r = subprocess.run(["pgrep", "-f", name_pattern], capture_output=True, text=True, timeout=5)
|
|
pids = [p.strip() for p in r.stdout.strip().split("\n") if p.strip()]
|
|
if pids:
|
|
result["status"] = "online"
|
|
result["proc_count"] = len(pids)
|
|
try:
|
|
u = subprocess.run(["ps", "-p", pids[0], "-o", "etime="], capture_output=True, text=True, timeout=5)
|
|
result["uptime"] = u.stdout.strip()
|
|
except: pass
|
|
result["detail"] = f"{result['uptime']} · {result['proc_count']}프로세스"
|
|
except: pass
|
|
return result
|
|
|
|
def remote_proc(via, name_pattern, engine_name="?"):
|
|
result = {"status": "offline", "version": "?", "uptime": "?", "proc_count": 0,
|
|
"engine": engine_name, "detail": "not running", "mem": None}
|
|
try:
|
|
r = subprocess.run(
|
|
["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no", via,
|
|
f"ps aux | grep '{name_pattern}' | grep -v grep || true"],
|
|
capture_output=True, text=True, timeout=8)
|
|
lines = [l for l in r.stdout.strip().split("\n") if l.strip()]
|
|
if lines:
|
|
result["status"] = "online"
|
|
result["proc_count"] = len(lines)
|
|
result["detail"] = f"{result['proc_count']}프로세스"
|
|
except: pass
|
|
return result
|
|
|
|
# ===== 버전/TS IP =====
|
|
def get_local_ts_ip():
|
|
try:
|
|
r = subprocess.run(["tailscale", "ip", "-4"], capture_output=True, text=True, timeout=5)
|
|
return r.stdout.strip()
|
|
except: return "?"
|
|
|
|
def get_remote_ts(via):
|
|
try:
|
|
r = subprocess.run(
|
|
["ssh", "-o", "ConnectTimeout=5", via, "tailscale ip -4"],
|
|
capture_output=True, text=True, timeout=8)
|
|
return r.stdout.strip() if r.returncode == 0 else "?"
|
|
except: return "?"
|
|
|
|
def get_local_version(label):
|
|
try:
|
|
if label == "openclaw":
|
|
r = subprocess.run(["openclaw", "--version"], capture_output=True, text=True, timeout=5)
|
|
v = r.stdout.strip().replace("OpenClaw ", "")
|
|
v = re.sub(r'\s+\([^)]+\)', '', v).strip()
|
|
return v if v else "?"
|
|
elif label == "hermes":
|
|
r = subprocess.run(["/Users/pinksky/.hermes/hermes-agent/venv/bin/python","-m","hermes_cli.main","--version"],
|
|
capture_output=True, text=True, timeout=5)
|
|
m = re.search(r'v[\d.]+', r.stdout.strip())
|
|
return m.group(0) if m else "?"
|
|
except: return "?"
|
|
|
|
def get_remote_hermes_version(via):
|
|
try:
|
|
r = subprocess.run(
|
|
["ssh", "-o", "ConnectTimeout=5", via,
|
|
"export PATH=\"$HOME/.local/bin:$PATH\"; hermes --version"],
|
|
capture_output=True, text=True, timeout=8)
|
|
m = re.search(r'v[\d.]+', r.stdout.strip())
|
|
return m.group(0) if m else "?"
|
|
except: return "?"
|
|
|
|
# ===== 메인 =====
|
|
def run_checks():
|
|
ts = datetime.now(KST)
|
|
ts_ip = get_local_ts_ip()
|
|
|
|
# --- PC ---
|
|
mainpc_ssh = ssh_check("mainpc-win", 6)
|
|
mainpc_ts = get_remote_ts("mainpc-wsl")
|
|
subpc_ssh = ssh_check("subpc-wsl", 6)
|
|
subpc_ts = get_remote_ts("subpc-wsl")
|
|
office_pc = tailscale_ping("office-pc", 6)
|
|
|
|
pcs = [
|
|
{"name": "맥미니 M4 Pro", "emoji": "🖥️", "spec": "Apple M4 Pro · 64GB · 통합 GPU",
|
|
"net": f"{ts_ip} · pinksky.kr", "status": "online", "detail": ""},
|
|
{"name": "메인컴 (PS-i14700K)", "emoji": "🖥️", "spec": "Intel Core i7-14700K · 96GB · RTX 3090 24GB",
|
|
"net": f"{mainpc_ts} · mainpc-wsl.pinksky.kr", **mainpc_ssh},
|
|
{"name": "보조컴 (MH-3900x)", "emoji": "🖥️", "spec": "AMD Ryzen 9 3900X · 32GB · RTX 3080 12GB",
|
|
"net": f"{subpc_ts} · subpc-wsl.pinksky.kr", **subpc_ssh},
|
|
{"name": "회사 메인PC (PS-i14700K)", "emoji": "🖥️", "spec": "Intel Core i7-14700K · 96GB · RTX 3090",
|
|
"net": "", **office_pc},
|
|
]
|
|
|
|
# --- NAS ---
|
|
nas_mh = ssh_check("mh-nas", 6)
|
|
nas_gwenc2 = tailscale_ping("gwenc-nas2", 6)
|
|
nas_bk = tailscale_ping("ps-bk-nas", 6)
|
|
|
|
nas = [
|
|
{"name": "집 NAS (mh-nas)", "emoji": "🗄️", "spec": "Synology DS418+",
|
|
"net": "100.69.107.65 · minicity.kr", **nas_mh},
|
|
{"name": "회사 NAS (gwenc-nas2)", "emoji": "🗄️", "spec": "Synology",
|
|
"net": "100.105.95.19", **nas_gwenc2},
|
|
{"name": "백업 NAS (ps-bk-nas)", "emoji": "🗄️", "spec": "14TB HDD",
|
|
"net": "100.83.176.55", **nas_bk},
|
|
]
|
|
|
|
# --- 공유기 ---
|
|
routers = [
|
|
{"name": "집 공유기", "emoji": "📡", "spec": "아이피타임 AX8004BCM",
|
|
"net": "pinksky.iptime.org", "status": "online", "detail": "DDNS · 원격관리 9090"},
|
|
{"name": "아지트 공유기", "emoji": "📡", "spec": "TP-LINK Archer BE550",
|
|
"net": "pinksky.kr", "status": "online", "detail": "헬로비전 · TP-Link 앱"},
|
|
{"name": "회사 공유기", "emoji": "📡", "spec": "TP-LINK AX18000",
|
|
"net": "gwenc.kr", "status": "online", "detail": "회사 자산 · 원격관리 9000/9443"},
|
|
]
|
|
|
|
# --- 에이전트 ---
|
|
mimi = proc_check("openclaw-gateway", "openclaw")
|
|
mimi["name"] = "미미"
|
|
mimi["emoji"] = "🦞"
|
|
mimi["host"] = "맥미니 (openclaw)"
|
|
mimi["version"] = get_local_version("openclaw")
|
|
|
|
ruki = proc_check("hermes", "hermes")
|
|
ruki["name"] = "루키"
|
|
ruki["emoji"] = "🌱"
|
|
ruki["host"] = "맥미니 (hermes)"
|
|
ruki["version"] = get_local_version("hermes")
|
|
|
|
pink = remote_proc("mainpc-wsl", "hermes", "hermes")
|
|
pink["name"] = "분홍"
|
|
pink["emoji"] = "🤖"
|
|
pink["host"] = "메인컴 WSL"
|
|
pink["version"] = get_remote_hermes_version("mainpc-wsl")
|
|
|
|
sky = remote_proc("subpc-wsl", "hermes", "hermes")
|
|
sky["name"] = "하늘"
|
|
sky["emoji"] = "🤖"
|
|
sky["host"] = "보조컴 WSL"
|
|
sky["version"] = get_remote_hermes_version("subpc-wsl")
|
|
|
|
agents = [a for a in [mimi, ruki, pink, sky]]
|
|
|
|
# --- 도메인 ---
|
|
domains = [
|
|
{"name": "wiki.pinksky.kr", "url": "https://wiki.pinksky.kr/", "group": "pinksky.kr", "desc": "MiniCITY 지식정원"},
|
|
{"name": "openclaw.pinksky.kr", "url": "https://openclaw.pinksky.kr/", "group": "pinksky.kr", "desc": "OpenClaw 대시보드"},
|
|
{"name": "agent.pinksky.kr", "url": "https://agent.pinksky.kr/", "group": "pinksky.kr", "desc": "인프라 현황판"},
|
|
{"name": "search.pinksky.kr", "url": "https://search.pinksky.kr/search?q=test", "group": "pinksky.kr", "desc": "SearXNG 메타서치"},
|
|
{"name": "erp.pinksky.kr", "url": "https://erp.pinksky.kr/", "group": "pinksky.kr", "desc": "공사관리 ERP"},
|
|
{"name": "dify.pinksky.kr:8443", "url": "https://dify.pinksky.kr:8443/", "group": "pinksky.kr", "desc": "Dify 워크플로우"},
|
|
{"name": "gwenc.kr", "url": "https://gwenc.kr/", "group": "gwenc.kr", "desc": "회사 메인 사이트"},
|
|
{"name": "minicity.kr", "url": "https://minicity.kr/", "group": "minicity.kr", "desc": "집 NAS 웹 UI"},
|
|
]
|
|
domain_results = []
|
|
for d in domains:
|
|
chk = http_check(d["url"], 4)
|
|
domain_results.append({
|
|
"name": d["name"], "group": d["group"], "desc": d["desc"],
|
|
"status": chk["status"], "detail": chk["detail"],
|
|
"latency_ms": chk.get("latency_ms")
|
|
})
|
|
|
|
# --- 요약 ---
|
|
all_items = pcs + nas + routers + agents + domain_results
|
|
online = sum(1 for x in all_items if x["status"] == "online")
|
|
|
|
data = {
|
|
"pcs": pcs,
|
|
"nas": nas,
|
|
"routers": routers,
|
|
"agents": agents,
|
|
"domains": domain_results,
|
|
"timestamp": ts.isoformat(),
|
|
"timestamp_epoch": int(ts.timestamp()),
|
|
"summary": {"online": online, "total": len(all_items)}
|
|
}
|
|
|
|
STATUS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
print(f"[{ts.strftime('%H:%M:%S')}] 온라인 {online}/{len(all_items)}")
|
|
|
|
if __name__ == "__main__":
|
|
run_checks()
|