"""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)}