From 57526a0f131738790dca0a5c469c123477fa868b Mon Sep 17 00:00:00 2001 From: icurfer Date: Thu, 15 Jan 2026 00:32:00 +0900 Subject: [PATCH] =?UTF-8?q?v0.0.8=20|=20CORS=20=EC=84=A4=EC=A0=95=EC=97=90?= =?UTF-8?q?=20X-NHN-Appkey=20=ED=97=A4=EB=8D=94=20=ED=97=88=EC=9A=A9=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- API_SPEC.md | 37 +- README.md | 1 + nhn/packages/__init__.py | 2 + nhn/packages/base.py | 7 + nhn/packages/dnsplus.py | 911 ++++++++++++++++++++++++++++++ nhn/packages/nks.py | 425 ++++++++++++++ nhn/serializers.py | 488 ++++++++++++++++- nhn/tasks.py | 5 +- nhn/urls.py | 47 +- nhn/views.py | 1128 +++++++++++++++++++++++++++++++++++++- nhn_prj/settings.py | 1 + version | 2 +- 12 files changed, 3018 insertions(+), 36 deletions(-) create mode 100644 nhn/packages/dnsplus.py diff --git a/API_SPEC.md b/API_SPEC.md index a162451..3171358 100644 --- a/API_SPEC.md +++ b/API_SPEC.md @@ -411,7 +411,34 @@ GET /api/nhn/floatingip/ ## 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/ @@ -439,7 +466,7 @@ X-NHN-Token: {token} --- -### 4.2 클러스터 상세 조회 +### 4.3 클러스터 상세 조회 ``` 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/ @@ -462,7 +489,7 @@ GET /api/nhn/nks/clusters/{cluster_name}/config/ --- -### 4.4 클러스터 생성 +### 4.5 클러스터 생성 ``` 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}/ diff --git a/README.md b/README.md index 33fe1c2..9f53598 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # msa-django-nhn +python3 manage.py runserver 0.0.0.0:8900 \ No newline at end of file diff --git a/nhn/packages/__init__.py b/nhn/packages/__init__.py index 6ea3a8a..f71716e 100644 --- a/nhn/packages/__init__.py +++ b/nhn/packages/__init__.py @@ -3,6 +3,7 @@ from .compute import ApiCompute from .vpc import ApiVpc from .nks import ApiNks from .storage import ApiStorageObject +from .dnsplus import ApiDnsPlus __all__ = [ "NHNCloudToken", @@ -10,4 +11,5 @@ __all__ = [ "ApiVpc", "ApiNks", "ApiStorageObject", + "ApiDnsPlus", ] diff --git a/nhn/packages/base.py b/nhn/packages/base.py index a60281c..eb5d80a 100644 --- a/nhn/packages/base.py +++ b/nhn/packages/base.py @@ -46,6 +46,9 @@ class NHNCloudEndpoints: STORAGE_KR1 = "https://kr1-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): """NHN Cloud API 에러""" @@ -163,3 +166,7 @@ class BaseAPI: def _delete(self, url: str, **kwargs) -> dict: """DELETE 요청""" 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) diff --git a/nhn/packages/dnsplus.py b/nhn/packages/dnsplus.py new file mode 100644 index 0000000..0aa84c9 --- /dev/null +++ b/nhn/packages/dnsplus.py @@ -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) diff --git a/nhn/packages/nks.py b/nhn/packages/nks.py index ed9e5f1..680f1af 100644 --- a/nhn/packages/nks.py +++ b/nhn/packages/nks.py @@ -51,6 +51,13 @@ class ApiNks(BaseAPI): headers.update(extra_headers) return headers + # ==================== Supports ==================== + + def get_supports(self) -> dict: + """지원되는 Kubernetes 버전 및 작업 종류 조회""" + url = f"{self.nks_url}/v1/supports" + return self._get(url) + # ==================== Cluster ==================== def get_cluster_list(self) -> dict: @@ -195,6 +202,78 @@ class ApiNks(BaseAPI): logger.info(f"Private 클러스터 생성 요청: {cluster_name}") 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: """클러스터 삭제""" 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}" 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) diff --git a/nhn/serializers.py b/nhn/serializers.py index faf3402..1e7388c 100644 --- a/nhn/serializers.py +++ b/nhn/serializers.py @@ -148,17 +148,9 @@ class NksClusterSerializer(serializers.Serializer): help_text="가용 영역 (예: kr-pub-a)", ) is_public = serializers.BooleanField( - help_text="Public 클러스터 여부 (외부 접근 가능)", + help_text="Public 클러스터 여부 (외부 접근 가능, True 선택 시 External Network 자동 설정)", 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, @@ -176,19 +168,6 @@ class NksClusterSerializer(serializers.Serializer): 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 ==================== @@ -200,3 +179,468 @@ class StorageContainerSerializer(serializers.Serializer): help_text="컨테이너 이름", 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, + ) diff --git a/nhn/tasks.py b/nhn/tasks.py index 948df4f..35410f5 100644 --- a/nhn/tasks.py +++ b/nhn/tasks.py @@ -141,13 +141,12 @@ def create_instance_async(region, tenant_id, token, instance_data): return task -def create_nks_cluster_async(region, tenant_id, token, cluster_data): +def create_nks_cluster_async(region, token, cluster_data): """ NKS 클러스터 비동기 생성 Args: region: 리전 - tenant_id: 테넌트 ID token: API 토큰 cluster_data: 클러스터 생성 데이터 (dict) @@ -165,7 +164,7 @@ def create_nks_cluster_async(region, tenant_id, token, cluster_data): ) # API 객체 생성 - api = ApiNks(region, tenant_id, token) + api = ApiNks(region, token) # 비동기 실행 execute_async_task( diff --git a/nhn/urls.py b/nhn/urls.py index 9036981..74775ae 100644 --- a/nhn/urls.py +++ b/nhn/urls.py @@ -39,14 +39,59 @@ urlpatterns = [ path("tasks/", views.AsyncTaskListView.as_view(), name="task-list"), path("tasks//", 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/create/", views.NksClusterCreateView.as_view(), name="nks-cluster-create"), path("nks/clusters//", views.NksClusterDetailView.as_view(), name="nks-cluster-detail"), path("nks/clusters//config/", views.NksClusterConfigView.as_view(), name="nks-cluster-config"), + path("nks/clusters//resize/", views.NksClusterResizeView.as_view(), name="nks-cluster-resize"), + path("nks/clusters//upgrade/", views.NksClusterUpgradeView.as_view(), name="nks-cluster-upgrade"), + path("nks/clusters//certificates/", views.NksClusterCertificatesView.as_view(), name="nks-cluster-certificates"), + path("nks/clusters//ipacl/", views.NksClusterIpAclView.as_view(), name="nks-cluster-ipacl"), + + # NKS Cluster Events + path("nks/clusters//events/", views.NksClusterEventsView.as_view(), name="nks-cluster-events"), + path("nks/clusters//events//", views.NksClusterEventDetailView.as_view(), name="nks-cluster-event-detail"), + + # ==================== NKS Node Group ==================== + path("nks/clusters//nodegroups/", views.NksNodeGroupListView.as_view(), name="nks-nodegroup-list"), + path("nks/clusters//nodegroups/create/", views.NksNodeGroupCreateView.as_view(), name="nks-nodegroup-create"), + path("nks/clusters//nodegroups//", views.NksNodeGroupDetailView.as_view(), name="nks-nodegroup-detail"), + path("nks/clusters//nodegroups//upgrade/", views.NksNodeGroupUpgradeView.as_view(), name="nks-nodegroup-upgrade"), + path("nks/clusters//nodegroups//userscript/", views.NksNodeGroupUserScriptView.as_view(), name="nks-nodegroup-userscript"), + path("nks/clusters//nodegroups//autoscale/", views.NksAutoscaleConfigView.as_view(), name="nks-nodegroup-autoscale"), + path("nks/clusters//nodegroups//start_node/", views.NksNodeStartView.as_view(), name="nks-node-start"), + path("nks/clusters//nodegroups//stop_node/", views.NksNodeStopView.as_view(), name="nks-node-stop"), # ==================== 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//", 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//", views.DnsZoneDetailView.as_view(), name="dns-zone-detail"), + + # ==================== DNS Plus - RecordSet ==================== + path("dns/zones//recordsets/", views.DnsRecordSetListView.as_view(), name="dns-recordset-list"), + path("dns/zones//recordsets/create/", views.DnsRecordSetCreateView.as_view(), name="dns-recordset-create"), + path("dns/zones//recordsets//", 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//", views.DnsGslbDetailView.as_view(), name="dns-gslb-detail"), + path("dns/gslbs//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//", 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//", views.DnsHealthCheckDetailView.as_view(), name="dns-healthcheck-detail"), ] diff --git a/nhn/views.py b/nhn/views.py index c13fdf9..4152a75 100644 --- a/nhn/views.py +++ b/nhn/views.py @@ -20,10 +20,32 @@ from .serializers import ( VpcSerializer, SubnetSerializer, NksClusterSerializer, + NksNodeGroupSerializer, + NksNodeActionSerializer, + NksClusterResizeSerializer, + NksClusterUpgradeSerializer, + NksAutoscaleConfigSerializer, + NksNodeGroupUpgradeSerializer, + NksNodeGroupUpdateSerializer, + NksUserScriptSerializer, + NksApiEndpointIpAclSerializer, StorageContainerSerializer, ErrorResponseSerializer, + # DNS Plus + DnsZoneSerializer, + DnsZoneUpdateSerializer, + DnsRecordSetSerializer, + DnsRecordSetUpdateSerializer, + DnsPoolSerializer, + DnsPoolUpdateSerializer, + DnsGslbSerializer, + DnsGslbUpdateSerializer, + DnsPoolConnectSerializer, + DnsHealthCheckSerializer, + DnsHealthCheckUpdateSerializer, + DnsIdListSerializer, ) -from .packages import NHNCloudToken, ApiCompute, ApiVpc, ApiNks, ApiStorageObject +from .packages import NHNCloudToken, ApiCompute, ApiVpc, ApiNks, ApiStorageObject, ApiDnsPlus from .packages.base import NHNCloudAPIError logger = logging.getLogger(__name__) @@ -633,6 +655,23 @@ class AsyncTaskDetailView(APIView): # ==================== NKS API ==================== +class NksSupportsView(APIView): + """NKS 지원 버전 및 작업 종류 조회 API""" + + @swagger_auto_schema( + operation_summary="지원되는 Kubernetes 버전 조회", + 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_supports()) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + class NksClusterListView(APIView): """NKS 클러스터 목록 조회 API""" @@ -713,7 +752,7 @@ class NksClusterCreateView(APIView): @swagger_auto_schema( operation_summary="NKS 클러스터 생성 (비동기)", - manual_parameters=[region_header, token_header, tenant_header], + manual_parameters=[region_header, token_header], request_body=NksClusterSerializer, responses={202: "작업 ID 반환"}, ) @@ -724,23 +763,456 @@ class NksClusterCreateView(APIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) try: + cluster_data = serializer.validated_data.copy() + + # Public 클러스터인 경우 External Network/Subnet 자동 조회 + if cluster_data.get("is_public", True): + vpc_api = ApiVpc(headers["region"], headers["token"]) + external_networks = vpc_api.get_external_network_id() + networks = external_networks.get("networks", []) + + if not networks: + return Response( + {"error": "External Network를 찾을 수 없습니다."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # 첫 번째 External Network 사용 + external_network = networks[0] + cluster_data["external_network_id"] = external_network.get("id") + + # External Network의 서브넷 가져오기 + subnets = external_network.get("subnets", []) + if subnets: + cluster_data["external_subnet_id"] = subnets[0] + else: + return Response( + {"error": "External Subnet을 찾을 수 없습니다."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + logger.info( + f"[NKS] External Network 자동 설정 - network_id={cluster_data['external_network_id']}, " + f"subnet_id={cluster_data['external_subnet_id']}" + ) + 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, + cluster_data=cluster_data, ) return Response( {"task_id": str(task.id), "status": task.status, "message": "NKS 클러스터 생성 작업이 시작되었습니다."}, status=status.HTTP_202_ACCEPTED, ) + except NHNCloudAPIError as e: + logger.exception("External Network 조회 실패") + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.exception("NKS 클러스터 생성 작업 시작 실패") return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) +class NksClusterResizeView(APIView): + """NKS 클러스터 리사이즈 API""" + + @swagger_auto_schema( + operation_summary="NKS 클러스터 노드 수 조정", + manual_parameters=[region_header, token_header], + request_body=NksClusterResizeSerializer, + responses={200: "리사이즈 결과"}, + ) + def post(self, request, cluster_name): + headers = get_nhn_headers(request) + serializer = NksClusterResizeSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.resize_cluster(cluster_name, serializer.validated_data["node_count"]) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksClusterUpgradeView(APIView): + """NKS 클러스터 업그레이드 API""" + + @swagger_auto_schema( + operation_summary="NKS 클러스터 Kubernetes 버전 업그레이드", + manual_parameters=[region_header, token_header], + request_body=NksClusterUpgradeSerializer, + responses={200: "업그레이드 결과"}, + ) + def post(self, request, cluster_name): + headers = get_nhn_headers(request) + serializer = NksClusterUpgradeSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.upgrade_cluster(cluster_name, serializer.validated_data["kubernetes_version"]) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksClusterEventsView(APIView): + """NKS 클러스터 작업 이력 API""" + + @swagger_auto_schema( + operation_summary="NKS 클러스터 작업 이력 조회", + manual_parameters=[region_header, token_header], + responses={200: "작업 이력 목록"}, + ) + def get(self, request, cluster_uuid): + headers = get_nhn_headers(request) + try: + api = ApiNks(headers["region"], headers["token"]) + return Response(api.get_cluster_events(cluster_uuid)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksClusterEventDetailView(APIView): + """NKS 클러스터 작업 이력 상세 API""" + + @swagger_auto_schema( + operation_summary="NKS 클러스터 작업 이력 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "작업 이력 상세"}, + ) + def get(self, request, cluster_uuid, event_uuid): + headers = get_nhn_headers(request) + try: + api = ApiNks(headers["region"], headers["token"]) + return Response(api.get_cluster_event(cluster_uuid, event_uuid)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksClusterCertificatesView(APIView): + """NKS 클러스터 인증서 갱신 API""" + + @swagger_auto_schema( + operation_summary="NKS 클러스터 인증서 갱신", + manual_parameters=[region_header, token_header], + responses={200: "갱신 결과"}, + ) + def post(self, request, cluster_name): + headers = get_nhn_headers(request) + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.renew_certificates(cluster_name) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksClusterIpAclView(APIView): + """NKS 클러스터 API 엔드포인트 IP 접근 제어 API""" + + @swagger_auto_schema( + operation_summary="API 엔드포인트 IP 접근 제어 조회", + manual_parameters=[region_header, token_header], + responses={200: "IP 접근 제어 설정"}, + ) + def get(self, request, cluster_name): + headers = get_nhn_headers(request) + try: + api = ApiNks(headers["region"], headers["token"]) + return Response(api.get_api_endpoint_ipacl(cluster_name)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="API 엔드포인트 IP 접근 제어 설정", + manual_parameters=[region_header, token_header], + request_body=NksApiEndpointIpAclSerializer, + responses={200: "설정 결과"}, + ) + def post(self, request, cluster_name): + headers = get_nhn_headers(request) + serializer = NksApiEndpointIpAclSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.set_api_endpoint_ipacl( + cluster_name, + serializer.validated_data["enable"], + serializer.validated_data.get("allowed_cidrs"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== NKS Node Group API ==================== + + +class NksNodeGroupListView(APIView): + """NKS 노드 그룹 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="노드 그룹 목록 조회", + 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_nodegroup_list(cluster_name)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksNodeGroupCreateView(APIView): + """NKS 노드 그룹 생성 API""" + + @swagger_auto_schema( + operation_summary="노드 그룹 생성", + manual_parameters=[region_header, token_header], + request_body=NksNodeGroupSerializer, + responses={200: "생성된 노드 그룹 정보"}, + ) + def post(self, request, cluster_name): + headers = get_nhn_headers(request) + serializer = NksNodeGroupSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.create_nodegroup( + cluster_name=cluster_name, + nodegroup_name=serializer.validated_data["nodegroup_name"], + instance_type=serializer.validated_data["instance_type"], + node_count=serializer.validated_data.get("node_count", 1), + availability_zone=serializer.validated_data.get("availability_zone"), + boot_volume_size=serializer.validated_data.get("boot_volume_size", 50), + boot_volume_type=serializer.validated_data.get("boot_volume_type", "General SSD"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksNodeGroupDetailView(APIView): + """NKS 노드 그룹 상세/삭제/수정 API""" + + @swagger_auto_schema( + operation_summary="노드 그룹 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "노드 그룹 상세 정보"}, + ) + def get(self, request, cluster_name, nodegroup_name): + headers = get_nhn_headers(request) + try: + api = ApiNks(headers["region"], headers["token"]) + return Response(api.get_nodegroup_info(cluster_name, nodegroup_name)) + 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={200: "삭제 결과"}, + ) + def delete(self, request, cluster_name, nodegroup_name): + headers = get_nhn_headers(request) + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.delete_nodegroup(cluster_name, nodegroup_name) + return Response(result) + 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], + request_body=NksNodeGroupUpdateSerializer, + responses={200: "변경된 노드 그룹 정보"}, + ) + def patch(self, request, cluster_name, nodegroup_name): + headers = get_nhn_headers(request) + serializer = NksNodeGroupUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.update_nodegroup( + cluster_name=cluster_name, + nodegroup_name=nodegroup_name, + instance_type=serializer.validated_data.get("instance_type"), + node_count=serializer.validated_data.get("node_count"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksNodeGroupUpgradeView(APIView): + """NKS 노드 그룹 업그레이드 API""" + + @swagger_auto_schema( + operation_summary="노드 그룹 Kubernetes 버전 업그레이드", + manual_parameters=[region_header, token_header], + request_body=NksNodeGroupUpgradeSerializer, + responses={200: "업그레이드 결과"}, + ) + def post(self, request, cluster_name, nodegroup_name): + headers = get_nhn_headers(request) + serializer = NksNodeGroupUpgradeSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.upgrade_nodegroup( + cluster_name=cluster_name, + nodegroup_name=nodegroup_name, + kubernetes_version=serializer.validated_data["kubernetes_version"], + max_unavailable_worker=serializer.validated_data.get("max_unavailable_worker", 1), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksNodeGroupUserScriptView(APIView): + """NKS 노드 그룹 사용자 스크립트 API""" + + @swagger_auto_schema( + operation_summary="노드 그룹 사용자 스크립트 설정", + manual_parameters=[region_header, token_header], + request_body=NksUserScriptSerializer, + responses={200: "설정 결과"}, + ) + def post(self, request, cluster_name, nodegroup_name): + headers = get_nhn_headers(request) + serializer = NksUserScriptSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.set_nodegroup_userscript( + cluster_name=cluster_name, + nodegroup_name=nodegroup_name, + userscript=serializer.validated_data["userscript"], + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksNodeStartView(APIView): + """NKS 워커 노드 시작 API""" + + @swagger_auto_schema( + operation_summary="워커 노드 시작", + manual_parameters=[region_header, token_header], + request_body=NksNodeActionSerializer, + responses={200: "시작 결과"}, + ) + def post(self, request, cluster_name, nodegroup_name): + headers = get_nhn_headers(request) + serializer = NksNodeActionSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.start_node( + cluster_name=cluster_name, + nodegroup_name=nodegroup_name, + node_id=serializer.validated_data["node_id"], + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksNodeStopView(APIView): + """NKS 워커 노드 중지 API""" + + @swagger_auto_schema( + operation_summary="워커 노드 중지", + manual_parameters=[region_header, token_header], + request_body=NksNodeActionSerializer, + responses={200: "중지 결과"}, + ) + def post(self, request, cluster_name, nodegroup_name): + headers = get_nhn_headers(request) + serializer = NksNodeActionSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.stop_node( + cluster_name=cluster_name, + nodegroup_name=nodegroup_name, + node_id=serializer.validated_data["node_id"], + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class NksAutoscaleConfigView(APIView): + """NKS 오토스케일러 설정 API""" + + @swagger_auto_schema( + operation_summary="오토스케일러 설정 조회", + manual_parameters=[region_header, token_header], + responses={200: "오토스케일러 설정"}, + ) + def get(self, request, cluster_name, nodegroup_name): + headers = get_nhn_headers(request) + try: + api = ApiNks(headers["region"], headers["token"]) + return Response(api.get_autoscale_config(cluster_name, nodegroup_name)) + 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], + request_body=NksAutoscaleConfigSerializer, + responses={200: "변경된 오토스케일러 설정"}, + ) + def post(self, request, cluster_name, nodegroup_name): + headers = get_nhn_headers(request) + serializer = NksAutoscaleConfigSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiNks(headers["region"], headers["token"]) + result = api.set_autoscale_config( + cluster_name=cluster_name, + nodegroup_name=nodegroup_name, + ca_enable=serializer.validated_data["ca_enable"], + ca_max_node_count=serializer.validated_data.get("ca_max_node_count"), + ca_min_node_count=serializer.validated_data.get("ca_min_node_count"), + ca_scale_down_enable=serializer.validated_data.get("ca_scale_down_enable"), + ca_scale_down_delay_after_add=serializer.validated_data.get("ca_scale_down_delay_after_add"), + ca_scale_down_unneeded_time=serializer.validated_data.get("ca_scale_down_unneeded_time"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + # ==================== Storage API ==================== storage_account_header = openapi.Parameter( @@ -847,3 +1319,651 @@ class StorageContainerDetailView(APIView): except Exception as e: logger.exception("컨테이너 삭제 작업 시작 실패") return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== DNS Plus API ==================== + +appkey_header = openapi.Parameter( + "X-NHN-Appkey", + openapi.IN_HEADER, + description="NHN Cloud 앱키", + type=openapi.TYPE_STRING, + required=True, +) + + +def get_appkey(request): + """요청 헤더에서 앱키 추출""" + appkey = request.headers.get("X-NHN-Appkey") + if not appkey: + raise NHNCloudAPIError("X-NHN-Appkey 헤더가 필요합니다.", code=400) + return appkey + + +# ==================== DNS Zone API ==================== + + +class DnsZoneListView(APIView): + """DNS Zone 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="DNS Zone 목록 조회", + manual_parameters=[appkey_header], + responses={200: "Zone 목록"}, + ) + def get(self, request): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + page = int(request.query_params.get("page", 1)) + limit = int(request.query_params.get("limit", 50)) + search_name = request.query_params.get("search") + return Response(api.get_zone_list( + search_zone_name=search_name, + page=page, + limit=limit, + )) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class DnsZoneCreateView(APIView): + """DNS Zone 생성 API""" + + @swagger_auto_schema( + operation_summary="DNS Zone 생성", + manual_parameters=[appkey_header], + request_body=DnsZoneSerializer, + responses={200: "생성된 Zone 정보"}, + ) + def post(self, request): + try: + appkey = get_appkey(request) + serializer = DnsZoneSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.create_zone( + zone_name=serializer.validated_data["zone_name"], + description=serializer.validated_data.get("description"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class DnsZoneDetailView(APIView): + """DNS Zone 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="DNS Zone 상세 조회", + manual_parameters=[appkey_header], + responses={200: "Zone 상세 정보"}, + ) + def get(self, request, zone_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + return Response(api.get_zone(zone_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="DNS Zone 수정", + manual_parameters=[appkey_header], + request_body=DnsZoneUpdateSerializer, + responses={200: "수정된 Zone 정보"}, + ) + def put(self, request, zone_id): + try: + appkey = get_appkey(request) + serializer = DnsZoneUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.update_zone(zone_id, serializer.validated_data["description"]) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="DNS Zone 삭제", + manual_parameters=[appkey_header], + responses={200: "삭제 결과"}, + ) + def delete(self, request, zone_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + result = api.delete_zones([zone_id]) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== DNS Record Set API ==================== + + +class DnsRecordSetListView(APIView): + """DNS 레코드 세트 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="레코드 세트 목록 조회", + manual_parameters=[appkey_header], + responses={200: "레코드 세트 목록"}, + ) + def get(self, request, zone_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + page = int(request.query_params.get("page", 1)) + limit = int(request.query_params.get("limit", 50)) + search_name = request.query_params.get("search") + recordset_type = request.query_params.get("type") + return Response(api.get_recordset_list( + zone_id=zone_id, + recordset_type_list=[recordset_type] if recordset_type else None, + search_recordset_name=search_name, + page=page, + limit=limit, + )) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class DnsRecordSetCreateView(APIView): + """DNS 레코드 세트 생성 API""" + + @swagger_auto_schema( + operation_summary="레코드 세트 생성", + manual_parameters=[appkey_header], + request_body=DnsRecordSetSerializer, + responses={200: "생성된 레코드 세트 정보"}, + ) + def post(self, request, zone_id): + try: + appkey = get_appkey(request) + serializer = DnsRecordSetSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.create_recordset( + zone_id=zone_id, + recordset_name=serializer.validated_data["recordset_name"], + recordset_type=serializer.validated_data["recordset_type"], + recordset_ttl=serializer.validated_data["recordset_ttl"], + record_list=serializer.validated_data["record_list"], + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class DnsRecordSetDetailView(APIView): + """DNS 레코드 세트 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="레코드 세트 상세 조회", + manual_parameters=[appkey_header], + responses={200: "레코드 세트 상세 정보"}, + ) + def get(self, request, zone_id, recordset_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + return Response(api.get_recordset(zone_id, recordset_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="레코드 세트 수정", + manual_parameters=[appkey_header], + request_body=DnsRecordSetUpdateSerializer, + responses={200: "수정된 레코드 세트 정보"}, + ) + def put(self, request, zone_id, recordset_id): + try: + appkey = get_appkey(request) + serializer = DnsRecordSetUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.update_recordset( + zone_id=zone_id, + recordset_id=recordset_id, + recordset_type=serializer.validated_data["recordset_type"], + recordset_ttl=serializer.validated_data["recordset_ttl"], + record_list=serializer.validated_data["record_list"], + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="레코드 세트 삭제", + manual_parameters=[appkey_header], + responses={200: "삭제 결과"}, + ) + def delete(self, request, zone_id, recordset_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + result = api.delete_recordsets(zone_id, [recordset_id]) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== DNS Pool API ==================== + + +class DnsPoolListView(APIView): + """DNS Pool 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="Pool 목록 조회", + manual_parameters=[appkey_header], + responses={200: "Pool 목록"}, + ) + def get(self, request): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + page = int(request.query_params.get("page", 1)) + limit = int(request.query_params.get("limit", 50)) + search_name = request.query_params.get("search") + return Response(api.get_pool_list( + search_pool_name=search_name, + page=page, + limit=limit, + )) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class DnsPoolCreateView(APIView): + """DNS Pool 생성 API""" + + @swagger_auto_schema( + operation_summary="Pool 생성", + manual_parameters=[appkey_header], + request_body=DnsPoolSerializer, + responses={200: "생성된 Pool 정보"}, + ) + def post(self, request): + try: + appkey = get_appkey(request) + serializer = DnsPoolSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.create_pool( + pool_name=serializer.validated_data["pool_name"], + endpoint_list=serializer.validated_data["endpoint_list"], + pool_disabled=serializer.validated_data.get("pool_disabled", False), + health_check_id=serializer.validated_data.get("health_check_id"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class DnsPoolDetailView(APIView): + """DNS Pool 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="Pool 상세 조회", + manual_parameters=[appkey_header], + responses={200: "Pool 상세 정보"}, + ) + def get(self, request, pool_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + return Response(api.get_pool(pool_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="Pool 수정", + manual_parameters=[appkey_header], + request_body=DnsPoolUpdateSerializer, + responses={200: "수정된 Pool 정보"}, + ) + def put(self, request, pool_id): + try: + appkey = get_appkey(request) + serializer = DnsPoolUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.update_pool( + pool_id=pool_id, + pool_name=serializer.validated_data.get("pool_name"), + endpoint_list=serializer.validated_data.get("endpoint_list"), + pool_disabled=serializer.validated_data.get("pool_disabled"), + health_check_id=serializer.validated_data.get("health_check_id"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="Pool 삭제", + manual_parameters=[appkey_header], + responses={200: "삭제 결과"}, + ) + def delete(self, request, pool_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + result = api.delete_pools([pool_id]) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== DNS GSLB API ==================== + + +class DnsGslbListView(APIView): + """DNS GSLB 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="GSLB 목록 조회", + manual_parameters=[appkey_header], + responses={200: "GSLB 목록"}, + ) + def get(self, request): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + page = int(request.query_params.get("page", 1)) + limit = int(request.query_params.get("limit", 50)) + search_name = request.query_params.get("search") + return Response(api.get_gslb_list( + search_gslb_name=search_name, + page=page, + limit=limit, + )) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class DnsGslbCreateView(APIView): + """DNS GSLB 생성 API""" + + @swagger_auto_schema( + operation_summary="GSLB 생성", + manual_parameters=[appkey_header], + request_body=DnsGslbSerializer, + responses={200: "생성된 GSLB 정보"}, + ) + def post(self, request): + try: + appkey = get_appkey(request) + serializer = DnsGslbSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.create_gslb( + gslb_name=serializer.validated_data["gslb_name"], + gslb_ttl=serializer.validated_data["gslb_ttl"], + gslb_routing_rule=serializer.validated_data["gslb_routing_rule"], + gslb_disabled=serializer.validated_data.get("gslb_disabled", False), + connected_pool_list=serializer.validated_data.get("connected_pool_list"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class DnsGslbDetailView(APIView): + """DNS GSLB 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="GSLB 상세 조회", + manual_parameters=[appkey_header], + responses={200: "GSLB 상세 정보"}, + ) + def get(self, request, gslb_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + return Response(api.get_gslb(gslb_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="GSLB 수정", + manual_parameters=[appkey_header], + request_body=DnsGslbUpdateSerializer, + responses={200: "수정된 GSLB 정보"}, + ) + def put(self, request, gslb_id): + try: + appkey = get_appkey(request) + serializer = DnsGslbUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.update_gslb( + gslb_id=gslb_id, + gslb_name=serializer.validated_data.get("gslb_name"), + gslb_ttl=serializer.validated_data.get("gslb_ttl"), + gslb_routing_rule=serializer.validated_data.get("gslb_routing_rule"), + gslb_disabled=serializer.validated_data.get("gslb_disabled"), + connected_pool_list=serializer.validated_data.get("connected_pool_list"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="GSLB 삭제", + manual_parameters=[appkey_header], + responses={200: "삭제 결과"}, + ) + def delete(self, request, gslb_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + result = api.delete_gslbs([gslb_id]) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class DnsGslbPoolConnectView(APIView): + """GSLB에 Pool 연결/수정/해제 API""" + + @swagger_auto_schema( + operation_summary="GSLB에 Pool 연결", + manual_parameters=[appkey_header], + request_body=DnsPoolConnectSerializer, + responses={200: "연결 결과"}, + ) + def post(self, request, gslb_id, pool_id): + try: + appkey = get_appkey(request) + serializer = DnsPoolConnectSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.connect_pool( + gslb_id=gslb_id, + pool_id=pool_id, + connected_pool_order=serializer.validated_data["connected_pool_order"], + connected_pool_region_content=serializer.validated_data.get("connected_pool_region_content"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="연결된 Pool 수정", + manual_parameters=[appkey_header], + request_body=DnsPoolConnectSerializer, + responses={200: "수정 결과"}, + ) + def put(self, request, gslb_id, pool_id): + try: + appkey = get_appkey(request) + serializer = DnsPoolConnectSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.update_connected_pool( + gslb_id=gslb_id, + pool_id=pool_id, + connected_pool_order=serializer.validated_data["connected_pool_order"], + connected_pool_region_content=serializer.validated_data.get("connected_pool_region_content"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="Pool 연결 해제", + manual_parameters=[appkey_header], + responses={200: "연결 해제 결과"}, + ) + def delete(self, request, gslb_id, pool_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + result = api.disconnect_pools(gslb_id, [pool_id]) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== DNS Health Check API ==================== + + +class DnsHealthCheckListView(APIView): + """DNS Health Check 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="Health Check 목록 조회", + manual_parameters=[appkey_header], + responses={200: "Health Check 목록"}, + ) + def get(self, request): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + page = int(request.query_params.get("page", 1)) + limit = int(request.query_params.get("limit", 50)) + search_name = request.query_params.get("search") + return Response(api.get_health_check_list( + search_health_check_name=search_name, + page=page, + limit=limit, + )) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class DnsHealthCheckCreateView(APIView): + """DNS Health Check 생성 API""" + + @swagger_auto_schema( + operation_summary="Health Check 생성", + manual_parameters=[appkey_header], + request_body=DnsHealthCheckSerializer, + responses={200: "생성된 Health Check 정보"}, + ) + def post(self, request): + try: + appkey = get_appkey(request) + serializer = DnsHealthCheckSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.create_health_check( + health_check_name=serializer.validated_data["health_check_name"], + health_check_protocol=serializer.validated_data["health_check_protocol"], + health_check_port=serializer.validated_data["health_check_port"], + health_check_interval=serializer.validated_data.get("health_check_interval", 30), + health_check_timeout=serializer.validated_data.get("health_check_timeout", 5), + health_check_retries=serializer.validated_data.get("health_check_retries", 2), + health_check_path=serializer.validated_data.get("health_check_path"), + health_check_expected_codes=serializer.validated_data.get("health_check_expected_codes"), + health_check_expected_body=serializer.validated_data.get("health_check_expected_body"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class DnsHealthCheckDetailView(APIView): + """DNS Health Check 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="Health Check 상세 조회", + manual_parameters=[appkey_header], + responses={200: "Health Check 상세 정보"}, + ) + def get(self, request, health_check_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + return Response(api.get_health_check(health_check_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="Health Check 수정", + manual_parameters=[appkey_header], + request_body=DnsHealthCheckUpdateSerializer, + responses={200: "수정된 Health Check 정보"}, + ) + def put(self, request, health_check_id): + try: + appkey = get_appkey(request) + serializer = DnsHealthCheckUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + api = ApiDnsPlus(appkey) + result = api.update_health_check( + health_check_id=health_check_id, + health_check_name=serializer.validated_data.get("health_check_name"), + health_check_protocol=serializer.validated_data.get("health_check_protocol"), + health_check_port=serializer.validated_data.get("health_check_port"), + health_check_interval=serializer.validated_data.get("health_check_interval"), + health_check_timeout=serializer.validated_data.get("health_check_timeout"), + health_check_retries=serializer.validated_data.get("health_check_retries"), + health_check_path=serializer.validated_data.get("health_check_path"), + health_check_expected_codes=serializer.validated_data.get("health_check_expected_codes"), + health_check_expected_body=serializer.validated_data.get("health_check_expected_body"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="Health Check 삭제", + manual_parameters=[appkey_header], + responses={200: "삭제 결과"}, + ) + def delete(self, request, health_check_id): + try: + appkey = get_appkey(request) + api = ApiDnsPlus(appkey) + result = api.delete_health_checks([health_check_id]) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) diff --git a/nhn_prj/settings.py b/nhn_prj/settings.py index 8f9710c..6cec346 100644 --- a/nhn_prj/settings.py +++ b/nhn_prj/settings.py @@ -158,6 +158,7 @@ CORS_ALLOW_HEADERS = [ "x-nhn-region", "x-nhn-tenant-id", "x-nhn-storage-account", + "x-nhn-appkey", ] # REST Framework settings diff --git a/version b/version index 04eddb2..3ce186f 100644 --- a/version +++ b/version @@ -1 +1 @@ -v0.0.7 \ No newline at end of file +v0.0.8