141 lines
6.3 KiB
Python
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),
|
|
}
|