diff --git a/nhn/migrations/0002_alter_asynctask_task_type.py b/nhn/migrations/0002_alter_asynctask_task_type.py new file mode 100644 index 0000000..2197431 --- /dev/null +++ b/nhn/migrations/0002_alter_asynctask_task_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.14 on 2026-01-15 14:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nhn', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='asynctask', + name='task_type', + field=models.CharField(choices=[('instance_create', '인스턴스 생성'), ('instance_delete', '인스턴스 삭제'), ('instance_action', '인스턴스 액션'), ('vpc_create', 'VPC 생성'), ('vpc_delete', 'VPC 삭제'), ('subnet_create', '서브넷 생성'), ('subnet_delete', '서브넷 삭제'), ('nks_create', 'NKS 클러스터 생성'), ('nks_delete', 'NKS 클러스터 삭제'), ('storage_create', '스토리지 컨테이너 생성'), ('storage_delete', '스토리지 컨테이너 삭제'), ('lb_create', '로드밸런서 생성'), ('lb_delete', '로드밸런서 삭제')], max_length=50), + ), + ] diff --git a/nhn/models.py b/nhn/models.py index 557e369..191f7dc 100644 --- a/nhn/models.py +++ b/nhn/models.py @@ -33,6 +33,9 @@ class AsyncTask(models.Model): # Storage STORAGE_CREATE = "storage_create", "스토리지 컨테이너 생성" STORAGE_DELETE = "storage_delete", "스토리지 컨테이너 삭제" + # Load Balancer + LB_CREATE = "lb_create", "로드밸런서 생성" + LB_DELETE = "lb_delete", "로드밸런서 삭제" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) task_type = models.CharField(max_length=50, choices=TaskType.choices) diff --git a/nhn/packages/__init__.py b/nhn/packages/__init__.py index f71716e..ab28101 100644 --- a/nhn/packages/__init__.py +++ b/nhn/packages/__init__.py @@ -4,6 +4,7 @@ from .vpc import ApiVpc from .nks import ApiNks from .storage import ApiStorageObject from .dnsplus import ApiDnsPlus +from .loadbalancer import ApiLoadBalancer __all__ = [ "NHNCloudToken", @@ -12,4 +13,5 @@ __all__ = [ "ApiNks", "ApiStorageObject", "ApiDnsPlus", + "ApiLoadBalancer", ] diff --git a/nhn/packages/loadbalancer.py b/nhn/packages/loadbalancer.py new file mode 100644 index 0000000..85581c0 --- /dev/null +++ b/nhn/packages/loadbalancer.py @@ -0,0 +1,931 @@ +""" +NHN Cloud Load Balancer API Module + +로드밸런서, 리스너, 풀, 멤버, 헬스 모니터, L7 정책 관리 +""" + +import logging +from typing import Optional, List + +from .base import BaseAPI, NHNCloudEndpoints, Region + +logger = logging.getLogger(__name__) + + +class ApiLoadBalancer(BaseAPI): + """NHN Cloud Load Balancer API 클래스""" + + def __init__(self, region: str, token: str): + """ + Args: + region: 리전 (kr1: 판교, kr2: 평촌) + token: API 인증 토큰 + """ + super().__init__(region, token) + + if self.region == Region.KR1: + self.lb_url = NHNCloudEndpoints.NETWORK_KR1 + else: + self.lb_url = NHNCloudEndpoints.NETWORK_KR2 + + # ==================== Load Balancer ==================== + + def get_loadbalancer_list( + self, + id: Optional[str] = None, + name: Optional[str] = None, + provisioning_status: Optional[str] = None, + vip_address: Optional[str] = None, + vip_subnet_id: Optional[str] = None, + ) -> dict: + """ + 로드밸런서 목록 조회 + + Args: + id: 로드밸런서 ID로 필터링 + name: 로드밸런서 이름으로 필터링 + provisioning_status: 프로비저닝 상태로 필터링 (ACTIVE, PENDING_CREATE 등) + vip_address: VIP 주소로 필터링 + vip_subnet_id: VIP 서브넷 ID로 필터링 + """ + url = f"{self.lb_url}/v2.0/lbaas/loadbalancers" + params = {} + if id: + params["id"] = id + if name: + params["name"] = name + if provisioning_status: + params["provisioning_status"] = provisioning_status + if vip_address: + params["vip_address"] = vip_address + if vip_subnet_id: + params["vip_subnet_id"] = vip_subnet_id + + logger.info(f"[NHN API] 로드밸런서 목록 조회 요청 - URL={url}") + result = self._get(url, params=params if params else None) + lb_count = len(result.get("loadbalancers", [])) + logger.info(f"[NHN API] 로드밸런서 목록 조회 완료 - count={lb_count}") + return result + + def get_loadbalancer_info(self, loadbalancer_id: str) -> dict: + """로드밸런서 상세 조회""" + url = f"{self.lb_url}/v2.0/lbaas/loadbalancers/{loadbalancer_id}" + logger.info(f"[NHN API] 로드밸런서 상세 조회 요청 - loadbalancer_id={loadbalancer_id}") + result = self._get(url) + logger.info(f"[NHN API] 로드밸런서 상세 조회 완료 - loadbalancer_id={loadbalancer_id}") + return result + + def create_loadbalancer( + self, + vip_subnet_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + vip_address: Optional[str] = None, + admin_state_up: bool = True, + loadbalancer_type: str = "shared", + ) -> dict: + """ + 로드밸런서 생성 + + Args: + vip_subnet_id: VIP가 할당될 서브넷 ID (필수) + name: 로드밸런서 이름 + description: 설명 + vip_address: VIP 주소 (지정하지 않으면 자동 할당) + admin_state_up: 관리자 상태 + loadbalancer_type: 로드밸런서 타입 (shared 또는 dedicated) + """ + url = f"{self.lb_url}/v2.0/lbaas/loadbalancers" + payload = { + "loadbalancer": { + "vip_subnet_id": vip_subnet_id, + "admin_state_up": admin_state_up, + "loadbalancer_type": loadbalancer_type, + } + } + if name: + payload["loadbalancer"]["name"] = name + if description: + payload["loadbalancer"]["description"] = description + if vip_address: + payload["loadbalancer"]["vip_address"] = vip_address + + logger.info(f"[NHN API] 로드밸런서 생성 요청 - name={name}, vip_subnet_id={vip_subnet_id}") + return self._post(url, payload) + + def update_loadbalancer( + self, + loadbalancer_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + admin_state_up: Optional[bool] = None, + ) -> dict: + """ + 로드밸런서 수정 + + Args: + loadbalancer_id: 로드밸런서 ID + name: 변경할 이름 + description: 변경할 설명 + admin_state_up: 관리자 상태 + """ + url = f"{self.lb_url}/v2.0/lbaas/loadbalancers/{loadbalancer_id}" + payload = {"loadbalancer": {}} + if name is not None: + payload["loadbalancer"]["name"] = name + if description is not None: + payload["loadbalancer"]["description"] = description + if admin_state_up is not None: + payload["loadbalancer"]["admin_state_up"] = admin_state_up + + logger.info(f"[NHN API] 로드밸런서 수정 요청 - loadbalancer_id={loadbalancer_id}") + return self._put(url, payload) + + def delete_loadbalancer(self, loadbalancer_id: str) -> dict: + """로드밸런서 삭제""" + url = f"{self.lb_url}/v2.0/lbaas/loadbalancers/{loadbalancer_id}" + logger.info(f"[NHN API] 로드밸런서 삭제 요청 - loadbalancer_id={loadbalancer_id}") + return self._delete(url) + + # ==================== Listener ==================== + + def get_listener_list( + self, + loadbalancer_id: Optional[str] = None, + protocol: Optional[str] = None, + protocol_port: Optional[int] = None, + ) -> dict: + """ + 리스너 목록 조회 + + Args: + loadbalancer_id: 로드밸런서 ID로 필터링 + protocol: 프로토콜로 필터링 (TCP, HTTP, HTTPS, TERMINATED_HTTPS) + protocol_port: 포트 번호로 필터링 + """ + url = f"{self.lb_url}/v2.0/lbaas/listeners" + params = {} + if loadbalancer_id: + params["loadbalancer_id"] = loadbalancer_id + if protocol: + params["protocol"] = protocol + if protocol_port: + params["protocol_port"] = protocol_port + + logger.info(f"[NHN API] 리스너 목록 조회 요청") + result = self._get(url, params=params if params else None) + listener_count = len(result.get("listeners", [])) + logger.info(f"[NHN API] 리스너 목록 조회 완료 - count={listener_count}") + return result + + def get_listener_info(self, listener_id: str) -> dict: + """리스너 상세 조회""" + url = f"{self.lb_url}/v2.0/lbaas/listeners/{listener_id}" + logger.info(f"[NHN API] 리스너 상세 조회 요청 - listener_id={listener_id}") + return self._get(url) + + def create_listener( + self, + loadbalancer_id: str, + protocol: str, + protocol_port: int, + name: Optional[str] = None, + description: Optional[str] = None, + default_pool_id: Optional[str] = None, + connection_limit: int = -1, + keepalive_timeout: int = 300, + admin_state_up: bool = True, + default_tls_container_ref: Optional[str] = None, + sni_container_refs: Optional[List[str]] = None, + insert_headers: Optional[dict] = None, + ) -> dict: + """ + 리스너 생성 + + Args: + loadbalancer_id: 로드밸런서 ID (필수) + protocol: 프로토콜 (TCP, HTTP, HTTPS, TERMINATED_HTTPS) + protocol_port: 포트 번호 (1-65535) + name: 리스너 이름 + description: 설명 + default_pool_id: 기본 풀 ID + connection_limit: 연결 제한 (-1: 무제한) + keepalive_timeout: Keepalive 타임아웃 (초) + admin_state_up: 관리자 상태 + default_tls_container_ref: TLS 인증서 컨테이너 참조 (TERMINATED_HTTPS 사용 시) + sni_container_refs: SNI 인증서 컨테이너 참조 목록 + insert_headers: 삽입할 헤더 (X-Forwarded-For, X-Forwarded-Port 등) + """ + url = f"{self.lb_url}/v2.0/lbaas/listeners" + payload = { + "listener": { + "loadbalancer_id": loadbalancer_id, + "protocol": protocol, + "protocol_port": protocol_port, + "connection_limit": connection_limit, + "keepalive_timeout": keepalive_timeout, + "admin_state_up": admin_state_up, + } + } + if name: + payload["listener"]["name"] = name + if description: + payload["listener"]["description"] = description + if default_pool_id: + payload["listener"]["default_pool_id"] = default_pool_id + if default_tls_container_ref: + payload["listener"]["default_tls_container_ref"] = default_tls_container_ref + if sni_container_refs: + payload["listener"]["sni_container_refs"] = sni_container_refs + if insert_headers: + payload["listener"]["insert_headers"] = insert_headers + + logger.info(f"[NHN API] 리스너 생성 요청 - protocol={protocol}, port={protocol_port}") + return self._post(url, payload) + + def update_listener( + self, + listener_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + default_pool_id: Optional[str] = None, + connection_limit: Optional[int] = None, + keepalive_timeout: Optional[int] = None, + admin_state_up: Optional[bool] = None, + default_tls_container_ref: Optional[str] = None, + sni_container_refs: Optional[List[str]] = None, + insert_headers: Optional[dict] = None, + ) -> dict: + """리스너 수정""" + url = f"{self.lb_url}/v2.0/lbaas/listeners/{listener_id}" + payload = {"listener": {}} + if name is not None: + payload["listener"]["name"] = name + if description is not None: + payload["listener"]["description"] = description + if default_pool_id is not None: + payload["listener"]["default_pool_id"] = default_pool_id + if connection_limit is not None: + payload["listener"]["connection_limit"] = connection_limit + if keepalive_timeout is not None: + payload["listener"]["keepalive_timeout"] = keepalive_timeout + if admin_state_up is not None: + payload["listener"]["admin_state_up"] = admin_state_up + if default_tls_container_ref is not None: + payload["listener"]["default_tls_container_ref"] = default_tls_container_ref + if sni_container_refs is not None: + payload["listener"]["sni_container_refs"] = sni_container_refs + if insert_headers is not None: + payload["listener"]["insert_headers"] = insert_headers + + logger.info(f"[NHN API] 리스너 수정 요청 - listener_id={listener_id}") + return self._put(url, payload) + + def delete_listener(self, listener_id: str) -> dict: + """리스너 삭제""" + url = f"{self.lb_url}/v2.0/lbaas/listeners/{listener_id}" + logger.info(f"[NHN API] 리스너 삭제 요청 - listener_id={listener_id}") + return self._delete(url) + + # ==================== Pool ==================== + + def get_pool_list( + self, + loadbalancer_id: Optional[str] = None, + protocol: Optional[str] = None, + lb_algorithm: Optional[str] = None, + ) -> dict: + """ + 풀 목록 조회 + + Args: + loadbalancer_id: 로드밸런서 ID로 필터링 + protocol: 프로토콜로 필터링 + lb_algorithm: 로드밸런싱 알고리즘으로 필터링 + """ + url = f"{self.lb_url}/v2.0/lbaas/pools" + params = {} + if loadbalancer_id: + params["loadbalancer_id"] = loadbalancer_id + if protocol: + params["protocol"] = protocol + if lb_algorithm: + params["lb_algorithm"] = lb_algorithm + + logger.info(f"[NHN API] 풀 목록 조회 요청") + result = self._get(url, params=params if params else None) + pool_count = len(result.get("pools", [])) + logger.info(f"[NHN API] 풀 목록 조회 완료 - count={pool_count}") + return result + + def get_pool_info(self, pool_id: str) -> dict: + """풀 상세 조회""" + url = f"{self.lb_url}/v2.0/lbaas/pools/{pool_id}" + logger.info(f"[NHN API] 풀 상세 조회 요청 - pool_id={pool_id}") + return self._get(url) + + def create_pool( + self, + lb_algorithm: str, + protocol: str, + loadbalancer_id: Optional[str] = None, + listener_id: Optional[str] = None, + name: Optional[str] = None, + description: Optional[str] = None, + admin_state_up: bool = True, + session_persistence: Optional[dict] = None, + ) -> dict: + """ + 풀 생성 + + Args: + lb_algorithm: 로드밸런싱 알고리즘 (ROUND_ROBIN, LEAST_CONNECTIONS, SOURCE_IP) + protocol: 프로토콜 (TCP, HTTP, HTTPS, PROXY) + loadbalancer_id: 로드밸런서 ID (listener_id와 둘 중 하나 필수) + listener_id: 리스너 ID (loadbalancer_id와 둘 중 하나 필수) + name: 풀 이름 + description: 설명 + admin_state_up: 관리자 상태 + session_persistence: 세션 지속성 설정 {type, cookie_name} + - type: SOURCE_IP, HTTP_COOKIE, APP_COOKIE + - cookie_name: APP_COOKIE 타입 사용 시 쿠키 이름 + """ + url = f"{self.lb_url}/v2.0/lbaas/pools" + payload = { + "pool": { + "lb_algorithm": lb_algorithm, + "protocol": protocol, + "admin_state_up": admin_state_up, + } + } + if loadbalancer_id: + payload["pool"]["loadbalancer_id"] = loadbalancer_id + if listener_id: + payload["pool"]["listener_id"] = listener_id + if name: + payload["pool"]["name"] = name + if description: + payload["pool"]["description"] = description + if session_persistence: + payload["pool"]["session_persistence"] = session_persistence + + logger.info(f"[NHN API] 풀 생성 요청 - algorithm={lb_algorithm}, protocol={protocol}") + return self._post(url, payload) + + def update_pool( + self, + pool_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + lb_algorithm: Optional[str] = None, + admin_state_up: Optional[bool] = None, + session_persistence: Optional[dict] = None, + ) -> dict: + """풀 수정""" + url = f"{self.lb_url}/v2.0/lbaas/pools/{pool_id}" + payload = {"pool": {}} + if name is not None: + payload["pool"]["name"] = name + if description is not None: + payload["pool"]["description"] = description + if lb_algorithm is not None: + payload["pool"]["lb_algorithm"] = lb_algorithm + if admin_state_up is not None: + payload["pool"]["admin_state_up"] = admin_state_up + if session_persistence is not None: + payload["pool"]["session_persistence"] = session_persistence + + logger.info(f"[NHN API] 풀 수정 요청 - pool_id={pool_id}") + return self._put(url, payload) + + def delete_pool(self, pool_id: str) -> dict: + """풀 삭제""" + url = f"{self.lb_url}/v2.0/lbaas/pools/{pool_id}" + logger.info(f"[NHN API] 풀 삭제 요청 - pool_id={pool_id}") + return self._delete(url) + + # ==================== Member ==================== + + def get_member_list(self, pool_id: str) -> dict: + """풀의 멤버 목록 조회""" + url = f"{self.lb_url}/v2.0/lbaas/pools/{pool_id}/members" + logger.info(f"[NHN API] 멤버 목록 조회 요청 - pool_id={pool_id}") + result = self._get(url) + member_count = len(result.get("members", [])) + logger.info(f"[NHN API] 멤버 목록 조회 완료 - count={member_count}") + return result + + def get_member_info(self, pool_id: str, member_id: str) -> dict: + """멤버 상세 조회""" + url = f"{self.lb_url}/v2.0/lbaas/pools/{pool_id}/members/{member_id}" + logger.info(f"[NHN API] 멤버 상세 조회 요청 - pool_id={pool_id}, member_id={member_id}") + return self._get(url) + + def create_member( + self, + pool_id: str, + address: str, + protocol_port: int, + subnet_id: Optional[str] = None, + weight: int = 1, + admin_state_up: bool = True, + ) -> dict: + """ + 멤버 추가 + + Args: + pool_id: 풀 ID + address: 멤버 IP 주소 + protocol_port: 멤버 포트 번호 + subnet_id: 멤버가 속한 서브넷 ID + weight: 가중치 (1-256) + admin_state_up: 관리자 상태 + """ + url = f"{self.lb_url}/v2.0/lbaas/pools/{pool_id}/members" + payload = { + "member": { + "address": address, + "protocol_port": protocol_port, + "weight": weight, + "admin_state_up": admin_state_up, + } + } + if subnet_id: + payload["member"]["subnet_id"] = subnet_id + + logger.info(f"[NHN API] 멤버 추가 요청 - pool_id={pool_id}, address={address}:{protocol_port}") + return self._post(url, payload) + + def update_member( + self, + pool_id: str, + member_id: str, + weight: Optional[int] = None, + admin_state_up: Optional[bool] = None, + ) -> dict: + """멤버 수정""" + url = f"{self.lb_url}/v2.0/lbaas/pools/{pool_id}/members/{member_id}" + payload = {"member": {}} + if weight is not None: + payload["member"]["weight"] = weight + if admin_state_up is not None: + payload["member"]["admin_state_up"] = admin_state_up + + logger.info(f"[NHN API] 멤버 수정 요청 - pool_id={pool_id}, member_id={member_id}") + return self._put(url, payload) + + def delete_member(self, pool_id: str, member_id: str) -> dict: + """멤버 삭제""" + url = f"{self.lb_url}/v2.0/lbaas/pools/{pool_id}/members/{member_id}" + logger.info(f"[NHN API] 멤버 삭제 요청 - pool_id={pool_id}, member_id={member_id}") + return self._delete(url) + + # ==================== Health Monitor ==================== + + def get_healthmonitor_list(self, pool_id: Optional[str] = None) -> dict: + """헬스 모니터 목록 조회""" + url = f"{self.lb_url}/v2.0/lbaas/healthmonitors" + params = {} + if pool_id: + params["pool_id"] = pool_id + + logger.info(f"[NHN API] 헬스 모니터 목록 조회 요청") + result = self._get(url, params=params if params else None) + hm_count = len(result.get("healthmonitors", [])) + logger.info(f"[NHN API] 헬스 모니터 목록 조회 완료 - count={hm_count}") + return result + + def get_healthmonitor_info(self, healthmonitor_id: str) -> dict: + """헬스 모니터 상세 조회""" + url = f"{self.lb_url}/v2.0/lbaas/healthmonitors/{healthmonitor_id}" + logger.info(f"[NHN API] 헬스 모니터 상세 조회 요청 - healthmonitor_id={healthmonitor_id}") + return self._get(url) + + def create_healthmonitor( + self, + pool_id: str, + type: str, + delay: int, + timeout: int, + max_retries: int, + max_retries_down: int = 3, + http_method: str = "GET", + url_path: str = "/", + expected_codes: str = "200", + admin_state_up: bool = True, + host_header: Optional[str] = None, + ) -> dict: + """ + 헬스 모니터 생성 + + Args: + pool_id: 풀 ID + type: 헬스 체크 타입 (TCP, HTTP, HTTPS) + delay: 헬스 체크 간격 (초) + timeout: 응답 대기 시간 (초) + max_retries: 최대 재시도 횟수 (정상 판정) + max_retries_down: 최대 재시도 횟수 (비정상 판정) + http_method: HTTP 메서드 (HTTP/HTTPS 타입 시) + url_path: URL 경로 (HTTP/HTTPS 타입 시) + expected_codes: 예상 응답 코드 (예: "200", "200,201", "200-204") + admin_state_up: 관리자 상태 + host_header: Host 헤더 값 + """ + url = f"{self.lb_url}/v2.0/lbaas/healthmonitors" + payload = { + "healthmonitor": { + "pool_id": pool_id, + "type": type, + "delay": delay, + "timeout": timeout, + "max_retries": max_retries, + "max_retries_down": max_retries_down, + "admin_state_up": admin_state_up, + } + } + # HTTP/HTTPS 전용 옵션 + if type in ["HTTP", "HTTPS"]: + payload["healthmonitor"]["http_method"] = http_method + payload["healthmonitor"]["url_path"] = url_path + payload["healthmonitor"]["expected_codes"] = expected_codes + if host_header: + payload["healthmonitor"]["host_header"] = host_header + + logger.info(f"[NHN API] 헬스 모니터 생성 요청 - pool_id={pool_id}, type={type}") + return self._post(url, payload) + + def update_healthmonitor( + self, + healthmonitor_id: str, + delay: Optional[int] = None, + timeout: Optional[int] = None, + max_retries: Optional[int] = None, + max_retries_down: Optional[int] = None, + http_method: Optional[str] = None, + url_path: Optional[str] = None, + expected_codes: Optional[str] = None, + admin_state_up: Optional[bool] = None, + host_header: Optional[str] = None, + ) -> dict: + """헬스 모니터 수정""" + url = f"{self.lb_url}/v2.0/lbaas/healthmonitors/{healthmonitor_id}" + payload = {"healthmonitor": {}} + if delay is not None: + payload["healthmonitor"]["delay"] = delay + if timeout is not None: + payload["healthmonitor"]["timeout"] = timeout + if max_retries is not None: + payload["healthmonitor"]["max_retries"] = max_retries + if max_retries_down is not None: + payload["healthmonitor"]["max_retries_down"] = max_retries_down + if http_method is not None: + payload["healthmonitor"]["http_method"] = http_method + if url_path is not None: + payload["healthmonitor"]["url_path"] = url_path + if expected_codes is not None: + payload["healthmonitor"]["expected_codes"] = expected_codes + if admin_state_up is not None: + payload["healthmonitor"]["admin_state_up"] = admin_state_up + if host_header is not None: + payload["healthmonitor"]["host_header"] = host_header + + logger.info(f"[NHN API] 헬스 모니터 수정 요청 - healthmonitor_id={healthmonitor_id}") + return self._put(url, payload) + + def delete_healthmonitor(self, healthmonitor_id: str) -> dict: + """헬스 모니터 삭제""" + url = f"{self.lb_url}/v2.0/lbaas/healthmonitors/{healthmonitor_id}" + logger.info(f"[NHN API] 헬스 모니터 삭제 요청 - healthmonitor_id={healthmonitor_id}") + return self._delete(url) + + # ==================== L7 Policy ==================== + + def get_l7policy_list(self, listener_id: Optional[str] = None) -> dict: + """L7 정책 목록 조회""" + url = f"{self.lb_url}/v2.0/lbaas/l7policies" + params = {} + if listener_id: + params["listener_id"] = listener_id + + logger.info(f"[NHN API] L7 정책 목록 조회 요청") + result = self._get(url, params=params if params else None) + policy_count = len(result.get("l7policies", [])) + logger.info(f"[NHN API] L7 정책 목록 조회 완료 - count={policy_count}") + return result + + def get_l7policy_info(self, l7policy_id: str) -> dict: + """L7 정책 상세 조회""" + url = f"{self.lb_url}/v2.0/lbaas/l7policies/{l7policy_id}" + logger.info(f"[NHN API] L7 정책 상세 조회 요청 - l7policy_id={l7policy_id}") + return self._get(url) + + def create_l7policy( + self, + listener_id: str, + action: str, + position: int = 1, + name: Optional[str] = None, + description: Optional[str] = None, + redirect_pool_id: Optional[str] = None, + redirect_url: Optional[str] = None, + admin_state_up: bool = True, + ) -> dict: + """ + L7 정책 생성 + + Args: + listener_id: 리스너 ID + action: 액션 (REDIRECT_TO_POOL, REDIRECT_TO_URL, REJECT) + position: 정책 우선순위 (낮을수록 먼저 적용) + name: 정책 이름 + description: 설명 + redirect_pool_id: 리다이렉트 대상 풀 ID (REDIRECT_TO_POOL 액션 시) + redirect_url: 리다이렉트 URL (REDIRECT_TO_URL 액션 시) + admin_state_up: 관리자 상태 + """ + url = f"{self.lb_url}/v2.0/lbaas/l7policies" + payload = { + "l7policy": { + "listener_id": listener_id, + "action": action, + "position": position, + "admin_state_up": admin_state_up, + } + } + if name: + payload["l7policy"]["name"] = name + if description: + payload["l7policy"]["description"] = description + if redirect_pool_id: + payload["l7policy"]["redirect_pool_id"] = redirect_pool_id + if redirect_url: + payload["l7policy"]["redirect_url"] = redirect_url + + logger.info(f"[NHN API] L7 정책 생성 요청 - listener_id={listener_id}, action={action}") + return self._post(url, payload) + + def update_l7policy( + self, + l7policy_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + action: Optional[str] = None, + position: Optional[int] = None, + redirect_pool_id: Optional[str] = None, + redirect_url: Optional[str] = None, + admin_state_up: Optional[bool] = None, + ) -> dict: + """L7 정책 수정""" + url = f"{self.lb_url}/v2.0/lbaas/l7policies/{l7policy_id}" + payload = {"l7policy": {}} + if name is not None: + payload["l7policy"]["name"] = name + if description is not None: + payload["l7policy"]["description"] = description + if action is not None: + payload["l7policy"]["action"] = action + if position is not None: + payload["l7policy"]["position"] = position + if redirect_pool_id is not None: + payload["l7policy"]["redirect_pool_id"] = redirect_pool_id + if redirect_url is not None: + payload["l7policy"]["redirect_url"] = redirect_url + if admin_state_up is not None: + payload["l7policy"]["admin_state_up"] = admin_state_up + + logger.info(f"[NHN API] L7 정책 수정 요청 - l7policy_id={l7policy_id}") + return self._put(url, payload) + + def delete_l7policy(self, l7policy_id: str) -> dict: + """L7 정책 삭제""" + url = f"{self.lb_url}/v2.0/lbaas/l7policies/{l7policy_id}" + logger.info(f"[NHN API] L7 정책 삭제 요청 - l7policy_id={l7policy_id}") + return self._delete(url) + + # ==================== L7 Rule ==================== + + def get_l7rule_list(self, l7policy_id: str) -> dict: + """L7 룰 목록 조회""" + url = f"{self.lb_url}/v2.0/lbaas/l7policies/{l7policy_id}/rules" + logger.info(f"[NHN API] L7 룰 목록 조회 요청 - l7policy_id={l7policy_id}") + result = self._get(url) + rule_count = len(result.get("rules", [])) + logger.info(f"[NHN API] L7 룰 목록 조회 완료 - count={rule_count}") + return result + + def get_l7rule_info(self, l7policy_id: str, l7rule_id: str) -> dict: + """L7 룰 상세 조회""" + url = f"{self.lb_url}/v2.0/lbaas/l7policies/{l7policy_id}/rules/{l7rule_id}" + logger.info(f"[NHN API] L7 룰 상세 조회 요청 - l7policy_id={l7policy_id}, l7rule_id={l7rule_id}") + return self._get(url) + + def create_l7rule( + self, + l7policy_id: str, + type: str, + compare_type: str, + value: str, + key: Optional[str] = None, + invert: bool = False, + admin_state_up: bool = True, + ) -> dict: + """ + L7 룰 생성 + + Args: + l7policy_id: L7 정책 ID + type: 룰 타입 (COOKIE, FILE_TYPE, HEADER, HOST_NAME, PATH) + compare_type: 비교 방식 (CONTAINS, ENDS_WITH, STARTS_WITH, EQUAL_TO, REGEX) + value: 비교할 값 + key: 키 값 (COOKIE, HEADER 타입 사용 시) + invert: 조건 반전 여부 + admin_state_up: 관리자 상태 + """ + url = f"{self.lb_url}/v2.0/lbaas/l7policies/{l7policy_id}/rules" + payload = { + "rule": { + "type": type, + "compare_type": compare_type, + "value": value, + "invert": invert, + "admin_state_up": admin_state_up, + } + } + if key: + payload["rule"]["key"] = key + + logger.info(f"[NHN API] L7 룰 생성 요청 - l7policy_id={l7policy_id}, type={type}") + return self._post(url, payload) + + def update_l7rule( + self, + l7policy_id: str, + l7rule_id: str, + type: Optional[str] = None, + compare_type: Optional[str] = None, + value: Optional[str] = None, + key: Optional[str] = None, + invert: Optional[bool] = None, + admin_state_up: Optional[bool] = None, + ) -> dict: + """L7 룰 수정""" + url = f"{self.lb_url}/v2.0/lbaas/l7policies/{l7policy_id}/rules/{l7rule_id}" + payload = {"rule": {}} + if type is not None: + payload["rule"]["type"] = type + if compare_type is not None: + payload["rule"]["compare_type"] = compare_type + if value is not None: + payload["rule"]["value"] = value + if key is not None: + payload["rule"]["key"] = key + if invert is not None: + payload["rule"]["invert"] = invert + if admin_state_up is not None: + payload["rule"]["admin_state_up"] = admin_state_up + + logger.info(f"[NHN API] L7 룰 수정 요청 - l7policy_id={l7policy_id}, l7rule_id={l7rule_id}") + return self._put(url, payload) + + def delete_l7rule(self, l7policy_id: str, l7rule_id: str) -> dict: + """L7 룰 삭제""" + url = f"{self.lb_url}/v2.0/lbaas/l7policies/{l7policy_id}/rules/{l7rule_id}" + logger.info(f"[NHN API] L7 룰 삭제 요청 - l7policy_id={l7policy_id}, l7rule_id={l7rule_id}") + return self._delete(url) + + # ==================== IP ACL Group ==================== + + def get_ipacl_group_list(self) -> dict: + """IP ACL 그룹 목록 조회""" + url = f"{self.lb_url}/v2.0/lbaas/ipacl-groups" + logger.info(f"[NHN API] IP ACL 그룹 목록 조회 요청") + result = self._get(url) + group_count = len(result.get("ipacl_groups", [])) + logger.info(f"[NHN API] IP ACL 그룹 목록 조회 완료 - count={group_count}") + return result + + def get_ipacl_group_info(self, ipacl_group_id: str) -> dict: + """IP ACL 그룹 상세 조회""" + url = f"{self.lb_url}/v2.0/lbaas/ipacl-groups/{ipacl_group_id}" + logger.info(f"[NHN API] IP ACL 그룹 상세 조회 요청 - ipacl_group_id={ipacl_group_id}") + return self._get(url) + + def create_ipacl_group( + self, + action: str, + name: Optional[str] = None, + description: Optional[str] = None, + ipacl_targets: Optional[List[dict]] = None, + loadbalancers: Optional[List[str]] = None, + ) -> dict: + """ + IP ACL 그룹 생성 + + Args: + action: 액션 (ALLOW 또는 DENY) + name: 그룹 이름 + description: 설명 + ipacl_targets: IP ACL 타깃 목록 [{ip_address, description}] + loadbalancers: 적용할 로드밸런서 ID 목록 + """ + url = f"{self.lb_url}/v2.0/lbaas/ipacl-groups" + payload = { + "ipacl_group": { + "action": action, + } + } + if name: + payload["ipacl_group"]["name"] = name + if description: + payload["ipacl_group"]["description"] = description + if ipacl_targets: + payload["ipacl_group"]["ipacl_targets"] = ipacl_targets + if loadbalancers: + payload["ipacl_group"]["loadbalancers"] = [{"loadbalancer_id": lb_id} for lb_id in loadbalancers] + + logger.info(f"[NHN API] IP ACL 그룹 생성 요청 - action={action}") + return self._post(url, payload) + + def update_ipacl_group( + self, + ipacl_group_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + loadbalancers: Optional[List[str]] = None, + ) -> dict: + """IP ACL 그룹 수정""" + url = f"{self.lb_url}/v2.0/lbaas/ipacl-groups/{ipacl_group_id}" + payload = {"ipacl_group": {}} + if name is not None: + payload["ipacl_group"]["name"] = name + if description is not None: + payload["ipacl_group"]["description"] = description + if loadbalancers is not None: + payload["ipacl_group"]["loadbalancers"] = [{"loadbalancer_id": lb_id} for lb_id in loadbalancers] + + logger.info(f"[NHN API] IP ACL 그룹 수정 요청 - ipacl_group_id={ipacl_group_id}") + return self._put(url, payload) + + def delete_ipacl_group(self, ipacl_group_id: str) -> dict: + """IP ACL 그룹 삭제""" + url = f"{self.lb_url}/v2.0/lbaas/ipacl-groups/{ipacl_group_id}" + logger.info(f"[NHN API] IP ACL 그룹 삭제 요청 - ipacl_group_id={ipacl_group_id}") + return self._delete(url) + + # ==================== IP ACL Target ==================== + + def create_ipacl_target( + self, + ipacl_group_id: str, + ip_address: str, + description: Optional[str] = None, + ) -> dict: + """ + IP ACL 타깃 추가 + + Args: + ipacl_group_id: IP ACL 그룹 ID + ip_address: IP 주소 또는 CIDR (예: 10.0.0.1, 10.0.0.0/24) + description: 설명 + """ + url = f"{self.lb_url}/v2.0/lbaas/ipacl-targets" + payload = { + "ipacl_target": { + "ipacl_group_id": ipacl_group_id, + "ip_address": ip_address, + } + } + if description: + payload["ipacl_target"]["description"] = description + + logger.info(f"[NHN API] IP ACL 타깃 추가 요청 - ipacl_group_id={ipacl_group_id}, ip_address={ip_address}") + return self._post(url, payload) + + def update_ipacl_target( + self, + ipacl_target_id: str, + description: str, + ) -> dict: + """IP ACL 타깃 수정 (설명만 수정 가능)""" + url = f"{self.lb_url}/v2.0/lbaas/ipacl-targets/{ipacl_target_id}" + payload = { + "ipacl_target": { + "description": description, + } + } + logger.info(f"[NHN API] IP ACL 타깃 수정 요청 - ipacl_target_id={ipacl_target_id}") + return self._put(url, payload) + + def delete_ipacl_target(self, ipacl_target_id: str) -> dict: + """IP ACL 타깃 삭제""" + url = f"{self.lb_url}/v2.0/lbaas/ipacl-targets/{ipacl_target_id}" + logger.info(f"[NHN API] IP ACL 타깃 삭제 요청 - ipacl_target_id={ipacl_target_id}") + return self._delete(url) + + # ==================== Quota ==================== + + def get_quota(self) -> dict: + """로드밸런서 쿼타 조회""" + url = f"{self.lb_url}/v2.0/lbaas/quotas/default" + logger.info(f"[NHN API] 로드밸런서 쿼타 조회 요청") + return self._get(url) diff --git a/nhn/serializers.py b/nhn/serializers.py index 1e7388c..c07b7d5 100644 --- a/nhn/serializers.py +++ b/nhn/serializers.py @@ -644,3 +644,574 @@ class DnsIdListSerializer(serializers.Serializer): help_text="ID 목록", min_length=1, ) + + +# ==================== Load Balancer ==================== + + +class LoadBalancerSerializer(serializers.Serializer): + """로드밸런서 생성 요청""" + + vip_subnet_id = serializers.CharField( + help_text="VIP가 할당될 서브넷 ID (필수)", + ) + name = serializers.CharField( + help_text="로드밸런서 이름", + required=False, + max_length=255, + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) + vip_address = serializers.CharField( + help_text="VIP 주소 (지정하지 않으면 자동 할당)", + required=False, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + default=True, + ) + loadbalancer_type = serializers.ChoiceField( + choices=["shared", "dedicated"], + help_text="로드밸런서 타입 (shared: 공유형, dedicated: 전용형)", + default="shared", + ) + + +class LoadBalancerUpdateSerializer(serializers.Serializer): + """로드밸런서 수정 요청""" + + name = serializers.CharField( + help_text="로드밸런서 이름", + required=False, + max_length=255, + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + required=False, + ) + + +class LBListenerSerializer(serializers.Serializer): + """리스너 생성 요청""" + + loadbalancer_id = serializers.CharField( + help_text="로드밸런서 ID", + ) + protocol = serializers.ChoiceField( + choices=["TCP", "HTTP", "HTTPS", "TERMINATED_HTTPS"], + help_text="프로토콜", + ) + protocol_port = serializers.IntegerField( + help_text="포트 번호 (1-65535)", + min_value=1, + max_value=65535, + ) + name = serializers.CharField( + help_text="리스너 이름", + required=False, + max_length=255, + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) + default_pool_id = serializers.CharField( + help_text="기본 풀 ID", + required=False, + ) + connection_limit = serializers.IntegerField( + help_text="연결 제한 (-1: 무제한)", + default=-1, + ) + keepalive_timeout = serializers.IntegerField( + help_text="Keepalive 타임아웃 (초)", + default=300, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + default=True, + ) + default_tls_container_ref = serializers.CharField( + help_text="TLS 인증서 컨테이너 참조 (TERMINATED_HTTPS 사용 시)", + required=False, + ) + + +class LBListenerUpdateSerializer(serializers.Serializer): + """리스너 수정 요청""" + + name = serializers.CharField( + help_text="리스너 이름", + required=False, + max_length=255, + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) + default_pool_id = serializers.CharField( + help_text="기본 풀 ID", + required=False, + allow_null=True, + ) + connection_limit = serializers.IntegerField( + help_text="연결 제한 (-1: 무제한)", + required=False, + ) + keepalive_timeout = serializers.IntegerField( + help_text="Keepalive 타임아웃 (초)", + required=False, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + required=False, + ) + + +class SessionPersistenceSerializer(serializers.Serializer): + """세션 지속성 설정""" + + type = serializers.ChoiceField( + choices=["SOURCE_IP", "HTTP_COOKIE", "APP_COOKIE"], + help_text="세션 지속성 타입", + ) + cookie_name = serializers.CharField( + help_text="쿠키 이름 (APP_COOKIE 타입 사용 시)", + required=False, + ) + + +class LBPoolSerializer(serializers.Serializer): + """풀 생성 요청""" + + lb_algorithm = serializers.ChoiceField( + choices=["ROUND_ROBIN", "LEAST_CONNECTIONS", "SOURCE_IP"], + help_text="로드밸런싱 알고리즘", + ) + protocol = serializers.ChoiceField( + choices=["TCP", "HTTP", "HTTPS", "PROXY"], + help_text="프로토콜", + ) + loadbalancer_id = serializers.CharField( + help_text="로드밸런서 ID (listener_id와 둘 중 하나 필수)", + required=False, + ) + listener_id = serializers.CharField( + help_text="리스너 ID (loadbalancer_id와 둘 중 하나 필수)", + required=False, + ) + name = serializers.CharField( + help_text="풀 이름", + required=False, + max_length=255, + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + default=True, + ) + session_persistence = SessionPersistenceSerializer( + help_text="세션 지속성 설정", + required=False, + ) + + +class LBPoolUpdateSerializer(serializers.Serializer): + """풀 수정 요청""" + + name = serializers.CharField( + help_text="풀 이름", + required=False, + max_length=255, + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) + lb_algorithm = serializers.ChoiceField( + choices=["ROUND_ROBIN", "LEAST_CONNECTIONS", "SOURCE_IP"], + help_text="로드밸런싱 알고리즘", + required=False, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + required=False, + ) + session_persistence = SessionPersistenceSerializer( + help_text="세션 지속성 설정", + required=False, + allow_null=True, + ) + + +class LBMemberSerializer(serializers.Serializer): + """멤버 추가 요청""" + + address = serializers.CharField( + help_text="멤버 IP 주소", + ) + protocol_port = serializers.IntegerField( + help_text="멤버 포트 번호", + min_value=1, + max_value=65535, + ) + subnet_id = serializers.CharField( + help_text="멤버가 속한 서브넷 ID", + required=False, + ) + weight = serializers.IntegerField( + help_text="가중치 (1-256)", + default=1, + min_value=1, + max_value=256, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + default=True, + ) + + +class LBMemberUpdateSerializer(serializers.Serializer): + """멤버 수정 요청""" + + weight = serializers.IntegerField( + help_text="가중치 (1-256)", + required=False, + min_value=1, + max_value=256, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + required=False, + ) + + +class LBHealthMonitorSerializer(serializers.Serializer): + """헬스 모니터 생성 요청""" + + pool_id = serializers.CharField( + help_text="풀 ID", + ) + type = serializers.ChoiceField( + choices=["TCP", "HTTP", "HTTPS"], + help_text="헬스 체크 타입", + ) + delay = serializers.IntegerField( + help_text="헬스 체크 간격 (초)", + min_value=1, + ) + timeout = serializers.IntegerField( + help_text="응답 대기 시간 (초)", + min_value=1, + ) + max_retries = serializers.IntegerField( + help_text="최대 재시도 횟수 (정상 판정)", + min_value=1, + max_value=10, + ) + max_retries_down = serializers.IntegerField( + help_text="최대 재시도 횟수 (비정상 판정)", + default=3, + min_value=1, + max_value=10, + ) + http_method = serializers.ChoiceField( + choices=["GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "OPTIONS", "CONNECT", "PATCH"], + help_text="HTTP 메서드 (HTTP/HTTPS 타입 시)", + default="GET", + ) + url_path = serializers.CharField( + help_text="URL 경로 (HTTP/HTTPS 타입 시)", + default="/", + ) + expected_codes = serializers.CharField( + help_text="예상 응답 코드 (예: 200, 200,201, 200-204)", + default="200", + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + default=True, + ) + host_header = serializers.CharField( + help_text="Host 헤더 값", + required=False, + ) + + +class LBHealthMonitorUpdateSerializer(serializers.Serializer): + """헬스 모니터 수정 요청""" + + delay = serializers.IntegerField( + help_text="헬스 체크 간격 (초)", + required=False, + min_value=1, + ) + timeout = serializers.IntegerField( + help_text="응답 대기 시간 (초)", + required=False, + min_value=1, + ) + max_retries = serializers.IntegerField( + help_text="최대 재시도 횟수 (정상 판정)", + required=False, + min_value=1, + max_value=10, + ) + max_retries_down = serializers.IntegerField( + help_text="최대 재시도 횟수 (비정상 판정)", + required=False, + min_value=1, + max_value=10, + ) + http_method = serializers.ChoiceField( + choices=["GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "OPTIONS", "CONNECT", "PATCH"], + help_text="HTTP 메서드", + required=False, + ) + url_path = serializers.CharField( + help_text="URL 경로", + required=False, + ) + expected_codes = serializers.CharField( + help_text="예상 응답 코드", + required=False, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + required=False, + ) + host_header = serializers.CharField( + help_text="Host 헤더 값", + required=False, + ) + + +class LBL7PolicySerializer(serializers.Serializer): + """L7 정책 생성 요청""" + + listener_id = serializers.CharField( + help_text="리스너 ID", + ) + action = serializers.ChoiceField( + choices=["REDIRECT_TO_POOL", "REDIRECT_TO_URL", "REJECT"], + help_text="액션", + ) + position = serializers.IntegerField( + help_text="정책 우선순위 (낮을수록 먼저 적용)", + default=1, + min_value=1, + ) + name = serializers.CharField( + help_text="정책 이름", + required=False, + max_length=255, + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) + redirect_pool_id = serializers.CharField( + help_text="리다이렉트 대상 풀 ID (REDIRECT_TO_POOL 액션 시)", + required=False, + ) + redirect_url = serializers.CharField( + help_text="리다이렉트 URL (REDIRECT_TO_URL 액션 시)", + required=False, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + default=True, + ) + + +class LBL7PolicyUpdateSerializer(serializers.Serializer): + """L7 정책 수정 요청""" + + name = serializers.CharField( + help_text="정책 이름", + required=False, + max_length=255, + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) + action = serializers.ChoiceField( + choices=["REDIRECT_TO_POOL", "REDIRECT_TO_URL", "REJECT"], + help_text="액션", + required=False, + ) + position = serializers.IntegerField( + help_text="정책 우선순위", + required=False, + min_value=1, + ) + redirect_pool_id = serializers.CharField( + help_text="리다이렉트 대상 풀 ID", + required=False, + allow_null=True, + ) + redirect_url = serializers.CharField( + help_text="리다이렉트 URL", + required=False, + allow_null=True, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + required=False, + ) + + +class LBL7RuleSerializer(serializers.Serializer): + """L7 룰 생성 요청""" + + type = serializers.ChoiceField( + choices=["COOKIE", "FILE_TYPE", "HEADER", "HOST_NAME", "PATH"], + help_text="룰 타입", + ) + compare_type = serializers.ChoiceField( + choices=["CONTAINS", "ENDS_WITH", "STARTS_WITH", "EQUAL_TO", "REGEX"], + help_text="비교 방식", + ) + value = serializers.CharField( + help_text="비교할 값", + ) + key = serializers.CharField( + help_text="키 값 (COOKIE, HEADER 타입 사용 시)", + required=False, + ) + invert = serializers.BooleanField( + help_text="조건 반전 여부", + default=False, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + default=True, + ) + + +class LBL7RuleUpdateSerializer(serializers.Serializer): + """L7 룰 수정 요청""" + + type = serializers.ChoiceField( + choices=["COOKIE", "FILE_TYPE", "HEADER", "HOST_NAME", "PATH"], + help_text="룰 타입", + required=False, + ) + compare_type = serializers.ChoiceField( + choices=["CONTAINS", "ENDS_WITH", "STARTS_WITH", "EQUAL_TO", "REGEX"], + help_text="비교 방식", + required=False, + ) + value = serializers.CharField( + help_text="비교할 값", + required=False, + ) + key = serializers.CharField( + help_text="키 값", + required=False, + allow_null=True, + ) + invert = serializers.BooleanField( + help_text="조건 반전 여부", + required=False, + ) + admin_state_up = serializers.BooleanField( + help_text="관리자 상태", + required=False, + ) + + +class LBIpAclTargetSerializer(serializers.Serializer): + """IP ACL 타깃""" + + ip_address = serializers.CharField( + help_text="IP 주소 또는 CIDR (예: 10.0.0.1, 10.0.0.0/24)", + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) + + +class LBIpAclGroupSerializer(serializers.Serializer): + """IP ACL 그룹 생성 요청""" + + action = serializers.ChoiceField( + choices=["ALLOW", "DENY"], + help_text="액션 (ALLOW 또는 DENY)", + ) + name = serializers.CharField( + help_text="그룹 이름", + required=False, + max_length=255, + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) + ipacl_targets = serializers.ListField( + child=LBIpAclTargetSerializer(), + help_text="IP ACL 타깃 목록", + required=False, + ) + loadbalancers = serializers.ListField( + child=serializers.CharField(), + help_text="적용할 로드밸런서 ID 목록", + required=False, + ) + + +class LBIpAclGroupUpdateSerializer(serializers.Serializer): + """IP ACL 그룹 수정 요청""" + + name = serializers.CharField( + help_text="그룹 이름", + required=False, + max_length=255, + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) + loadbalancers = serializers.ListField( + child=serializers.CharField(), + help_text="적용할 로드밸런서 ID 목록", + required=False, + ) + + +class LBIpAclTargetCreateSerializer(serializers.Serializer): + """IP ACL 타깃 추가 요청""" + + ipacl_group_id = serializers.CharField( + help_text="IP ACL 그룹 ID", + ) + ip_address = serializers.CharField( + help_text="IP 주소 또는 CIDR (예: 10.0.0.1, 10.0.0.0/24)", + ) + description = serializers.CharField( + help_text="설명", + required=False, + allow_blank=True, + ) diff --git a/nhn/tasks.py b/nhn/tasks.py index 35410f5..a51a4fe 100644 --- a/nhn/tasks.py +++ b/nhn/tasks.py @@ -396,3 +396,64 @@ def delete_storage_container_async(region, token, storage_account, container_nam ) return task + + +# ==================== Load Balancer 비동기 작업 ==================== + + +def create_loadbalancer_async(region, token, lb_data): + """ + 로드밸런서 비동기 생성 + + Args: + region: 리전 + token: API 토큰 + lb_data: 로드밸런서 생성 데이터 (dict) + + Returns: + AsyncTask: 생성된 작업 객체 + """ + from .models import AsyncTask + from .packages.loadbalancer import ApiLoadBalancer + + task = AsyncTask.objects.create( + task_type=AsyncTask.TaskType.LB_CREATE, + request_data=lb_data, + resource_name=lb_data.get("name", ""), + ) + + api = ApiLoadBalancer(region, token) + execute_async_task( + task_id=task.id, + task_func=api.create_loadbalancer, + vip_subnet_id=lb_data["vip_subnet_id"], + name=lb_data.get("name"), + description=lb_data.get("description"), + vip_address=lb_data.get("vip_address"), + admin_state_up=lb_data.get("admin_state_up", True), + loadbalancer_type=lb_data.get("loadbalancer_type", "shared"), + ) + + return task + + +def delete_loadbalancer_async(region, token, loadbalancer_id, loadbalancer_name=""): + """로드밸런서 비동기 삭제""" + from .models import AsyncTask + from .packages.loadbalancer import ApiLoadBalancer + + task = AsyncTask.objects.create( + task_type=AsyncTask.TaskType.LB_DELETE, + request_data={"loadbalancer_id": loadbalancer_id}, + resource_name=loadbalancer_name, + resource_id=loadbalancer_id, + ) + + api = ApiLoadBalancer(region, token) + execute_async_task( + task_id=task.id, + task_func=api.delete_loadbalancer, + loadbalancer_id=loadbalancer_id, + ) + + return task diff --git a/nhn/urls.py b/nhn/urls.py index 74775ae..8b251f7 100644 --- a/nhn/urls.py +++ b/nhn/urls.py @@ -94,4 +94,49 @@ urlpatterns = [ 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"), + + # ==================== Load Balancer ==================== + path("lb/loadbalancers/", views.LoadBalancerListView.as_view(), name="lb-list"), + path("lb/loadbalancers/create/", views.LoadBalancerCreateView.as_view(), name="lb-create"), + path("lb/loadbalancers//", views.LoadBalancerDetailView.as_view(), name="lb-detail"), + + # ==================== LB Listener ==================== + path("lb/listeners/", views.LBListenerListView.as_view(), name="lb-listener-list"), + path("lb/listeners/create/", views.LBListenerCreateView.as_view(), name="lb-listener-create"), + path("lb/listeners//", views.LBListenerDetailView.as_view(), name="lb-listener-detail"), + + # ==================== LB Pool ==================== + path("lb/pools/", views.LBPoolListView.as_view(), name="lb-pool-list"), + path("lb/pools/create/", views.LBPoolCreateView.as_view(), name="lb-pool-create"), + path("lb/pools//", views.LBPoolDetailView.as_view(), name="lb-pool-detail"), + + # ==================== LB Member ==================== + path("lb/pools//members/", views.LBMemberListView.as_view(), name="lb-member-list"), + path("lb/pools//members/create/", views.LBMemberCreateView.as_view(), name="lb-member-create"), + path("lb/pools//members//", views.LBMemberDetailView.as_view(), name="lb-member-detail"), + + # ==================== LB Health Monitor ==================== + path("lb/healthmonitors/", views.LBHealthMonitorListView.as_view(), name="lb-healthmonitor-list"), + path("lb/healthmonitors/create/", views.LBHealthMonitorCreateView.as_view(), name="lb-healthmonitor-create"), + path("lb/healthmonitors//", views.LBHealthMonitorDetailView.as_view(), name="lb-healthmonitor-detail"), + + # ==================== LB L7 Policy ==================== + path("lb/l7policies/", views.LBL7PolicyListView.as_view(), name="lb-l7policy-list"), + path("lb/l7policies/create/", views.LBL7PolicyCreateView.as_view(), name="lb-l7policy-create"), + path("lb/l7policies//", views.LBL7PolicyDetailView.as_view(), name="lb-l7policy-detail"), + + # ==================== LB L7 Rule ==================== + path("lb/l7policies//rules/", views.LBL7RuleListView.as_view(), name="lb-l7rule-list"), + path("lb/l7policies//rules/create/", views.LBL7RuleCreateView.as_view(), name="lb-l7rule-create"), + path("lb/l7policies//rules//", views.LBL7RuleDetailView.as_view(), name="lb-l7rule-detail"), + + # ==================== LB IP ACL ==================== + path("lb/ipacl-groups/", views.LBIpAclGroupListView.as_view(), name="lb-ipacl-group-list"), + path("lb/ipacl-groups/create/", views.LBIpAclGroupCreateView.as_view(), name="lb-ipacl-group-create"), + path("lb/ipacl-groups//", views.LBIpAclGroupDetailView.as_view(), name="lb-ipacl-group-detail"), + path("lb/ipacl-targets/create/", views.LBIpAclTargetCreateView.as_view(), name="lb-ipacl-target-create"), + path("lb/ipacl-targets//", views.LBIpAclTargetDetailView.as_view(), name="lb-ipacl-target-detail"), + + # ==================== LB Quota ==================== + path("lb/quota/", views.LBQuotaView.as_view(), name="lb-quota"), ] diff --git a/nhn/views.py b/nhn/views.py index 4152a75..40d7e78 100644 --- a/nhn/views.py +++ b/nhn/views.py @@ -44,8 +44,27 @@ from .serializers import ( DnsHealthCheckSerializer, DnsHealthCheckUpdateSerializer, DnsIdListSerializer, + # Load Balancer + LoadBalancerSerializer, + LoadBalancerUpdateSerializer, + LBListenerSerializer, + LBListenerUpdateSerializer, + LBPoolSerializer, + LBPoolUpdateSerializer, + LBMemberSerializer, + LBMemberUpdateSerializer, + LBHealthMonitorSerializer, + LBHealthMonitorUpdateSerializer, + LBL7PolicySerializer, + LBL7PolicyUpdateSerializer, + LBL7RuleSerializer, + LBL7RuleUpdateSerializer, + LBIpAclGroupSerializer, + LBIpAclGroupUpdateSerializer, + LBIpAclTargetCreateSerializer, ) from .packages import NHNCloudToken, ApiCompute, ApiVpc, ApiNks, ApiStorageObject, ApiDnsPlus +from .packages.loadbalancer import ApiLoadBalancer from .packages.base import NHNCloudAPIError logger = logging.getLogger(__name__) @@ -1967,3 +1986,1010 @@ class DnsHealthCheckDetailView(APIView): return Response(result) except NHNCloudAPIError as e: return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== Load Balancer API ==================== + + +class LoadBalancerListView(APIView): + """로드밸런서 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="로드밸런서 목록 조회", + manual_parameters=[region_header, token_header], + responses={200: "로드밸런서 목록"}, + ) + def get(self, request): + logger.info(f"[LB] 로드밸런서 목록 조회 요청") + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.get_loadbalancer_list( + name=request.query_params.get("name"), + provisioning_status=request.query_params.get("provisioning_status"), + vip_address=request.query_params.get("vip_address"), + ) + lb_count = len(result.get("loadbalancers", [])) + logger.info(f"[LB] 로드밸런서 목록 조회 성공 - count={lb_count}") + return Response(result) + except NHNCloudAPIError as e: + logger.error(f"[LB] 로드밸런서 목록 조회 실패 - error={e.message}") + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LoadBalancerCreateView(APIView): + """로드밸런서 생성 API (비동기)""" + + @swagger_auto_schema( + operation_summary="로드밸런서 생성 (비동기)", + manual_parameters=[region_header, token_header], + request_body=LoadBalancerSerializer, + responses={202: "작업 ID 반환"}, + ) + def post(self, request): + headers = get_nhn_headers(request) + serializer = LoadBalancerSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + from .tasks import create_loadbalancer_async + + task = create_loadbalancer_async( + region=headers["region"], + token=headers["token"], + lb_data=serializer.validated_data, + ) + return Response( + {"task_id": str(task.id), "status": task.status, "message": "로드밸런서 생성 작업이 시작되었습니다."}, + status=status.HTTP_202_ACCEPTED, + ) + except Exception as e: + logger.exception("로드밸런서 생성 작업 시작 실패") + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +class LoadBalancerDetailView(APIView): + """로드밸런서 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="로드밸런서 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "로드밸런서 상세 정보"}, + ) + def get(self, request, loadbalancer_id): + logger.info(f"[LB] 로드밸런서 상세 조회 요청 - loadbalancer_id={loadbalancer_id}") + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.get_loadbalancer_info(loadbalancer_id) + logger.info(f"[LB] 로드밸런서 상세 조회 성공 - loadbalancer_id={loadbalancer_id}") + return Response(result) + except NHNCloudAPIError as e: + logger.error(f"[LB] 로드밸런서 상세 조회 실패 - loadbalancer_id={loadbalancer_id}, error={e.message}") + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="로드밸런서 수정", + manual_parameters=[region_header, token_header], + request_body=LoadBalancerUpdateSerializer, + responses={200: "수정된 로드밸런서 정보"}, + ) + def put(self, request, loadbalancer_id): + headers = get_nhn_headers(request) + serializer = LoadBalancerUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.update_loadbalancer( + loadbalancer_id=loadbalancer_id, + name=serializer.validated_data.get("name"), + description=serializer.validated_data.get("description"), + admin_state_up=serializer.validated_data.get("admin_state_up"), + ) + 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], + responses={202: "작업 ID 반환"}, + ) + def delete(self, request, loadbalancer_id): + logger.info(f"[LB] 로드밸런서 삭제 요청 - loadbalancer_id={loadbalancer_id}") + headers = get_nhn_headers(request) + try: + from .tasks import delete_loadbalancer_async + + task = delete_loadbalancer_async( + region=headers["region"], + token=headers["token"], + loadbalancer_id=loadbalancer_id, + loadbalancer_name=request.query_params.get("name", ""), + ) + logger.info(f"[LB] 로드밸런서 삭제 작업 시작 - loadbalancer_id={loadbalancer_id}, task_id={task.id}") + return Response( + {"task_id": str(task.id), "status": task.status, "message": "로드밸런서 삭제 작업이 시작되었습니다."}, + status=status.HTTP_202_ACCEPTED, + ) + except Exception as e: + logger.exception(f"[LB] 로드밸런서 삭제 작업 시작 실패 - loadbalancer_id={loadbalancer_id}") + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== LB Listener API ==================== + + +class LBListenerListView(APIView): + """리스너 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="리스너 목록 조회", + manual_parameters=[region_header, token_header], + responses={200: "리스너 목록"}, + ) + def get(self, request): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.get_listener_list( + loadbalancer_id=request.query_params.get("loadbalancer_id"), + protocol=request.query_params.get("protocol"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBListenerCreateView(APIView): + """리스너 생성 API""" + + @swagger_auto_schema( + operation_summary="리스너 생성", + manual_parameters=[region_header, token_header], + request_body=LBListenerSerializer, + responses={200: "생성된 리스너 정보"}, + ) + def post(self, request): + headers = get_nhn_headers(request) + serializer = LBListenerSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.create_listener( + loadbalancer_id=serializer.validated_data["loadbalancer_id"], + protocol=serializer.validated_data["protocol"], + protocol_port=serializer.validated_data["protocol_port"], + name=serializer.validated_data.get("name"), + description=serializer.validated_data.get("description"), + default_pool_id=serializer.validated_data.get("default_pool_id"), + connection_limit=serializer.validated_data.get("connection_limit", -1), + keepalive_timeout=serializer.validated_data.get("keepalive_timeout", 300), + admin_state_up=serializer.validated_data.get("admin_state_up", True), + default_tls_container_ref=serializer.validated_data.get("default_tls_container_ref"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBListenerDetailView(APIView): + """리스너 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="리스너 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "리스너 상세 정보"}, + ) + def get(self, request, listener_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + return Response(api.get_listener_info(listener_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="리스너 수정", + manual_parameters=[region_header, token_header], + request_body=LBListenerUpdateSerializer, + responses={200: "수정된 리스너 정보"}, + ) + def put(self, request, listener_id): + headers = get_nhn_headers(request) + serializer = LBListenerUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.update_listener( + listener_id=listener_id, + name=serializer.validated_data.get("name"), + description=serializer.validated_data.get("description"), + default_pool_id=serializer.validated_data.get("default_pool_id"), + connection_limit=serializer.validated_data.get("connection_limit"), + keepalive_timeout=serializer.validated_data.get("keepalive_timeout"), + admin_state_up=serializer.validated_data.get("admin_state_up"), + ) + 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], + responses={200: "삭제 결과"}, + ) + def delete(self, request, listener_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.delete_listener(listener_id) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== LB Pool API ==================== + + +class LBPoolListView(APIView): + """풀 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="풀 목록 조회", + manual_parameters=[region_header, token_header], + responses={200: "풀 목록"}, + ) + def get(self, request): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.get_pool_list( + loadbalancer_id=request.query_params.get("loadbalancer_id"), + protocol=request.query_params.get("protocol"), + lb_algorithm=request.query_params.get("lb_algorithm"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBPoolCreateView(APIView): + """풀 생성 API""" + + @swagger_auto_schema( + operation_summary="풀 생성", + manual_parameters=[region_header, token_header], + request_body=LBPoolSerializer, + responses={200: "생성된 풀 정보"}, + ) + def post(self, request): + headers = get_nhn_headers(request) + serializer = LBPoolSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.create_pool( + lb_algorithm=serializer.validated_data["lb_algorithm"], + protocol=serializer.validated_data["protocol"], + loadbalancer_id=serializer.validated_data.get("loadbalancer_id"), + listener_id=serializer.validated_data.get("listener_id"), + name=serializer.validated_data.get("name"), + description=serializer.validated_data.get("description"), + admin_state_up=serializer.validated_data.get("admin_state_up", True), + session_persistence=serializer.validated_data.get("session_persistence"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBPoolDetailView(APIView): + """풀 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="풀 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "풀 상세 정보"}, + ) + def get(self, request, pool_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + return Response(api.get_pool_info(pool_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="풀 수정", + manual_parameters=[region_header, token_header], + request_body=LBPoolUpdateSerializer, + responses={200: "수정된 풀 정보"}, + ) + def put(self, request, pool_id): + headers = get_nhn_headers(request) + serializer = LBPoolUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.update_pool( + pool_id=pool_id, + name=serializer.validated_data.get("name"), + description=serializer.validated_data.get("description"), + lb_algorithm=serializer.validated_data.get("lb_algorithm"), + admin_state_up=serializer.validated_data.get("admin_state_up"), + session_persistence=serializer.validated_data.get("session_persistence"), + ) + 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], + responses={200: "삭제 결과"}, + ) + def delete(self, request, pool_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.delete_pool(pool_id) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== LB Member API ==================== + + +class LBMemberListView(APIView): + """멤버 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="멤버 목록 조회", + manual_parameters=[region_header, token_header], + responses={200: "멤버 목록"}, + ) + def get(self, request, pool_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + return Response(api.get_member_list(pool_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBMemberCreateView(APIView): + """멤버 추가 API""" + + @swagger_auto_schema( + operation_summary="멤버 추가", + manual_parameters=[region_header, token_header], + request_body=LBMemberSerializer, + responses={200: "추가된 멤버 정보"}, + ) + def post(self, request, pool_id): + headers = get_nhn_headers(request) + serializer = LBMemberSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.create_member( + pool_id=pool_id, + address=serializer.validated_data["address"], + protocol_port=serializer.validated_data["protocol_port"], + subnet_id=serializer.validated_data.get("subnet_id"), + weight=serializer.validated_data.get("weight", 1), + admin_state_up=serializer.validated_data.get("admin_state_up", True), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBMemberDetailView(APIView): + """멤버 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="멤버 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "멤버 상세 정보"}, + ) + def get(self, request, pool_id, member_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + return Response(api.get_member_info(pool_id, member_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="멤버 수정", + manual_parameters=[region_header, token_header], + request_body=LBMemberUpdateSerializer, + responses={200: "수정된 멤버 정보"}, + ) + def put(self, request, pool_id, member_id): + headers = get_nhn_headers(request) + serializer = LBMemberUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.update_member( + pool_id=pool_id, + member_id=member_id, + weight=serializer.validated_data.get("weight"), + admin_state_up=serializer.validated_data.get("admin_state_up"), + ) + 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], + responses={200: "삭제 결과"}, + ) + def delete(self, request, pool_id, member_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.delete_member(pool_id, member_id) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== LB Health Monitor API ==================== + + +class LBHealthMonitorListView(APIView): + """헬스 모니터 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="헬스 모니터 목록 조회", + manual_parameters=[region_header, token_header], + responses={200: "헬스 모니터 목록"}, + ) + def get(self, request): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.get_healthmonitor_list( + pool_id=request.query_params.get("pool_id"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBHealthMonitorCreateView(APIView): + """헬스 모니터 생성 API""" + + @swagger_auto_schema( + operation_summary="헬스 모니터 생성", + manual_parameters=[region_header, token_header], + request_body=LBHealthMonitorSerializer, + responses={200: "생성된 헬스 모니터 정보"}, + ) + def post(self, request): + headers = get_nhn_headers(request) + serializer = LBHealthMonitorSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.create_healthmonitor( + pool_id=serializer.validated_data["pool_id"], + type=serializer.validated_data["type"], + delay=serializer.validated_data["delay"], + timeout=serializer.validated_data["timeout"], + max_retries=serializer.validated_data["max_retries"], + max_retries_down=serializer.validated_data.get("max_retries_down", 3), + http_method=serializer.validated_data.get("http_method", "GET"), + url_path=serializer.validated_data.get("url_path", "/"), + expected_codes=serializer.validated_data.get("expected_codes", "200"), + admin_state_up=serializer.validated_data.get("admin_state_up", True), + host_header=serializer.validated_data.get("host_header"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBHealthMonitorDetailView(APIView): + """헬스 모니터 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="헬스 모니터 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "헬스 모니터 상세 정보"}, + ) + def get(self, request, healthmonitor_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + return Response(api.get_healthmonitor_info(healthmonitor_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="헬스 모니터 수정", + manual_parameters=[region_header, token_header], + request_body=LBHealthMonitorUpdateSerializer, + responses={200: "수정된 헬스 모니터 정보"}, + ) + def put(self, request, healthmonitor_id): + headers = get_nhn_headers(request) + serializer = LBHealthMonitorUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.update_healthmonitor( + healthmonitor_id=healthmonitor_id, + delay=serializer.validated_data.get("delay"), + timeout=serializer.validated_data.get("timeout"), + max_retries=serializer.validated_data.get("max_retries"), + max_retries_down=serializer.validated_data.get("max_retries_down"), + http_method=serializer.validated_data.get("http_method"), + url_path=serializer.validated_data.get("url_path"), + expected_codes=serializer.validated_data.get("expected_codes"), + admin_state_up=serializer.validated_data.get("admin_state_up"), + host_header=serializer.validated_data.get("host_header"), + ) + 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], + responses={200: "삭제 결과"}, + ) + def delete(self, request, healthmonitor_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.delete_healthmonitor(healthmonitor_id) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== LB L7 Policy API ==================== + + +class LBL7PolicyListView(APIView): + """L7 정책 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="L7 정책 목록 조회", + manual_parameters=[region_header, token_header], + responses={200: "L7 정책 목록"}, + ) + def get(self, request): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.get_l7policy_list( + listener_id=request.query_params.get("listener_id"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBL7PolicyCreateView(APIView): + """L7 정책 생성 API""" + + @swagger_auto_schema( + operation_summary="L7 정책 생성", + manual_parameters=[region_header, token_header], + request_body=LBL7PolicySerializer, + responses={200: "생성된 L7 정책 정보"}, + ) + def post(self, request): + headers = get_nhn_headers(request) + serializer = LBL7PolicySerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.create_l7policy( + listener_id=serializer.validated_data["listener_id"], + action=serializer.validated_data["action"], + position=serializer.validated_data.get("position", 1), + name=serializer.validated_data.get("name"), + description=serializer.validated_data.get("description"), + redirect_pool_id=serializer.validated_data.get("redirect_pool_id"), + redirect_url=serializer.validated_data.get("redirect_url"), + admin_state_up=serializer.validated_data.get("admin_state_up", True), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBL7PolicyDetailView(APIView): + """L7 정책 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="L7 정책 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "L7 정책 상세 정보"}, + ) + def get(self, request, l7policy_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + return Response(api.get_l7policy_info(l7policy_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="L7 정책 수정", + manual_parameters=[region_header, token_header], + request_body=LBL7PolicyUpdateSerializer, + responses={200: "수정된 L7 정책 정보"}, + ) + def put(self, request, l7policy_id): + headers = get_nhn_headers(request) + serializer = LBL7PolicyUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.update_l7policy( + l7policy_id=l7policy_id, + name=serializer.validated_data.get("name"), + description=serializer.validated_data.get("description"), + action=serializer.validated_data.get("action"), + position=serializer.validated_data.get("position"), + redirect_pool_id=serializer.validated_data.get("redirect_pool_id"), + redirect_url=serializer.validated_data.get("redirect_url"), + admin_state_up=serializer.validated_data.get("admin_state_up"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="L7 정책 삭제", + manual_parameters=[region_header, token_header], + responses={200: "삭제 결과"}, + ) + def delete(self, request, l7policy_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.delete_l7policy(l7policy_id) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== LB L7 Rule API ==================== + + +class LBL7RuleListView(APIView): + """L7 룰 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="L7 룰 목록 조회", + manual_parameters=[region_header, token_header], + responses={200: "L7 룰 목록"}, + ) + def get(self, request, l7policy_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + return Response(api.get_l7rule_list(l7policy_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBL7RuleCreateView(APIView): + """L7 룰 생성 API""" + + @swagger_auto_schema( + operation_summary="L7 룰 생성", + manual_parameters=[region_header, token_header], + request_body=LBL7RuleSerializer, + responses={200: "생성된 L7 룰 정보"}, + ) + def post(self, request, l7policy_id): + headers = get_nhn_headers(request) + serializer = LBL7RuleSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.create_l7rule( + l7policy_id=l7policy_id, + type=serializer.validated_data["type"], + compare_type=serializer.validated_data["compare_type"], + value=serializer.validated_data["value"], + key=serializer.validated_data.get("key"), + invert=serializer.validated_data.get("invert", False), + admin_state_up=serializer.validated_data.get("admin_state_up", True), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBL7RuleDetailView(APIView): + """L7 룰 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="L7 룰 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "L7 룰 상세 정보"}, + ) + def get(self, request, l7policy_id, l7rule_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + return Response(api.get_l7rule_info(l7policy_id, l7rule_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="L7 룰 수정", + manual_parameters=[region_header, token_header], + request_body=LBL7RuleUpdateSerializer, + responses={200: "수정된 L7 룰 정보"}, + ) + def put(self, request, l7policy_id, l7rule_id): + headers = get_nhn_headers(request) + serializer = LBL7RuleUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.update_l7rule( + l7policy_id=l7policy_id, + l7rule_id=l7rule_id, + type=serializer.validated_data.get("type"), + compare_type=serializer.validated_data.get("compare_type"), + value=serializer.validated_data.get("value"), + key=serializer.validated_data.get("key"), + invert=serializer.validated_data.get("invert"), + admin_state_up=serializer.validated_data.get("admin_state_up"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="L7 룰 삭제", + manual_parameters=[region_header, token_header], + responses={200: "삭제 결과"}, + ) + def delete(self, request, l7policy_id, l7rule_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.delete_l7rule(l7policy_id, l7rule_id) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== LB IP ACL Group API ==================== + + +class LBIpAclGroupListView(APIView): + """IP ACL 그룹 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="IP ACL 그룹 목록 조회", + manual_parameters=[region_header, token_header], + responses={200: "IP ACL 그룹 목록"}, + ) + def get(self, request): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + return Response(api.get_ipacl_group_list()) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBIpAclGroupCreateView(APIView): + """IP ACL 그룹 생성 API""" + + @swagger_auto_schema( + operation_summary="IP ACL 그룹 생성", + manual_parameters=[region_header, token_header], + request_body=LBIpAclGroupSerializer, + responses={200: "생성된 IP ACL 그룹 정보"}, + ) + def post(self, request): + headers = get_nhn_headers(request) + serializer = LBIpAclGroupSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.create_ipacl_group( + action=serializer.validated_data["action"], + name=serializer.validated_data.get("name"), + description=serializer.validated_data.get("description"), + ipacl_targets=serializer.validated_data.get("ipacl_targets"), + loadbalancers=serializer.validated_data.get("loadbalancers"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class LBIpAclGroupDetailView(APIView): + """IP ACL 그룹 상세/수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="IP ACL 그룹 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "IP ACL 그룹 상세 정보"}, + ) + def get(self, request, ipacl_group_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + return Response(api.get_ipacl_group_info(ipacl_group_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="IP ACL 그룹 수정", + manual_parameters=[region_header, token_header], + request_body=LBIpAclGroupUpdateSerializer, + responses={200: "수정된 IP ACL 그룹 정보"}, + ) + def put(self, request, ipacl_group_id): + headers = get_nhn_headers(request) + serializer = LBIpAclGroupUpdateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.update_ipacl_group( + ipacl_group_id=ipacl_group_id, + name=serializer.validated_data.get("name"), + description=serializer.validated_data.get("description"), + loadbalancers=serializer.validated_data.get("loadbalancers"), + ) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="IP ACL 그룹 삭제", + manual_parameters=[region_header, token_header], + responses={200: "삭제 결과"}, + ) + def delete(self, request, ipacl_group_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.delete_ipacl_group(ipacl_group_id) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== LB IP ACL Target API ==================== + + +class LBIpAclTargetCreateView(APIView): + """IP ACL 타깃 추가 API""" + + @swagger_auto_schema( + operation_summary="IP ACL 타깃 추가", + manual_parameters=[region_header, token_header], + request_body=LBIpAclTargetCreateSerializer, + responses={200: "추가된 IP ACL 타깃 정보"}, + ) + def post(self, request): + headers = get_nhn_headers(request) + serializer = LBIpAclTargetCreateSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.create_ipacl_target( + ipacl_group_id=serializer.validated_data["ipacl_group_id"], + ip_address=serializer.validated_data["ip_address"], + 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 LBIpAclTargetDetailView(APIView): + """IP ACL 타깃 수정/삭제 API""" + + @swagger_auto_schema( + operation_summary="IP ACL 타깃 수정 (설명만)", + manual_parameters=[region_header, token_header], + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "description": openapi.Schema(type=openapi.TYPE_STRING, description="설명"), + }, + required=["description"], + ), + responses={200: "수정된 IP ACL 타깃 정보"}, + ) + def put(self, request, ipacl_target_id): + headers = get_nhn_headers(request) + description = request.data.get("description") + if description is None: + return Response({"error": "description is required"}, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.update_ipacl_target(ipacl_target_id, description) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="IP ACL 타깃 삭제", + manual_parameters=[region_header, token_header], + responses={200: "삭제 결과"}, + ) + def delete(self, request, ipacl_target_id): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + result = api.delete_ipacl_target(ipacl_target_id) + return Response(result) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== LB Quota API ==================== + + +class LBQuotaView(APIView): + """로드밸런서 쿼타 조회 API""" + + @swagger_auto_schema( + operation_summary="로드밸런서 쿼타 조회", + manual_parameters=[region_header, token_header], + responses={200: "쿼타 정보"}, + ) + def get(self, request): + headers = get_nhn_headers(request) + try: + api = ApiLoadBalancer(headers["region"], headers["token"]) + return Response(api.get_quota()) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) diff --git a/version b/version index 3ce186f..6183501 100644 --- a/version +++ b/version @@ -1 +1 @@ -v0.0.8 +v0.0.9 \ No newline at end of file