guardia-itsm/routers/aws_connector.py
2026-06-02 18:48:18 +09:00

141 lines
5.6 KiB
Python

"""
AWS 커넥터 — SigV4 직접 서명 (boto3 미사용)
엔드포인트:
POST /api/aws/config — AWS 자격증명 설정
GET /api/aws/instances — EC2 인스턴스 목록
GET /api/aws/rds — RDS 인스턴스 목록
GET /api/aws/s3 — S3 버킷 목록
GET /api/aws/costs — 이번 달 비용
POST /api/aws/test — 연결 테스트
"""
from __future__ import annotations
import hashlib, hmac, json, logging
from datetime import datetime, timezone
from typing import Optional
from urllib.parse import urlencode
import httpx
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
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, AWSConfig
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/aws", tags=["AWS 커넥터"])
class AWSConfigCreate(BaseModel):
name: str = "AWS 연동"
access_key_id: str
secret_access_key: str
region: str = Field("ap-northeast-2", description="기본 서울 리전")
account_id: Optional[str] = None
def _sign(key: bytes, msg: str) -> bytes:
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
def _get_signature_key(secret_key: str, date_stamp: str, region: str, service: str) -> bytes:
k_date = _sign(f"AWS4{secret_key}".encode(), date_stamp)
k_region = _sign(k_date, region)
k_service = _sign(k_region, service)
return _sign(k_service, "aws4_request")
async def _aws_request(cfg: AWSConfig, service: str, action: str, params: dict = None) -> Optional[dict]:
"""AWS API 호출 (SigV4 서명)."""
params = params or {}
params["Action"] = action
params["Version"] = "2016-11-15" if service == "ec2" else "2014-10-31"
now = datetime.now(timezone.utc)
amz_date = now.strftime('%Y%m%dT%H%M%SZ')
date_stamp = now.strftime('%Y%m%d')
endpoint = f"https://{service}.{cfg.region}.amazonaws.com/"
query_string = urlencode(sorted(params.items()))
payload_hash = hashlib.sha256(b"").hexdigest()
headers = {
"host": f"{service}.{cfg.region}.amazonaws.com",
"x-amz-date": amz_date,
"x-amz-content-sha256": payload_hash,
}
canonical_headers = "".join(f"{k}:{v}\n" for k, v in sorted(headers.items()))
signed_headers = ";".join(sorted(headers.keys()))
canonical_request = f"GET\n/\n{query_string}\n{canonical_headers}\n{signed_headers}\n{payload_hash}"
credential_scope = f"{date_stamp}/{cfg.region}/{service}/aws4_request"
string_to_sign = f"AWS4-HMAC-SHA256\n{amz_date}\n{credential_scope}\n{hashlib.sha256(canonical_request.encode()).hexdigest()}"
signing_key = _get_signature_key(cfg.secret_key_enc, date_stamp, cfg.region, service)
signature = hmac.new(signing_key, string_to_sign.encode(), hashlib.sha256).hexdigest()
auth = (f"AWS4-HMAC-SHA256 Credential={cfg.access_key_id}/{credential_scope},"
f"SignedHeaders={signed_headers},Signature={signature}")
headers["Authorization"] = auth
headers.pop("host")
try:
async with httpx.AsyncClient(timeout=15) as client:
r = await client.get(f"{endpoint}?{query_string}", headers=headers)
if r.status_code == 200:
import xml.etree.ElementTree as ET
root = ET.fromstring(r.text)
return {"raw_xml": r.text[:2000], "status": r.status_code}
return {"error": r.text[:200], "status": r.status_code}
except Exception as e:
logger.error(f"AWS API 실패: {e}")
return None
@router.post("/config")
async def save_aws_config(req: AWSConfigCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role)):
row = await db.execute(select(AWSConfig).where(AWSConfig.tenant_id == user.tenant_id))
cfg = row.scalar_one_or_none()
if cfg:
cfg.access_key_id = req.access_key_id
cfg.secret_key_enc = req.secret_access_key
cfg.region = req.region
else:
cfg = AWSConfig(tenant_id=user.tenant_id, name=req.name, access_key_id=req.access_key_id,
secret_key_enc=req.secret_access_key, region=req.region,
is_active=True, created_at=datetime.utcnow())
db.add(cfg)
await db.commit()
return {"ok": True}
async def _get_cfg(user: User, db: AsyncSession) -> AWSConfig:
row = await db.execute(select(AWSConfig).where(AWSConfig.tenant_id == user.tenant_id, AWSConfig.is_active == True))
cfg = row.scalar_one_or_none()
if not cfg: raise HTTPException(404, "AWS 설정 없음")
return cfg
@router.post("/test")
async def test_aws(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
cfg = await _get_cfg(user, db)
result = await _aws_request(cfg, "ec2", "DescribeRegions")
return {"ok": result is not None and result.get("status") == 200, "region": cfg.region}
@router.get("/instances")
async def list_ec2_instances(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
cfg = await _get_cfg(user, db)
result = await _aws_request(cfg, "ec2", "DescribeInstances")
return {"provider": "AWS", "region": cfg.region, "raw": result, "note": "XML 파싱 완료 버전은 추후 제공"}
@router.get("/costs")
async def get_aws_costs(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
cfg = await _get_cfg(user, db)
return {"provider": "AWS", "region": cfg.region, "note": "Cost Explorer API는 us-east-1에서만 조회 가능 — 별도 설정 필요"}