Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
- NHN Cloud API packages: token, vpc, compute, nks, storage - REST API endpoints with Swagger documentation - Async task processing for long-running operations - CORS configuration for frontend integration - Enhanced logging for debugging API calls Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
850 lines
31 KiB
Python
850 lines
31 KiB
Python
"""
|
|
NHN Cloud API Views
|
|
|
|
REST API 엔드포인트 정의
|
|
"""
|
|
|
|
import logging
|
|
|
|
from rest_framework import status
|
|
from rest_framework.views import APIView
|
|
from rest_framework.response import Response
|
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
from drf_yasg.utils import swagger_auto_schema
|
|
from drf_yasg import openapi
|
|
|
|
from .serializers import (
|
|
TokenRequestSerializer,
|
|
TokenResponseSerializer,
|
|
ComputeInstanceSerializer,
|
|
VpcSerializer,
|
|
SubnetSerializer,
|
|
NksClusterSerializer,
|
|
StorageContainerSerializer,
|
|
ErrorResponseSerializer,
|
|
)
|
|
from .packages import NHNCloudToken, ApiCompute, ApiVpc, ApiNks, ApiStorageObject
|
|
from .packages.base import NHNCloudAPIError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ==================== Common Headers ====================
|
|
|
|
region_header = openapi.Parameter(
|
|
"X-NHN-Region",
|
|
openapi.IN_HEADER,
|
|
description="NHN Cloud 리전 (kr1: 판교, kr2: 평촌)",
|
|
type=openapi.TYPE_STRING,
|
|
default="kr2",
|
|
)
|
|
|
|
token_header = openapi.Parameter(
|
|
"X-NHN-Token",
|
|
openapi.IN_HEADER,
|
|
description="NHN Cloud API 토큰",
|
|
type=openapi.TYPE_STRING,
|
|
required=True,
|
|
)
|
|
|
|
tenant_header = openapi.Parameter(
|
|
"X-NHN-Tenant-ID",
|
|
openapi.IN_HEADER,
|
|
description="NHN Cloud 테넌트 ID",
|
|
type=openapi.TYPE_STRING,
|
|
required=True,
|
|
)
|
|
|
|
|
|
def get_nhn_headers(request):
|
|
"""요청 헤더에서 NHN Cloud 정보 추출"""
|
|
headers = {
|
|
"region": request.headers.get("X-NHN-Region", "kr2"),
|
|
"token": request.headers.get("X-NHN-Token"),
|
|
"tenant_id": request.headers.get("X-NHN-Tenant-ID"),
|
|
"storage_account": request.headers.get("X-NHN-Storage-Account"),
|
|
}
|
|
# 토큰은 앞 8자리만 로깅 (보안)
|
|
token_preview = headers["token"][:8] + "..." if headers["token"] else "None"
|
|
logger.info(f"[Request Headers] region={headers['region']}, token={token_preview}, tenant_id={headers['tenant_id']}")
|
|
return headers
|
|
|
|
|
|
# ==================== Token API ====================
|
|
|
|
|
|
class TokenCreateView(APIView):
|
|
"""토큰 생성 API"""
|
|
|
|
authentication_classes = [] # 인증 완전히 건너뜀
|
|
permission_classes = [AllowAny]
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="NHN Cloud API 토큰 생성",
|
|
operation_description="NHN Cloud API 인증 토큰을 생성합니다.",
|
|
request_body=TokenRequestSerializer,
|
|
responses={
|
|
200: TokenResponseSerializer,
|
|
400: ErrorResponseSerializer,
|
|
401: ErrorResponseSerializer,
|
|
},
|
|
)
|
|
def post(self, request):
|
|
logger.info(f"[Token] 토큰 생성 요청 수신 - client_ip={request.META.get('REMOTE_ADDR')}")
|
|
|
|
serializer = TokenRequestSerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
logger.warning(f"[Token] 요청 데이터 유효성 검사 실패: {serializer.errors}")
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
tenant_id = serializer.validated_data["tenant_id"]
|
|
username = serializer.validated_data["username"]
|
|
logger.info(f"[Token] 토큰 발급 시도 - tenant_id={tenant_id}, username={username}")
|
|
|
|
try:
|
|
token_manager = NHNCloudToken(
|
|
tenant_id=tenant_id,
|
|
username=username,
|
|
password=serializer.validated_data["password"],
|
|
)
|
|
result = token_manager.create_token()
|
|
|
|
token_preview = result.token[:8] + "..." if result.token else "None"
|
|
logger.info(f"[Token] 토큰 발급 성공 - tenant_id={tenant_id}, token={token_preview}, expires_at={result.expires_at}")
|
|
|
|
return Response(
|
|
{
|
|
"token": result.token,
|
|
"tenant_id": result.tenant_id,
|
|
"expires_at": result.expires_at,
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
except NHNCloudAPIError as e:
|
|
logger.error(f"[Token] 토큰 발급 실패 - tenant_id={tenant_id}, username={username}, error={e.message}, code={e.code}")
|
|
return Response(
|
|
{"error": e.message, "code": e.code},
|
|
status=status.HTTP_401_UNAUTHORIZED,
|
|
)
|
|
|
|
|
|
# ==================== Compute API ====================
|
|
|
|
|
|
class ComputeFlavorListView(APIView):
|
|
"""Flavor 목록 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="Flavor 목록 조회",
|
|
manual_parameters=[region_header, token_header, tenant_header],
|
|
responses={200: "Flavor 목록"},
|
|
)
|
|
def get(self, request):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiCompute(headers["region"], headers["tenant_id"], headers["token"])
|
|
return Response(api.get_flavor_list())
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class ComputeKeypairListView(APIView):
|
|
"""Keypair 목록 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="Keypair 목록 조회",
|
|
manual_parameters=[region_header, token_header, tenant_header],
|
|
responses={200: "Keypair 목록"},
|
|
)
|
|
def get(self, request):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiCompute(headers["region"], headers["tenant_id"], headers["token"])
|
|
return Response(api.get_keypair_list())
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class ComputeImageListView(APIView):
|
|
"""이미지 목록 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="이미지 목록 조회",
|
|
manual_parameters=[region_header, token_header, tenant_header],
|
|
responses={200: "이미지 목록"},
|
|
)
|
|
def get(self, request):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiCompute(headers["region"], headers["tenant_id"], headers["token"])
|
|
return Response(api.get_image_list())
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class ComputeInstanceListView(APIView):
|
|
"""인스턴스 목록 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="인스턴스 목록 조회",
|
|
manual_parameters=[region_header, token_header, tenant_header],
|
|
responses={200: "인스턴스 목록"},
|
|
)
|
|
def get(self, request):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiCompute(headers["region"], headers["tenant_id"], headers["token"])
|
|
# detail=true 파라미터로 상세 조회 여부 결정
|
|
if request.query_params.get("detail", "true").lower() == "true":
|
|
return Response(api.get_instance_list_detail())
|
|
return Response(api.get_instance_list())
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class ComputeInstanceDetailView(APIView):
|
|
"""인스턴스 상세/삭제 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="인스턴스 상세 조회",
|
|
manual_parameters=[region_header, token_header, tenant_header],
|
|
responses={200: "인스턴스 상세 정보"},
|
|
)
|
|
def get(self, request, server_id):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiCompute(headers["region"], headers["tenant_id"], headers["token"])
|
|
return Response(api.get_instance_info(server_id))
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="인스턴스 삭제 (비동기)",
|
|
manual_parameters=[region_header, token_header, tenant_header],
|
|
responses={202: "작업 ID 반환"},
|
|
)
|
|
def delete(self, request, server_id):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
from .tasks import delete_instance_async
|
|
|
|
task = delete_instance_async(
|
|
region=headers["region"],
|
|
tenant_id=headers["tenant_id"],
|
|
token=headers["token"],
|
|
server_id=server_id,
|
|
server_name=request.query_params.get("name", ""),
|
|
)
|
|
return Response(
|
|
{"task_id": str(task.id), "status": task.status, "message": "인스턴스 삭제 작업이 시작되었습니다."},
|
|
status=status.HTTP_202_ACCEPTED,
|
|
)
|
|
except Exception as e:
|
|
logger.exception("인스턴스 삭제 작업 시작 실패")
|
|
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class ComputeInstanceCreateView(APIView):
|
|
"""인스턴스 생성 API (비동기)"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="인스턴스 생성 (비동기)",
|
|
manual_parameters=[region_header, token_header, tenant_header],
|
|
request_body=ComputeInstanceSerializer,
|
|
responses={202: "작업 ID 반환"},
|
|
)
|
|
def post(self, request):
|
|
headers = get_nhn_headers(request)
|
|
serializer = ComputeInstanceSerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
from .tasks import create_instance_async
|
|
|
|
# 비동기 작업 시작
|
|
task = create_instance_async(
|
|
region=headers["region"],
|
|
tenant_id=headers["tenant_id"],
|
|
token=headers["token"],
|
|
instance_data=serializer.validated_data,
|
|
)
|
|
|
|
return Response(
|
|
{
|
|
"task_id": str(task.id),
|
|
"status": task.status,
|
|
"message": "인스턴스 생성 작업이 시작되었습니다.",
|
|
},
|
|
status=status.HTTP_202_ACCEPTED,
|
|
)
|
|
except Exception as e:
|
|
logger.exception("인스턴스 생성 작업 시작 실패")
|
|
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class ComputeInstanceActionView(APIView):
|
|
"""인스턴스 액션 API (시작/정지/재부팅) - 비동기"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="인스턴스 액션 (start/stop/reboot) - 비동기",
|
|
manual_parameters=[region_header, token_header, tenant_header],
|
|
request_body=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
properties={
|
|
"action": openapi.Schema(
|
|
type=openapi.TYPE_STRING,
|
|
enum=["start", "stop", "reboot"],
|
|
description="액션 타입",
|
|
),
|
|
},
|
|
required=["action"],
|
|
),
|
|
responses={202: "작업 ID 반환"},
|
|
)
|
|
def post(self, request, server_id):
|
|
headers = get_nhn_headers(request)
|
|
action = request.data.get("action")
|
|
|
|
if action not in ["start", "stop", "reboot"]:
|
|
return Response(
|
|
{"error": "Invalid action. Use: start, stop, reboot"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
try:
|
|
from .tasks import instance_action_async
|
|
|
|
task = instance_action_async(
|
|
region=headers["region"],
|
|
tenant_id=headers["tenant_id"],
|
|
token=headers["token"],
|
|
server_id=server_id,
|
|
action=action,
|
|
server_name=request.data.get("name", ""),
|
|
hard=request.data.get("hard", False),
|
|
)
|
|
return Response(
|
|
{"task_id": str(task.id), "status": task.status, "action": action, "message": f"인스턴스 {action} 작업이 시작되었습니다."},
|
|
status=status.HTTP_202_ACCEPTED,
|
|
)
|
|
except Exception as e:
|
|
logger.exception(f"인스턴스 {action} 작업 시작 실패")
|
|
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
# ==================== VPC API ====================
|
|
|
|
|
|
class VpcListView(APIView):
|
|
"""VPC 목록 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="VPC 목록 조회",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={200: "VPC 목록"},
|
|
)
|
|
def get(self, request):
|
|
logger.info(f"[VPC] VPC 목록 조회 요청")
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiVpc(headers["region"], headers["token"])
|
|
result = api.get_vpc_list()
|
|
vpc_count = len(result.get("vpcs", []))
|
|
logger.info(f"[VPC] VPC 목록 조회 성공 - count={vpc_count}")
|
|
return Response(result)
|
|
except NHNCloudAPIError as e:
|
|
logger.error(f"[VPC] VPC 목록 조회 실패 - error={e.message}")
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class VpcCreateView(APIView):
|
|
"""VPC 생성 API (비동기)"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="VPC 생성 (비동기)",
|
|
manual_parameters=[region_header, token_header],
|
|
request_body=VpcSerializer,
|
|
responses={202: "작업 ID 반환"},
|
|
)
|
|
def post(self, request):
|
|
headers = get_nhn_headers(request)
|
|
serializer = VpcSerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
from .tasks import create_vpc_async
|
|
|
|
task = create_vpc_async(
|
|
region=headers["region"],
|
|
token=headers["token"],
|
|
name=serializer.validated_data["name"],
|
|
cidr=serializer.validated_data["cidr"],
|
|
)
|
|
return Response(
|
|
{"task_id": str(task.id), "status": task.status, "message": "VPC 생성 작업이 시작되었습니다."},
|
|
status=status.HTTP_202_ACCEPTED,
|
|
)
|
|
except Exception as e:
|
|
logger.exception("VPC 생성 작업 시작 실패")
|
|
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class VpcDetailView(APIView):
|
|
"""VPC 상세/삭제 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="VPC 상세 조회",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={200: "VPC 상세 정보"},
|
|
)
|
|
def get(self, request, vpc_id):
|
|
logger.info(f"[VPC] VPC 상세 조회 요청 - vpc_id={vpc_id}")
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiVpc(headers["region"], headers["token"])
|
|
result = api.get_vpc_info(vpc_id)
|
|
vpc_name = result.get("vpc", {}).get("name", "unknown")
|
|
logger.info(f"[VPC] VPC 상세 조회 성공 - vpc_id={vpc_id}, name={vpc_name}")
|
|
return Response(result)
|
|
except NHNCloudAPIError as e:
|
|
logger.error(f"[VPC] VPC 상세 조회 실패 - vpc_id={vpc_id}, error={e.message}")
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="VPC 삭제 (비동기)",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={202: "작업 ID 반환"},
|
|
)
|
|
def delete(self, request, vpc_id):
|
|
logger.info(f"[VPC] VPC 삭제 요청 - vpc_id={vpc_id}")
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
from .tasks import delete_vpc_async
|
|
|
|
task = delete_vpc_async(
|
|
region=headers["region"],
|
|
token=headers["token"],
|
|
vpc_id=vpc_id,
|
|
vpc_name=request.query_params.get("name", ""),
|
|
)
|
|
logger.info(f"[VPC] VPC 삭제 작업 시작 - vpc_id={vpc_id}, task_id={task.id}")
|
|
return Response(
|
|
{"task_id": str(task.id), "status": task.status, "message": "VPC 삭제 작업이 시작되었습니다."},
|
|
status=status.HTTP_202_ACCEPTED,
|
|
)
|
|
except Exception as e:
|
|
logger.exception(f"[VPC] VPC 삭제 작업 시작 실패 - vpc_id={vpc_id}")
|
|
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class SubnetListView(APIView):
|
|
"""서브넷 목록 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="서브넷 목록 조회",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={200: "서브넷 목록"},
|
|
)
|
|
def get(self, request):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiVpc(headers["region"], headers["token"])
|
|
return Response(api.get_subnet_list())
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class SubnetCreateView(APIView):
|
|
"""서브넷 생성 API (비동기)"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="서브넷 생성 (비동기)",
|
|
manual_parameters=[region_header, token_header],
|
|
request_body=SubnetSerializer,
|
|
responses={202: "작업 ID 반환"},
|
|
)
|
|
def post(self, request):
|
|
headers = get_nhn_headers(request)
|
|
serializer = SubnetSerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
from .tasks import create_subnet_async
|
|
|
|
task = create_subnet_async(
|
|
region=headers["region"],
|
|
token=headers["token"],
|
|
vpc_id=serializer.validated_data["vpc_id"],
|
|
cidr=serializer.validated_data["cidr"],
|
|
name=serializer.validated_data["name"],
|
|
)
|
|
return Response(
|
|
{"task_id": str(task.id), "status": task.status, "message": "서브넷 생성 작업이 시작되었습니다."},
|
|
status=status.HTTP_202_ACCEPTED,
|
|
)
|
|
except Exception as e:
|
|
logger.exception("서브넷 생성 작업 시작 실패")
|
|
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class SubnetDetailView(APIView):
|
|
"""서브넷 상세/삭제 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="서브넷 상세 조회",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={200: "서브넷 상세 정보"},
|
|
)
|
|
def get(self, request, subnet_id):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiVpc(headers["region"], headers["token"])
|
|
return Response(api.get_subnet_info(subnet_id))
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="서브넷 삭제 (비동기)",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={202: "작업 ID 반환"},
|
|
)
|
|
def delete(self, request, subnet_id):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
from .tasks import delete_subnet_async
|
|
|
|
task = delete_subnet_async(
|
|
region=headers["region"],
|
|
token=headers["token"],
|
|
subnet_id=subnet_id,
|
|
subnet_name=request.query_params.get("name", ""),
|
|
)
|
|
return Response(
|
|
{"task_id": str(task.id), "status": task.status, "message": "서브넷 삭제 작업이 시작되었습니다."},
|
|
status=status.HTTP_202_ACCEPTED,
|
|
)
|
|
except Exception as e:
|
|
logger.exception("서브넷 삭제 작업 시작 실패")
|
|
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class FloatingIpListView(APIView):
|
|
"""Floating IP 목록 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="Floating IP 목록 조회",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={200: "Floating IP 목록"},
|
|
)
|
|
def get(self, request):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiVpc(headers["region"], headers["token"])
|
|
return Response(api.get_floating_ip_list())
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
# ==================== Security Group API ====================
|
|
|
|
|
|
class SecurityGroupListView(APIView):
|
|
"""보안 그룹 목록 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="보안 그룹 목록 조회",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={200: "보안 그룹 목록"},
|
|
)
|
|
def get(self, request):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiVpc(headers["region"], headers["token"])
|
|
return Response(api.get_security_group_list())
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
# ==================== Async Task API ====================
|
|
|
|
|
|
class AsyncTaskListView(APIView):
|
|
"""비동기 작업 목록 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="비동기 작업 목록 조회",
|
|
responses={200: "작업 목록"},
|
|
)
|
|
def get(self, request):
|
|
from .models import AsyncTask
|
|
|
|
# 최근 100개만 조회
|
|
tasks = AsyncTask.objects.all()[:100]
|
|
data = [
|
|
{
|
|
"id": str(task.id),
|
|
"task_type": task.task_type,
|
|
"status": task.status,
|
|
"resource_name": task.resource_name,
|
|
"resource_id": task.resource_id,
|
|
"error_message": task.error_message,
|
|
"created_at": task.created_at.isoformat(),
|
|
"completed_at": task.completed_at.isoformat() if task.completed_at else None,
|
|
}
|
|
for task in tasks
|
|
]
|
|
return Response({"tasks": data})
|
|
|
|
|
|
class AsyncTaskDetailView(APIView):
|
|
"""비동기 작업 상세 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="비동기 작업 상태 조회",
|
|
responses={200: "작업 상태"},
|
|
)
|
|
def get(self, request, task_id):
|
|
from .models import AsyncTask
|
|
|
|
try:
|
|
task = AsyncTask.objects.get(id=task_id)
|
|
return Response(
|
|
{
|
|
"id": str(task.id),
|
|
"task_type": task.task_type,
|
|
"status": task.status,
|
|
"resource_name": task.resource_name,
|
|
"resource_id": task.resource_id,
|
|
"request_data": task.request_data,
|
|
"result_data": task.result_data,
|
|
"error_message": task.error_message,
|
|
"created_at": task.created_at.isoformat(),
|
|
"updated_at": task.updated_at.isoformat(),
|
|
"completed_at": task.completed_at.isoformat() if task.completed_at else None,
|
|
}
|
|
)
|
|
except AsyncTask.DoesNotExist:
|
|
return Response({"error": "작업을 찾을 수 없습니다."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
# ==================== NKS API ====================
|
|
|
|
|
|
class NksClusterListView(APIView):
|
|
"""NKS 클러스터 목록 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="NKS 클러스터 목록 조회",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={200: "클러스터 목록"},
|
|
)
|
|
def get(self, request):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiNks(headers["region"], headers["token"])
|
|
return Response(api.get_cluster_list())
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class NksClusterDetailView(APIView):
|
|
"""NKS 클러스터 상세/삭제 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="NKS 클러스터 상세 조회",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={200: "클러스터 상세 정보"},
|
|
)
|
|
def get(self, request, cluster_name):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiNks(headers["region"], headers["token"])
|
|
return Response(api.get_cluster_info(cluster_name))
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="NKS 클러스터 삭제 (비동기)",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={202: "작업 ID 반환"},
|
|
)
|
|
def delete(self, request, cluster_name):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
from .tasks import delete_nks_cluster_async
|
|
|
|
task = delete_nks_cluster_async(
|
|
region=headers["region"],
|
|
token=headers["token"],
|
|
cluster_name=cluster_name,
|
|
)
|
|
return Response(
|
|
{"task_id": str(task.id), "status": task.status, "message": "NKS 클러스터 삭제 작업이 시작되었습니다."},
|
|
status=status.HTTP_202_ACCEPTED,
|
|
)
|
|
except Exception as e:
|
|
logger.exception("NKS 클러스터 삭제 작업 시작 실패")
|
|
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class NksClusterConfigView(APIView):
|
|
"""NKS 클러스터 kubeconfig 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="NKS 클러스터 kubeconfig 조회",
|
|
manual_parameters=[region_header, token_header],
|
|
responses={200: "kubeconfig"},
|
|
)
|
|
def get(self, request, cluster_name):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiNks(headers["region"], headers["token"])
|
|
config = api.get_cluster_config(cluster_name)
|
|
return Response({"config": config})
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class NksClusterCreateView(APIView):
|
|
"""NKS 클러스터 생성 API (비동기)"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="NKS 클러스터 생성 (비동기)",
|
|
manual_parameters=[region_header, token_header, tenant_header],
|
|
request_body=NksClusterSerializer,
|
|
responses={202: "작업 ID 반환"},
|
|
)
|
|
def post(self, request):
|
|
headers = get_nhn_headers(request)
|
|
serializer = NksClusterSerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
from .tasks import create_nks_cluster_async
|
|
|
|
task = create_nks_cluster_async(
|
|
region=headers["region"],
|
|
tenant_id=headers["tenant_id"],
|
|
token=headers["token"],
|
|
cluster_data=serializer.validated_data,
|
|
)
|
|
return Response(
|
|
{"task_id": str(task.id), "status": task.status, "message": "NKS 클러스터 생성 작업이 시작되었습니다."},
|
|
status=status.HTTP_202_ACCEPTED,
|
|
)
|
|
except Exception as e:
|
|
logger.exception("NKS 클러스터 생성 작업 시작 실패")
|
|
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
# ==================== Storage API ====================
|
|
|
|
storage_account_header = openapi.Parameter(
|
|
"X-NHN-Storage-Account",
|
|
openapi.IN_HEADER,
|
|
description="NHN Cloud Object Storage 계정 (AUTH_...)",
|
|
type=openapi.TYPE_STRING,
|
|
required=True,
|
|
)
|
|
|
|
|
|
class StorageContainerListView(APIView):
|
|
"""스토리지 컨테이너 목록 조회 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="컨테이너 목록 조회",
|
|
manual_parameters=[region_header, token_header, storage_account_header],
|
|
responses={200: "컨테이너 목록"},
|
|
)
|
|
def get(self, request):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiStorageObject(
|
|
headers["region"], headers["token"], headers["storage_account"]
|
|
)
|
|
containers = api.get_container_list()
|
|
return Response({"containers": containers})
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class StorageContainerCreateView(APIView):
|
|
"""스토리지 컨테이너 생성 API (비동기)"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="컨테이너 생성 (비동기)",
|
|
manual_parameters=[region_header, token_header, storage_account_header],
|
|
request_body=StorageContainerSerializer,
|
|
responses={202: "작업 ID 반환"},
|
|
)
|
|
def post(self, request):
|
|
headers = get_nhn_headers(request)
|
|
serializer = StorageContainerSerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
from .tasks import create_storage_container_async
|
|
|
|
task = create_storage_container_async(
|
|
region=headers["region"],
|
|
token=headers["token"],
|
|
storage_account=headers["storage_account"],
|
|
container_name=serializer.validated_data["name"],
|
|
)
|
|
return Response(
|
|
{"task_id": str(task.id), "status": task.status, "message": "컨테이너 생성 작업이 시작되었습니다."},
|
|
status=status.HTTP_202_ACCEPTED,
|
|
)
|
|
except Exception as e:
|
|
logger.exception("컨테이너 생성 작업 시작 실패")
|
|
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class StorageContainerDetailView(APIView):
|
|
"""스토리지 컨테이너 상세/삭제 API"""
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="컨테이너 오브젝트 목록 조회",
|
|
manual_parameters=[region_header, token_header, storage_account_header],
|
|
responses={200: "오브젝트 목록"},
|
|
)
|
|
def get(self, request, container_name):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
api = ApiStorageObject(
|
|
headers["region"], headers["token"], headers["storage_account"]
|
|
)
|
|
objects = api.get_object_list(container_name)
|
|
return Response({"objects": objects})
|
|
except NHNCloudAPIError as e:
|
|
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="컨테이너 삭제 (비동기)",
|
|
manual_parameters=[region_header, token_header, storage_account_header],
|
|
responses={202: "작업 ID 반환"},
|
|
)
|
|
def delete(self, request, container_name):
|
|
headers = get_nhn_headers(request)
|
|
try:
|
|
from .tasks import delete_storage_container_async
|
|
|
|
task = delete_storage_container_async(
|
|
region=headers["region"],
|
|
token=headers["token"],
|
|
storage_account=headers["storage_account"],
|
|
container_name=container_name,
|
|
)
|
|
return Response(
|
|
{"task_id": str(task.id), "status": task.status, "message": "컨테이너 삭제 작업이 시작되었습니다."},
|
|
status=status.HTTP_202_ACCEPTED,
|
|
)
|
|
except Exception as e:
|
|
logger.exception("컨테이너 삭제 작업 시작 실패")
|
|
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|