Add NHN Cloud API integration with async task support
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
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>
This commit is contained in:
0
nhn/__init__.py
Normal file
0
nhn/__init__.py
Normal file
3
nhn/admin.py
Normal file
3
nhn/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
nhn/apps.py
Normal file
6
nhn/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NhnConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "nhn"
|
||||
28
nhn/authentication.py
Normal file
28
nhn/authentication.py
Normal file
@ -0,0 +1,28 @@
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken
|
||||
|
||||
|
||||
class StatelessUser:
|
||||
"""
|
||||
Stateless user class for JWT authentication.
|
||||
Does not require database User model.
|
||||
"""
|
||||
|
||||
def __init__(self, username):
|
||||
self.username = username
|
||||
self.is_authenticated = True
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
|
||||
class StatelessJWTAuthentication(JWTAuthentication):
|
||||
"""
|
||||
Custom JWT authentication that extracts user from token's 'name' claim.
|
||||
"""
|
||||
|
||||
def get_user(self, validated_token):
|
||||
name = validated_token.get("name")
|
||||
if not name:
|
||||
raise InvalidToken("Token does not contain 'name' claim.")
|
||||
return StatelessUser(username=name)
|
||||
36
nhn/migrations/0001_initial.py
Normal file
36
nhn/migrations/0001_initial.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Generated by Django 4.2.14 on 2026-01-13 16:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AsyncTask',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('task_type', models.CharField(choices=[('instance_create', '인스턴스 생성'), ('instance_delete', '인스턴스 삭제'), ('nks_create', 'NKS 클러스터 생성'), ('nks_delete', 'NKS 클러스터 삭제')], max_length=50)),
|
||||
('status', models.CharField(choices=[('pending', '대기중'), ('running', '실행중'), ('success', '성공'), ('failed', '실패')], default='pending', max_length=20)),
|
||||
('request_data', models.JSONField(default=dict, help_text='요청 데이터')),
|
||||
('result_data', models.JSONField(default=dict, help_text='결과 데이터')),
|
||||
('error_message', models.TextField(blank=True, help_text='에러 메시지')),
|
||||
('resource_id', models.CharField(blank=True, help_text='생성된 리소스 ID', max_length=255)),
|
||||
('resource_name', models.CharField(blank=True, help_text='리소스 이름', max_length=255)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('completed_at', models.DateTimeField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '비동기 작업',
|
||||
'verbose_name_plural': '비동기 작업들',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
nhn/migrations/__init__.py
Normal file
0
nhn/migrations/__init__.py
Normal file
89
nhn/models.py
Normal file
89
nhn/models.py
Normal file
@ -0,0 +1,89 @@
|
||||
"""
|
||||
NHN Cloud Async Task Models
|
||||
|
||||
비동기 작업 상태 추적
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AsyncTask(models.Model):
|
||||
"""비동기 작업 상태 추적 모델"""
|
||||
|
||||
class Status(models.TextChoices):
|
||||
PENDING = "pending", "대기중"
|
||||
RUNNING = "running", "실행중"
|
||||
SUCCESS = "success", "성공"
|
||||
FAILED = "failed", "실패"
|
||||
|
||||
class TaskType(models.TextChoices):
|
||||
# Compute
|
||||
INSTANCE_CREATE = "instance_create", "인스턴스 생성"
|
||||
INSTANCE_DELETE = "instance_delete", "인스턴스 삭제"
|
||||
INSTANCE_ACTION = "instance_action", "인스턴스 액션"
|
||||
# VPC/Network
|
||||
VPC_CREATE = "vpc_create", "VPC 생성"
|
||||
VPC_DELETE = "vpc_delete", "VPC 삭제"
|
||||
SUBNET_CREATE = "subnet_create", "서브넷 생성"
|
||||
SUBNET_DELETE = "subnet_delete", "서브넷 삭제"
|
||||
# NKS
|
||||
NKS_CREATE = "nks_create", "NKS 클러스터 생성"
|
||||
NKS_DELETE = "nks_delete", "NKS 클러스터 삭제"
|
||||
# Storage
|
||||
STORAGE_CREATE = "storage_create", "스토리지 컨테이너 생성"
|
||||
STORAGE_DELETE = "storage_delete", "스토리지 컨테이너 삭제"
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
task_type = models.CharField(max_length=50, choices=TaskType.choices)
|
||||
status = models.CharField(
|
||||
max_length=20, choices=Status.choices, default=Status.PENDING
|
||||
)
|
||||
|
||||
# 요청 정보
|
||||
request_data = models.JSONField(default=dict, help_text="요청 데이터")
|
||||
|
||||
# 결과 정보
|
||||
result_data = models.JSONField(default=dict, help_text="결과 데이터")
|
||||
error_message = models.TextField(blank=True, help_text="에러 메시지")
|
||||
|
||||
# NHN Cloud 리소스 정보
|
||||
resource_id = models.CharField(max_length=255, blank=True, help_text="생성된 리소스 ID")
|
||||
resource_name = models.CharField(max_length=255, blank=True, help_text="리소스 이름")
|
||||
|
||||
# 메타 정보
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
verbose_name = "비동기 작업"
|
||||
verbose_name_plural = "비동기 작업들"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.task_type} - {self.status} ({self.id})"
|
||||
|
||||
def mark_running(self):
|
||||
"""작업 시작"""
|
||||
self.status = self.Status.RUNNING
|
||||
self.save(update_fields=["status", "updated_at"])
|
||||
|
||||
def mark_success(self, result_data=None, resource_id=None):
|
||||
"""작업 성공"""
|
||||
from django.utils import timezone
|
||||
self.status = self.Status.SUCCESS
|
||||
if result_data:
|
||||
self.result_data = result_data
|
||||
if resource_id:
|
||||
self.resource_id = resource_id
|
||||
self.completed_at = timezone.now()
|
||||
self.save()
|
||||
|
||||
def mark_failed(self, error_message):
|
||||
"""작업 실패"""
|
||||
from django.utils import timezone
|
||||
self.status = self.Status.FAILED
|
||||
self.error_message = error_message
|
||||
self.completed_at = timezone.now()
|
||||
self.save()
|
||||
13
nhn/packages/__init__.py
Normal file
13
nhn/packages/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
from .token import NHNCloudToken
|
||||
from .compute import ApiCompute
|
||||
from .vpc import ApiVpc
|
||||
from .nks import ApiNks
|
||||
from .storage import ApiStorageObject
|
||||
|
||||
__all__ = [
|
||||
"NHNCloudToken",
|
||||
"ApiCompute",
|
||||
"ApiVpc",
|
||||
"ApiNks",
|
||||
"ApiStorageObject",
|
||||
]
|
||||
165
nhn/packages/base.py
Normal file
165
nhn/packages/base.py
Normal file
@ -0,0 +1,165 @@
|
||||
"""
|
||||
NHN Cloud API Base Module
|
||||
|
||||
공통 기능을 제공하는 베이스 클래스
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, Any
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Region(str, Enum):
|
||||
"""NHN Cloud 리전"""
|
||||
KR1 = "kr1" # 판교
|
||||
KR2 = "kr2" # 평촌
|
||||
|
||||
|
||||
@dataclass
|
||||
class NHNCloudEndpoints:
|
||||
"""NHN Cloud API Endpoints"""
|
||||
# Identity
|
||||
IDENTITY = "https://api-identity-infrastructure.nhncloudservice.com"
|
||||
|
||||
# Compute
|
||||
COMPUTE_KR1 = "https://kr1-api-instance-infrastructure.nhncloudservice.com"
|
||||
COMPUTE_KR2 = "https://kr2-api-instance-infrastructure.nhncloudservice.com"
|
||||
|
||||
# Image
|
||||
IMAGE_KR1 = "https://kr1-api-image-infrastructure.nhncloudservice.com"
|
||||
IMAGE_KR2 = "https://kr2-api-image-infrastructure.nhncloudservice.com"
|
||||
|
||||
# Network (VPC)
|
||||
NETWORK_KR1 = "https://kr1-api-network-infrastructure.nhncloudservice.com"
|
||||
NETWORK_KR2 = "https://kr2-api-network-infrastructure.nhncloudservice.com"
|
||||
|
||||
# Kubernetes (NKS)
|
||||
NKS_KR1 = "https://kr1-api-kubernetes-infrastructure.nhncloudservice.com"
|
||||
NKS_KR2 = "https://kr2-api-kubernetes-infrastructure.nhncloudservice.com"
|
||||
|
||||
# Object Storage
|
||||
STORAGE_KR1 = "https://kr1-api-object-storage.nhncloudservice.com/v1"
|
||||
STORAGE_KR2 = "https://kr2-api-object-storage.nhncloudservice.com/v1"
|
||||
|
||||
|
||||
class NHNCloudAPIError(Exception):
|
||||
"""NHN Cloud API 에러"""
|
||||
|
||||
def __init__(self, message: str, code: Optional[int] = None, details: Optional[dict] = None):
|
||||
self.message = message
|
||||
self.code = code
|
||||
self.details = details or {}
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class BaseAPI:
|
||||
"""NHN Cloud API 베이스 클래스"""
|
||||
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
def __init__(self, region: str, token: str):
|
||||
self.region = Region(region.lower()) if isinstance(region, str) else region
|
||||
self.token = token
|
||||
self._session = requests.Session()
|
||||
|
||||
def _get_headers(self, extra_headers: Optional[dict] = None) -> dict:
|
||||
"""기본 헤더 생성"""
|
||||
headers = {
|
||||
"X-Auth-Token": self.token,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
return headers
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
url: str,
|
||||
params: Optional[dict] = None,
|
||||
json_data: Optional[dict] = None,
|
||||
headers: Optional[dict] = None,
|
||||
timeout: Optional[int] = None,
|
||||
) -> dict:
|
||||
"""HTTP 요청 실행"""
|
||||
# 토큰 앞 8자리만 로깅 (보안)
|
||||
token_preview = self.token[:8] + "..." if self.token else "None"
|
||||
logger.info(f"[BaseAPI] 요청 시작 - method={method}, url={url}, token={token_preview}")
|
||||
if params:
|
||||
logger.info(f"[BaseAPI] 요청 파라미터 - params={params}")
|
||||
if json_data:
|
||||
logger.info(f"[BaseAPI] 요청 바디 - json={json_data}")
|
||||
|
||||
try:
|
||||
response = self._session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
params=params,
|
||||
json=json_data,
|
||||
headers=self._get_headers(headers),
|
||||
timeout=timeout or self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
logger.info(f"[BaseAPI] 응답 수신 - method={method}, url={url}, status_code={response.status_code}")
|
||||
|
||||
if response.status_code >= 400:
|
||||
logger.error(f"[BaseAPI] 에러 응답 - status_code={response.status_code}, body={response.text[:500]}")
|
||||
self._handle_error(response)
|
||||
|
||||
if response.text:
|
||||
return response.json()
|
||||
return {}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error(f"[BaseAPI] 타임아웃 - url={url}")
|
||||
raise NHNCloudAPIError("요청 시간이 초과되었습니다.", code=408)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.error(f"[BaseAPI] 연결 오류 - url={url}, error={e}")
|
||||
raise NHNCloudAPIError("서버에 연결할 수 없습니다.", code=503)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"[BaseAPI] 요청 오류 - url={url}, error={e}")
|
||||
raise NHNCloudAPIError(f"요청 중 오류가 발생했습니다: {e}")
|
||||
|
||||
def _handle_error(self, response: requests.Response) -> None:
|
||||
"""에러 응답 처리"""
|
||||
try:
|
||||
error_data = response.json()
|
||||
if "error" in error_data:
|
||||
error = error_data["error"]
|
||||
raise NHNCloudAPIError(
|
||||
message=error.get("message", "알 수 없는 오류"),
|
||||
code=error.get("code", response.status_code),
|
||||
details=error,
|
||||
)
|
||||
raise NHNCloudAPIError(
|
||||
message=f"API 오류: {response.status_code}",
|
||||
code=response.status_code,
|
||||
details=error_data,
|
||||
)
|
||||
except ValueError:
|
||||
raise NHNCloudAPIError(
|
||||
message=f"API 오류: {response.status_code} - {response.text}",
|
||||
code=response.status_code,
|
||||
)
|
||||
|
||||
def _get(self, url: str, params: Optional[dict] = None, **kwargs) -> dict:
|
||||
"""GET 요청"""
|
||||
return self._request("GET", url, params=params, **kwargs)
|
||||
|
||||
def _post(self, url: str, json_data: Optional[dict] = None, **kwargs) -> dict:
|
||||
"""POST 요청"""
|
||||
return self._request("POST", url, json_data=json_data, **kwargs)
|
||||
|
||||
def _put(self, url: str, json_data: Optional[dict] = None, **kwargs) -> dict:
|
||||
"""PUT 요청"""
|
||||
return self._request("PUT", url, json_data=json_data, **kwargs)
|
||||
|
||||
def _delete(self, url: str, **kwargs) -> dict:
|
||||
"""DELETE 요청"""
|
||||
return self._request("DELETE", url, **kwargs)
|
||||
220
nhn/packages/compute.py
Normal file
220
nhn/packages/compute.py
Normal file
@ -0,0 +1,220 @@
|
||||
"""
|
||||
NHN Cloud Compute API Module
|
||||
|
||||
인스턴스, 이미지, Flavor, Keypair 관리
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
from .base import BaseAPI, NHNCloudEndpoints, Region
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiCompute(BaseAPI):
|
||||
"""NHN Cloud Compute API 클래스"""
|
||||
|
||||
def __init__(self, region: str, tenant_id: str, token: str):
|
||||
"""
|
||||
Args:
|
||||
region: 리전 (kr1: 판교, kr2: 평촌)
|
||||
tenant_id: 테넌트 ID
|
||||
token: API 인증 토큰
|
||||
"""
|
||||
super().__init__(region, token)
|
||||
self.tenant_id = tenant_id
|
||||
|
||||
if self.region == Region.KR1:
|
||||
self.compute_url = NHNCloudEndpoints.COMPUTE_KR1
|
||||
self.image_url = NHNCloudEndpoints.IMAGE_KR1
|
||||
else:
|
||||
self.compute_url = NHNCloudEndpoints.COMPUTE_KR2
|
||||
self.image_url = NHNCloudEndpoints.IMAGE_KR2
|
||||
|
||||
# ==================== Flavor ====================
|
||||
|
||||
def get_flavor_list(self) -> dict:
|
||||
"""Flavor 목록 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/flavors"
|
||||
return self._get(url)
|
||||
|
||||
def get_flavor_detail(self, flavor_id: str) -> dict:
|
||||
"""Flavor 상세 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/flavors/{flavor_id}"
|
||||
return self._get(url)
|
||||
|
||||
def get_flavor_id_by_name(self, flavor_name: str) -> Optional[str]:
|
||||
"""Flavor 이름으로 ID 조회"""
|
||||
data = self.get_flavor_list()
|
||||
flavors = data.get("flavors", [])
|
||||
|
||||
for flavor in flavors:
|
||||
if flavor_name in flavor.get("name", ""):
|
||||
return flavor.get("id")
|
||||
return None
|
||||
|
||||
# ==================== Keypair ====================
|
||||
|
||||
def get_keypair_list(self) -> dict:
|
||||
"""Keypair 목록 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/os-keypairs"
|
||||
return self._get(url)
|
||||
|
||||
def get_keypair_info(self, keypair_name: str) -> dict:
|
||||
"""Keypair 상세 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/os-keypairs/{keypair_name}"
|
||||
return self._get(url)
|
||||
|
||||
# ==================== Instance ====================
|
||||
|
||||
def get_instance_list(self) -> dict:
|
||||
"""인스턴스 목록 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers"
|
||||
return self._get(url)
|
||||
|
||||
def get_instance_list_detail(self) -> dict:
|
||||
"""인스턴스 상세 목록 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/detail"
|
||||
return self._get(url)
|
||||
|
||||
def get_instance_info(self, server_id: str) -> dict:
|
||||
"""인스턴스 상세 정보 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/{server_id}"
|
||||
return self._get(url)
|
||||
|
||||
def get_instance_status(self, server_id: str) -> str:
|
||||
"""인스턴스 상태 조회"""
|
||||
data = self.get_instance_info(server_id)
|
||||
return data.get("server", {}).get("status", "UNKNOWN")
|
||||
|
||||
def get_instance_id_by_name(self, instance_name: str) -> Optional[str]:
|
||||
"""인스턴스 이름으로 ID 조회"""
|
||||
data = self.get_instance_list()
|
||||
servers = data.get("servers", [])
|
||||
|
||||
for server in servers:
|
||||
if server.get("name") == instance_name:
|
||||
return server.get("id")
|
||||
return None
|
||||
|
||||
def create_instance(
|
||||
self,
|
||||
name: str,
|
||||
image_id: str,
|
||||
flavor_id: str,
|
||||
subnet_id: str,
|
||||
keypair_name: str,
|
||||
volume_size: int = 50,
|
||||
volume_type: str = "General SSD",
|
||||
security_groups: Optional[List[str]] = None,
|
||||
availability_zone: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
인스턴스 생성
|
||||
|
||||
Args:
|
||||
name: 인스턴스 이름
|
||||
image_id: 이미지 ID
|
||||
flavor_id: Flavor ID
|
||||
subnet_id: 서브넷 ID
|
||||
keypair_name: Keypair 이름
|
||||
volume_size: 볼륨 크기 (GB, 기본 50)
|
||||
volume_type: 볼륨 타입 (General SSD, General HDD)
|
||||
security_groups: 보안 그룹 목록 (기본 ["default"])
|
||||
availability_zone: 가용 영역
|
||||
|
||||
Returns:
|
||||
dict: 생성된 인스턴스 정보
|
||||
"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers"
|
||||
|
||||
security_groups = security_groups or ["default"]
|
||||
sg_list = [{"name": sg} for sg in security_groups]
|
||||
|
||||
payload = {
|
||||
"server": {
|
||||
"name": name,
|
||||
"imageRef": image_id,
|
||||
"flavorRef": flavor_id,
|
||||
"networks": [{"subnet": subnet_id}],
|
||||
"key_name": keypair_name,
|
||||
"max_count": 1,
|
||||
"min_count": 1,
|
||||
"block_device_mapping_v2": [
|
||||
{
|
||||
"uuid": image_id,
|
||||
"boot_index": 0,
|
||||
"volume_size": volume_size,
|
||||
"volume_type": volume_type,
|
||||
"device_name": "vda",
|
||||
"source_type": "image",
|
||||
"destination_type": "volume",
|
||||
"delete_on_termination": True,
|
||||
}
|
||||
],
|
||||
"security_groups": sg_list,
|
||||
}
|
||||
}
|
||||
|
||||
if availability_zone:
|
||||
payload["server"]["availability_zone"] = availability_zone
|
||||
|
||||
logger.info(f"인스턴스 생성 요청: {name}")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_instance(self, server_id: str) -> dict:
|
||||
"""인스턴스 삭제"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/{server_id}"
|
||||
logger.info(f"인스턴스 삭제 요청: {server_id}")
|
||||
return self._delete(url)
|
||||
|
||||
def start_instance(self, server_id: str) -> dict:
|
||||
"""인스턴스 시작"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/{server_id}/action"
|
||||
return self._post(url, {"os-start": None})
|
||||
|
||||
def stop_instance(self, server_id: str) -> dict:
|
||||
"""인스턴스 정지"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/{server_id}/action"
|
||||
return self._post(url, {"os-stop": None})
|
||||
|
||||
def reboot_instance(self, server_id: str, hard: bool = False) -> dict:
|
||||
"""인스턴스 재부팅"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/{server_id}/action"
|
||||
reboot_type = "HARD" if hard else "SOFT"
|
||||
return self._post(url, {"reboot": {"type": reboot_type}})
|
||||
|
||||
# ==================== Image ====================
|
||||
|
||||
def get_image_list(self) -> dict:
|
||||
"""이미지 목록 조회"""
|
||||
url = f"{self.image_url}/v2/images"
|
||||
return self._get(url)
|
||||
|
||||
def get_image_info(self, image_id: str) -> dict:
|
||||
"""이미지 상세 조회"""
|
||||
url = f"{self.image_url}/v2/images/{image_id}"
|
||||
return self._get(url)
|
||||
|
||||
def get_image_id_by_name(self, image_name: str, exclude_container: bool = True) -> Optional[str]:
|
||||
"""
|
||||
이미지 이름으로 ID 조회
|
||||
|
||||
Args:
|
||||
image_name: 이미지 이름 (부분 일치)
|
||||
exclude_container: 컨테이너 이미지 제외 여부
|
||||
|
||||
Returns:
|
||||
str: 이미지 ID 또는 None
|
||||
"""
|
||||
data = self.get_image_list()
|
||||
images = data.get("images", [])
|
||||
|
||||
for image in images:
|
||||
name = image.get("name", "")
|
||||
if name.startswith(image_name):
|
||||
if exclude_container and "Container" in name:
|
||||
continue
|
||||
return image.get("id")
|
||||
return None
|
||||
214
nhn/packages/nks.py
Normal file
214
nhn/packages/nks.py
Normal file
@ -0,0 +1,214 @@
|
||||
"""
|
||||
NHN Cloud NKS (Kubernetes Service) API Module
|
||||
|
||||
Kubernetes 클러스터 관리
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .base import BaseAPI, NHNCloudEndpoints, Region
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NKSNodeImages:
|
||||
"""NKS 노드 이미지 ID"""
|
||||
# Ubuntu 20.04 이미지 ID (리전별)
|
||||
UBUNTU_20_04_KR1 = "1213d033-bdf6-4d73-9763-4e8e57c745fb"
|
||||
UBUNTU_20_04_KR2 = "dabb6d10-937d-4952-9ce0-1e576e9164e8"
|
||||
|
||||
|
||||
class ApiNks(BaseAPI):
|
||||
"""NHN Cloud NKS API 클래스"""
|
||||
|
||||
def __init__(self, region: str, token: str):
|
||||
"""
|
||||
Args:
|
||||
region: 리전 (kr1: 판교, kr2: 평촌)
|
||||
token: API 인증 토큰
|
||||
"""
|
||||
super().__init__(region, token)
|
||||
|
||||
if self.region == Region.KR1:
|
||||
self.nks_url = NHNCloudEndpoints.NKS_KR1
|
||||
self.default_node_image = NKSNodeImages.UBUNTU_20_04_KR1
|
||||
else:
|
||||
self.nks_url = NHNCloudEndpoints.NKS_KR2
|
||||
self.default_node_image = NKSNodeImages.UBUNTU_20_04_KR2
|
||||
|
||||
def _get_headers(self, extra_headers: Optional[dict] = None) -> dict:
|
||||
"""NKS API 전용 헤더"""
|
||||
headers = {
|
||||
"X-Auth-Token": self.token,
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"OpenStack-API-Version": "container-infra latest",
|
||||
}
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
return headers
|
||||
|
||||
# ==================== Cluster ====================
|
||||
|
||||
def get_cluster_list(self) -> dict:
|
||||
"""클러스터 목록 조회"""
|
||||
url = f"{self.nks_url}/v1/clusters"
|
||||
return self._get(url)
|
||||
|
||||
def get_cluster_info(self, cluster_name: str) -> dict:
|
||||
"""클러스터 상세 조회"""
|
||||
url = f"{self.nks_url}/v1/clusters/{cluster_name}"
|
||||
return self._get(url)
|
||||
|
||||
def get_cluster_config(self, cluster_name: str) -> str:
|
||||
"""클러스터 kubeconfig 조회"""
|
||||
url = f"{self.nks_url}/v1/clusters/{cluster_name}/config"
|
||||
data = self._get(url)
|
||||
return data.get("config", "")
|
||||
|
||||
def create_public_cluster(
|
||||
self,
|
||||
cluster_name: str,
|
||||
vpc_id: str,
|
||||
subnet_id: str,
|
||||
instance_type: str,
|
||||
keypair_name: str,
|
||||
kubernetes_version: str,
|
||||
external_network_id: str,
|
||||
external_subnet_id: str,
|
||||
availability_zone: str,
|
||||
node_count: int = 1,
|
||||
boot_volume_size: int = 50,
|
||||
boot_volume_type: str = "General SSD",
|
||||
node_image: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Public 클러스터 생성 (외부 접근 가능)
|
||||
|
||||
Args:
|
||||
cluster_name: 클러스터 이름
|
||||
vpc_id: VPC ID
|
||||
subnet_id: 서브넷 ID
|
||||
instance_type: 인스턴스 타입 (Flavor ID)
|
||||
keypair_name: Keypair 이름
|
||||
kubernetes_version: Kubernetes 버전 (예: v1.28.3)
|
||||
external_network_id: 외부 네트워크 ID
|
||||
external_subnet_id: 외부 서브넷 ID
|
||||
availability_zone: 가용 영역 (예: kr-pub-a)
|
||||
node_count: 노드 수 (기본 1)
|
||||
boot_volume_size: 부팅 볼륨 크기 (GB, 기본 50)
|
||||
boot_volume_type: 볼륨 타입 (기본 "General SSD")
|
||||
node_image: 노드 이미지 ID (기본 Ubuntu 20.04)
|
||||
|
||||
Returns:
|
||||
dict: 생성된 클러스터 정보
|
||||
"""
|
||||
url = f"{self.nks_url}/v1/clusters"
|
||||
|
||||
payload = {
|
||||
"cluster_template_id": "iaas_console",
|
||||
"create_timeout": 60,
|
||||
"fixed_network": vpc_id,
|
||||
"fixed_subnet": subnet_id,
|
||||
"flavor_id": instance_type,
|
||||
"keypair": keypair_name,
|
||||
"labels": {
|
||||
"availability_zone": availability_zone,
|
||||
"boot_volume_size": str(boot_volume_size),
|
||||
"boot_volume_type": boot_volume_type,
|
||||
"ca_enable": "false",
|
||||
"cert_manager_api": "True",
|
||||
"clusterautoscale": "nodegroupfeature",
|
||||
"external_network_id": external_network_id,
|
||||
"external_subnet_id_list": external_subnet_id,
|
||||
"kube_tag": kubernetes_version,
|
||||
"master_lb_floating_ip_enabled": "True",
|
||||
"node_image": node_image or self.default_node_image,
|
||||
},
|
||||
"name": cluster_name,
|
||||
"node_count": str(node_count),
|
||||
}
|
||||
|
||||
logger.info(f"Public 클러스터 생성 요청: {cluster_name}")
|
||||
return self._post(url, payload)
|
||||
|
||||
def create_private_cluster(
|
||||
self,
|
||||
cluster_name: str,
|
||||
vpc_id: str,
|
||||
subnet_id: str,
|
||||
instance_type: str,
|
||||
keypair_name: str,
|
||||
kubernetes_version: str,
|
||||
availability_zone: str,
|
||||
node_count: int = 1,
|
||||
boot_volume_size: int = 50,
|
||||
boot_volume_type: str = "General SSD",
|
||||
node_image: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Private 클러스터 생성 (내부 접근만 가능)
|
||||
|
||||
Args:
|
||||
cluster_name: 클러스터 이름
|
||||
vpc_id: VPC ID
|
||||
subnet_id: 서브넷 ID
|
||||
instance_type: 인스턴스 타입 (Flavor ID)
|
||||
keypair_name: Keypair 이름
|
||||
kubernetes_version: Kubernetes 버전 (예: v1.28.3)
|
||||
availability_zone: 가용 영역 (예: kr-pub-a)
|
||||
node_count: 노드 수 (기본 1)
|
||||
boot_volume_size: 부팅 볼륨 크기 (GB, 기본 50)
|
||||
boot_volume_type: 볼륨 타입 (기본 "General SSD")
|
||||
node_image: 노드 이미지 ID (기본 Ubuntu 20.04)
|
||||
|
||||
Returns:
|
||||
dict: 생성된 클러스터 정보
|
||||
"""
|
||||
url = f"{self.nks_url}/v1/clusters"
|
||||
|
||||
payload = {
|
||||
"cluster_template_id": "iaas_console",
|
||||
"create_timeout": 60,
|
||||
"fixed_network": vpc_id,
|
||||
"fixed_subnet": subnet_id,
|
||||
"flavor_id": instance_type,
|
||||
"keypair": keypair_name,
|
||||
"labels": {
|
||||
"availability_zone": availability_zone,
|
||||
"boot_volume_size": str(boot_volume_size),
|
||||
"boot_volume_type": boot_volume_type,
|
||||
"ca_enable": "false",
|
||||
"cert_manager_api": "True",
|
||||
"clusterautoscale": "nodegroupfeature",
|
||||
"kube_tag": kubernetes_version,
|
||||
"master_lb_floating_ip_enabled": "False",
|
||||
"node_image": node_image or self.default_node_image,
|
||||
},
|
||||
"name": cluster_name,
|
||||
"node_count": str(node_count),
|
||||
}
|
||||
|
||||
logger.info(f"Private 클러스터 생성 요청: {cluster_name}")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_cluster(self, cluster_name: str) -> dict:
|
||||
"""클러스터 삭제"""
|
||||
url = f"{self.nks_url}/v1/clusters/{cluster_name}"
|
||||
logger.info(f"클러스터 삭제 요청: {cluster_name}")
|
||||
return self._delete(url)
|
||||
|
||||
# ==================== Node Group ====================
|
||||
|
||||
def get_nodegroup_list(self, cluster_name: str) -> dict:
|
||||
"""노드 그룹 목록 조회"""
|
||||
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups"
|
||||
return self._get(url)
|
||||
|
||||
def get_nodegroup_info(self, cluster_name: str, nodegroup_name: str) -> dict:
|
||||
"""노드 그룹 상세 조회"""
|
||||
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}"
|
||||
return self._get(url)
|
||||
279
nhn/packages/storage.py
Normal file
279
nhn/packages/storage.py
Normal file
@ -0,0 +1,279 @@
|
||||
"""
|
||||
NHN Cloud Object Storage API Module
|
||||
|
||||
Object Storage 컨테이너 및 오브젝트 관리
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
import requests
|
||||
|
||||
from .base import BaseAPI, NHNCloudEndpoints, Region, NHNCloudAPIError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiStorageObject(BaseAPI):
|
||||
"""NHN Cloud Object Storage API 클래스"""
|
||||
|
||||
def __init__(self, region: str, token: str, storage_account: str):
|
||||
"""
|
||||
Args:
|
||||
region: 리전 (kr1: 판교, kr2: 평촌)
|
||||
token: API 인증 토큰
|
||||
storage_account: 스토리지 계정 (AUTH_...)
|
||||
"""
|
||||
super().__init__(region, token)
|
||||
self.storage_account = storage_account
|
||||
|
||||
if self.region == Region.KR1:
|
||||
self.storage_url = NHNCloudEndpoints.STORAGE_KR1
|
||||
else:
|
||||
self.storage_url = NHNCloudEndpoints.STORAGE_KR2
|
||||
|
||||
def _get_url(self, container: Optional[str] = None, obj: Optional[str] = None) -> str:
|
||||
"""URL 생성"""
|
||||
parts = [self.storage_url, self.storage_account]
|
||||
if container:
|
||||
parts.append(container)
|
||||
if obj:
|
||||
parts.append(obj)
|
||||
return "/".join(parts)
|
||||
|
||||
def _get_headers(self, extra_headers: Optional[dict] = None) -> dict:
|
||||
"""Object Storage 전용 헤더"""
|
||||
headers = {"X-Auth-Token": self.token}
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
return headers
|
||||
|
||||
# ==================== Container ====================
|
||||
|
||||
def get_container_list(self) -> List[str]:
|
||||
"""컨테이너 목록 조회"""
|
||||
url = self._get_url()
|
||||
try:
|
||||
response = self._session.get(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
self._handle_error(response)
|
||||
return [c for c in response.text.split("\n") if c]
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"컨테이너 목록 조회 실패: {e}")
|
||||
raise NHNCloudAPIError(f"컨테이너 목록 조회 실패: {e}")
|
||||
|
||||
def create_container(self, container_name: str) -> dict:
|
||||
"""
|
||||
컨테이너 생성
|
||||
|
||||
Args:
|
||||
container_name: 컨테이너 이름
|
||||
|
||||
Returns:
|
||||
dict: 생성 결과
|
||||
"""
|
||||
url = self._get_url(container_name)
|
||||
try:
|
||||
response = self._session.put(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"컨테이너 생성: {container_name}")
|
||||
return {"status": response.status_code, "container": container_name}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"컨테이너 생성 실패: {e}")
|
||||
raise NHNCloudAPIError(f"컨테이너 생성 실패: {e}")
|
||||
|
||||
def delete_container(self, container_name: str) -> dict:
|
||||
"""컨테이너 삭제"""
|
||||
url = self._get_url(container_name)
|
||||
try:
|
||||
response = self._session.delete(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"컨테이너 삭제: {container_name}")
|
||||
return {"status": response.status_code, "container": container_name}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"컨테이너 삭제 실패: {e}")
|
||||
raise NHNCloudAPIError(f"컨테이너 삭제 실패: {e}")
|
||||
|
||||
def set_container_public(self, container_name: str, is_public: bool = True) -> dict:
|
||||
"""
|
||||
컨테이너 공개/비공개 설정
|
||||
|
||||
Args:
|
||||
container_name: 컨테이너 이름
|
||||
is_public: 공개 여부 (True: 공개, False: 비공개)
|
||||
|
||||
Returns:
|
||||
dict: 설정 결과
|
||||
"""
|
||||
url = self._get_url(container_name)
|
||||
headers = self._get_headers()
|
||||
headers["X-Container-Read"] = ".r:*" if is_public else ""
|
||||
|
||||
try:
|
||||
response = self._session.post(
|
||||
url,
|
||||
headers=headers,
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"컨테이너 공개 설정: {container_name} -> {is_public}")
|
||||
return {"status": response.status_code, "public": is_public}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"컨테이너 공개 설정 실패: {e}")
|
||||
raise NHNCloudAPIError(f"컨테이너 공개 설정 실패: {e}")
|
||||
|
||||
# ==================== Object ====================
|
||||
|
||||
def get_object_list(self, container_name: str) -> List[str]:
|
||||
"""오브젝트 목록 조회"""
|
||||
url = self._get_url(container_name)
|
||||
try:
|
||||
response = self._session.get(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
self._handle_error(response)
|
||||
return [o for o in response.text.split("\n") if o]
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 목록 조회 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 목록 조회 실패: {e}")
|
||||
|
||||
def upload_object(
|
||||
self,
|
||||
container_name: str,
|
||||
object_name: str,
|
||||
file_path: str,
|
||||
) -> dict:
|
||||
"""
|
||||
오브젝트 업로드
|
||||
|
||||
Args:
|
||||
container_name: 컨테이너 이름
|
||||
object_name: 오브젝트 이름
|
||||
file_path: 업로드할 파일 경로
|
||||
|
||||
Returns:
|
||||
dict: 업로드 결과
|
||||
"""
|
||||
url = self._get_url(container_name, object_name)
|
||||
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
response = self._session.put(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
data=f.read(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"오브젝트 업로드: {container_name}/{object_name}")
|
||||
return {"status": response.status_code, "object": object_name}
|
||||
except FileNotFoundError:
|
||||
raise NHNCloudAPIError(f"파일을 찾을 수 없습니다: {file_path}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 업로드 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 업로드 실패: {e}")
|
||||
|
||||
def upload_object_data(
|
||||
self,
|
||||
container_name: str,
|
||||
object_name: str,
|
||||
data: bytes,
|
||||
content_type: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
데이터를 오브젝트로 업로드
|
||||
|
||||
Args:
|
||||
container_name: 컨테이너 이름
|
||||
object_name: 오브젝트 이름
|
||||
data: 업로드할 데이터
|
||||
content_type: Content-Type 헤더
|
||||
|
||||
Returns:
|
||||
dict: 업로드 결과
|
||||
"""
|
||||
url = self._get_url(container_name, object_name)
|
||||
headers = self._get_headers()
|
||||
if content_type:
|
||||
headers["Content-Type"] = content_type
|
||||
|
||||
try:
|
||||
response = self._session.put(
|
||||
url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"오브젝트 업로드: {container_name}/{object_name}")
|
||||
return {"status": response.status_code, "object": object_name}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 업로드 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 업로드 실패: {e}")
|
||||
|
||||
def download_object(self, container_name: str, object_name: str) -> bytes:
|
||||
"""
|
||||
오브젝트 다운로드
|
||||
|
||||
Args:
|
||||
container_name: 컨테이너 이름
|
||||
object_name: 오브젝트 이름
|
||||
|
||||
Returns:
|
||||
bytes: 오브젝트 데이터
|
||||
"""
|
||||
url = self._get_url(container_name, object_name)
|
||||
|
||||
try:
|
||||
response = self._session.get(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
self._handle_error(response)
|
||||
return response.content
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 다운로드 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 다운로드 실패: {e}")
|
||||
|
||||
def delete_object(self, container_name: str, object_name: str) -> dict:
|
||||
"""오브젝트 삭제"""
|
||||
url = self._get_url(container_name, object_name)
|
||||
|
||||
try:
|
||||
response = self._session.delete(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"오브젝트 삭제: {container_name}/{object_name}")
|
||||
return {"status": response.status_code, "object": object_name}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 삭제 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 삭제 실패: {e}")
|
||||
|
||||
def get_object_info(self, container_name: str, object_name: str) -> dict:
|
||||
"""오브젝트 메타데이터 조회"""
|
||||
url = self._get_url(container_name, object_name)
|
||||
|
||||
try:
|
||||
response = self._session.head(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
return dict(response.headers)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 정보 조회 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 정보 조회 실패: {e}")
|
||||
123
nhn/packages/token.py
Normal file
123
nhn/packages/token.py
Normal file
@ -0,0 +1,123 @@
|
||||
"""
|
||||
NHN Cloud Token Management Module
|
||||
|
||||
NHN Cloud API 인증 토큰 관리
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
import requests
|
||||
|
||||
from .base import NHNCloudEndpoints, NHNCloudAPIError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenResponse:
|
||||
"""토큰 응답 데이터"""
|
||||
token: str
|
||||
tenant_id: str
|
||||
expires_at: Optional[str] = None
|
||||
|
||||
|
||||
class NHNCloudToken:
|
||||
"""NHN Cloud 토큰 관리 클래스"""
|
||||
|
||||
TOKEN_URL = f"{NHNCloudEndpoints.IDENTITY}/v2.0/tokens"
|
||||
|
||||
def __init__(self, tenant_id: str, username: str, password: str):
|
||||
"""
|
||||
Args:
|
||||
tenant_id: NHN Cloud 테넌트 ID
|
||||
username: NHN Cloud 사용자 이메일
|
||||
password: NHN Cloud API 비밀번호
|
||||
"""
|
||||
self.tenant_id = tenant_id
|
||||
self.username = username
|
||||
self.password = password
|
||||
self._token: Optional[str] = None
|
||||
|
||||
def create_token(self) -> TokenResponse:
|
||||
"""
|
||||
새 토큰 생성
|
||||
|
||||
Returns:
|
||||
TokenResponse: 생성된 토큰 정보
|
||||
|
||||
Raises:
|
||||
NHNCloudAPIError: 토큰 생성 실패 시
|
||||
"""
|
||||
payload = {
|
||||
"auth": {
|
||||
"tenantId": self.tenant_id,
|
||||
"passwordCredentials": {
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"[NHN API] 토큰 생성 요청 - URL={self.TOKEN_URL}, tenant_id={self.tenant_id}, username={self.username}")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
self.TOKEN_URL,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
logger.info(f"[NHN API] 토큰 생성 응답 - status_code={response.status_code}")
|
||||
|
||||
data = response.json()
|
||||
|
||||
if "error" in data:
|
||||
error = data["error"]
|
||||
logger.error(f"[NHN API] 토큰 생성 실패 - code={error.get('code')}, message={error.get('message')}, details={error}")
|
||||
raise NHNCloudAPIError(
|
||||
message=error.get("message", "토큰 생성 실패"),
|
||||
code=error.get("code"),
|
||||
details=error,
|
||||
)
|
||||
|
||||
access = data.get("access", {})
|
||||
token_info = access.get("token", {})
|
||||
self._token = token_info.get("id")
|
||||
|
||||
token_preview = self._token[:8] + "..." if self._token else "None"
|
||||
logger.info(f"[NHN API] 토큰 생성 성공 - tenant_id={self.tenant_id}, token={token_preview}, expires={token_info.get('expires')}")
|
||||
|
||||
return TokenResponse(
|
||||
token=self._token,
|
||||
tenant_id=self.tenant_id,
|
||||
expires_at=token_info.get("expires"),
|
||||
)
|
||||
|
||||
except requests.exceptions.Timeout as e:
|
||||
logger.error(f"[NHN API] 토큰 생성 타임아웃 - URL={self.TOKEN_URL}, error={e}")
|
||||
raise NHNCloudAPIError(f"토큰 생성 요청 타임아웃: {e}")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.error(f"[NHN API] 토큰 생성 연결 실패 - URL={self.TOKEN_URL}, error={e}")
|
||||
raise NHNCloudAPIError(f"토큰 생성 서버 연결 실패: {e}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"[NHN API] 토큰 생성 요청 실패 - URL={self.TOKEN_URL}, error={e}")
|
||||
raise NHNCloudAPIError(f"토큰 생성 요청 실패: {e}")
|
||||
|
||||
@property
|
||||
def token(self) -> Optional[str]:
|
||||
"""현재 토큰 반환"""
|
||||
return self._token
|
||||
|
||||
def get_token(self) -> str:
|
||||
"""
|
||||
토큰 반환 (없으면 생성)
|
||||
|
||||
Returns:
|
||||
str: API 토큰
|
||||
"""
|
||||
if not self._token:
|
||||
self.create_token()
|
||||
return self._token
|
||||
272
nhn/packages/vpc.py
Normal file
272
nhn/packages/vpc.py
Normal file
@ -0,0 +1,272 @@
|
||||
"""
|
||||
NHN Cloud VPC API Module
|
||||
|
||||
VPC, Subnet, Routing Table, Floating IP 관리
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .base import BaseAPI, NHNCloudEndpoints, Region
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiVpc(BaseAPI):
|
||||
"""NHN Cloud VPC API 클래스"""
|
||||
|
||||
def __init__(self, region: str, token: str):
|
||||
"""
|
||||
Args:
|
||||
region: 리전 (kr1: 판교, kr2: 평촌)
|
||||
token: API 인증 토큰
|
||||
"""
|
||||
super().__init__(region, token)
|
||||
|
||||
if self.region == Region.KR1:
|
||||
self.vpc_url = NHNCloudEndpoints.NETWORK_KR1
|
||||
else:
|
||||
self.vpc_url = NHNCloudEndpoints.NETWORK_KR2
|
||||
|
||||
# ==================== VPC ====================
|
||||
|
||||
def get_vpc_list(self) -> dict:
|
||||
"""VPC 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcs"
|
||||
logger.info(f"[NHN API] VPC 목록 조회 요청 - URL={url}")
|
||||
result = self._get(url)
|
||||
vpc_count = len(result.get("vpcs", []))
|
||||
logger.info(f"[NHN API] VPC 목록 조회 완료 - count={vpc_count}")
|
||||
return result
|
||||
|
||||
def get_vpc_info(self, vpc_id: str) -> dict:
|
||||
"""VPC 상세 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcs/{vpc_id}"
|
||||
logger.info(f"[NHN API] VPC 상세 조회 요청 - URL={url}, vpc_id={vpc_id}")
|
||||
result = self._get(url)
|
||||
logger.info(f"[NHN API] VPC 상세 조회 완료 - vpc_id={vpc_id}")
|
||||
return result
|
||||
|
||||
def get_vpc_id_by_name(self, vpc_name: str) -> Optional[str]:
|
||||
"""VPC 이름으로 ID 조회"""
|
||||
data = self.get_vpc_list()
|
||||
vpcs = data.get("vpcs", [])
|
||||
|
||||
for vpc in vpcs:
|
||||
if vpc.get("name", "").startswith(vpc_name):
|
||||
return vpc.get("id")
|
||||
return None
|
||||
|
||||
def create_vpc(self, name: str, cidr: str) -> dict:
|
||||
"""
|
||||
VPC 생성
|
||||
|
||||
Args:
|
||||
name: VPC 이름
|
||||
cidr: CIDR 블록 (예: 10.0.0.0/16)
|
||||
|
||||
Returns:
|
||||
dict: 생성된 VPC 정보
|
||||
"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcs"
|
||||
payload = {"vpc": {"name": name, "cidrv4": cidr}}
|
||||
logger.info(f"VPC 생성 요청: {name} ({cidr})")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_vpc(self, vpc_id: str) -> dict:
|
||||
"""VPC 삭제"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcs/{vpc_id}"
|
||||
logger.info(f"VPC 삭제 요청: {vpc_id}")
|
||||
return self._delete(url)
|
||||
|
||||
# ==================== Subnet ====================
|
||||
|
||||
def get_subnet_list(self) -> dict:
|
||||
"""서브넷 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcsubnets"
|
||||
return self._get(url)
|
||||
|
||||
def get_subnet_info(self, subnet_id: str) -> dict:
|
||||
"""서브넷 상세 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcsubnets/{subnet_id}"
|
||||
return self._get(url)
|
||||
|
||||
def get_subnet_id_by_name(self, subnet_name: str) -> Optional[str]:
|
||||
"""서브넷 이름으로 ID 조회"""
|
||||
data = self.get_subnet_list()
|
||||
subnets = data.get("vpcsubnets", [])
|
||||
|
||||
for subnet in subnets:
|
||||
name = subnet.get("name", "")
|
||||
if name.startswith(subnet_name) and "rt" not in name:
|
||||
return subnet.get("id")
|
||||
return None
|
||||
|
||||
def create_subnet(self, vpc_id: str, cidr: str, name: str) -> dict:
|
||||
"""
|
||||
서브넷 생성
|
||||
|
||||
Args:
|
||||
vpc_id: VPC ID
|
||||
cidr: CIDR 블록 (예: 10.0.1.0/24)
|
||||
name: 서브넷 이름
|
||||
|
||||
Returns:
|
||||
dict: 생성된 서브넷 정보
|
||||
"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcsubnets"
|
||||
payload = {"vpcsubnet": {"vpc_id": vpc_id, "cidr": cidr, "name": name}}
|
||||
logger.info(f"서브넷 생성 요청: {name} ({cidr})")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_subnet(self, subnet_id: str) -> dict:
|
||||
"""서브넷 삭제"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcsubnets/{subnet_id}"
|
||||
logger.info(f"서브넷 삭제 요청: {subnet_id}")
|
||||
return self._delete(url)
|
||||
|
||||
# ==================== Routing Table ====================
|
||||
|
||||
def get_routing_table_list(self, detail: bool = True) -> dict:
|
||||
"""라우팅 테이블 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables"
|
||||
params = {"detail": "true"} if detail else None
|
||||
return self._get(url, params=params)
|
||||
|
||||
def get_routing_table_info(self, routingtable_id: str) -> dict:
|
||||
"""라우팅 테이블 상세 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables/{routingtable_id}"
|
||||
return self._get(url)
|
||||
|
||||
def get_default_routing_table_id(self) -> Optional[str]:
|
||||
"""기본 라우팅 테이블 ID 조회"""
|
||||
data = self.get_routing_table_list(detail=False)
|
||||
tables = data.get("routingtables", [])
|
||||
return tables[0].get("id") if tables else None
|
||||
|
||||
def create_routing_table(self, name: str, vpc_id: str, distributed: bool = True) -> dict:
|
||||
"""
|
||||
라우팅 테이블 생성
|
||||
|
||||
Args:
|
||||
name: 라우팅 테이블 이름
|
||||
vpc_id: VPC ID
|
||||
distributed: 분산 라우팅 여부 (기본 True)
|
||||
|
||||
Returns:
|
||||
dict: 생성된 라우팅 테이블 정보
|
||||
"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables"
|
||||
payload = {
|
||||
"routingtable": {
|
||||
"name": name,
|
||||
"vpc_id": vpc_id,
|
||||
"distributed": str(distributed).lower(),
|
||||
}
|
||||
}
|
||||
logger.info(f"라우팅 테이블 생성 요청: {name}")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_routing_table(self, routingtable_id: str) -> dict:
|
||||
"""라우팅 테이블 삭제"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables/{routingtable_id}"
|
||||
logger.info(f"라우팅 테이블 삭제 요청: {routingtable_id}")
|
||||
return self._delete(url)
|
||||
|
||||
def set_default_routing_table(self, routingtable_id: str) -> dict:
|
||||
"""라우팅 테이블을 기본으로 설정"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables/{routingtable_id}/set_as_default"
|
||||
return self._put(url)
|
||||
|
||||
def attach_gateway_to_routing_table(self, routingtable_id: str, gateway_id: str) -> dict:
|
||||
"""라우팅 테이블에 인터넷 게이트웨이 연결"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables/{routingtable_id}/attach_gateway"
|
||||
payload = {"gateway_id": gateway_id}
|
||||
logger.info(f"게이트웨이 연결 요청: {routingtable_id} -> {gateway_id}")
|
||||
return self._put(url, payload)
|
||||
|
||||
def detach_gateway_from_routing_table(self, routingtable_id: str) -> dict:
|
||||
"""라우팅 테이블에서 인터넷 게이트웨이 분리"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables/{routingtable_id}/detach_gateway"
|
||||
return self._put(url)
|
||||
|
||||
def attach_routing_table_to_subnet(self, subnet_id: str, routingtable_id: str) -> dict:
|
||||
"""서브넷에 라우팅 테이블 연결"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcsubnets/{subnet_id}/attach_routingtable"
|
||||
payload = {"routingtable_id": routingtable_id}
|
||||
return self._put(url, payload)
|
||||
|
||||
# ==================== Floating IP ====================
|
||||
|
||||
def get_external_network_id(self) -> dict:
|
||||
"""외부 네트워크 ID 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/networks"
|
||||
params = {"router:external": "True"}
|
||||
return self._get(url, params=params)
|
||||
|
||||
def get_floating_ip_list(self) -> dict:
|
||||
"""Floating IP 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/floatingips"
|
||||
return self._get(url)
|
||||
|
||||
def create_floating_ip(self, external_network_id: str) -> dict:
|
||||
"""
|
||||
Floating IP 생성
|
||||
|
||||
Args:
|
||||
external_network_id: 외부 네트워크 ID
|
||||
|
||||
Returns:
|
||||
dict: 생성된 Floating IP 정보
|
||||
"""
|
||||
url = f"{self.vpc_url}/v2.0/floatingips"
|
||||
payload = {"floatingip": {"floating_network_id": external_network_id}}
|
||||
logger.info("Floating IP 생성 요청")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_floating_ip(self, floating_ip_id: str) -> dict:
|
||||
"""Floating IP 삭제"""
|
||||
url = f"{self.vpc_url}/v2.0/floatingips/{floating_ip_id}"
|
||||
logger.info(f"Floating IP 삭제 요청: {floating_ip_id}")
|
||||
return self._delete(url)
|
||||
|
||||
def attach_floating_ip(self, floating_ip_id: str, port_id: str) -> dict:
|
||||
"""Floating IP를 포트에 연결"""
|
||||
url = f"{self.vpc_url}/v2.0/floatingips/{floating_ip_id}"
|
||||
payload = {"floatingip": {"port_id": port_id}}
|
||||
return self._put(url, payload)
|
||||
|
||||
def detach_floating_ip(self, floating_ip_id: str) -> dict:
|
||||
"""Floating IP 연결 해제"""
|
||||
url = f"{self.vpc_url}/v2.0/floatingips/{floating_ip_id}"
|
||||
payload = {"floatingip": {"port_id": None}}
|
||||
return self._put(url, payload)
|
||||
|
||||
# ==================== Security Group ====================
|
||||
|
||||
def get_security_group_list(self) -> dict:
|
||||
"""보안 그룹 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/security-groups"
|
||||
return self._get(url)
|
||||
|
||||
def get_security_group_info(self, security_group_id: str) -> dict:
|
||||
"""보안 그룹 상세 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/security-groups/{security_group_id}"
|
||||
return self._get(url)
|
||||
|
||||
# ==================== Port (NIC) ====================
|
||||
|
||||
def get_port_list(self) -> dict:
|
||||
"""포트 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/ports"
|
||||
return self._get(url)
|
||||
|
||||
def get_port_id_by_device(self, device_id: str) -> Optional[str]:
|
||||
"""디바이스 ID로 포트 ID 조회"""
|
||||
data = self.get_port_list()
|
||||
ports = data.get("ports", [])
|
||||
|
||||
for port in ports:
|
||||
if port.get("device_id", "").startswith(device_id):
|
||||
return port.get("id")
|
||||
return None
|
||||
201
nhn/serializers.py
Normal file
201
nhn/serializers.py
Normal file
@ -0,0 +1,201 @@
|
||||
"""
|
||||
NHN Cloud API Serializers
|
||||
|
||||
API 요청/응답 직렬화
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
# ==================== Token ====================
|
||||
|
||||
|
||||
class TokenRequestSerializer(serializers.Serializer):
|
||||
"""토큰 생성 요청"""
|
||||
|
||||
tenant_id = serializers.CharField(
|
||||
help_text="NHN Cloud 테넌트 ID",
|
||||
max_length=64,
|
||||
)
|
||||
username = serializers.EmailField(
|
||||
help_text="NHN Cloud 사용자 이메일",
|
||||
)
|
||||
password = serializers.CharField(
|
||||
help_text="NHN Cloud API 비밀번호",
|
||||
write_only=True,
|
||||
)
|
||||
|
||||
|
||||
class TokenResponseSerializer(serializers.Serializer):
|
||||
"""토큰 생성 응답"""
|
||||
|
||||
token = serializers.CharField(help_text="API 토큰")
|
||||
tenant_id = serializers.CharField(help_text="테넌트 ID")
|
||||
expires_at = serializers.CharField(help_text="만료 시간", allow_null=True)
|
||||
|
||||
|
||||
# ==================== Common ====================
|
||||
|
||||
|
||||
class ErrorResponseSerializer(serializers.Serializer):
|
||||
"""에러 응답"""
|
||||
|
||||
error = serializers.CharField(help_text="에러 메시지")
|
||||
code = serializers.IntegerField(help_text="에러 코드", required=False)
|
||||
|
||||
|
||||
# ==================== Compute ====================
|
||||
|
||||
|
||||
class ComputeInstanceSerializer(serializers.Serializer):
|
||||
"""인스턴스 생성 요청"""
|
||||
|
||||
name = serializers.CharField(
|
||||
help_text="인스턴스 이름",
|
||||
max_length=255,
|
||||
)
|
||||
image_id = serializers.CharField(
|
||||
help_text="이미지 ID",
|
||||
)
|
||||
flavor_id = serializers.CharField(
|
||||
help_text="Flavor ID",
|
||||
)
|
||||
subnet_id = serializers.CharField(
|
||||
help_text="서브넷 ID",
|
||||
)
|
||||
keypair_name = serializers.CharField(
|
||||
help_text="Keypair 이름",
|
||||
)
|
||||
volume_size = serializers.IntegerField(
|
||||
help_text="볼륨 크기 (GB)",
|
||||
default=50,
|
||||
min_value=20,
|
||||
max_value=2048,
|
||||
)
|
||||
volume_type = serializers.CharField(
|
||||
help_text="볼륨 타입 (General SSD, General HDD)",
|
||||
default="General SSD",
|
||||
required=False,
|
||||
)
|
||||
security_groups = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
help_text="보안 그룹 목록",
|
||||
required=False,
|
||||
)
|
||||
availability_zone = serializers.CharField(
|
||||
help_text="가용 영역",
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
# ==================== VPC ====================
|
||||
|
||||
|
||||
class VpcSerializer(serializers.Serializer):
|
||||
"""VPC 생성 요청"""
|
||||
|
||||
name = serializers.CharField(
|
||||
help_text="VPC 이름",
|
||||
max_length=255,
|
||||
)
|
||||
cidr = serializers.CharField(
|
||||
help_text="CIDR 블록 (예: 10.0.0.0/16)",
|
||||
)
|
||||
|
||||
|
||||
class SubnetSerializer(serializers.Serializer):
|
||||
"""서브넷 생성 요청"""
|
||||
|
||||
vpc_id = serializers.CharField(
|
||||
help_text="VPC ID",
|
||||
)
|
||||
name = serializers.CharField(
|
||||
help_text="서브넷 이름",
|
||||
max_length=255,
|
||||
)
|
||||
cidr = serializers.CharField(
|
||||
help_text="CIDR 블록 (예: 10.0.1.0/24)",
|
||||
)
|
||||
|
||||
|
||||
# ==================== NKS ====================
|
||||
|
||||
|
||||
class NksClusterSerializer(serializers.Serializer):
|
||||
"""NKS 클러스터 생성 요청"""
|
||||
|
||||
cluster_name = serializers.CharField(
|
||||
help_text="클러스터 이름",
|
||||
max_length=255,
|
||||
)
|
||||
vpc_id = serializers.CharField(
|
||||
help_text="VPC ID",
|
||||
)
|
||||
subnet_id = serializers.CharField(
|
||||
help_text="서브넷 ID",
|
||||
)
|
||||
instance_type = serializers.CharField(
|
||||
help_text="인스턴스 타입 (Flavor ID)",
|
||||
)
|
||||
keypair_name = serializers.CharField(
|
||||
help_text="Keypair 이름",
|
||||
)
|
||||
kubernetes_version = serializers.CharField(
|
||||
help_text="Kubernetes 버전 (예: v1.28.3)",
|
||||
)
|
||||
availability_zone = serializers.CharField(
|
||||
help_text="가용 영역 (예: kr-pub-a)",
|
||||
)
|
||||
is_public = serializers.BooleanField(
|
||||
help_text="Public 클러스터 여부 (외부 접근 가능)",
|
||||
default=True,
|
||||
)
|
||||
external_network_id = serializers.CharField(
|
||||
help_text="외부 네트워크 ID (Public 클러스터 필수)",
|
||||
required=False,
|
||||
)
|
||||
external_subnet_id = serializers.CharField(
|
||||
help_text="외부 서브넷 ID (Public 클러스터 필수)",
|
||||
required=False,
|
||||
)
|
||||
node_count = serializers.IntegerField(
|
||||
help_text="노드 수",
|
||||
default=1,
|
||||
min_value=1,
|
||||
max_value=100,
|
||||
)
|
||||
boot_volume_size = serializers.IntegerField(
|
||||
help_text="부팅 볼륨 크기 (GB)",
|
||||
default=50,
|
||||
min_value=50,
|
||||
max_value=1000,
|
||||
)
|
||||
boot_volume_type = serializers.CharField(
|
||||
help_text="볼륨 타입",
|
||||
default="General SSD",
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
"""Public 클러스터인 경우 external 관련 필드 필수"""
|
||||
if data.get("is_public", True):
|
||||
if not data.get("external_network_id"):
|
||||
raise serializers.ValidationError(
|
||||
{"external_network_id": "Public 클러스터에는 외부 네트워크 ID가 필요합니다."}
|
||||
)
|
||||
if not data.get("external_subnet_id"):
|
||||
raise serializers.ValidationError(
|
||||
{"external_subnet_id": "Public 클러스터에는 외부 서브넷 ID가 필요합니다."}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
# ==================== Storage ====================
|
||||
|
||||
|
||||
class StorageContainerSerializer(serializers.Serializer):
|
||||
"""스토리지 컨테이너 생성 요청"""
|
||||
|
||||
name = serializers.CharField(
|
||||
help_text="컨테이너 이름",
|
||||
max_length=255,
|
||||
)
|
||||
399
nhn/tasks.py
Normal file
399
nhn/tasks.py
Normal file
@ -0,0 +1,399 @@
|
||||
"""
|
||||
NHN Cloud Async Task Runner
|
||||
|
||||
Threading 기반 비동기 작업 실행
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from functools import wraps
|
||||
|
||||
import django
|
||||
from django.db import close_old_connections
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_async(func):
|
||||
"""
|
||||
함수를 비동기로 실행하는 데코레이터
|
||||
Django DB 연결 관리 포함
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
def run():
|
||||
try:
|
||||
# Django 설정 확인
|
||||
django.setup()
|
||||
# 기존 DB 연결 정리
|
||||
close_old_connections()
|
||||
# 실제 함수 실행
|
||||
func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.exception(f"Async task error: {e}")
|
||||
finally:
|
||||
# DB 연결 정리
|
||||
close_old_connections()
|
||||
|
||||
thread = threading.Thread(target=run, daemon=True)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def execute_async_task(task_id, task_func, *args, **kwargs):
|
||||
"""
|
||||
AsyncTask 모델과 연동하여 비동기 작업 실행
|
||||
|
||||
Args:
|
||||
task_id: AsyncTask 모델의 ID
|
||||
task_func: 실행할 함수 (ApiCompute.create_instance 등)
|
||||
*args, **kwargs: 함수에 전달할 인자
|
||||
"""
|
||||
from .models import AsyncTask
|
||||
|
||||
def run():
|
||||
try:
|
||||
django.setup()
|
||||
close_old_connections()
|
||||
|
||||
# 작업 시작
|
||||
task = AsyncTask.objects.get(id=task_id)
|
||||
task.mark_running()
|
||||
logger.info(f"[AsyncTask] 작업 시작: {task_id} ({task.task_type})")
|
||||
|
||||
# 실제 작업 실행
|
||||
result = task_func(*args, **kwargs)
|
||||
|
||||
# 결과에서 리소스 ID 추출
|
||||
resource_id = None
|
||||
if isinstance(result, dict):
|
||||
# 인스턴스 생성 결과
|
||||
if "server" in result:
|
||||
resource_id = result["server"].get("id")
|
||||
# NKS 클러스터 생성 결과
|
||||
elif "uuid" in result:
|
||||
resource_id = result.get("uuid")
|
||||
|
||||
# 성공 처리
|
||||
task.mark_success(result_data=result, resource_id=resource_id)
|
||||
logger.info(f"[AsyncTask] 작업 완료: {task_id}, resource_id={resource_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"[AsyncTask] 작업 실패: {task_id}")
|
||||
try:
|
||||
task = AsyncTask.objects.get(id=task_id)
|
||||
task.mark_failed(str(e))
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
close_old_connections()
|
||||
|
||||
thread = threading.Thread(target=run, daemon=True)
|
||||
thread.start()
|
||||
logger.info(f"[AsyncTask] 백그라운드 스레드 시작: {task_id}")
|
||||
return thread
|
||||
|
||||
|
||||
def create_instance_async(region, tenant_id, token, instance_data):
|
||||
"""
|
||||
인스턴스 비동기 생성
|
||||
|
||||
Args:
|
||||
region: 리전
|
||||
tenant_id: 테넌트 ID
|
||||
token: API 토큰
|
||||
instance_data: 인스턴스 생성 데이터 (dict)
|
||||
|
||||
Returns:
|
||||
AsyncTask: 생성된 작업 객체
|
||||
"""
|
||||
from .models import AsyncTask
|
||||
from .packages.compute import ApiCompute
|
||||
|
||||
# 작업 레코드 생성
|
||||
task = AsyncTask.objects.create(
|
||||
task_type=AsyncTask.TaskType.INSTANCE_CREATE,
|
||||
request_data=instance_data,
|
||||
resource_name=instance_data.get("name", ""),
|
||||
)
|
||||
|
||||
# API 객체 생성
|
||||
api = ApiCompute(region, tenant_id, token)
|
||||
|
||||
# 비동기 실행
|
||||
execute_async_task(
|
||||
task_id=task.id,
|
||||
task_func=api.create_instance,
|
||||
name=instance_data["name"],
|
||||
image_id=instance_data["image_id"],
|
||||
flavor_id=instance_data["flavor_id"],
|
||||
subnet_id=instance_data["subnet_id"],
|
||||
keypair_name=instance_data.get("keypair_name", ""),
|
||||
volume_size=instance_data.get("volume_size", 50),
|
||||
volume_type=instance_data.get("volume_type", "General SSD"),
|
||||
security_groups=instance_data.get("security_groups"),
|
||||
availability_zone=instance_data.get("availability_zone"),
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
def create_nks_cluster_async(region, tenant_id, token, cluster_data):
|
||||
"""
|
||||
NKS 클러스터 비동기 생성
|
||||
|
||||
Args:
|
||||
region: 리전
|
||||
tenant_id: 테넌트 ID
|
||||
token: API 토큰
|
||||
cluster_data: 클러스터 생성 데이터 (dict)
|
||||
|
||||
Returns:
|
||||
AsyncTask: 생성된 작업 객체
|
||||
"""
|
||||
from .models import AsyncTask
|
||||
from .packages.nks import ApiNks
|
||||
|
||||
# 작업 레코드 생성
|
||||
task = AsyncTask.objects.create(
|
||||
task_type=AsyncTask.TaskType.NKS_CREATE,
|
||||
request_data=cluster_data,
|
||||
resource_name=cluster_data.get("cluster_name", ""),
|
||||
)
|
||||
|
||||
# API 객체 생성
|
||||
api = ApiNks(region, tenant_id, token)
|
||||
|
||||
# 비동기 실행
|
||||
execute_async_task(
|
||||
task_id=task.id,
|
||||
task_func=api.create_cluster,
|
||||
**cluster_data,
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
# ==================== Instance 비동기 작업 ====================
|
||||
|
||||
|
||||
def delete_instance_async(region, tenant_id, token, server_id, server_name=""):
|
||||
"""인스턴스 비동기 삭제"""
|
||||
from .models import AsyncTask
|
||||
from .packages.compute import ApiCompute
|
||||
|
||||
task = AsyncTask.objects.create(
|
||||
task_type=AsyncTask.TaskType.INSTANCE_DELETE,
|
||||
request_data={"server_id": server_id},
|
||||
resource_name=server_name,
|
||||
resource_id=server_id,
|
||||
)
|
||||
|
||||
api = ApiCompute(region, tenant_id, token)
|
||||
execute_async_task(
|
||||
task_id=task.id,
|
||||
task_func=api.delete_instance,
|
||||
server_id=server_id,
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
def instance_action_async(region, tenant_id, token, server_id, action, server_name="", hard=False):
|
||||
"""인스턴스 액션 비동기 실행 (start/stop/reboot)"""
|
||||
from .models import AsyncTask
|
||||
from .packages.compute import ApiCompute
|
||||
|
||||
task = AsyncTask.objects.create(
|
||||
task_type=AsyncTask.TaskType.INSTANCE_ACTION,
|
||||
request_data={"server_id": server_id, "action": action, "hard": hard},
|
||||
resource_name=server_name,
|
||||
resource_id=server_id,
|
||||
)
|
||||
|
||||
api = ApiCompute(region, tenant_id, token)
|
||||
|
||||
if action == "start":
|
||||
task_func = api.start_instance
|
||||
elif action == "stop":
|
||||
task_func = api.stop_instance
|
||||
elif action == "reboot":
|
||||
def task_func(server_id):
|
||||
return api.reboot_instance(server_id, hard=hard)
|
||||
else:
|
||||
raise ValueError(f"Invalid action: {action}")
|
||||
|
||||
execute_async_task(
|
||||
task_id=task.id,
|
||||
task_func=task_func,
|
||||
server_id=server_id,
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
# ==================== VPC 비동기 작업 ====================
|
||||
|
||||
|
||||
def create_vpc_async(region, token, name, cidr):
|
||||
"""VPC 비동기 생성"""
|
||||
from .models import AsyncTask
|
||||
from .packages.vpc import ApiVpc
|
||||
|
||||
task = AsyncTask.objects.create(
|
||||
task_type=AsyncTask.TaskType.VPC_CREATE,
|
||||
request_data={"name": name, "cidr": cidr},
|
||||
resource_name=name,
|
||||
)
|
||||
|
||||
api = ApiVpc(region, token)
|
||||
execute_async_task(
|
||||
task_id=task.id,
|
||||
task_func=api.create_vpc,
|
||||
name=name,
|
||||
cidr=cidr,
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
def delete_vpc_async(region, token, vpc_id, vpc_name=""):
|
||||
"""VPC 비동기 삭제"""
|
||||
from .models import AsyncTask
|
||||
from .packages.vpc import ApiVpc
|
||||
|
||||
task = AsyncTask.objects.create(
|
||||
task_type=AsyncTask.TaskType.VPC_DELETE,
|
||||
request_data={"vpc_id": vpc_id},
|
||||
resource_name=vpc_name,
|
||||
resource_id=vpc_id,
|
||||
)
|
||||
|
||||
api = ApiVpc(region, token)
|
||||
execute_async_task(
|
||||
task_id=task.id,
|
||||
task_func=api.delete_vpc,
|
||||
vpc_id=vpc_id,
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
# ==================== Subnet 비동기 작업 ====================
|
||||
|
||||
|
||||
def create_subnet_async(region, token, vpc_id, cidr, name):
|
||||
"""서브넷 비동기 생성"""
|
||||
from .models import AsyncTask
|
||||
from .packages.vpc import ApiVpc
|
||||
|
||||
task = AsyncTask.objects.create(
|
||||
task_type=AsyncTask.TaskType.SUBNET_CREATE,
|
||||
request_data={"vpc_id": vpc_id, "cidr": cidr, "name": name},
|
||||
resource_name=name,
|
||||
)
|
||||
|
||||
api = ApiVpc(region, token)
|
||||
execute_async_task(
|
||||
task_id=task.id,
|
||||
task_func=api.create_subnet,
|
||||
vpc_id=vpc_id,
|
||||
cidr=cidr,
|
||||
name=name,
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
def delete_subnet_async(region, token, subnet_id, subnet_name=""):
|
||||
"""서브넷 비동기 삭제"""
|
||||
from .models import AsyncTask
|
||||
from .packages.vpc import ApiVpc
|
||||
|
||||
task = AsyncTask.objects.create(
|
||||
task_type=AsyncTask.TaskType.SUBNET_DELETE,
|
||||
request_data={"subnet_id": subnet_id},
|
||||
resource_name=subnet_name,
|
||||
resource_id=subnet_id,
|
||||
)
|
||||
|
||||
api = ApiVpc(region, token)
|
||||
execute_async_task(
|
||||
task_id=task.id,
|
||||
task_func=api.delete_subnet,
|
||||
subnet_id=subnet_id,
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
# ==================== NKS 비동기 작업 ====================
|
||||
|
||||
|
||||
def delete_nks_cluster_async(region, token, cluster_name):
|
||||
"""NKS 클러스터 비동기 삭제"""
|
||||
from .models import AsyncTask
|
||||
from .packages.nks import ApiNks
|
||||
|
||||
task = AsyncTask.objects.create(
|
||||
task_type=AsyncTask.TaskType.NKS_DELETE,
|
||||
request_data={"cluster_name": cluster_name},
|
||||
resource_name=cluster_name,
|
||||
)
|
||||
|
||||
api = ApiNks(region, token)
|
||||
execute_async_task(
|
||||
task_id=task.id,
|
||||
task_func=api.delete_cluster,
|
||||
cluster_name=cluster_name,
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
# ==================== Storage 비동기 작업 ====================
|
||||
|
||||
|
||||
def create_storage_container_async(region, token, storage_account, container_name):
|
||||
"""스토리지 컨테이너 비동기 생성"""
|
||||
from .models import AsyncTask
|
||||
from .packages.storage import ApiStorageObject
|
||||
|
||||
task = AsyncTask.objects.create(
|
||||
task_type=AsyncTask.TaskType.STORAGE_CREATE,
|
||||
request_data={"container_name": container_name},
|
||||
resource_name=container_name,
|
||||
)
|
||||
|
||||
api = ApiStorageObject(region, token, storage_account)
|
||||
execute_async_task(
|
||||
task_id=task.id,
|
||||
task_func=api.create_container,
|
||||
container_name=container_name,
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
def delete_storage_container_async(region, token, storage_account, container_name):
|
||||
"""스토리지 컨테이너 비동기 삭제"""
|
||||
from .models import AsyncTask
|
||||
from .packages.storage import ApiStorageObject
|
||||
|
||||
task = AsyncTask.objects.create(
|
||||
task_type=AsyncTask.TaskType.STORAGE_DELETE,
|
||||
request_data={"container_name": container_name},
|
||||
resource_name=container_name,
|
||||
)
|
||||
|
||||
api = ApiStorageObject(region, token, storage_account)
|
||||
execute_async_task(
|
||||
task_id=task.id,
|
||||
task_func=api.delete_container,
|
||||
container_name=container_name,
|
||||
)
|
||||
|
||||
return task
|
||||
3
nhn/tests.py
Normal file
3
nhn/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
52
nhn/urls.py
Normal file
52
nhn/urls.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""
|
||||
NHN Cloud API URL Configuration
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
# ==================== Token ====================
|
||||
path("token/", views.TokenCreateView.as_view(), name="token-create"),
|
||||
|
||||
# ==================== Compute ====================
|
||||
path("compute/flavors/", views.ComputeFlavorListView.as_view(), name="compute-flavor-list"),
|
||||
path("compute/keypairs/", views.ComputeKeypairListView.as_view(), name="compute-keypair-list"),
|
||||
path("compute/images/", views.ComputeImageListView.as_view(), name="compute-image-list"),
|
||||
path("compute/instances/", views.ComputeInstanceListView.as_view(), name="compute-instance-list"),
|
||||
path("compute/instances/create/", views.ComputeInstanceCreateView.as_view(), name="compute-instance-create"),
|
||||
path("compute/instances/<str:server_id>/", views.ComputeInstanceDetailView.as_view(), name="compute-instance-detail"),
|
||||
path("compute/instances/<str:server_id>/action/", views.ComputeInstanceActionView.as_view(), name="compute-instance-action"),
|
||||
|
||||
# ==================== VPC ====================
|
||||
path("vpc/", views.VpcListView.as_view(), name="vpc-list"),
|
||||
path("vpc/create/", views.VpcCreateView.as_view(), name="vpc-create"),
|
||||
path("vpc/<str:vpc_id>/", views.VpcDetailView.as_view(), name="vpc-detail"),
|
||||
|
||||
# ==================== Subnet ====================
|
||||
path("subnet/", views.SubnetListView.as_view(), name="subnet-list"),
|
||||
path("subnet/create/", views.SubnetCreateView.as_view(), name="subnet-create"),
|
||||
path("subnet/<str:subnet_id>/", views.SubnetDetailView.as_view(), name="subnet-detail"),
|
||||
|
||||
# ==================== Floating IP ====================
|
||||
path("floatingip/", views.FloatingIpListView.as_view(), name="floatingip-list"),
|
||||
|
||||
# ==================== Security Group ====================
|
||||
path("securitygroup/", views.SecurityGroupListView.as_view(), name="securitygroup-list"),
|
||||
|
||||
# ==================== Async Task ====================
|
||||
path("tasks/", views.AsyncTaskListView.as_view(), name="task-list"),
|
||||
path("tasks/<str:task_id>/", views.AsyncTaskDetailView.as_view(), name="task-detail"),
|
||||
|
||||
# ==================== NKS ====================
|
||||
path("nks/clusters/", views.NksClusterListView.as_view(), name="nks-cluster-list"),
|
||||
path("nks/clusters/create/", views.NksClusterCreateView.as_view(), name="nks-cluster-create"),
|
||||
path("nks/clusters/<str:cluster_name>/", views.NksClusterDetailView.as_view(), name="nks-cluster-detail"),
|
||||
path("nks/clusters/<str:cluster_name>/config/", views.NksClusterConfigView.as_view(), name="nks-cluster-config"),
|
||||
|
||||
# ==================== Storage ====================
|
||||
path("storage/containers/", views.StorageContainerListView.as_view(), name="storage-container-list"),
|
||||
path("storage/containers/create/", views.StorageContainerCreateView.as_view(), name="storage-container-create"),
|
||||
path("storage/containers/<str:container_name>/", views.StorageContainerDetailView.as_view(), name="storage-container-detail"),
|
||||
]
|
||||
31
nhn/utils.py
Normal file
31
nhn/utils.py
Normal file
@ -0,0 +1,31 @@
|
||||
import requests
|
||||
from django.conf import settings
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def verify_token_with_auth_server(token: str):
|
||||
"""
|
||||
Verify token with external auth server.
|
||||
"""
|
||||
url = settings.AUTH_VERIFY_URL
|
||||
if not url:
|
||||
logger.warning("AUTH_VERIFY_URL is not configured.")
|
||||
return None
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
json={"token": token},
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=5,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logger.error(f"Auth server returned status {response.status_code}")
|
||||
return None
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Failed to verify token: {e}")
|
||||
return None
|
||||
849
nhn/views.py
Normal file
849
nhn/views.py
Normal file
@ -0,0 +1,849 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user