manual-deploy 2026-06-07 08:13
This commit is contained in:
parent
49ea17589e
commit
ee8564972b
123
backend/routers/adv_security_mgr.py
Normal file
123
backend/routers/adv_security_mgr.py
Normal 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)}
|
||||
92
backend/routers/ai_analytics2.py
Normal file
92
backend/routers/ai_analytics2.py
Normal 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)}
|
||||
119
backend/routers/cross_system.py
Normal file
119
backend/routers/cross_system.py
Normal 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
124
backend/routers/finops2.py
Normal 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)}
|
||||
134
backend/routers/ops_automation.py
Normal file
134
backend/routers/ops_automation.py
Normal 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)}
|
||||
119
backend/routers/platform_mgmt.py
Normal file
119
backend/routers/platform_mgmt.py
Normal 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)}
|
||||
Loading…
Reference in New Issue
Block a user