manual-deploy 2026-06-07 08:13

This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-06-07 08:13:48 +09:00
parent 49ea17589e
commit ee8564972b
6 changed files with 711 additions and 0 deletions

View File

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

View File

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

View File

@ -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}

124
backend/routers/finops2.py Normal file
View File

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

View File

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

View File

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