v0.0.8 | CORS 설정에 X-NHN-Appkey 헤더 허용 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 36s

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 00:32:00 +09:00
parent 10ba64d3d1
commit 57526a0f13
12 changed files with 3018 additions and 36 deletions

View File

@ -411,7 +411,34 @@ GET /api/nhn/floatingip/
## 4. NKS (Kubernetes) API ## 4. NKS (Kubernetes) API
### 4.1 클러스터 목록 조회 ### 4.1 지원 버전 조회
```
GET /api/nhn/nks/supports/
```
**Headers**
```
X-NHN-Region: kr2
X-NHN-Token: {token}
```
**Response (200 OK)**
```json
{
"supported_k8s": {
"v1.33.4": "True",
"v1.32.4": "True",
"v1.31.4": "True",
"v1.30.4": "False"
},
"supported_event_type": {...}
}
```
---
### 4.2 클러스터 목록 조회
``` ```
GET /api/nhn/nks/clusters/ GET /api/nhn/nks/clusters/
@ -439,7 +466,7 @@ X-NHN-Token: {token}
--- ---
### 4.2 클러스터 상세 조회 ### 4.3 클러스터 상세 조회
``` ```
GET /api/nhn/nks/clusters/{cluster_name}/ GET /api/nhn/nks/clusters/{cluster_name}/
@ -447,7 +474,7 @@ GET /api/nhn/nks/clusters/{cluster_name}/
--- ---
### 4.3 클러스터 kubeconfig 조회 ### 4.4 클러스터 kubeconfig 조회
``` ```
GET /api/nhn/nks/clusters/{cluster_name}/config/ GET /api/nhn/nks/clusters/{cluster_name}/config/
@ -462,7 +489,7 @@ GET /api/nhn/nks/clusters/{cluster_name}/config/
--- ---
### 4.4 클러스터 생성 ### 4.5 클러스터 생성
``` ```
POST /api/nhn/nks/clusters/create/ POST /api/nhn/nks/clusters/create/
@ -505,7 +532,7 @@ POST /api/nhn/nks/clusters/create/
--- ---
### 4.5 클러스터 삭제 ### 4.6 클러스터 삭제
``` ```
DELETE /api/nhn/nks/clusters/{cluster_name}/ DELETE /api/nhn/nks/clusters/{cluster_name}/

View File

@ -1,2 +1,3 @@
# msa-django-nhn # msa-django-nhn
python3 manage.py runserver 0.0.0.0:8900

View File

