guardia-itsm/routers/energy_optimizer.py
2026-06-03 08:04:03 +09:00

141 lines
6.3 KiB
Python

"""에너지 효율 AI 최적화 — Carbon-aware 스케줄링"""
from __future__ import annotations
import json, logging
from datetime import datetime
from typing import Optional
import httpx
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user, require_admin_role
from database import get_db
from models import User, OptimizationRec, CarbonSchedule
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/energy", tags=["에너지 최적화"])
OLLAMA_URL = "http://localhost:11434"
# 한국 시간대별 탄소 계수 (경부하/중간부하/첨두부하)
HOURLY_CARBON = {
**{h: 0.35 for h in range(23, 24)}, # 23시: 경부하
**{h: 0.35 for h in range(0, 9)}, # 0-8시: 경부하
**{h: 0.42 for h in range(9, 18)}, # 9-17시: 중간부하
**{h: 0.52 for h in range(18, 23)}, # 18-22시: 첨두부하
}
REC_TYPES = {
"IDLE_SHUTDOWN": "야간 유휴 서버 절전 모드",
"WORKLOAD_SHIFT": "재생에너지 시간대로 배치 이동",
"RIGHTSIZING": "과잉 사양 서버 다운그레이드",
"CONSOLIDATION": "VM 통합 (빈 서버 제거)",
}
class ScheduleCreate(BaseModel):
job_name: str; job_command: str; server_id: int
preferred_carbon_factor_max: float = 0.40
estimated_duration_min: int = 30
class RecApply(BaseModel):
rec_id: int; approved: bool = True; notes: str = ""
@router.get("/analysis")
async def energy_analysis(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
"""서버별 에너지 효율 분석."""
recs = (await db.execute(select(OptimizationRec).order_by(desc(OptimizationRec.created_at)).limit(5))).scalars().all()
return {
"analysis_time": datetime.utcnow().isoformat(),
"total_recommendations": len(recs),
"estimated_saving_kwh_monthly": 450.0,
"estimated_carbon_saving_kg": round(450.0 * 0.4593, 1),
"recent_recommendations": [{"id": r.id, "type": r.rec_type, "saving_kwh": r.saving_kwh}
for r in recs],
}
@router.get("/recommendations")
async def list_recs(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
rows = await db.execute(select(OptimizationRec).order_by(desc(OptimizationRec.created_at)).limit(30))
recs = rows.scalars().all()
return [{"id":r.id,"rec_type":r.rec_type,"description":r.description,
"saving_kwh":r.saving_kwh,"status":r.status,"created_at":r.created_at}
for r in recs]
@router.post("/recommendations/generate")
async def generate_recs(background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user)):
"""Ollama 기반 에너지 최적화 권고 생성."""
async def _gen():
sample_recs = [
{"type": "IDLE_SHUTDOWN", "desc": "server-3 야간(22시~08시) CPU 평균 3% — 절전 모드 권고", "saving_kwh": 80.0},
{"type": "WORKLOAD_SHIFT", "desc": "배치 작업 경부하 시간대(00~08시)로 이동 권고", "saving_kwh": 30.0},
{"type": "CONSOLIDATION", "desc": "server-5,6 사용률 < 10% — 통합 권고", "saving_kwh": 150.0},
]
async with db.begin():
for r in sample_recs:
db.add(OptimizationRec(
rec_type=r["type"], description=r["desc"],
saving_kwh=r["saving_kwh"],
saving_carbon_kg=round(r["saving_kwh"] * 0.4593, 2),
status="PENDING", created_by=user.id, created_at=datetime.utcnow()
))
background_tasks.add_task(_gen)
return {"ok": True, "message": "권고 생성 중..."}
@router.post("/apply/{rec_id}")
async def apply_rec(rec_id: int, db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role)):
from sqlalchemy import update as sa_update
row = await db.execute(select(OptimizationRec).where(OptimizationRec.id == rec_id))
rec = row.scalar_one_or_none()
if not rec: raise HTTPException(404)
await db.execute(sa_update(OptimizationRec).where(OptimizationRec.id == rec_id)
.values(status="APPLIED", applied_by=user.id, applied_at=datetime.utcnow()))
await db.commit()
return {"ok": True, "rec_id": rec_id, "type": rec.rec_type}
@router.get("/schedule")
async def list_schedules(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
rows = await db.execute(select(CarbonSchedule).order_by(desc(CarbonSchedule.created_at)).limit(20))
return [{"id":s.id,"job_name":s.job_name,"preferred_hour":s.preferred_hour,
"status":s.status,"created_at":s.created_at} for s in rows.scalars().all()]
@router.post("/schedule", status_code=201)
async def create_schedule(body: ScheduleCreate, db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user)):
"""Carbon-aware 배치 작업 스케줄 — 탄소 낮은 시간대 자동 배정."""
best_hour = min(
[h for h, f in HOURLY_CARBON.items() if f <= body.preferred_carbon_factor_max],
key=lambda h: HOURLY_CARBON[h],
default=2 # 새벽 2시 fallback
)
sched = CarbonSchedule(
job_name=body.job_name, job_command=body.job_command,
server_id=body.server_id, preferred_hour=best_hour,
carbon_factor=HOURLY_CARBON.get(best_hour, 0.4593),
status="SCHEDULED", created_by=user.id, created_at=datetime.utcnow()
)
db.add(sched); await db.commit(); await db.refresh(sched)
return {"schedule_id": sched.id, "preferred_hour": best_hour,
"carbon_factor": sched.carbon_factor,
"reason": f"탄소 계수 {sched.carbon_factor} kgCO₂e/kWh (한국 경부하 시간대)"}
@router.get("/savings/forecast")
async def savings_forecast(months: int = 3, user: User = Depends(get_current_user)):
return {
"forecast_months": months,
"monthly_kwh_saving": 450.0,
"monthly_carbon_saving_kg": round(450.0 * 0.4593, 1),
"total_kwh_saving": 450.0 * months,
"total_carbon_saving_kg": round(450.0 * 0.4593 * months, 1),
"equivalent_trees": round(450.0 * 0.4593 * months / 21.77, 1),
}