diff --git a/backend/routers/adv_security_mgr.py b/backend/routers/adv_security_mgr.py new file mode 100644 index 0000000..bd6ee99 --- /dev/null +++ b/backend/routers/adv_security_mgr.py @@ -0,0 +1,123 @@ +"""Manager Gen6 — 고급 보안 관리: ZeroTrust UI·위협헌팅·SOC·취약점 관제""" +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/adv-security", tags=["Advanced Security Manager"]) + +_threats: Dict[str, Dict] = {} +_hunts: Dict[str, Dict] = {} +_soc_incidents: Dict[str, Dict] = {} + +class ThreatHunt(BaseModel): + name: str; hypothesis: str; ioc_list: List[str] = [] + scope: str = "all" # all|network|endpoint|app + created_by: str = "analyst" + +class SOCIncident(BaseModel): + title: str; severity: str = "medium" # low|medium|high|critical + source: str = "SIEM"; affected_assets: List[str] = [] + +class ZTPolicy(BaseModel): + name: str; subject: str; resource: str + conditions: Dict[str, Any] = {}; action: str = "allow" + +# ── ZeroTrust 정책 UI ─────────────────────────────────────────────────────── +_zt_policies: Dict[str, Dict] = {} + +@router.post("/zt-policies") +async def create_zt_policy(policy: ZTPolicy): + pid = f"ZTP-{uuid.uuid4().hex[:8].upper()}" + _zt_policies[pid] = {**policy.model_dump(), "id": pid, "status": "active", + "created_at": datetime.utcnow().isoformat(), "hits": 0} + return _zt_policies[pid] + +@router.get("/zt-policies") +async def list_zt_policies(): + default = [ + {"id": "ZTP-DEFAULT01", "name": "DevOps 접근 정책", "subject": "devops", "resource": "ITSM API", "action": "allow", "status": "active", "hits": 1240}, + {"id": "ZTP-DEFAULT02", "name": "외부망 차단", "subject": "*", "resource": "Internal DB", "action": "deny", "status": "active", "hits": 320}, + ] + custom = list(_zt_policies.values()) + return {"policies": default + custom, "total": len(default) + len(custom)} + +@router.delete("/zt-policies/{pid}") +async def revoke_zt_policy(pid: str): + _zt_policies.pop(pid, None) + return {"revoked": pid} + +@router.get("/zt-score") +async def zt_score_dashboard(): + return {"overall_score": 82.4, "grade": "B+", + "dimensions": {"identity": 88.0, "device": 79.5, "network": 83.1, "workload": 80.2, "data": 85.0}, + "trend": "+3.2% vs last month", "ts": datetime.utcnow().isoformat()} + +# ── 위협 헌팅 ────────────────────────────────────────────────────────────── +@router.post("/threat-hunts") +async def start_threat_hunt(hunt: ThreatHunt): + hid = f"HUNT-{uuid.uuid4().hex[:8].upper()}" + _hunts[hid] = {**hunt.model_dump(), "id": hid, "status": "running", + "findings": 0, "started_at": datetime.utcnow().isoformat()} + # 시뮬레이션: 즉시 완료 + _hunts[hid]["status"] = "completed" + _hunts[hid]["findings"] = len(hunt.ioc_list) // 2 + _hunts[hid]["completed_at"] = datetime.utcnow().isoformat() + return _hunts[hid] + +@router.get("/threat-hunts") +async def list_hunts(): + return {"hunts": list(_hunts.values()), "total": len(_hunts)} + +@router.get("/threat-hunts/{hid}/results") +async def hunt_results(hid: str): + h = _hunts.get(hid) + if not h: raise HTTPException(404) + return {**h, "matched_iocs": h["ioc_list"][:h.get("findings", 0)], + "affected_hosts": ["app-01"] if h.get("findings") else [], + "recommendation": "경보 규칙 업데이트 권장" if h.get("findings") else "이상 없음"} + +# ── SOC 통합 대시보드 ───────────────────────────────────────────────────── +@router.post("/soc/incidents") +async def create_soc_incident(inc: SOCIncident): + iid = f"SOC-{uuid.uuid4().hex[:8].upper()}" + _soc_incidents[iid] = {**inc.model_dump(), "id": iid, "status": "open", + "created_at": datetime.utcnow().isoformat(), "assignee": None} + return _soc_incidents[iid] + +@router.get("/soc/incidents") +async def list_soc_incidents(status: Optional[str] = None, severity: Optional[str] = None): + items = list(_soc_incidents.values()) + if status: items = [i for i in items if i["status"] == status] + if severity: items = [i for i in items if i["severity"] == severity] + return {"incidents": items, "total": len(items)} + +@router.patch("/soc/incidents/{iid}") +async def update_soc_incident(iid: str, status: str = Query(...)): + inc = _soc_incidents.get(iid) + if not inc: raise HTTPException(404) + inc["status"] = status; inc["updated_at"] = datetime.utcnow().isoformat() + return inc + +@router.get("/soc/dashboard") +async def soc_dashboard(): + return {"summary": {"open": 3, "in_progress": 1, "resolved_today": 5}, + "severity_breakdown": {"critical": 0, "high": 1, "medium": 2, "low": 3}, + "mean_time_to_detect_min": 12.4, "mean_time_to_respond_min": 38.7, + "top_sources": ["SIEM", "IDS", "위협인텔"], + "ts": datetime.utcnow().isoformat()} + +# ── 취약점 관제 ──────────────────────────────────────────────────────────── +@router.get("/vulnerabilities") +async def list_vulnerabilities(severity: Optional[str] = None): + vulns = [ + {"cve": "CVE-2023-44487", "severity": "high", "asset": "app-01", "status": "patched"}, + {"cve": "CVE-2024-3094", "severity": "critical", "asset": "db-01", "status": "pending"}, + ] + if severity: vulns = [v for v in vulns if v["severity"] == severity] + return {"vulnerabilities": vulns, "total": len(vulns), "unpatched": sum(1 for v in vulns if v["status"] == "pending")} + +@router.get("/health") +async def health(): + return {"status": "ok", "zt_policies": len(_zt_policies), "hunts": len(_hunts), "soc_incidents": len(_soc_incidents)} diff --git a/backend/routers/ai_analytics2.py b/backend/routers/ai_analytics2.py new file mode 100644 index 0000000..8e7a181 --- /dev/null +++ b/backend/routers/ai_analytics2.py @@ -0,0 +1,92 @@ +"""Manager Gen6 — AI 분석 대시보드 v2: 예측 KPI·이상 패턴·AI 리포트""" +import uuid +from datetime import datetime +from typing import Any, Dict, List, Optional +import httpx +from fastapi import APIRouter, HTTPException, Query +from pydantic import BaseModel + +router = APIRouter(prefix="/api/ai-analytics", tags=["AI Analytics v2"]) +ITSM = "http://localhost:8001" + +class KPICreate(BaseModel): + name: str; metric: str; target: float; unit: str = "%" + alert_threshold: float = 0.8; owner: str = "platform" + +class ReportRequest(BaseModel): + period: str = "weekly" # daily|weekly|monthly + sections: List[str] = ["sr", "deploy", "server", "ai"] + +_kpis: Dict[str, Dict] = {} +_reports: Dict[str, Dict] = {} +_anomaly_history: List[Dict] = [] + +@router.get("/kpis") +async def list_kpis(): + default = [ + {"id": "kpi-001", "name": "SR 평균 처리시간", "metric": "sr_avg_time", "current": 4.2, "target": 5.0, "unit": "h", "status": "good"}, + {"id": "kpi-002", "name": "서비스 가용성", "metric": "availability", "current": 99.95, "target": 99.9, "unit": "%", "status": "good"}, + {"id": "kpi-003", "name": "배포 성공률", "metric": "deploy_success", "current": 97.3, "target": 95.0, "unit": "%", "status": "good"}, + {"id": "kpi-004", "name": "AI 분류 정확도", "metric": "ai_accuracy", "current": 91.2, "target": 90.0, "unit": "%", "status": "good"}, + ] + custom = list(_kpis.values()) + return {"kpis": default + custom, "total": len(default) + len(custom)} + +@router.post("/kpis") +async def create_kpi(kpi: KPICreate): + kid = f"kpi-{uuid.uuid4().hex[:8]}" + _kpis[kid] = {**kpi.model_dump(), "id": kid, "current": 0.0, "status": "unknown", + "created_at": datetime.utcnow().isoformat()} + return _kpis[kid] + +@router.get("/kpis/{kid}/trend") +async def kpi_trend(kid: str, days: int = 30): + import random + return {"kpi_id": kid, "days": days, + "trend": [{"date": f"2026-06-{i:02d}", "value": round(random.uniform(90, 99), 1)} + for i in range(1, min(days + 1, 32))]} + +@router.get("/anomalies") +async def list_anomalies(severity: Optional[str] = None, limit: int = 50): + items = _anomaly_history if not severity else [a for a in _anomaly_history if a.get("severity") == severity] + default = [ + {"id": "ANO-001", "metric": "cpu_usage", "server": "app-01", "severity": "medium", + "detected_at": datetime.utcnow().isoformat(), "value": 87.3, "threshold": 80.0, "status": "active"}, + ] + return {"anomalies": (default + items)[-limit:], "total": len(default) + len(items)} + +@router.post("/anomalies/detect") +async def detect_anomalies(metric: str, window_min: int = 60): + return {"metric": metric, "window_min": window_min, "anomalies_found": 2, + "algorithm": "isolation_forest", "confidence": 0.87, + "detected_at": datetime.utcnow().isoformat()} + +@router.post("/reports/generate") +async def generate_report(req: ReportRequest): + rid = f"RPT-{uuid.uuid4().hex[:8].upper()}" + _reports[rid] = {**req.model_dump(), "id": rid, "status": "generating", + "created_at": datetime.utcnow().isoformat()} + _reports[rid]["status"] = "ready" + _reports[rid]["sections_generated"] = req.sections + return _reports[rid] + +@router.get("/reports") +async def list_reports(): return {"reports": list(_reports.values()), "total": len(_reports)} + +@router.get("/reports/{rid}") +async def get_report(rid: str): + r = _reports.get(rid) + if not r: raise HTTPException(404) + return {**r, "summary": f"AI 자동 생성 {r['period']} 보고서 — 주요 지표 양호", + "charts": ["sr_trend", "server_health", "deploy_timeline"]} + +@router.get("/insights") +async def ai_insights(): + return {"insights": [ + {"type": "prediction", "title": "SR 급증 예측", "detail": "내일 오전 10-11시 SR 40% 급증 예상", "confidence": 0.82}, + {"type": "optimization", "title": "비용 절감 기회", "detail": "app-03 서버 유휴 상태 — 통합 권장", "potential_saving": 120000}, + {"type": "risk", "title": "디스크 용량 경보", "detail": "db-01 30일 내 포화 예상", "severity": "high"}, + ], "generated_at": datetime.utcnow().isoformat()} + +@router.get("/health") +async def health(): return {"status": "ok", "kpis": len(_kpis), "reports": len(_reports)} diff --git a/backend/routers/cross_system.py b/backend/routers/cross_system.py new file mode 100644 index 0000000..babee3a --- /dev/null +++ b/backend/routers/cross_system.py @@ -0,0 +1,119 @@ +"""Manager Gen6 — 크로스 시스템 연동: ITSM EventBus 구독·데이터 동기화·상태 집계""" +import httpx +import uuid +from datetime import datetime +from typing import Any, Dict, List, Optional +from fastapi import APIRouter, HTTPException, Query +from fastapi.responses import StreamingResponse +from pydantic import BaseModel +import asyncio +import json + +router = APIRouter(prefix="/api/cross", tags=["Cross System"]) + +ITSM = "http://localhost:8001" +_subscriptions: Dict[str, Dict] = {} +_sync_state: Dict[str, Any] = {"last_sync": None, "sr_count": 0, "server_count": 0} +_event_buffer: List[Dict] = [] + +class SubscribeRequest(BaseModel): + channels: List[str] # sr|alert|deploy|server|approval|metric|chat|incident|audit|system + subscriber: str; callback_url: str = "" + +# ── ITSM EventBus 구독 ─────────────────────────────────────────────────────── +@router.post("/subscribe") +async def subscribe_channels(req: SubscribeRequest): + sid = f"SUB-{uuid.uuid4().hex[:8].upper()}" + _subscriptions[sid] = {**req.model_dump(), "id": sid, "active": True, + "created_at": datetime.utcnow().isoformat(), "events_received": 0} + return _subscriptions[sid] + +@router.get("/subscriptions") +async def list_subscriptions(): + return {"subscriptions": list(_subscriptions.values()), "total": len(_subscriptions)} + +@router.delete("/subscriptions/{sid}") +async def unsubscribe(sid: str): + _subscriptions.pop(sid, None) + return {"unsubscribed": sid} + +# ── ITSM 데이터 스냅샷 ───────────────────────────────────────────────────── +@router.get("/snapshot/sr") +async def snapshot_sr(): + try: + async with httpx.AsyncClient(timeout=10.0) as c: + r = await c.get(f"{ITSM}/api/tasks", headers={"X-Internal": "manager"}) + if r.status_code == 200: + data = r.json() + _sync_state["sr_count"] = data.get("total", 0) + _sync_state["last_sync"] = datetime.utcnow().isoformat() + return {"source": "itsm", "sr": data, "synced_at": _sync_state["last_sync"]} + except Exception: + pass + return {"source": "itsm", "sr": {"items": [], "total": 0}, "error": "ITSM 연결 불가"} + +@router.get("/snapshot/servers") +async def snapshot_servers(): + try: + async with httpx.AsyncClient(timeout=10.0) as c: + r = await c.get(f"{ITSM}/api/cmdb/servers", headers={"X-Internal": "manager"}) + if r.status_code == 200: + data = r.json() + _sync_state["server_count"] = len(data.get("servers", [])) + return {"source": "itsm", "servers": data, "synced_at": datetime.utcnow().isoformat()} + except Exception: + pass + return {"source": "itsm", "servers": {"servers": [], "total": 0}, "error": "ITSM 연결 불가"} + +@router.get("/snapshot/alerts") +async def snapshot_alerts(): + try: + async with httpx.AsyncClient(timeout=10.0) as c: + r = await c.get(f"{ITSM}/api/alert-rules/active", headers={"X-Internal": "manager"}) + if r.status_code == 200: + return {"source": "itsm", "alerts": r.json(), "synced_at": datetime.utcnow().isoformat()} + except Exception: + pass + return {"source": "itsm", "alerts": [], "error": "ITSM 연결 불가"} + +# ── 통합 현황 집계 ───────────────────────────────────────────────────────── +@router.get("/aggregate") +async def aggregate_status(): + return {"itsm": {"url": ITSM, "sr_count": _sync_state["sr_count"], + "server_count": _sync_state["server_count"], + "last_sync": _sync_state["last_sync"]}, + "manager": {"subscriptions": len(_subscriptions), "events_buffered": len(_event_buffer)}, + "messenger": {"connected": False, "push_enabled": False}, + "ts": datetime.utcnow().isoformat()} + +# ── Manager → ITSM 이벤트 발행 ──────────────────────────────────────────── +@router.post("/publish") +async def publish_to_itsm(channel: str, payload: Dict[str, Any]): + try: + async with httpx.AsyncClient(timeout=10.0) as c: + r = await c.post(f"{ITSM}/api/sync/publish/{channel}", + json=payload, headers={"X-Internal": "manager"}) + if r.status_code == 200: + return {"published": True, "channel": channel, "payload": payload} + except Exception as e: + pass + event = {"channel": channel, "payload": payload, "ts": datetime.utcnow().isoformat(), "buffered": True} + _event_buffer.append(event) + return event + +# ── SSE 이벤트 스트림 (Manager용 ITSM 구독) ───────────────────────────────── +@router.get("/stream") +async def manager_event_stream(channels: str = "sr,alert"): + channel_list = [c.strip() for c in channels.split(",")] + async def generate(): + while True: + for event in _event_buffer[-5:]: + if event.get("channel") in channel_list: + yield f"data: {json.dumps(event)}\n\n" + await asyncio.sleep(2) + return StreamingResponse(generate(), media_type="text/event-stream") + +@router.get("/health") +async def health(): + return {"status": "ok", "subscriptions": len(_subscriptions), "event_buffer": len(_event_buffer), + "sync_state": _sync_state} diff --git a/backend/routers/finops2.py b/backend/routers/finops2.py new file mode 100644 index 0000000..8fb20cb --- /dev/null +++ b/backend/routers/finops2.py @@ -0,0 +1,124 @@ +"""Manager Gen6 — FinOps v2: 비용 최적화·예산 예측·자원 효율화·태깅""" +import uuid +from datetime import datetime, timedelta +from typing import Any, Dict, List, Optional +from fastapi import APIRouter, HTTPException, Query +from pydantic import BaseModel + +router = APIRouter(prefix="/api/finops2", tags=["FinOps v2"]) + +_budgets: Dict[str, Dict] = {} +_cost_policies: Dict[str, Dict] = {} +_allocations: Dict[str, Dict] = {} + +class BudgetCreate(BaseModel): + name: str; amount: float; period: str = "monthly" # monthly|quarterly|annual + category: str = "infra"; owner: str = "" + alert_threshold: float = 0.8 # 80% 도달 시 알림 + +class CostPolicy(BaseModel): + name: str; rule: str; action: str = "alert" # alert|block|scale_down + threshold: float; resource_type: str = "compute" + +# ── 비용 현황 ──────────────────────────────────────────────────────────────── +@router.get("/costs/summary") +async def cost_summary(period: str = "monthly"): + return {"period": period, "total": 3420000, "currency": "KRW", + "breakdown": [ + {"category": "compute", "amount": 1820000, "ratio": 53.2}, + {"category": "storage", "amount": 820000, "ratio": 24.0}, + {"category": "network", "amount": 480000, "ratio": 14.0}, + {"category": "license", "amount": 300000, "ratio": 8.8}, + ], "vs_last_period": -5.2, "ts": datetime.utcnow().isoformat()} + +@router.get("/costs/trend") +async def cost_trend(months: int = 6): + import random + return {"months": months, "trend": [ + {"month": f"2026-{6-i:02d}", "amount": round(random.uniform(3000000, 3800000))} + for i in range(months) + ]} + +@router.get("/costs/by-resource") +async def cost_by_resource(resource_type: str = "compute"): + return {"resource_type": resource_type, "resources": [ + {"id": "app-01", "name": "Application Server 01", "cost": 680000, "usage_pct": 71.2, "efficiency": "good"}, + {"id": "app-02", "name": "Application Server 02", "cost": 520000, "usage_pct": 43.1, "efficiency": "poor"}, + {"id": "db-01", "name": "Database Server 01", "cost": 620000, "usage_pct": 82.4, "efficiency": "good"}, + ]} + +# ── 예산 관리 ──────────────────────────────────────────────────────────────── +@router.post("/budgets") +async def create_budget(budget: BudgetCreate): + bid = f"BUD-{uuid.uuid4().hex[:8].upper()}" + _budgets[bid] = {**budget.model_dump(), "id": bid, "spent": 0.0, + "created_at": datetime.utcnow().isoformat()} + return _budgets[bid] + +@router.get("/budgets") +async def list_budgets(): + default = [ + {"id": "BUD-2026-01", "name": "2026 인프라 예산", "amount": 50000000, "spent": 20520000, + "ratio": 41.0, "period": "annual", "status": "on_track"}, + ] + custom = list(_budgets.values()) + return {"budgets": default + custom, "total": len(default) + len(custom)} + +@router.get("/budgets/{bid}/forecast") +async def budget_forecast(bid: str): + b = _budgets.get(bid) + return {"budget_id": bid, "forecast_eop": 42000000, + "projected_ratio": 84.0, "risk": "low", + "months_remaining": 6, "avg_monthly_spend": 3420000} + +# ── 비용 최적화 권고 ───────────────────────────────────────────────────────── +@router.get("/optimizations") +async def optimization_recommendations(): + return {"recommendations": [ + {"id": "OPT-001", "resource": "app-02", "type": "right_sizing", + "current_spec": "4vCPU/8GB", "recommended": "2vCPU/4GB", + "monthly_saving": 240000, "risk": "low", "confidence": 0.91}, + {"id": "OPT-002", "resource": "app-03", "type": "shutdown_schedule", + "detail": "야간·주말 비사용 — 스케줄 종료 권장", + "monthly_saving": 180000, "risk": "low", "confidence": 0.95}, + {"id": "OPT-003", "resource": "storage-old", "type": "archive", + "detail": "180일 이상 미접근 데이터 아카이브 권장", + "monthly_saving": 60000, "risk": "low", "confidence": 0.88}, + ], "total_potential_saving": 480000, "ts": datetime.utcnow().isoformat()} + +@router.post("/optimizations/{oid}/apply") +async def apply_optimization(oid: str, auto: bool = False): + return {"optimization_id": oid, "applied": True, "auto": auto, + "ts": datetime.utcnow().isoformat(), "estimated_saving": 240000} + +# ── 비용 정책 ──────────────────────────────────────────────────────────────── +@router.post("/cost-policies") +async def create_cost_policy(policy: CostPolicy): + pid = f"CPOL-{uuid.uuid4().hex[:8].upper()}" + _cost_policies[pid] = {**policy.model_dump(), "id": pid, "active": True, + "triggered_count": 0, "created_at": datetime.utcnow().isoformat()} + return _cost_policies[pid] + +@router.get("/cost-policies") +async def list_cost_policies(): + return {"policies": list(_cost_policies.values()), "total": len(_cost_policies)} + +# ── 태깅 거버넌스 ──────────────────────────────────────────────────────────── +@router.get("/tags/compliance") +async def tag_compliance(): + return {"total_resources": 48, "tagged": 41, "untagged": 7, + "compliance_rate": 85.4, + "required_tags": ["environment", "owner", "cost_center", "project"], + "untagged_resources": ["network-fw-01", "backup-storage-02"]} + +@router.get("/tags/allocation") +async def cost_allocation_by_tag(tag_key: str = "project"): + return {"tag_key": tag_key, "allocations": [ + {"tag_value": "guardia", "cost": 2100000, "ratio": 61.4}, + {"tag_value": "zioinfo-web", "cost": 820000, "ratio": 24.0}, + {"tag_value": "shared", "cost": 500000, "ratio": 14.6}, + ]} + +@router.get("/health") +async def health(): + return {"status": "ok", "budgets": len(_budgets), "policies": len(_cost_policies)} diff --git a/backend/routers/ops_automation.py b/backend/routers/ops_automation.py new file mode 100644 index 0000000..8c7f74d --- /dev/null +++ b/backend/routers/ops_automation.py @@ -0,0 +1,134 @@ +"""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)} diff --git a/backend/routers/platform_mgmt.py b/backend/routers/platform_mgmt.py new file mode 100644 index 0000000..7797fe0 --- /dev/null +++ b/backend/routers/platform_mgmt.py @@ -0,0 +1,119 @@ +"""Manager Gen6 — 플랫폼 관리: 멀티클러스터·GitOps·배포맵·서비스카탈로그""" +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/platform-mgmt", tags=["Platform Management"]) + +_clusters: Dict[str, Dict] = {} +_gitops: Dict[str, Dict] = {} +_service_map: Dict[str, Dict] = {} + +class ClusterCreate(BaseModel): + name: str; type: str = "kubernetes" # kubernetes|docker-swarm|bare-metal + endpoint: str; region: str = "kr-1"; tags: List[str] = [] + +class GitOpsCreate(BaseModel): + name: str; repo_url: str; branch: str = "main" + target_cluster: str; auto_sync: bool = True + +class ServiceMapEntry(BaseModel): + service: str; version: str; cluster: str + replicas: int = 1; expose: bool = True + +# ── 멀티 클러스터 ───────────────────────────────────────────────────────── +@router.post("/clusters") +async def register_cluster(cluster: ClusterCreate): + cid = f"CLU-{uuid.uuid4().hex[:8].upper()}" + _clusters[cid] = {**cluster.model_dump(), "id": cid, "status": "connected", + "node_count": 3, "registered_at": datetime.utcnow().isoformat()} + return _clusters[cid] + +@router.get("/clusters") +async def list_clusters(): + clusters = list(_clusters.values()) or [ + {"id": "CLU-PROD01", "name": "production", "type": "kubernetes", "node_count": 5, "status": "healthy"}, + {"id": "CLU-DEV01", "name": "development", "type": "kubernetes", "node_count": 2, "status": "healthy"}, + ] + return {"clusters": clusters, "total": len(clusters)} + +@router.get("/clusters/{cid}/nodes") +async def cluster_nodes(cid: str): + return {"cluster_id": cid, "nodes": [ + {"name": "node-01", "status": "ready", "cpu_usage": 42.1, "mem_usage": 67.8, "pods": 12}, + {"name": "node-02", "status": "ready", "cpu_usage": 38.4, "mem_usage": 55.2, "pods": 9}, + {"name": "node-03", "status": "ready", "cpu_usage": 51.0, "mem_usage": 71.3, "pods": 14}, + ]} + +@router.get("/clusters/{cid}/workloads") +async def cluster_workloads(cid: str): + return {"cluster_id": cid, "workloads": [ + {"name": "guardia-itsm", "namespace": "default", "replicas": "2/2", "status": "running"}, + {"name": "guardia-manager", "namespace": "default", "replicas": "1/1", "status": "running"}, + {"name": "postgres", "namespace": "data", "replicas": "1/1", "status": "running"}, + ]} + +# ── GitOps ──────────────────────────────────────────────────────────────── +@router.post("/gitops") +async def create_gitops(gitops: GitOpsCreate): + gid = f"GIT-{uuid.uuid4().hex[:8].upper()}" + _gitops[gid] = {**gitops.model_dump(), "id": gid, "status": "synced", + "last_sync": datetime.utcnow().isoformat(), "drift": False} + return _gitops[gid] + +@router.get("/gitops") +async def list_gitops(): + items = list(_gitops.values()) or [ + {"id": "GIT-001", "name": "guardia-itsm-deploy", "branch": "main", "status": "synced", "drift": False}, + ] + return {"gitops": items, "total": len(items)} + +@router.post("/gitops/{gid}/sync") +async def sync_gitops(gid: str): + g = _gitops.get(gid) + if not g: raise HTTPException(404) + g["last_sync"] = datetime.utcnow().isoformat(); g["drift"] = False + return {"synced": True, **g} + +@router.get("/gitops/{gid}/diff") +async def gitops_diff(gid: str): + return {"gitops_id": gid, "diff": [ + {"resource": "Deployment/guardia-itsm", "type": "modified", "fields": ["image.tag"]}, + ], "drift_detected": False} + +# ── 서비스 맵 (배포 위상도) ──────────────────────────────────────────────── +@router.post("/service-map") +async def register_service(entry: ServiceMapEntry): + eid = f"SVC-{uuid.uuid4().hex[:8].upper()}" + _service_map[eid] = {**entry.model_dump(), "id": eid, "health": "healthy", + "registered_at": datetime.utcnow().isoformat()} + return _service_map[eid] + +@router.get("/service-map") +async def get_service_map(): + svcs = list(_service_map.values()) or [ + {"service": "guardia-itsm", "cluster": "production", "version": "2.1.0", "health": "healthy"}, + {"service": "guardia-manager", "cluster": "production", "version": "1.5.0", "health": "healthy"}, + {"service": "guardia-messenger", "cluster": "N/A", "version": "1.0.0", "health": "healthy"}, + ] + return {"services": svcs, "edges": [ + {"from": "guardia-manager", "to": "guardia-itsm", "type": "api"}, + ]} + +# ── 배포 파이프라인 관제 ──────────────────────────────────────────────────── +@router.get("/pipelines") +async def list_pipelines(): + return {"pipelines": [ + {"id": "PIPE-001", "name": "guardia-itsm-ci", "status": "success", "last_run": datetime.utcnow().isoformat(), "duration_sec": 124}, + {"id": "PIPE-002", "name": "guardia-manager-ci", "status": "success", "last_run": datetime.utcnow().isoformat(), "duration_sec": 87}, + ]} + +@router.post("/pipelines/{pid}/trigger") +async def trigger_pipeline(pid: str, branch: str = "main"): + return {"pipeline_id": pid, "branch": branch, "triggered": True, "run_id": str(uuid.uuid4()), + "ts": datetime.utcnow().isoformat()} + +@router.get("/health") +async def health(): return {"status": "ok", "clusters": len(_clusters), "gitops": len(_gitops)}