@ -3,6 +3,7 @@ from .compute import ApiCompute
from .vpc import ApiVpc from .vpc import ApiVpc
from .nks import ApiNks from .nks import ApiNks
from .storage import ApiStorageObject from .storage import ApiStorageObject
from .dnsplus import ApiDnsPlus
__all__ = [ __all__ = [
"NHNCloudToken", "NHNCloudToken",
@ -10,4 +11,5 @@ __all__ = [
"ApiVpc", "ApiVpc",
"ApiNks", "ApiNks",
"ApiStorageObject", "ApiStorageObject",
"ApiDnsPlus",
] ]

View File

@ -46,6 +46,9 @@ class NHNCloudEndpoints:
STORAGE_KR1 = "https://kr1-api-object-storage.nhncloudservice.com/v1" STORAGE_KR1 = "https://kr1-api-object-storage.nhncloudservice.com/v1"
STORAGE_KR2 = "https://kr2-api-object-storage.nhncloudservice.com/v1" STORAGE_KR2 = "https://kr2-api-object-storage.nhncloudservice.com/v1"
# DNS Plus (글로벌 서비스 - 리전 무관)
DNSPLUS = "https://dnsplus.api.nhncloudservice.com"
class NHNCloudAPIError(Exception): class NHNCloudAPIError(Exception):
"""NHN Cloud API 에러""" """NHN Cloud API 에러"""
@ -163,3 +166,7 @@ class BaseAPI:
def _delete(self, url: str, **kwargs) -> dict: def _delete(self, url: str, **kwargs) -> dict:
"""DELETE 요청""" """DELETE 요청"""
return self._request("DELETE", url, **kwargs) return self._request("DELETE", url, **kwargs)
def _patch(self, url: str, json_data: Optional[dict] = None, **kwargs) -> dict:
"""PATCH 요청"""
return self._request("PATCH", url, json_data=json_data, **kwargs)

911
nhn/packages/dnsplus.py Normal file
View File

@ -0,0 +1,911 @@
"""
NHN Cloud DNS Plus API Module
DNS Zone, 레코드 세트, GSLB, Pool, Health Check 관리
"""
import logging
from typing import Optional, List
import requests
from .base import NHNCloudEndpoints, NHNCloudAPIError
logger = logging.getLogger(__name__)
class ApiDnsPlus:
"""NHN Cloud DNS Plus API 클래스"""
DEFAULT_TIMEOUT = 30
def __init__(self, appkey: str):
"""
Args:
appkey: NHN Cloud 앱키
"""
self.appkey = appkey
self.base_url = f"{NHNCloudEndpoints.DNSPLUS}/dnsplus/v1.0/appkeys/{appkey}"
self._session = requests.Session()
def _get_headers(self) -> dict:
"""DNS Plus API 헤더"""
return {
"Content-Type": "application/json",
"Accept": "application/json",
}
def _request(
self,
method: str,
url: str,
params: Optional[dict] = None,
json_data: Optional[dict] = None,
timeout: Optional[int] = None,
) -> dict:
"""HTTP 요청 실행"""
logger.info(f"[DnsPlus] 요청 시작 - method={method}, url={url}")
if params:
logger.info(f"[DnsPlus] 요청 파라미터 - params={params}")
if json_data:
logger.info(f"[DnsPlus] 요청 바디 - json={json_data}")
try:
response = self._session.request(
method=method,
url=url,
params=params,
json=json_data,
headers=self._get_headers(),
timeout=timeout or self.DEFAULT_TIMEOUT,
)
logger.info(f"[DnsPlus] 응답 수신 - status_code={response.status_code}")
if response.text:
result = response.json()
# DNS Plus API는 header.isSuccessful로 성공 여부 판단
if "header" in result and not result["header"].get("isSuccessful", True):
error_msg = result["header"].get("resultMessage", "알 수 없는 오류")
error_code = result["header"].get("resultCode", response.status_code)
logger.error(f"[DnsPlus] API 오류 - code={error_code}, message={error_msg}")
raise NHNCloudAPIError(message=error_msg, code=error_code, details=result)
return result
return {}
except requests.exceptions.Timeout:
logger.error(f"[DnsPlus] 타임아웃 - url={url}")
raise NHNCloudAPIError("요청 시간이 초과되었습니다.", code=408)
except requests.exceptions.ConnectionError as e:
logger.error(f"[DnsPlus] 연결 오류 - url={url}, error={e}")
raise NHNCloudAPIError("서버에 연결할 수 없습니다.", code=503)
except requests.exceptions.RequestException as e:
logger.error(f"[DnsPlus] 요청 오류 - url={url}, error={e}")
raise NHNCloudAPIError(f"요청 중 오류가 발생했습니다: {e}")
def _get(self, url: str, params: Optional[dict] = None) -> dict:
return self._request("GET", url, params=params)
def _post(self, url: str, json_data: Optional[dict] = None) -> dict:
return self._request("POST", url, json_data=json_data)
def _put(self, url: str, json_data: Optional[dict] = None) -> dict:
return self._request("PUT", url, json_data=json_data)
def _delete(self, url: str, params: Optional[dict] = None) -> dict:
return self._request("DELETE", url, params=params)
# ==================== DNS Zone ====================
def get_zone_list(
self,
zone_id_list: Optional[List[str]] = None,
zone_status_list: Optional[List[str]] = None,
search_zone_name: Optional[str] = None,
page: int = 1,
limit: int = 50,
) -> dict:
"""
DNS Zone 목록 조회
Args:
zone_id_list: Zone ID 목록
zone_status_list: Zone 상태 목록 (USE, DISUSE)
search_zone_name: 검색할 Zone 이름
page: 페이지 번호
limit: 페이지당 항목 수
Returns:
dict: Zone 목록
"""
url = f"{self.base_url}/zones"
params = {"page": page, "limit": limit}
if zone_id_list:
params["zoneIdList"] = ",".join(zone_id_list)
if zone_status_list:
params["zoneStatusList"] = ",".join(zone_status_list)
if search_zone_name:
params["searchZoneName"] = search_zone_name
return self._get(url, params)
def get_zone(self, zone_id: str) -> dict:
"""
DNS Zone 상세 조회
Args:
zone_id: Zone ID
Returns:
dict: Zone 상세 정보
"""
url = f"{self.base_url}/zones"
params = {"zoneIdList": zone_id}
result = self._get(url, params)
zones = result.get("zoneList", [])
if zones:
return {"zone": zones[0]}
raise NHNCloudAPIError(f"Zone을 찾을 수 없습니다: {zone_id}", code=404)
def create_zone(self, zone_name: str, description: Optional[str] = None) -> dict:
"""
DNS Zone 생성
Args:
zone_name: Zone 이름 (도메인, 예: example.com.)
description: 설명
Returns:
dict: 생성된 Zone 정보
"""
url = f"{self.base_url}/zones"
payload = {
"zone": {
"zoneName": zone_name,
}
}
if description:
payload["zone"]["description"] = description
logger.info(f"DNS Zone 생성 요청: {zone_name}")
return self._post(url, payload)
def update_zone(self, zone_id: str, description: str) -> dict:
"""
DNS Zone 수정
Args:
zone_id: Zone ID
description: 설명
Returns:
dict: 수정된 Zone 정보
"""
url = f"{self.base_url}/zones/{zone_id}"
payload = {
"zone": {
"description": description,
}
}
logger.info(f"DNS Zone 수정 요청: {zone_id}")
return self._put(url, payload)
def delete_zones(self, zone_id_list: List[str]) -> dict:
"""
DNS Zone 삭제 (비동기)
Args:
zone_id_list: 삭제할 Zone ID 목록
Returns:
dict: 삭제 결과
"""
url = f"{self.base_url}/zones/async"
params = {"zoneIdList": ",".join(zone_id_list)}
logger.info(f"DNS Zone 삭제 요청: {zone_id_list}")
return self._delete(url, params)
# ==================== Record Set ====================
def get_recordset_list(
self,
zone_id: str,
recordset_id_list: Optional[List[str]] = None,
recordset_type_list: Optional[List[str]] = None,
search_recordset_name: Optional[str] = None,
page: int = 1,
limit: int = 50,
) -> dict:
"""
레코드 세트 목록 조회
Args:
zone_id: Zone ID
recordset_id_list: 레코드 세트 ID 목록
recordset_type_list: 레코드 타입 목록 (A, AAAA, CNAME, MX, TXT, NS, SRV, etc.)
search_recordset_name: 검색할 레코드 세트 이름
page: 페이지 번호
limit: 페이지당 항목 수
Returns:
dict: 레코드 세트 목록
"""
url = f"{self.base_url}/zones/{zone_id}/recordsets"
params = {"page": page, "limit": limit}
if recordset_id_list:
params["recordsetIdList"] = ",".join(recordset_id_list)
if recordset_type_list:
params["recordsetTypeList"] = ",".join(recordset_type_list)
if search_recordset_name:
params["searchRecordsetName"] = search_recordset_name
return self._get(url, params)
def get_recordset(self, zone_id: str, recordset_id: str) -> dict:
"""
레코드 세트 상세 조회
Args:
zone_id: Zone ID
recordset_id: 레코드 세트 ID
Returns:
dict: 레코드 세트 상세 정보
"""
url = f"{self.base_url}/zones/{zone_id}/recordsets"
params = {"recordsetIdList": recordset_id}
result = self._get(url, params)
recordsets = result.get("recordsetList", [])
if recordsets:
return {"recordset": recordsets[0]}
raise NHNCloudAPIError(f"레코드 세트를 찾을 수 없습니다: {recordset_id}", code=404)
def create_recordset(
self,
zone_id: str,
recordset_name: str,
recordset_type: str,
recordset_ttl: int,
record_list: List[dict],
) -> dict:
"""
레코드 세트 생성
Args:
zone_id: Zone ID
recordset_name: 레코드 세트 이름
recordset_type: 레코드 타입 (A, AAAA, CNAME, MX, TXT, NS, SRV, etc.)
recordset_ttl: TTL (초)
record_list: 레코드 목록 [{"recordContent": "...", "recordDisabled": False}]
Returns:
dict: 생성된 레코드 세트 정보
"""
url = f"{self.base_url}/zones/{zone_id}/recordsets"
payload = {
"recordset": {
"recordsetName": recordset_name,
"recordsetType": recordset_type,
"recordsetTtl": recordset_ttl,
"recordList": record_list,
}
}
logger.info(f"레코드 세트 생성 요청: {recordset_name} ({recordset_type})")
return self._post(url, payload)
def create_recordset_bulk(
self,
zone_id: str,
recordset_list: List[dict],
) -> dict:
"""
레코드 세트 대량 생성
Args:
zone_id: Zone ID
recordset_list: 레코드 세트 목록 (최대 2,000개)
Returns:
dict: 생성된 레코드 세트 정보
"""
url = f"{self.base_url}/zones/{zone_id}/recordsets/list"
payload = {"recordsetList": recordset_list}
logger.info(f"레코드 세트 대량 생성 요청: {len(recordset_list)}")
return self._post(url, payload)
def update_recordset(
self,
zone_id: str,
recordset_id: str,
recordset_type: str,
recordset_ttl: int,
record_list: List[dict],
) -> dict:
"""
레코드 세트 수정
Args:
zone_id: Zone ID
recordset_id: 레코드 세트 ID
recordset_type: 레코드 타입
recordset_ttl: TTL (초)
record_list: 레코드 목록
Returns:
dict: 수정된 레코드 세트 정보
"""
url = f"{self.base_url}/zones/{zone_id}/recordsets/{recordset_id}"
payload = {
"recordset": {
"recordsetType": recordset_type,
"recordsetTtl": recordset_ttl,
"recordList": record_list,
}
}
logger.info(f"레코드 세트 수정 요청: {recordset_id}")
return self._put(url, payload)
def delete_recordsets(self, zone_id: str, recordset_id_list: List[str]) -> dict:
"""
레코드 세트 삭제
Args:
zone_id: Zone ID
recordset_id_list: 삭제할 레코드 세트 ID 목록
Returns:
dict: 삭제 결과
"""
url = f"{self.base_url}/zones/{zone_id}/recordsets"
params = {"recordsetIdList": ",".join(recordset_id_list)}
logger.info(f"레코드 세트 삭제 요청: {recordset_id_list}")
return self._delete(url, params)
# ==================== GSLB ====================
def get_gslb_list(
self,
gslb_id_list: Optional[List[str]] = None,
search_gslb_name: Optional[str] = None,
gslb_domain: Optional[str] = None,
page: int = 1,
limit: int = 50,
) -> dict:
"""
GSLB 목록 조회
Args:
gslb_id_list: GSLB ID 목록
search_gslb_name: 검색할 GSLB 이름
gslb_domain: GSLB 도메인
page: 페이지 번호
limit: 페이지당 항목 수
Returns:
dict: GSLB 목록
"""
url = f"{self.base_url}/gslbs"
params = {"page": page, "limit": limit}
if gslb_id_list:
params["gslbIdList"] = ",".join(gslb_id_list)
if search_gslb_name:
params["searchGslbName"] = search_gslb_name
if gslb_domain:
params["gslbDomain"] = gslb_domain
return self._get(url, params)
def get_gslb(self, gslb_id: str) -> dict:
"""
GSLB 상세 조회
Args:
gslb_id: GSLB ID
Returns:
dict: GSLB 상세 정보
"""
url = f"{self.base_url}/gslbs"
params = {"gslbIdList": gslb_id}
result = self._get(url, params)
gslbs = result.get("gslbList", [])
if gslbs:
return {"gslb": gslbs[0]}
raise NHNCloudAPIError(f"GSLB를 찾을 수 없습니다: {gslb_id}", code=404)
def create_gslb(
self,
gslb_name: str,
gslb_ttl: int,
gslb_routing_rule: str,
gslb_disabled: bool = False,
connected_pool_list: Optional[List[dict]] = None,
) -> dict:
"""
GSLB 생성
Args:
gslb_name: GSLB 이름
gslb_ttl: TTL (초)
gslb_routing_rule: 라우팅 규칙 (FAILOVER, RANDOM, GEOLOCATION)
gslb_disabled: 비활성화 여부
connected_pool_list: 연결된 Pool 목록
Returns:
dict: 생성된 GSLB 정보
"""
url = f"{self.base_url}/gslbs"
payload = {
"gslb": {
"gslbName": gslb_name,
"gslbTtl": gslb_ttl,
"gslbRoutingRule": gslb_routing_rule,
"gslbDisabled": gslb_disabled,
}
}
if connected_pool_list:
payload["gslb"]["connectedPoolList"] = connected_pool_list
logger.info(f"GSLB 생성 요청: {gslb_name}")
return self._post(url, payload)
def update_gslb(
self,
gslb_id: str,
gslb_name: Optional[str] = None,
gslb_ttl: Optional[int] = None,
gslb_routing_rule: Optional[str] = None,
gslb_disabled: Optional[bool] = None,
connected_pool_list: Optional[List[dict]] = None,
) -> dict:
"""
GSLB 수정
Args:
gslb_id: GSLB ID
gslb_name: GSLB 이름
gslb_ttl: TTL (초)
gslb_routing_rule: 라우팅 규칙
gslb_disabled: 비활성화 여부
connected_pool_list: 연결된 Pool 목록
Returns:
dict: 수정된 GSLB 정보
"""
url = f"{self.base_url}/gslbs/{gslb_id}"
gslb_data = {}
if gslb_name is not None:
gslb_data["gslbName"] = gslb_name
if gslb_ttl is not None:
gslb_data["gslbTtl"] = gslb_ttl
if gslb_routing_rule is not None:
gslb_data["gslbRoutingRule"] = gslb_routing_rule
if gslb_disabled is not None:
gslb_data["gslbDisabled"] = gslb_disabled
if connected_pool_list is not None:
gslb_data["connectedPoolList"] = connected_pool_list
payload = {"gslb": gslb_data}
logger.info(f"GSLB 수정 요청: {gslb_id}")
return self._put(url, payload)
def delete_gslbs(self, gslb_id_list: List[str]) -> dict:
"""
GSLB 삭제
Args:
gslb_id_list: 삭제할 GSLB ID 목록
Returns:
dict: 삭제 결과
"""
url = f"{self.base_url}/gslbs"
params = {"gslbIdList": ",".join(gslb_id_list)}
logger.info(f"GSLB 삭제 요청: {gslb_id_list}")
return self._delete(url, params)
def connect_pool(
self,
gslb_id: str,
pool_id: str,
connected_pool_order: int,
connected_pool_region_content: Optional[str] = None,
) -> dict:
"""
GSLB에 Pool 연결
Args:
gslb_id: GSLB ID
pool_id: Pool ID
connected_pool_order: 연결 순서 (우선순위)
connected_pool_region_content: 지역 콘텐츠 (GEOLOCATION 규칙 사용 시)
Returns:
dict: 연결 결과
"""
url = f"{self.base_url}/gslbs/{gslb_id}/connected-pools/{pool_id}"
payload = {
"connectedPool": {
"connectedPoolOrder": connected_pool_order,
}
}
if connected_pool_region_content:
payload["connectedPool"]["connectedPoolRegionContent"] = connected_pool_region_content
logger.info(f"Pool 연결 요청: GSLB={gslb_id}, Pool={pool_id}")
return self._post(url, payload)
def update_connected_pool(
self,
gslb_id: str,
pool_id: str,
connected_pool_order: int,
connected_pool_region_content: Optional[str] = None,
) -> dict:
"""
연결된 Pool 수정
Args:
gslb_id: GSLB ID
pool_id: Pool ID
connected_pool_order: 연결 순서 (우선순위)
connected_pool_region_content: 지역 콘텐츠
Returns:
dict: 수정 결과
"""
url = f"{self.base_url}/gslbs/{gslb_id}/connected-pools/{pool_id}"
payload = {
"connectedPool": {
"connectedPoolOrder": connected_pool_order,
}
}
if connected_pool_region_content:
payload["connectedPool"]["connectedPoolRegionContent"] = connected_pool_region_content
logger.info(f"연결된 Pool 수정 요청: GSLB={gslb_id}, Pool={pool_id}")
return self._put(url, payload)
def disconnect_pools(self, gslb_id: str, pool_id_list: List[str]) -> dict:
"""
GSLB에서 Pool 연결 해제
Args:
gslb_id: GSLB ID
pool_id_list: 연결 해제할 Pool ID 목록
Returns:
dict: 연결 해제 결과
"""
url = f"{self.base_url}/gslbs/{gslb_id}/connected-pools"
params = {"poolIdList": ",".join(pool_id_list)}
logger.info(f"Pool 연결 해제 요청: GSLB={gslb_id}, Pools={pool_id_list}")
return self._delete(url, params)
# ==================== Pool ====================
def get_pool_list(
self,
pool_id_list: Optional[List[str]] = None,
search_pool_name: Optional[str] = None,
health_check_id: Optional[str] = None,
page: int = 1,
limit: int = 50,
) -> dict:
"""
Pool 목록 조회
Args:
pool_id_list: Pool ID 목록
search_pool_name: 검색할 Pool 이름
health_check_id: Health Check ID
page: 페이지 번호
limit: 페이지당 항목 수
Returns:
dict: Pool 목록
"""
url = f"{self.base_url}/pools"
params = {"page": page, "limit": limit}
if pool_id_list:
params["poolIdList"] = ",".join(pool_id_list)
if search_pool_name:
params["searchPoolName"] = search_pool_name
if health_check_id:
params["healthCheckId"] = health_check_id
return self._get(url, params)
def get_pool(self, pool_id: str) -> dict:
"""
Pool 상세 조회
Args:
pool_id: Pool ID
Returns:
dict: Pool 상세 정보
"""
url = f"{self.base_url}/pools"
params = {"poolIdList": pool_id}
result = self._get(url, params)
pools = result.get("poolList", [])
if pools:
return {"pool": pools[0]}
raise NHNCloudAPIError(f"Pool을 찾을 수 없습니다: {pool_id}", code=404)
def create_pool(
self,
pool_name: str,
endpoint_list: List[dict],
pool_disabled: bool = False,
health_check_id: Optional[str] = None,
) -> dict:
"""
Pool 생성
Args:
pool_name: Pool 이름
endpoint_list: 엔드포인트 목록
[{"endpointAddress": "1.1.1.1", "endpointWeight": 1.0, "endpointDisabled": False}]
pool_disabled: 비활성화 여부
health_check_id: Health Check ID
Returns:
dict: 생성된 Pool 정보
"""
url = f"{self.base_url}/pools"
payload = {
"pool": {
"poolName": pool_name,
"poolDisabled": pool_disabled,
"endpointList": endpoint_list,
}
}
if health_check_id:
payload["pool"]["healthCheckId"] = health_check_id
logger.info(f"Pool 생성 요청: {pool_name}")
return self._post(url, payload)
def update_pool(
self,
pool_id: str,
pool_name: Optional[str] = None,
endpoint_list: Optional[List[dict]] = None,
pool_disabled: Optional[bool] = None,
health_check_id: Optional[str] = None,
) -> dict:
"""
Pool 수정
Args:
pool_id: Pool ID
pool_name: Pool 이름
endpoint_list: 엔드포인트 목록
pool_disabled: 비활성화 여부
health_check_id: Health Check ID
Returns:
dict: 수정된 Pool 정보
"""
url = f"{self.base_url}/pools/{pool_id}"
pool_data = {}
if pool_name is not None:
pool_data["poolName"] = pool_name
if endpoint_list is not None:
pool_data["endpointList"] = endpoint_list
if pool_disabled is not None:
pool_data["poolDisabled"] = pool_disabled
if health_check_id is not None:
pool_data["healthCheckId"] = health_check_id
payload = {"pool": pool_data}
logger.info(f"Pool 수정 요청: {pool_id}")
return self._put(url, payload)
def delete_pools(self, pool_id_list: List[str]) -> dict:
"""
Pool 삭제
Args:
pool_id_list: 삭제할 Pool ID 목록
Returns:
dict: 삭제 결과
"""
url = f"{self.base_url}/pools"
params = {"poolIdList": ",".join(pool_id_list)}
logger.info(f"Pool 삭제 요청: {pool_id_list}")
return self._delete(url, params)
# ==================== Health Check ====================
def get_health_check_list(
self,
health_check_id_list: Optional[List[str]] = None,
search_health_check_name: Optional[str] = None,
page: int = 1,
limit: int = 50,
) -> dict:
"""
Health Check 목록 조회
Args:
health_check_id_list: Health Check ID 목록
search_health_check_name: 검색할 Health Check 이름
page: 페이지 번호
limit: 페이지당 항목 수
Returns:
dict: Health Check 목록
"""
url = f"{self.base_url}/health-checks"
params = {"page": page, "limit": limit}
if health_check_id_list:
params["healthCheckIdList"] = ",".join(health_check_id_list)
if search_health_check_name:
params["searchHealthCheckName"] = search_health_check_name
return self._get(url, params)
def get_health_check(self, health_check_id: str) -> dict:
"""
Health Check 상세 조회
Args:
health_check_id: Health Check ID
Returns:
dict: Health Check 상세 정보
"""
url = f"{self.base_url}/health-checks"
params = {"healthCheckIdList": health_check_id}
result = self._get(url, params)
health_checks = result.get("healthCheckList", [])
if health_checks:
return {"healthCheck": health_checks[0]}
raise NHNCloudAPIError(f"Health Check를 찾을 수 없습니다: {health_check_id}", code=404)
def create_health_check(
self,
health_check_name: str,
health_check_protocol: str,
health_check_port: int,
health_check_interval: int = 30,
health_check_timeout: int = 5,
health_check_retries: int = 2,
health_check_path: Optional[str] = None,
health_check_expected_codes: Optional[str] = None,
health_check_expected_body: Optional[str] = None,
health_check_request_header_list: Optional[List[dict]] = None,
) -> dict:
"""
Health Check 생성
Args:
health_check_name: Health Check 이름
health_check_protocol: 프로토콜 (HTTPS, HTTP, TCP)
health_check_port: 포트
health_check_interval: 체크 간격 (초)
health_check_timeout: 타임아웃 (초)
health_check_retries: 재시도 횟수
health_check_path: 경로 (HTTP/HTTPS 전용)
health_check_expected_codes: 예상 응답 코드 (HTTP/HTTPS 전용)
health_check_expected_body: 예상 응답 본문 (HTTP/HTTPS 전용)
health_check_request_header_list: 요청 헤더 목록 (HTTP/HTTPS 전용)
Returns:
dict: 생성된 Health Check 정보
"""
url = f"{self.base_url}/health-checks"
payload = {
"healthCheck": {
"healthCheckName": health_check_name,
"healthCheckProtocol": health_check_protocol,
"healthCheckPort": health_check_port,
"healthCheckInterval": health_check_interval,
"healthCheckTimeout": health_check_timeout,
"healthCheckRetries": health_check_retries,
}
}
# HTTP/HTTPS 전용 옵션
if health_check_path:
payload["healthCheck"]["healthCheckPath"] = health_check_path
if health_check_expected_codes:
payload["healthCheck"]["healthCheckExpectedCodes"] = health_check_expected_codes
if health_check_expected_body:
payload["healthCheck"]["healthCheckExpectedBody"] = health_check_expected_body
if health_check_request_header_list:
payload["healthCheck"]["healthCheckRequestHeaderList"] = health_check_request_header_list
logger.info(f"Health Check 생성 요청: {health_check_name}")
return self._post(url, payload)
def update_health_check(
self,
health_check_id: str,
health_check_name: Optional[str] = None,
health_check_protocol: Optional[str] = None,
health_check_port: Optional[int] = None,
health_check_interval: Optional[int] = None,
health_check_timeout: Optional[int] = None,
health_check_retries: Optional[int] = None,
health_check_path: Optional[str] = None,
health_check_expected_codes: Optional[str] = None,
health_check_expected_body: Optional[str] = None,
health_check_request_header_list: Optional[List[dict]] = None,
) -> dict:
"""
Health Check 수정
Args:
health_check_id: Health Check ID
(나머지 파라미터는 create_health_check과 동일)
Returns:
dict: 수정된 Health Check 정보
"""
url = f"{self.base_url}/health-checks/{health_check_id}"
health_check_data = {}
if health_check_name is not None:
health_check_data["healthCheckName"] = health_check_name
if health_check_protocol is not None:
health_check_data["healthCheckProtocol"] = health_check_protocol
if health_check_port is not None:
health_check_data["healthCheckPort"] = health_check_port
if health_check_interval is not None:
health_check_data["healthCheckInterval"] = health_check_interval
if health_check_timeout is not None:
health_check_data["healthCheckTimeout"] = health_check_timeout
if health_check_retries is not None:
health_check_data["healthCheckRetries"] = health_check_retries
if health_check_path is not None:
health_check_data["healthCheckPath"] = health_check_path
if health_check_expected_codes is not None:
health_check_data["healthCheckExpectedCodes"] = health_check_expected_codes
if health_check_expected_body is not None:
health_check_data["healthCheckExpectedBody"] = health_check_expected_body
if health_check_request_header_list is not None:
health_check_data["healthCheckRequestHeaderList"] = health_check_request_header_list
payload = {"healthCheck": health_check_data}
logger.info(f"Health Check 수정 요청: {health_check_id}")
return self._put(url, payload)
def delete_health_checks(self, health_check_id_list: List[str]) -> dict:
"""
Health Check 삭제
Args:
health_check_id_list: 삭제할 Health Check ID 목록
Returns:
dict: 삭제 결과
"""
url = f"{self.base_url}/health-checks"
params = {"healthCheckIdList": ",".join(health_check_id_list)}
logger.info(f"Health Check 삭제 요청: {health_check_id_list}")
return self._delete(url, params)

View File

@ -51,6 +51,13 @@ class ApiNks(BaseAPI):
headers.update(extra_headers) headers.update(extra_headers)
return headers return headers
# ==================== Supports ====================
def get_supports(self) -> dict:
"""지원되는 Kubernetes 버전 및 작업 종류 조회"""
url = f"{self.nks_url}/v1/supports"
return self._get(url)
# ==================== Cluster ==================== # ==================== Cluster ====================
def get_cluster_list(self) -> dict: def get_cluster_list(self) -> dict:
@ -195,6 +202,78 @@ class ApiNks(BaseAPI):
logger.info(f"Private 클러스터 생성 요청: {cluster_name}") logger.info(f"Private 클러스터 생성 요청: {cluster_name}")
return self._post(url, payload) return self._post(url, payload)
def create_cluster(
self,
cluster_name: str,
vpc_id: str,
subnet_id: str,
instance_type: str,
keypair_name: str,
kubernetes_version: str,
availability_zone: str,
is_public: bool = True,
external_network_id: Optional[str] = None,
external_subnet_id: Optional[str] = None,
node_count: int = 1,
boot_volume_size: int = 50,
boot_volume_type: str = "General SSD",
node_image: Optional[str] = None,
) -> dict:
"""
클러스터 생성 (Public/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)
is_public: Public 클러스터 여부 (기본 True)
external_network_id: 외부 네트워크 ID (Public 클러스터 필수)
external_subnet_id: 외부 서브넷 ID (Public 클러스터 필수)
node_count: 노드 수 (기본 1)
boot_volume_size: 부팅 볼륨 크기 (GB, 기본 50)
boot_volume_type: 볼륨 타입 (기본 "General SSD")
node_image: 노드 이미지 ID (기본 Ubuntu 20.04)
Returns:
dict: 생성된 클러스터 정보
"""
if is_public:
if not external_network_id or not external_subnet_id:
raise ValueError("Public 클러스터에는 external_network_id와 external_subnet_id가 필요합니다.")
return self.create_public_cluster(
cluster_name=cluster_name,
vpc_id=vpc_id,
subnet_id=subnet_id,
instance_type=instance_type,
keypair_name=keypair_name,
kubernetes_version=kubernetes_version,
external_network_id=external_network_id,
external_subnet_id=external_subnet_id,
availability_zone=availability_zone,
node_count=node_count,
boot_volume_size=boot_volume_size,
boot_volume_type=boot_volume_type,
node_image=node_image,
)
else:
return self.create_private_cluster(
cluster_name=cluster_name,
vpc_id=vpc_id,
subnet_id=subnet_id,
instance_type=instance_type,
keypair_name=keypair_name,
kubernetes_version=kubernetes_version,
availability_zone=availability_zone,
node_count=node_count,
boot_volume_size=boot_volume_size,
boot_volume_type=boot_volume_type,
node_image=node_image,
)
def delete_cluster(self, cluster_name: str) -> dict: def delete_cluster(self, cluster_name: str) -> dict:
"""클러스터 삭제""" """클러스터 삭제"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}" url = f"{self.nks_url}/v1/clusters/{cluster_name}"
@ -212,3 +291,349 @@ class ApiNks(BaseAPI):
"""노드 그룹 상세 조회""" """노드 그룹 상세 조회"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}" url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}"
return self._get(url) return self._get(url)
def create_nodegroup(
self,
cluster_name: str,
nodegroup_name: str,
instance_type: str,
node_count: int = 1,
availability_zone: Optional[str] = None,
boot_volume_size: int = 50,
boot_volume_type: str = "General SSD",
node_image: Optional[str] = None,
) -> dict:
"""
노드 그룹 생성
Args:
cluster_name: 클러스터 이름
nodegroup_name: 노드 그룹 이름
instance_type: 인스턴스 타입 (Flavor ID)
node_count: 노드 수 (기본 1)
availability_zone: 가용 영역 (예: kr-pub-a)
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/{cluster_name}/nodegroups"
payload = {
"name": nodegroup_name,
"flavor_id": instance_type,
"node_count": node_count,
"labels": {
"boot_volume_size": str(boot_volume_size),
"boot_volume_type": boot_volume_type,
"node_image": node_image or self.default_node_image,
},
}
if availability_zone:
payload["labels"]["availability_zone"] = availability_zone
logger.info(f"노드 그룹 생성 요청: {cluster_name}/{nodegroup_name}")
return self._post(url, payload)
def delete_nodegroup(self, cluster_name: str, nodegroup_name: str) -> dict:
"""노드 그룹 삭제"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}"
logger.info(f"노드 그룹 삭제 요청: {cluster_name}/{nodegroup_name}")
return self._delete(url)
def start_node(self, cluster_name: str, nodegroup_name: str, node_id: str) -> dict:
"""
워커 노드 시작
Args:
cluster_name: 클러스터 이름
nodegroup_name: 노드 그룹 이름
node_id: 노드 ID
Returns:
dict: 응답 결과
"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}/start_node"
payload = {"node_id": node_id}
logger.info(f"워커 노드 시작 요청: {cluster_name}/{nodegroup_name}/{node_id}")
return self._post(url, payload)
def stop_node(self, cluster_name: str, nodegroup_name: str, node_id: str) -> dict:
"""
워커 노드 중지
Args:
cluster_name: 클러스터 이름
nodegroup_name: 노드 그룹 이름
node_id: 노드 ID
Returns:
dict: 응답 결과
"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}/stop_node"
payload = {"node_id": node_id}
logger.info(f"워커 노드 중지 요청: {cluster_name}/{nodegroup_name}/{node_id}")
return self._post(url, payload)
# ==================== Cluster Operations ====================
def resize_cluster(self, cluster_name: str, node_count: int) -> dict:
"""
클러스터 노드 수 조정 (리사이즈)
Args:
cluster_name: 클러스터 이름
node_count: 조정할 노드 수
Returns:
dict: 응답 결과
"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/actions/resize"
payload = {"node_count": node_count}
logger.info(f"클러스터 리사이즈 요청: {cluster_name}, node_count={node_count}")
return self._post(url, payload)
def upgrade_cluster(self, cluster_name: str, kubernetes_version: str) -> dict:
"""
클러스터 Kubernetes 버전 업그레이드
Args:
cluster_name: 클러스터 이름
kubernetes_version: 업그레이드할 Kubernetes 버전
Returns:
dict: 응답 결과
"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/actions/upgrade"
payload = {"kube_tag": kubernetes_version}
logger.info(f"클러스터 업그레이드 요청: {cluster_name}, version={kubernetes_version}")
return self._post(url, payload)
def get_cluster_events(self, cluster_uuid: str) -> dict:
"""
클러스터 작업 이력 목록 조회
Args:
cluster_uuid: 클러스터 UUID
Returns:
dict: 작업 이력 목록
"""
url = f"{self.nks_url}/v1/clusters/{cluster_uuid}/events"
return self._get(url)
def get_cluster_event(self, cluster_uuid: str, event_uuid: str) -> dict:
"""
클러스터 작업 이력 상세 조회
Args:
cluster_uuid: 클러스터 UUID
event_uuid: 작업 이력 UUID
Returns:
dict: 작업 이력 상세 정보
"""
url = f"{self.nks_url}/v1/clusters/{cluster_uuid}/events/{event_uuid}"
return self._get(url)
# ==================== Autoscaler ====================
def get_autoscale_config(self, cluster_name: str, nodegroup_name: str) -> dict:
"""
오토스케일러 설정 조회
Args:
cluster_name: 클러스터 이름
nodegroup_name: 노드 그룹 이름
Returns:
dict: 오토스케일러 설정
"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}/autoscale"
return self._get(url)
def set_autoscale_config(
self,
cluster_name: str,
nodegroup_name: str,
ca_enable: bool,
ca_max_node_count: Optional[int] = None,
ca_min_node_count: Optional[int] = None,
ca_scale_down_enable: Optional[bool] = None,
ca_scale_down_delay_after_add: Optional[int] = None,
ca_scale_down_unneeded_time: Optional[int] = None,
) -> dict:
"""
오토스케일러 설정 변경
Args:
cluster_name: 클러스터 이름
nodegroup_name: 노드 그룹 이름
ca_enable: 오토스케일러 활성화 여부
ca_max_node_count: 최대 노드 수
ca_min_node_count: 최소 노드 수
ca_scale_down_enable: 스케일 다운 활성화 여부
ca_scale_down_delay_after_add: 스케일 다운 지연 시간 (분)
ca_scale_down_unneeded_time: 불필요 노드 대기 시간 (분)
Returns:
dict: 변경된 오토스케일러 설정
"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}/autoscale"
payload = {"ca_enable": ca_enable}
if ca_max_node_count is not None:
payload["ca_max_node_count"] = ca_max_node_count
if ca_min_node_count is not None:
payload["ca_min_node_count"] = ca_min_node_count
if ca_scale_down_enable is not None:
payload["ca_scale_down_enable"] = ca_scale_down_enable
if ca_scale_down_delay_after_add is not None:
payload["ca_scale_down_delay_after_add"] = ca_scale_down_delay_after_add
if ca_scale_down_unneeded_time is not None:
payload["ca_scale_down_unneeded_time"] = ca_scale_down_unneeded_time
logger.info(f"오토스케일러 설정 변경 요청: {cluster_name}/{nodegroup_name}")
return self._post(url, payload)
# ==================== Node Group Configuration ====================
def upgrade_nodegroup(
self,
cluster_name: str,
nodegroup_name: str,
kubernetes_version: str,
max_unavailable_worker: int = 1,
) -> dict:
"""
노드 그룹 Kubernetes 버전 업그레이드
Args:
cluster_name: 클러스터 이름
nodegroup_name: 노드 그룹 이름
kubernetes_version: 업그레이드할 Kubernetes 버전
max_unavailable_worker: 동시 업그레이드 가능한 노드 수
Returns:
dict: 응답 결과
"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}/upgrade"
payload = {
"kube_tag": kubernetes_version,
"max_unavailable_worker": max_unavailable_worker,
}
logger.info(f"노드 그룹 업그레이드 요청: {cluster_name}/{nodegroup_name}, version={kubernetes_version}")
return self._post(url, payload)
def set_nodegroup_userscript(
self,
cluster_name: str,
nodegroup_name: str,
userscript: str,
) -> dict:
"""
노드 그룹 사용자 스크립트 설정
Args:
cluster_name: 클러스터 이름
nodegroup_name: 노드 그룹 이름
userscript: 사용자 스크립트 내용
Returns:
dict: 응답 결과
"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}/userscript"
payload = {"userscript": userscript}
logger.info(f"노드 그룹 사용자 스크립트 설정 요청: {cluster_name}/{nodegroup_name}")
return self._post(url, payload)
def update_nodegroup(
self,
cluster_name: str,
nodegroup_name: str,
instance_type: Optional[str] = None,
node_count: Optional[int] = None,
) -> dict:
"""
노드 그룹 설정 변경 (인스턴스 타입, 노드 수 등)
Args:
cluster_name: 클러스터 이름
nodegroup_name: 노드 그룹 이름
instance_type: 인스턴스 타입 (Flavor ID)
node_count: 노드 수
Returns:
dict: 변경된 노드 그룹 정보
"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}"
payload = {}
if instance_type is not None:
payload["flavor_id"] = instance_type
if node_count is not None:
payload["node_count"] = node_count
logger.info(f"노드 그룹 설정 변경 요청: {cluster_name}/{nodegroup_name}")
return self._patch(url, payload)
# ==================== Certificates ====================
def renew_certificates(self, cluster_name: str) -> dict:
"""
클러스터 인증서 갱신
Args:
cluster_name: 클러스터 이름
Returns:
dict: 응답 결과
"""
url = f"{self.nks_url}/v1/certificates/{cluster_name}"
logger.info(f"클러스터 인증서 갱신 요청: {cluster_name}")
return self._patch(url, {})
# ==================== API Endpoint IP ACL ====================
def get_api_endpoint_ipacl(self, cluster_name: str) -> dict:
"""
API 엔드포인트 IP 접근 제어 조회
Args:
cluster_name: 클러스터 이름
Returns:
dict: IP 접근 제어 설정
"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/api_ep_ipacl"
return self._get(url)
def set_api_endpoint_ipacl(
self,
cluster_name: str,
enable: bool,
allowed_cidrs: Optional[list] = None,
) -> dict:
"""
API 엔드포인트 IP 접근 제어 설정
Args:
cluster_name: 클러스터 이름
enable: IP 접근 제어 활성화 여부
allowed_cidrs: 허용할 CIDR 목록 (예: ["192.168.0.0/24", "10.0.0.0/8"])
Returns:
dict: 변경된 IP 접근 제어 설정
"""
url = f"{self.nks_url}/v1/clusters/{cluster_name}/api_ep_ipacl"
payload = {"enable": enable}
if allowed_cidrs is not None:
payload["allowed_cidrs"] = allowed_cidrs
logger.info(f"API 엔드포인트 IP 접근 제어 설정 요청: {cluster_name}")
return self._post(url, payload)

