guardia-manager/backend/routers/ops_automation.py
2026-06-07 08:13:48 +09:00

135 lines
6.1 KiB
Python

"""Manager Gen6 — 운영 자동화: 노코드 자동화·정책UI·런북 관리·작업 스케줄러"""
import uuid
from datetime import datetime
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
router = APIRouter(prefix="/api/ops-auto", tags=["Operations Automation"])
_automations: Dict[str, Dict] = {}
_runbooks: Dict[str, Dict] = {}
_schedules: Dict[str, Dict] = {}
_executions: List[Dict] = []
class AutomationCreate(BaseModel):
name: str; trigger: str # schedule|event|threshold|manual
conditions: List[Dict[str, Any]] = []
actions: List[Dict[str, Any]] = []
enabled: bool = True
class RunbookCreate(BaseModel):
name: str; description: str = ""; category: str = "incident"
steps: List[Dict[str, Any]] = []; owner: str = ""
class ScheduleCreate(BaseModel):
name: str; cron: str; automation_id: str; timezone: str = "Asia/Seoul"
# ── 노코드 자동화 빌더 ───────────────────────────────────────────────────────
@router.post("/automations")
async def create_automation(auto: AutomationCreate):
aid = f"AUTO-{uuid.uuid4().hex[:8].upper()}"
_automations[aid] = {**auto.model_dump(), "id": aid,
"run_count": 0, "last_run": None,
"created_at": datetime.utcnow().isoformat()}
return _automations[aid]
@router.get("/automations")
async def list_automations(enabled: Optional[bool] = None):
autos = list(_automations.values())
if enabled is not None: autos = [a for a in autos if a["enabled"] == enabled]
default = [
{"id": "AUTO-SYS01", "name": "디스크 임계 → 알림", "trigger": "threshold", "enabled": True, "run_count": 42},
{"id": "AUTO-SYS02", "name": "야간 백업 실행", "trigger": "schedule", "enabled": True, "run_count": 186},
]
return {"automations": default + autos, "total": len(default) + len(autos)}
@router.get("/automations/{aid}")
async def get_automation(aid: str):
a = _automations.get(aid)
if not a: raise HTTPException(404)
return a
@router.patch("/automations/{aid}/toggle")
async def toggle_automation(aid: str):
a = _automations.get(aid)
if not a: raise HTTPException(404)
a["enabled"] = not a["enabled"]
return {"id": aid, "enabled": a["enabled"]}
@router.post("/automations/{aid}/run")
async def run_automation(aid: str, params: Dict[str, Any] = {}):
a = _automations.get(aid)
if not a: raise HTTPException(404)
exec_id = str(uuid.uuid4())
result = {"exec_id": exec_id, "automation_id": aid, "params": params,
"status": "completed", "duration_ms": 234,
"ts": datetime.utcnow().isoformat()}
_executions.append(result)
a["run_count"] = a.get("run_count", 0) + 1
a["last_run"] = datetime.utcnow().isoformat()
return result
@router.get("/automations/executions/history")
async def execution_history(limit: int = 50):
return {"executions": _executions[-limit:], "total": len(_executions)}
# ── 런북 관리 ────────────────────────────────────────────────────────────────
@router.post("/runbooks")
async def create_runbook(rb: RunbookCreate):
rid = f"RB-{uuid.uuid4().hex[:8].upper()}"
_runbooks[rid] = {**rb.model_dump(), "id": rid, "version": 1,
"created_at": datetime.utcnow().isoformat()}
return _runbooks[rid]
@router.get("/runbooks")
async def list_runbooks(category: Optional[str] = None):
rbs = list(_runbooks.values())
if category: rbs = [r for r in rbs if r["category"] == category]
default = [
{"id": "RB-001", "name": "서버 재시작 런북", "category": "maintenance", "version": 3, "steps": 5},
{"id": "RB-002", "name": "장애 대응 런북", "category": "incident", "version": 5, "steps": 8},
{"id": "RB-003", "name": "배포 롤백 런북", "category": "deploy", "version": 2, "steps": 6},
]
if category: default = [r for r in default if r["category"] == category]
return {"runbooks": default + rbs, "total": len(default) + len(rbs)}
@router.post("/runbooks/{rid}/execute")
async def execute_runbook(rid: str, target: str = "", params: Dict[str, Any] = {}):
rb = _runbooks.get(rid)
exec_id = str(uuid.uuid4())
return {"exec_id": exec_id, "runbook_id": rid, "target": target, "params": params,
"status": "completed", "steps_executed": 5,
"ts": datetime.utcnow().isoformat()}
# ── 작업 스케줄러 ────────────────────────────────────────────────────────────
@router.post("/schedules")
async def create_schedule(sch: ScheduleCreate):
sid = f"SCH-{uuid.uuid4().hex[:8].upper()}"
_schedules[sid] = {**sch.model_dump(), "id": sid, "status": "active",
"next_run": datetime.utcnow().isoformat(),
"created_at": datetime.utcnow().isoformat()}
return _schedules[sid]
@router.get("/schedules")
async def list_schedules():
return {"schedules": list(_schedules.values()), "total": len(_schedules)}
@router.delete("/schedules/{sid}")
async def delete_schedule(sid: str):
_schedules.pop(sid, None)
return {"deleted": sid}
# ── 정책 UI ────────────────────────────────────────────────────────────────
@router.get("/policies")
async def list_policies():
return {"policies": [
{"id": "POL-001", "name": "디스크 80% 경보", "type": "threshold", "active": True},
{"id": "POL-002", "name": "SLA 위반 에스컬레이션", "type": "sla", "active": True},
{"id": "POL-003", "name": "야간 배포 차단", "type": "access", "active": True},
]}
@router.get("/health")
async def health():
return {"status": "ok", "automations": len(_automations), "runbooks": len(_runbooks), "schedules": len(_schedules)}