CMDB 자동 발견 (4개): - autodiscovery.py: SSH 네트워크 스캔 + CMDB 자동 등록 - snmp_discovery.py: SNMP v2c/v3 장비 자동 발견 - dependency_map.py: 서비스 의존성 자동 매핑 (netstat) - config_inventory.py: 서버 인벤토리 자동 수집 (SSH) NL 쿼리 엔진 (3개): - nlquery.py: Text-to-SQL (SELECT 전용, DML 차단) - op_assistant.py: Multi-turn 대화형 운영 어시스턴트 - query_history.py: 쿼리 이력·즐겨찾기·공유 구성 드리프트 (3개): - drift_detection.py: 골든 구성 vs 실제 비교·SR 자동 생성 - golden_config.py: 내장 CSAP 템플릿 + 버전 관리 - auto_remediation.py: 승인 기반 자동 교정 + 롤백 멀티클라우드 (4개): - multicloud.py: 통합 관제 (NCloud+AWS+KT) - aws_connector.py: AWS SigV4 직접 서명 연동 - cost_optimizer.py: AI 비용 최적화 권고 - cloud_migration.py: On-prem→K-Cloud 체크리스트 공공기관 특화 (6개): - narasajang.py: 나라장터 OpenAPI 연동 - public_api_hub.py: data.go.kr KISA·기상청 허브 - isp_support.py: ISP 수립 지원 + AI 보고서 - network_zone.py: 행정망/인터넷망 분리 관리 - k_cloud.py: 정부 K-Cloud 전환 자동화 - e_procurement.py: 전자조달 계약·검수·납품 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
5.6 KiB
Python
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에서만 조회 가능 — 별도 설정 필요"}
|