View File

@ -148,17 +148,9 @@ class NksClusterSerializer(serializers.Serializer):
help_text="가용 영역 (예: kr-pub-a)", help_text="가용 영역 (예: kr-pub-a)",
) )
is_public = serializers.BooleanField( is_public = serializers.BooleanField(
help_text="Public 클러스터 여부 (외부 접근 가능)", help_text="Public 클러스터 여부 (외부 접근 가능, True 선택 시 External Network 자동 설정)",
default=True, 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( node_count = serializers.IntegerField(
help_text="노드 수", help_text="노드 수",
default=1, default=1,
@ -176,19 +168,6 @@ class NksClusterSerializer(serializers.Serializer):
default="General SSD", 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 ==================== # ==================== Storage ====================
@ -200,3 +179,468 @@ class StorageContainerSerializer(serializers.Serializer):
help_text="컨테이너 이름", help_text="컨테이너 이름",
max_length=255, max_length=255,
) )
# ==================== NKS Advanced ====================
class NksNodeGroupSerializer(serializers.Serializer):
"""NKS 노드 그룹 생성 요청"""
nodegroup_name = serializers.CharField(
help_text="노드 그룹 이름",
max_length=255,
)
instance_type = serializers.CharField(
help_text="인스턴스 타입 (Flavor ID)",
)
node_count = serializers.IntegerField(
help_text="노드 수",
default=1,
min_value=1,
max_value=100,
)
availability_zone = serializers.CharField(
help_text="가용 영역 (예: kr-pub-a)",
required=False,
)
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",
)
class NksNodeActionSerializer(serializers.Serializer):
"""워커 노드 액션 요청"""
node_id = serializers.CharField(
help_text="노드 ID",
)
class NksClusterResizeSerializer(serializers.Serializer):
"""클러스터 리사이즈 요청"""
node_count = serializers.IntegerField(
help_text="조정할 노드 수",
min_value=1,
max_value=100,
)
class NksClusterUpgradeSerializer(serializers.Serializer):
"""클러스터 업그레이드 요청"""
kubernetes_version = serializers.CharField(
help_text="업그레이드할 Kubernetes 버전 (예: v1.28.3)",
)
class NksAutoscaleConfigSerializer(serializers.Serializer):
"""오토스케일러 설정 요청"""
ca_enable = serializers.BooleanField(
help_text="오토스케일러 활성화 여부",
)
ca_max_node_count = serializers.IntegerField(
help_text="최대 노드 수",
required=False,
min_value=1,
max_value=100,
)
ca_min_node_count = serializers.IntegerField(
help_text="최소 노드 수",
required=False,
min_value=0,
max_value=100,
)
ca_scale_down_enable = serializers.BooleanField(
help_text="스케일 다운 활성화 여부",
required=False,
)
ca_scale_down_delay_after_add = serializers.IntegerField(
help_text="스케일 다운 지연 시간 (분)",
required=False,
)
ca_scale_down_unneeded_time = serializers.IntegerField(
help_text="불필요 노드 대기 시간 (분)",
required=False,
)
class NksNodeGroupUpgradeSerializer(serializers.Serializer):
"""노드 그룹 업그레이드 요청"""
kubernetes_version = serializers.CharField(
help_text="업그레이드할 Kubernetes 버전",
)
max_unavailable_worker = serializers.IntegerField(
help_text="동시 업그레이드 가능한 노드 수",
default=1,
min_value=1,
)
class NksNodeGroupUpdateSerializer(serializers.Serializer):
"""노드 그룹 설정 변경 요청"""
instance_type = serializers.CharField(
help_text="인스턴스 타입 (Flavor ID)",
required=False,
)
node_count = serializers.IntegerField(
help_text="노드 수",
required=False,
min_value=0,
max_value=100,
)
class NksUserScriptSerializer(serializers.Serializer):
"""사용자 스크립트 설정 요청"""
userscript = serializers.CharField(
help_text="사용자 스크립트 내용",
)
class NksApiEndpointIpAclSerializer(serializers.Serializer):
"""API 엔드포인트 IP 접근 제어 요청"""
enable = serializers.BooleanField(
help_text="IP 접근 제어 활성화 여부",
)
allowed_cidrs = serializers.ListField(
child=serializers.CharField(),
help_text="허용할 CIDR 목록",
required=False,
)
# ==================== DNS Plus ====================
class DnsZoneSerializer(serializers.Serializer):
"""DNS Zone 생성 요청"""
zone_name = serializers.CharField(
help_text="Zone 이름 (도메인, 예: example.com.)",
max_length=255,
)
description = serializers.CharField(
help_text="설명",
required=False,
allow_blank=True,
)
class DnsZoneUpdateSerializer(serializers.Serializer):
"""DNS Zone 수정 요청"""
description = serializers.CharField(
help_text="설명",
)
class DnsRecordSerializer(serializers.Serializer):
"""DNS 레코드"""
recordContent = serializers.CharField(
help_text="레코드 내용",
)
recordDisabled = serializers.BooleanField(
help_text="비활성화 여부",
default=False,
)
class DnsRecordSetSerializer(serializers.Serializer):
"""DNS 레코드 세트 생성 요청"""
recordset_name = serializers.CharField(
help_text="레코드 세트 이름",
max_length=255,
)
recordset_type = serializers.ChoiceField(
choices=["A", "AAAA", "CAA", "CNAME", "MX", "NAPTR", "PTR", "TXT", "SRV", "NS"],
help_text="레코드 타입",
)
recordset_ttl = serializers.IntegerField(
help_text="TTL (초)",
min_value=1,
max_value=2147483647,
)
record_list = serializers.ListField(
child=DnsRecordSerializer(),
help_text="레코드 목록",
)
class DnsRecordSetUpdateSerializer(serializers.Serializer):
"""DNS 레코드 세트 수정 요청"""
recordset_type = serializers.ChoiceField(
choices=["A", "AAAA", "CAA", "CNAME", "MX", "NAPTR", "PTR", "TXT", "SRV", "NS"],
help_text="레코드 타입",
)
recordset_ttl = serializers.IntegerField(
help_text="TTL (초)",
min_value=1,
max_value=2147483647,
)
record_list = serializers.ListField(
child=DnsRecordSerializer(),
help_text="레코드 목록",
)
class DnsEndpointSerializer(serializers.Serializer):
"""Pool 엔드포인트"""
endpointAddress = serializers.CharField(
help_text="엔드포인트 주소 (IP 또는 도메인)",
)
endpointWeight = serializers.FloatField(
help_text="가중치 (0~1.00)",
min_value=0,
max_value=1.0,
default=1.0,
)
endpointDisabled = serializers.BooleanField(
help_text="비활성화 여부",
default=False,
)
class DnsPoolSerializer(serializers.Serializer):
"""Pool 생성 요청"""
pool_name = serializers.CharField(
help_text="Pool 이름",
max_length=255,
)
endpoint_list = serializers.ListField(
child=DnsEndpointSerializer(),
help_text="엔드포인트 목록",
)
pool_disabled = serializers.BooleanField(
help_text="비활성화 여부",
default=False,
)
health_check_id = serializers.CharField(
help_text="Health Check ID",
required=False,
)
class DnsPoolUpdateSerializer(serializers.Serializer):
"""Pool 수정 요청"""
pool_name = serializers.CharField(
help_text="Pool 이름",
required=False,
)
endpoint_list = serializers.ListField(
child=DnsEndpointSerializer(),
help_text="엔드포인트 목록",
required=False,
)
pool_disabled = serializers.BooleanField(
help_text="비활성화 여부",
required=False,
)
health_check_id = serializers.CharField(
help_text="Health Check ID",
required=False,
allow_null=True,
)
class DnsConnectedPoolSerializer(serializers.Serializer):
"""GSLB 연결 Pool"""
poolId = serializers.CharField(
help_text="Pool ID",
)
connectedPoolOrder = serializers.IntegerField(
help_text="연결 순서 (우선순위)",
min_value=1,
)
connectedPoolRegionContent = serializers.CharField(
help_text="지역 콘텐츠 (GEOLOCATION 규칙 사용 시)",
required=False,
)
class DnsGslbSerializer(serializers.Serializer):
"""GSLB 생성 요청"""
gslb_name = serializers.CharField(
help_text="GSLB 이름",
max_length=255,
)
gslb_ttl = serializers.IntegerField(
help_text="TTL (초)",
min_value=1,
max_value=2147483647,
)
gslb_routing_rule = serializers.ChoiceField(
choices=["FAILOVER", "RANDOM", "GEOLOCATION"],
help_text="라우팅 규칙",
)
gslb_disabled = serializers.BooleanField(
help_text="비활성화 여부",
default=False,
)
connected_pool_list = serializers.ListField(
child=DnsConnectedPoolSerializer(),
help_text="연결된 Pool 목록",
required=False,
)
class DnsGslbUpdateSerializer(serializers.Serializer):
"""GSLB 수정 요청"""
gslb_name = serializers.CharField(
help_text="GSLB 이름",
required=False,
)
gslb_ttl = serializers.IntegerField(
help_text="TTL (초)",
required=False,
)
gslb_routing_rule = serializers.ChoiceField(
choices=["FAILOVER", "RANDOM", "GEOLOCATION"],
help_text="라우팅 규칙",
required=False,
)
gslb_disabled = serializers.BooleanField(
help_text="비활성화 여부",
required=False,
)
connected_pool_list = serializers.ListField(
child=DnsConnectedPoolSerializer(),
help_text="연결된 Pool 목록",
required=False,
)
class DnsPoolConnectSerializer(serializers.Serializer):
"""GSLB에 Pool 연결 요청"""
connected_pool_order = serializers.IntegerField(
help_text="연결 순서 (우선순위)",
min_value=1,
)
connected_pool_region_content = serializers.CharField(
help_text="지역 콘텐츠 (GEOLOCATION 규칙 사용 시)",
required=False,
)
class DnsHealthCheckSerializer(serializers.Serializer):
"""Health Check 생성 요청"""
health_check_name = serializers.CharField(
help_text="Health Check 이름",
max_length=255,
)
health_check_protocol = serializers.ChoiceField(
choices=["HTTPS", "HTTP", "TCP"],
help_text="프로토콜",
)
health_check_port = serializers.IntegerField(
help_text="포트",
min_value=1,
max_value=65535,
)
health_check_interval = serializers.IntegerField(
help_text="체크 간격 (초)",
default=30,
min_value=10,
max_value=3600,
)
health_check_timeout = serializers.IntegerField(
help_text="타임아웃 (초)",
default=5,
min_value=1,
max_value=10,
)
health_check_retries = serializers.IntegerField(
help_text="재시도 횟수",
default=2,
min_value=1,
max_value=10,
)
# HTTP/HTTPS 전용
health_check_path = serializers.CharField(
help_text="경로 (HTTP/HTTPS 전용)",
required=False,
)
health_check_expected_codes = serializers.CharField(
help_text="예상 응답 코드 (HTTP/HTTPS 전용)",
required=False,
)
health_check_expected_body = serializers.CharField(
help_text="예상 응답 본문 (HTTP/HTTPS 전용)",
required=False,
)
class DnsHealthCheckUpdateSerializer(serializers.Serializer):
"""Health Check 수정 요청"""
health_check_name = serializers.CharField(
help_text="Health Check 이름",
required=False,
)
health_check_protocol = serializers.ChoiceField(
choices=["HTTPS", "HTTP", "TCP"],
help_text="프로토콜",
required=False,
)
health_check_port = serializers.IntegerField(
help_text="포트",
required=False,
)
health_check_interval = serializers.IntegerField(
help_text="체크 간격 (초)",
required=False,
)
health_check_timeout = serializers.IntegerField(
help_text="타임아웃 (초)",
required=False,
)
health_check_retries = serializers.IntegerField(
help_text="재시도 횟수",
required=False,
)
health_check_path = serializers.CharField(
help_text="경로 (HTTP/HTTPS 전용)",
required=False,
)
health_check_expected_codes = serializers.CharField(
help_text="예상 응답 코드 (HTTP/HTTPS 전용)",
required=False,
)
health_check_expected_body = serializers.CharField(
help_text="예상 응답 본문 (HTTP/HTTPS 전용)",
required=False,
)
class DnsIdListSerializer(serializers.Serializer):
"""ID 목록 요청"""
id_list = serializers.ListField(
child=serializers.CharField(),
help_text="ID 목록",
min_length=1,
)

View File

@ -141,13 +141,12 @@ def create_instance_async(region, tenant_id, token, instance_data):
return task return task
def create_nks_cluster_async(region, tenant_id, token, cluster_data): def create_nks_cluster_async(region, token, cluster_data):
""" """
NKS 클러스터 비동기 생성 NKS 클러스터 비동기 생성
Args: Args:
region: 리전 region: 리전
tenant_id: 테넌트 ID
token: API 토큰 token: API 토큰
cluster_data: 클러스터 생성 데이터 (dict) cluster_data: 클러스터 생성 데이터 (dict)
@ -165,7 +164,7 @@ def create_nks_cluster_async(region, tenant_id, token, cluster_data):
) )
# API 객체 생성 # API 객체 생성
api = ApiNks(region, tenant_id, token) api = ApiNks(region, token)
# 비동기 실행 # 비동기 실행
execute_async_task( execute_async_task(

View File

@ -39,14 +39,59 @@ urlpatterns = [
path("tasks/", views.AsyncTaskListView.as_view(), name="task-list"), path("tasks/", views.AsyncTaskListView.as_view(), name="task-list"),
path("tasks/<str:task_id>/", views.AsyncTaskDetailView.as_view(), name="task-detail"), path("tasks/<str:task_id>/", views.AsyncTaskDetailView.as_view(), name="task-detail"),
# ==================== NKS ==================== # ==================== NKS Cluster ====================
path("nks/supports/", views.NksSupportsView.as_view(), name="nks-supports"),
path("nks/clusters/", views.NksClusterListView.as_view(), name="nks-cluster-list"), 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/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>/", views.NksClusterDetailView.as_view(), name="nks-cluster-detail"),
path("nks/clusters/<str:cluster_name>/config/", views.NksClusterConfigView.as_view(), name="nks-cluster-config"), path("nks/clusters/<str:cluster_name>/config/", views.NksClusterConfigView.as_view(), name="nks-cluster-config"),
path("nks/clusters/<str:cluster_name>/resize/", views.NksClusterResizeView.as_view(), name="nks-cluster-resize"),
path("nks/clusters/<str:cluster_name>/upgrade/", views.NksClusterUpgradeView.as_view(), name="nks-cluster-upgrade"),
path("nks/clusters/<str:cluster_name>/certificates/", views.NksClusterCertificatesView.as_view(), name="nks-cluster-certificates"),
path("nks/clusters/<str:cluster_name>/ipacl/", views.NksClusterIpAclView.as_view(), name="nks-cluster-ipacl"),
# NKS Cluster Events
path("nks/clusters/<str:cluster_uuid>/events/", views.NksClusterEventsView.as_view(), name="nks-cluster-events"),
path("nks/clusters/<str:cluster_uuid>/events/<str:event_uuid>/", views.NksClusterEventDetailView.as_view(), name="nks-cluster-event-detail"),
# ==================== NKS Node Group ====================
path("nks/clusters/<str:cluster_name>/nodegroups/", views.NksNodeGroupListView.as_view(), name="nks-nodegroup-list"),
path("nks/clusters/<str:cluster_name>/nodegroups/create/", views.NksNodeGroupCreateView.as_view(), name="nks-nodegroup-create"),
path("nks/clusters/<str:cluster_name>/nodegroups/<str:nodegroup_name>/", views.NksNodeGroupDetailView.as_view(), name="nks-nodegroup-detail"),
path("nks/clusters/<str:cluster_name>/nodegroups/<str:nodegroup_name>/upgrade/", views.NksNodeGroupUpgradeView.as_view(), name="nks-nodegroup-upgrade"),
path("nks/clusters/<str:cluster_name>/nodegroups/<str:nodegroup_name>/userscript/", views.NksNodeGroupUserScriptView.as_view(), name="nks-nodegroup-userscript"),
path("nks/clusters/<str:cluster_name>/nodegroups/<str:nodegroup_name>/autoscale/", views.NksAutoscaleConfigView.as_view(), name="nks-nodegroup-autoscale"),
path("nks/clusters/<str:cluster_name>/nodegroups/<str:nodegroup_name>/start_node/", views.NksNodeStartView.as_view(), name="nks-node-start"),
path("nks/clusters/<str:cluster_name>/nodegroups/<str:nodegroup_name>/stop_node/", views.NksNodeStopView.as_view(), name="nks-node-stop"),
# ==================== Storage ==================== # ==================== Storage ====================
path("storage/containers/", views.StorageContainerListView.as_view(), name="storage-container-list"), 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/create/", views.StorageContainerCreateView.as_view(), name="storage-container-create"),
path("storage/containers/<str:container_name>/", views.StorageContainerDetailView.as_view(), name="storage-container-detail"), path("storage/containers/<str:container_name>/", views.StorageContainerDetailView.as_view(), name="storage-container-detail"),
# ==================== DNS Plus - Zone ====================
path("dns/zones/", views.DnsZoneListView.as_view(), name="dns-zone-list"),
path("dns/zones/create/", views.DnsZoneCreateView.as_view(), name="dns-zone-create"),
path("dns/zones/<str:zone_id>/", views.DnsZoneDetailView.as_view(), name="dns-zone-detail"),
# ==================== DNS Plus - RecordSet ====================
path("dns/zones/<str:zone_id>/recordsets/", views.DnsRecordSetListView.as_view(), name="dns-recordset-list"),
path("dns/zones/<str:zone_id>/recordsets/create/", views.DnsRecordSetCreateView.as_view(), name="dns-recordset-create"),
path("dns/zones/<str:zone_id>/recordsets/<str:recordset_id>/", views.DnsRecordSetDetailView.as_view(), name="dns-recordset-detail"),
# ==================== DNS Plus - GSLB ====================
path("dns/gslbs/", views.DnsGslbListView.as_view(), name="dns-gslb-list"),
path("dns/gslbs/create/", views.DnsGslbCreateView.as_view(), name="dns-gslb-create"),
path("dns/gslbs/<str:gslb_id>/", views.DnsGslbDetailView.as_view(), name="dns-gslb-detail"),
path("dns/gslbs/<str:gslb_id>/pools/", views.DnsGslbPoolConnectView.as_view(), name="dns-gslb-pool-connection"),
# ==================== DNS Plus - Pool ====================
path("dns/pools/", views.DnsPoolListView.as_view(), name="dns-pool-list"),
path("dns/pools/create/", views.DnsPoolCreateView.as_view(), name="dns-pool-create"),
path("dns/pools/<str:pool_id>/", views.DnsPoolDetailView.as_view(), name="dns-pool-detail"),
# ==================== DNS Plus - Health Check ====================
path("dns/health-checks/", views.DnsHealthCheckListView.as_view(), name="dns-healthcheck-list"),
path("dns/health-checks/create/", views.DnsHealthCheckCreateView.as_view(), name="dns-healthcheck-create"),
path("dns/health-checks/<str:health_check_id>/", views.DnsHealthCheckDetailView.as_view(), name="dns-healthcheck-detail"),
] ]

File diff suppressed because it is too large Load Diff

View File

@ -158,6 +158,7 @@ CORS_ALLOW_HEADERS = [
"x-nhn-region", "x-nhn-region",
"x-nhn-tenant-id", "x-nhn-tenant-id",
"x-nhn-storage-account", "x-nhn-storage-account",
"x-nhn-appkey",
] ]
# REST Framework settings # REST Framework settings

View File

@ -1 +1 @@
v0.0.7 v0.0.8