Add NHN Cloud API integration with async task support
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
- NHN Cloud API packages: token, vpc, compute, nks, storage - REST API endpoints with Swagger documentation - Async task processing for long-running operations - CORS configuration for frontend integration - Enhanced logging for debugging API calls Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
13
nhn/packages/__init__.py
Normal file
13
nhn/packages/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
from .token import NHNCloudToken
|
||||
from .compute import ApiCompute
|
||||
from .vpc import ApiVpc
|
||||
from .nks import ApiNks
|
||||
from .storage import ApiStorageObject
|
||||
|
||||
__all__ = [
|
||||
"NHNCloudToken",
|
||||
"ApiCompute",
|
||||
"ApiVpc",
|
||||
"ApiNks",
|
||||
"ApiStorageObject",
|
||||
]
|
||||
165
nhn/packages/base.py
Normal file
165
nhn/packages/base.py
Normal file
@ -0,0 +1,165 @@
|
||||
"""
|
||||
NHN Cloud API Base Module
|
||||
|
||||
공통 기능을 제공하는 베이스 클래스
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, Any
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Region(str, Enum):
|
||||
"""NHN Cloud 리전"""
|
||||
KR1 = "kr1" # 판교
|
||||
KR2 = "kr2" # 평촌
|
||||
|
||||
|
||||
@dataclass
|
||||
class NHNCloudEndpoints:
|
||||
"""NHN Cloud API Endpoints"""
|
||||
# Identity
|
||||
IDENTITY = "https://api-identity-infrastructure.nhncloudservice.com"
|
||||
|
||||
# Compute
|
||||
COMPUTE_KR1 = "https://kr1-api-instance-infrastructure.nhncloudservice.com"
|
||||
COMPUTE_KR2 = "https://kr2-api-instance-infrastructure.nhncloudservice.com"
|
||||
|
||||
# Image
|
||||
IMAGE_KR1 = "https://kr1-api-image-infrastructure.nhncloudservice.com"
|
||||
IMAGE_KR2 = "https://kr2-api-image-infrastructure.nhncloudservice.com"
|
||||
|
||||
# Network (VPC)
|
||||
NETWORK_KR1 = "https://kr1-api-network-infrastructure.nhncloudservice.com"
|
||||
NETWORK_KR2 = "https://kr2-api-network-infrastructure.nhncloudservice.com"
|
||||
|
||||
# Kubernetes (NKS)
|
||||
NKS_KR1 = "https://kr1-api-kubernetes-infrastructure.nhncloudservice.com"
|
||||
NKS_KR2 = "https://kr2-api-kubernetes-infrastructure.nhncloudservice.com"
|
||||
|
||||
# Object Storage
|
||||
STORAGE_KR1 = "https://kr1-api-object-storage.nhncloudservice.com/v1"
|
||||
STORAGE_KR2 = "https://kr2-api-object-storage.nhncloudservice.com/v1"
|
||||
|
||||
|
||||
class NHNCloudAPIError(Exception):
|
||||
"""NHN Cloud API 에러"""
|
||||
|
||||
def __init__(self, message: str, code: Optional[int] = None, details: Optional[dict] = None):
|
||||
self.message = message
|
||||
self.code = code
|
||||
self.details = details or {}
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class BaseAPI:
|
||||
"""NHN Cloud API 베이스 클래스"""
|
||||
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
def __init__(self, region: str, token: str):
|
||||
self.region = Region(region.lower()) if isinstance(region, str) else region
|
||||
self.token = token
|
||||
self._session = requests.Session()
|
||||
|
||||
def _get_headers(self, extra_headers: Optional[dict] = None) -> dict:
|
||||
"""기본 헤더 생성"""
|
||||
headers = {
|
||||
"X-Auth-Token": self.token,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
return headers
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
url: str,
|
||||
params: Optional[dict] = None,
|
||||
json_data: Optional[dict] = None,
|
||||
headers: Optional[dict] = None,
|
||||
timeout: Optional[int] = None,
|
||||
) -> dict:
|
||||
"""HTTP 요청 실행"""
|
||||
# 토큰 앞 8자리만 로깅 (보안)
|
||||
token_preview = self.token[:8] + "..." if self.token else "None"
|
||||
logger.info(f"[BaseAPI] 요청 시작 - method={method}, url={url}, token={token_preview}")
|
||||
if params:
|
||||
logger.info(f"[BaseAPI] 요청 파라미터 - params={params}")
|
||||
if json_data:
|
||||
logger.info(f"[BaseAPI] 요청 바디 - json={json_data}")
|
||||
|
||||
try:
|
||||
response = self._session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
params=params,
|
||||
json=json_data,
|
||||
headers=self._get_headers(headers),
|
||||
timeout=timeout or self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
logger.info(f"[BaseAPI] 응답 수신 - method={method}, url={url}, status_code={response.status_code}")
|
||||
|
||||
if response.status_code >= 400:
|
||||
logger.error(f"[BaseAPI] 에러 응답 - status_code={response.status_code}, body={response.text[:500]}")
|
||||
self._handle_error(response)
|
||||
|
||||
if response.text:
|
||||
return response.json()
|
||||
return {}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error(f"[BaseAPI] 타임아웃 - url={url}")
|
||||
raise NHNCloudAPIError("요청 시간이 초과되었습니다.", code=408)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.error(f"[BaseAPI] 연결 오류 - url={url}, error={e}")
|
||||
raise NHNCloudAPIError("서버에 연결할 수 없습니다.", code=503)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"[BaseAPI] 요청 오류 - url={url}, error={e}")
|
||||
raise NHNCloudAPIError(f"요청 중 오류가 발생했습니다: {e}")
|
||||
|
||||
def _handle_error(self, response: requests.Response) -> None:
|
||||
"""에러 응답 처리"""
|
||||
try:
|
||||
error_data = response.json()
|
||||
if "error" in error_data:
|
||||
error = error_data["error"]
|
||||
raise NHNCloudAPIError(
|
||||
message=error.get("message", "알 수 없는 오류"),
|
||||
code=error.get("code", response.status_code),
|
||||
details=error,
|
||||
)
|
||||
raise NHNCloudAPIError(
|
||||
message=f"API 오류: {response.status_code}",
|
||||
code=response.status_code,
|
||||
details=error_data,
|
||||
)
|
||||
except ValueError:
|
||||
raise NHNCloudAPIError(
|
||||
message=f"API 오류: {response.status_code} - {response.text}",
|
||||
code=response.status_code,
|
||||
)
|
||||
|
||||
def _get(self, url: str, params: Optional[dict] = None, **kwargs) -> dict:
|
||||
"""GET 요청"""
|
||||
return self._request("GET", url, params=params, **kwargs)
|
||||
|
||||
def _post(self, url: str, json_data: Optional[dict] = None, **kwargs) -> dict:
|
||||
"""POST 요청"""
|
||||
return self._request("POST", url, json_data=json_data, **kwargs)
|
||||
|
||||
def _put(self, url: str, json_data: Optional[dict] = None, **kwargs) -> dict:
|
||||
"""PUT 요청"""
|
||||
return self._request("PUT", url, json_data=json_data, **kwargs)
|
||||
|
||||
def _delete(self, url: str, **kwargs) -> dict:
|
||||
"""DELETE 요청"""
|
||||
return self._request("DELETE", url, **kwargs)
|
||||
220
nhn/packages/compute.py
Normal file
220
nhn/packages/compute.py
Normal file
@ -0,0 +1,220 @@
|
||||
"""
|
||||
NHN Cloud Compute API Module
|
||||
|
||||
인스턴스, 이미지, Flavor, Keypair 관리
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
from .base import BaseAPI, NHNCloudEndpoints, Region
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiCompute(BaseAPI):
|
||||
"""NHN Cloud Compute API 클래스"""
|
||||
|
||||
def __init__(self, region: str, tenant_id: str, token: str):
|
||||
"""
|
||||
Args:
|
||||
region: 리전 (kr1: 판교, kr2: 평촌)
|
||||
tenant_id: 테넌트 ID
|
||||
token: API 인증 토큰
|
||||
"""
|
||||
super().__init__(region, token)
|
||||
self.tenant_id = tenant_id
|
||||
|
||||
if self.region == Region.KR1:
|
||||
self.compute_url = NHNCloudEndpoints.COMPUTE_KR1
|
||||
self.image_url = NHNCloudEndpoints.IMAGE_KR1
|
||||
else:
|
||||
self.compute_url = NHNCloudEndpoints.COMPUTE_KR2
|
||||
self.image_url = NHNCloudEndpoints.IMAGE_KR2
|
||||
|
||||
# ==================== Flavor ====================
|
||||
|
||||
def get_flavor_list(self) -> dict:
|
||||
"""Flavor 목록 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/flavors"
|
||||
return self._get(url)
|
||||
|
||||
def get_flavor_detail(self, flavor_id: str) -> dict:
|
||||
"""Flavor 상세 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/flavors/{flavor_id}"
|
||||
return self._get(url)
|
||||
|
||||
def get_flavor_id_by_name(self, flavor_name: str) -> Optional[str]:
|
||||
"""Flavor 이름으로 ID 조회"""
|
||||
data = self.get_flavor_list()
|
||||
flavors = data.get("flavors", [])
|
||||
|
||||
for flavor in flavors:
|
||||
if flavor_name in flavor.get("name", ""):
|
||||
return flavor.get("id")
|
||||
return None
|
||||
|
||||
# ==================== Keypair ====================
|
||||
|
||||
def get_keypair_list(self) -> dict:
|
||||
"""Keypair 목록 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/os-keypairs"
|
||||
return self._get(url)
|
||||
|
||||
def get_keypair_info(self, keypair_name: str) -> dict:
|
||||
"""Keypair 상세 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/os-keypairs/{keypair_name}"
|
||||
return self._get(url)
|
||||
|
||||
# ==================== Instance ====================
|
||||
|
||||
def get_instance_list(self) -> dict:
|
||||
"""인스턴스 목록 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers"
|
||||
return self._get(url)
|
||||
|
||||
def get_instance_list_detail(self) -> dict:
|
||||
"""인스턴스 상세 목록 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/detail"
|
||||
return self._get(url)
|
||||
|
||||
def get_instance_info(self, server_id: str) -> dict:
|
||||
"""인스턴스 상세 정보 조회"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/{server_id}"
|
||||
return self._get(url)
|
||||
|
||||
def get_instance_status(self, server_id: str) -> str:
|
||||
"""인스턴스 상태 조회"""
|
||||
data = self.get_instance_info(server_id)
|
||||
return data.get("server", {}).get("status", "UNKNOWN")
|
||||
|
||||
def get_instance_id_by_name(self, instance_name: str) -> Optional[str]:
|
||||
"""인스턴스 이름으로 ID 조회"""
|
||||
data = self.get_instance_list()
|
||||
servers = data.get("servers", [])
|
||||
|
||||
for server in servers:
|
||||
if server.get("name") == instance_name:
|
||||
return server.get("id")
|
||||
return None
|
||||
|
||||
def create_instance(
|
||||
self,
|
||||
name: str,
|
||||
image_id: str,
|
||||
flavor_id: str,
|
||||
subnet_id: str,
|
||||
keypair_name: str,
|
||||
volume_size: int = 50,
|
||||
volume_type: str = "General SSD",
|
||||
security_groups: Optional[List[str]] = None,
|
||||
availability_zone: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
인스턴스 생성
|
||||
|
||||
Args:
|
||||
name: 인스턴스 이름
|
||||
image_id: 이미지 ID
|
||||
flavor_id: Flavor ID
|
||||
subnet_id: 서브넷 ID
|
||||
keypair_name: Keypair 이름
|
||||
volume_size: 볼륨 크기 (GB, 기본 50)
|
||||
volume_type: 볼륨 타입 (General SSD, General HDD)
|
||||
security_groups: 보안 그룹 목록 (기본 ["default"])
|
||||
availability_zone: 가용 영역
|
||||
|
||||
Returns:
|
||||
dict: 생성된 인스턴스 정보
|
||||
"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers"
|
||||
|
||||
security_groups = security_groups or ["default"]
|
||||
sg_list = [{"name": sg} for sg in security_groups]
|
||||
|
||||
payload = {
|
||||
"server": {
|
||||
"name": name,
|
||||
"imageRef": image_id,
|
||||
"flavorRef": flavor_id,
|
||||
"networks": [{"subnet": subnet_id}],
|
||||
"key_name": keypair_name,
|
||||
"max_count": 1,
|
||||
"min_count": 1,
|
||||
"block_device_mapping_v2": [
|
||||
{
|
||||
"uuid": image_id,
|
||||
"boot_index": 0,
|
||||
"volume_size": volume_size,
|
||||
"volume_type": volume_type,
|
||||
"device_name": "vda",
|
||||
"source_type": "image",
|
||||
"destination_type": "volume",
|
||||
"delete_on_termination": True,
|
||||
}
|
||||
],
|
||||
"security_groups": sg_list,
|
||||
}
|
||||
}
|
||||
|
||||
if availability_zone:
|
||||
payload["server"]["availability_zone"] = availability_zone
|
||||
|
||||
logger.info(f"인스턴스 생성 요청: {name}")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_instance(self, server_id: str) -> dict:
|
||||
"""인스턴스 삭제"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/{server_id}"
|
||||
logger.info(f"인스턴스 삭제 요청: {server_id}")
|
||||
return self._delete(url)
|
||||
|
||||
def start_instance(self, server_id: str) -> dict:
|
||||
"""인스턴스 시작"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/{server_id}/action"
|
||||
return self._post(url, {"os-start": None})
|
||||
|
||||
def stop_instance(self, server_id: str) -> dict:
|
||||
"""인스턴스 정지"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/{server_id}/action"
|
||||
return self._post(url, {"os-stop": None})
|
||||
|
||||
def reboot_instance(self, server_id: str, hard: bool = False) -> dict:
|
||||
"""인스턴스 재부팅"""
|
||||
url = f"{self.compute_url}/v2/{self.tenant_id}/servers/{server_id}/action"
|
||||
reboot_type = "HARD" if hard else "SOFT"
|
||||
return self._post(url, {"reboot": {"type": reboot_type}})
|
||||
|
||||
# ==================== Image ====================
|
||||
|
||||
def get_image_list(self) -> dict:
|
||||
"""이미지 목록 조회"""
|
||||
url = f"{self.image_url}/v2/images"
|
||||
return self._get(url)
|
||||
|
||||
def get_image_info(self, image_id: str) -> dict:
|
||||
"""이미지 상세 조회"""
|
||||
url = f"{self.image_url}/v2/images/{image_id}"
|
||||
return self._get(url)
|
||||
|
||||
def get_image_id_by_name(self, image_name: str, exclude_container: bool = True) -> Optional[str]:
|
||||
"""
|
||||
이미지 이름으로 ID 조회
|
||||
|
||||
Args:
|
||||
image_name: 이미지 이름 (부분 일치)
|
||||
exclude_container: 컨테이너 이미지 제외 여부
|
||||
|
||||
Returns:
|
||||
str: 이미지 ID 또는 None
|
||||
"""
|
||||
data = self.get_image_list()
|
||||
images = data.get("images", [])
|
||||
|
||||
for image in images:
|
||||
name = image.get("name", "")
|
||||
if name.startswith(image_name):
|
||||
if exclude_container and "Container" in name:
|
||||
continue
|
||||
return image.get("id")
|
||||
return None
|
||||
214
nhn/packages/nks.py
Normal file
214
nhn/packages/nks.py
Normal file
@ -0,0 +1,214 @@
|
||||
"""
|
||||
NHN Cloud NKS (Kubernetes Service) API Module
|
||||
|
||||
Kubernetes 클러스터 관리
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .base import BaseAPI, NHNCloudEndpoints, Region
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NKSNodeImages:
|
||||
"""NKS 노드 이미지 ID"""
|
||||
# Ubuntu 20.04 이미지 ID (리전별)
|
||||
UBUNTU_20_04_KR1 = "1213d033-bdf6-4d73-9763-4e8e57c745fb"
|
||||
UBUNTU_20_04_KR2 = "dabb6d10-937d-4952-9ce0-1e576e9164e8"
|
||||
|
||||
|
||||
class ApiNks(BaseAPI):
|
||||
"""NHN Cloud NKS API 클래스"""
|
||||
|
||||
def __init__(self, region: str, token: str):
|
||||
"""
|
||||
Args:
|
||||
region: 리전 (kr1: 판교, kr2: 평촌)
|
||||
token: API 인증 토큰
|
||||
"""
|
||||
super().__init__(region, token)
|
||||
|
||||
if self.region == Region.KR1:
|
||||
self.nks_url = NHNCloudEndpoints.NKS_KR1
|
||||
self.default_node_image = NKSNodeImages.UBUNTU_20_04_KR1
|
||||
else:
|
||||
self.nks_url = NHNCloudEndpoints.NKS_KR2
|
||||
self.default_node_image = NKSNodeImages.UBUNTU_20_04_KR2
|
||||
|
||||
def _get_headers(self, extra_headers: Optional[dict] = None) -> dict:
|
||||
"""NKS API 전용 헤더"""
|
||||
headers = {
|
||||
"X-Auth-Token": self.token,
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"OpenStack-API-Version": "container-infra latest",
|
||||
}
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
return headers
|
||||
|
||||
# ==================== Cluster ====================
|
||||
|
||||
def get_cluster_list(self) -> dict:
|
||||
"""클러스터 목록 조회"""
|
||||
url = f"{self.nks_url}/v1/clusters"
|
||||
return self._get(url)
|
||||
|
||||
def get_cluster_info(self, cluster_name: str) -> dict:
|
||||
"""클러스터 상세 조회"""
|
||||
url = f"{self.nks_url}/v1/clusters/{cluster_name}"
|
||||
return self._get(url)
|
||||
|
||||
def get_cluster_config(self, cluster_name: str) -> str:
|
||||
"""클러스터 kubeconfig 조회"""
|
||||
url = f"{self.nks_url}/v1/clusters/{cluster_name}/config"
|
||||
data = self._get(url)
|
||||
return data.get("config", "")
|
||||
|
||||
def create_public_cluster(
|
||||
self,
|
||||
cluster_name: str,
|
||||
vpc_id: str,
|
||||
subnet_id: str,
|
||||
instance_type: str,
|
||||
keypair_name: str,
|
||||
kubernetes_version: str,
|
||||
external_network_id: str,
|
||||
external_subnet_id: str,
|
||||
availability_zone: str,
|
||||
node_count: int = 1,
|
||||
boot_volume_size: int = 50,
|
||||
boot_volume_type: str = "General SSD",
|
||||
node_image: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Public 클러스터 생성 (외부 접근 가능)
|
||||
|
||||
Args:
|
||||
cluster_name: 클러스터 이름
|
||||
vpc_id: VPC ID
|
||||
subnet_id: 서브넷 ID
|
||||
instance_type: 인스턴스 타입 (Flavor ID)
|
||||
keypair_name: Keypair 이름
|
||||
kubernetes_version: Kubernetes 버전 (예: v1.28.3)
|
||||
external_network_id: 외부 네트워크 ID
|
||||
external_subnet_id: 외부 서브넷 ID
|
||||
availability_zone: 가용 영역 (예: kr-pub-a)
|
||||
node_count: 노드 수 (기본 1)
|
||||
boot_volume_size: 부팅 볼륨 크기 (GB, 기본 50)
|
||||
boot_volume_type: 볼륨 타입 (기본 "General SSD")
|
||||
node_image: 노드 이미지 ID (기본 Ubuntu 20.04)
|
||||
|
||||
Returns:
|
||||
dict: 생성된 클러스터 정보
|
||||
"""
|
||||
url = f"{self.nks_url}/v1/clusters"
|
||||
|
||||
payload = {
|
||||
"cluster_template_id": "iaas_console",
|
||||
"create_timeout": 60,
|
||||
"fixed_network": vpc_id,
|
||||
"fixed_subnet": subnet_id,
|
||||
"flavor_id": instance_type,
|
||||
"keypair": keypair_name,
|
||||
"labels": {
|
||||
"availability_zone": availability_zone,
|
||||
"boot_volume_size": str(boot_volume_size),
|
||||
"boot_volume_type": boot_volume_type,
|
||||
"ca_enable": "false",
|
||||
"cert_manager_api": "True",
|
||||
"clusterautoscale": "nodegroupfeature",
|
||||
"external_network_id": external_network_id,
|
||||
"external_subnet_id_list": external_subnet_id,
|
||||
"kube_tag": kubernetes_version,
|
||||
"master_lb_floating_ip_enabled": "True",
|
||||
"node_image": node_image or self.default_node_image,
|
||||
},
|
||||
"name": cluster_name,
|
||||
"node_count": str(node_count),
|
||||
}
|
||||
|
||||
logger.info(f"Public 클러스터 생성 요청: {cluster_name}")
|
||||
return self._post(url, payload)
|
||||
|
||||
def create_private_cluster(
|
||||
self,
|
||||
cluster_name: str,
|
||||
vpc_id: str,
|
||||
subnet_id: str,
|
||||
instance_type: str,
|
||||
keypair_name: str,
|
||||
kubernetes_version: str,
|
||||
availability_zone: str,
|
||||
node_count: int = 1,
|
||||
boot_volume_size: int = 50,
|
||||
boot_volume_type: str = "General SSD",
|
||||
node_image: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Private 클러스터 생성 (내부 접근만 가능)
|
||||
|
||||
Args:
|
||||
cluster_name: 클러스터 이름
|
||||
vpc_id: VPC ID
|
||||
subnet_id: 서브넷 ID
|
||||
instance_type: 인스턴스 타입 (Flavor ID)
|
||||
keypair_name: Keypair 이름
|
||||
kubernetes_version: Kubernetes 버전 (예: v1.28.3)
|
||||
availability_zone: 가용 영역 (예: kr-pub-a)
|
||||
node_count: 노드 수 (기본 1)
|
||||
boot_volume_size: 부팅 볼륨 크기 (GB, 기본 50)
|
||||
boot_volume_type: 볼륨 타입 (기본 "General SSD")
|
||||
node_image: 노드 이미지 ID (기본 Ubuntu 20.04)
|
||||
|
||||
Returns:
|
||||
dict: 생성된 클러스터 정보
|
||||
"""
|
||||
url = f"{self.nks_url}/v1/clusters"
|
||||
|
||||
payload = {
|
||||
"cluster_template_id": "iaas_console",
|
||||
"create_timeout": 60,
|
||||
"fixed_network": vpc_id,
|
||||
"fixed_subnet": subnet_id,
|
||||
"flavor_id": instance_type,
|
||||
"keypair": keypair_name,
|
||||
"labels": {
|
||||
"availability_zone": availability_zone,
|
||||
"boot_volume_size": str(boot_volume_size),
|
||||
"boot_volume_type": boot_volume_type,
|
||||
"ca_enable": "false",
|
||||
"cert_manager_api": "True",
|
||||
"clusterautoscale": "nodegroupfeature",
|
||||
"kube_tag": kubernetes_version,
|
||||
"master_lb_floating_ip_enabled": "False",
|
||||
"node_image": node_image or self.default_node_image,
|
||||
},
|
||||
"name": cluster_name,
|
||||
"node_count": str(node_count),
|
||||
}
|
||||
|
||||
logger.info(f"Private 클러스터 생성 요청: {cluster_name}")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_cluster(self, cluster_name: str) -> dict:
|
||||
"""클러스터 삭제"""
|
||||
url = f"{self.nks_url}/v1/clusters/{cluster_name}"
|
||||
logger.info(f"클러스터 삭제 요청: {cluster_name}")
|
||||
return self._delete(url)
|
||||
|
||||
# ==================== Node Group ====================
|
||||
|
||||
def get_nodegroup_list(self, cluster_name: str) -> dict:
|
||||
"""노드 그룹 목록 조회"""
|
||||
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups"
|
||||
return self._get(url)
|
||||
|
||||
def get_nodegroup_info(self, cluster_name: str, nodegroup_name: str) -> dict:
|
||||
"""노드 그룹 상세 조회"""
|
||||
url = f"{self.nks_url}/v1/clusters/{cluster_name}/nodegroups/{nodegroup_name}"
|
||||
return self._get(url)
|
||||
279
nhn/packages/storage.py
Normal file
279
nhn/packages/storage.py
Normal file
@ -0,0 +1,279 @@
|
||||
"""
|
||||
NHN Cloud Object Storage API Module
|
||||
|
||||
Object Storage 컨테이너 및 오브젝트 관리
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
import requests
|
||||
|
||||
from .base import BaseAPI, NHNCloudEndpoints, Region, NHNCloudAPIError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiStorageObject(BaseAPI):
|
||||
"""NHN Cloud Object Storage API 클래스"""
|
||||
|
||||
def __init__(self, region: str, token: str, storage_account: str):
|
||||
"""
|
||||
Args:
|
||||
region: 리전 (kr1: 판교, kr2: 평촌)
|
||||
token: API 인증 토큰
|
||||
storage_account: 스토리지 계정 (AUTH_...)
|
||||
"""
|
||||
super().__init__(region, token)
|
||||
self.storage_account = storage_account
|
||||
|
||||
if self.region == Region.KR1:
|
||||
self.storage_url = NHNCloudEndpoints.STORAGE_KR1
|
||||
else:
|
||||
self.storage_url = NHNCloudEndpoints.STORAGE_KR2
|
||||
|
||||
def _get_url(self, container: Optional[str] = None, obj: Optional[str] = None) -> str:
|
||||
"""URL 생성"""
|
||||
parts = [self.storage_url, self.storage_account]
|
||||
if container:
|
||||
parts.append(container)
|
||||
if obj:
|
||||
parts.append(obj)
|
||||
return "/".join(parts)
|
||||
|
||||
def _get_headers(self, extra_headers: Optional[dict] = None) -> dict:
|
||||
"""Object Storage 전용 헤더"""
|
||||
headers = {"X-Auth-Token": self.token}
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
return headers
|
||||
|
||||
# ==================== Container ====================
|
||||
|
||||
def get_container_list(self) -> List[str]:
|
||||
"""컨테이너 목록 조회"""
|
||||
url = self._get_url()
|
||||
try:
|
||||
response = self._session.get(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
self._handle_error(response)
|
||||
return [c for c in response.text.split("\n") if c]
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"컨테이너 목록 조회 실패: {e}")
|
||||
raise NHNCloudAPIError(f"컨테이너 목록 조회 실패: {e}")
|
||||
|
||||
def create_container(self, container_name: str) -> dict:
|
||||
"""
|
||||
컨테이너 생성
|
||||
|
||||
Args:
|
||||
container_name: 컨테이너 이름
|
||||
|
||||
Returns:
|
||||
dict: 생성 결과
|
||||
"""
|
||||
url = self._get_url(container_name)
|
||||
try:
|
||||
response = self._session.put(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"컨테이너 생성: {container_name}")
|
||||
return {"status": response.status_code, "container": container_name}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"컨테이너 생성 실패: {e}")
|
||||
raise NHNCloudAPIError(f"컨테이너 생성 실패: {e}")
|
||||
|
||||
def delete_container(self, container_name: str) -> dict:
|
||||
"""컨테이너 삭제"""
|
||||
url = self._get_url(container_name)
|
||||
try:
|
||||
response = self._session.delete(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"컨테이너 삭제: {container_name}")
|
||||
return {"status": response.status_code, "container": container_name}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"컨테이너 삭제 실패: {e}")
|
||||
raise NHNCloudAPIError(f"컨테이너 삭제 실패: {e}")
|
||||
|
||||
def set_container_public(self, container_name: str, is_public: bool = True) -> dict:
|
||||
"""
|
||||
컨테이너 공개/비공개 설정
|
||||
|
||||
Args:
|
||||
container_name: 컨테이너 이름
|
||||
is_public: 공개 여부 (True: 공개, False: 비공개)
|
||||
|
||||
Returns:
|
||||
dict: 설정 결과
|
||||
"""
|
||||
url = self._get_url(container_name)
|
||||
headers = self._get_headers()
|
||||
headers["X-Container-Read"] = ".r:*" if is_public else ""
|
||||
|
||||
try:
|
||||
response = self._session.post(
|
||||
url,
|
||||
headers=headers,
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"컨테이너 공개 설정: {container_name} -> {is_public}")
|
||||
return {"status": response.status_code, "public": is_public}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"컨테이너 공개 설정 실패: {e}")
|
||||
raise NHNCloudAPIError(f"컨테이너 공개 설정 실패: {e}")
|
||||
|
||||
# ==================== Object ====================
|
||||
|
||||
def get_object_list(self, container_name: str) -> List[str]:
|
||||
"""오브젝트 목록 조회"""
|
||||
url = self._get_url(container_name)
|
||||
try:
|
||||
response = self._session.get(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
self._handle_error(response)
|
||||
return [o for o in response.text.split("\n") if o]
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 목록 조회 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 목록 조회 실패: {e}")
|
||||
|
||||
def upload_object(
|
||||
self,
|
||||
container_name: str,
|
||||
object_name: str,
|
||||
file_path: str,
|
||||
) -> dict:
|
||||
"""
|
||||
오브젝트 업로드
|
||||
|
||||
Args:
|
||||
container_name: 컨테이너 이름
|
||||
object_name: 오브젝트 이름
|
||||
file_path: 업로드할 파일 경로
|
||||
|
||||
Returns:
|
||||
dict: 업로드 결과
|
||||
"""
|
||||
url = self._get_url(container_name, object_name)
|
||||
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
response = self._session.put(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
data=f.read(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"오브젝트 업로드: {container_name}/{object_name}")
|
||||
return {"status": response.status_code, "object": object_name}
|
||||
except FileNotFoundError:
|
||||
raise NHNCloudAPIError(f"파일을 찾을 수 없습니다: {file_path}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 업로드 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 업로드 실패: {e}")
|
||||
|
||||
def upload_object_data(
|
||||
self,
|
||||
container_name: str,
|
||||
object_name: str,
|
||||
data: bytes,
|
||||
content_type: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
데이터를 오브젝트로 업로드
|
||||
|
||||
Args:
|
||||
container_name: 컨테이너 이름
|
||||
object_name: 오브젝트 이름
|
||||
data: 업로드할 데이터
|
||||
content_type: Content-Type 헤더
|
||||
|
||||
Returns:
|
||||
dict: 업로드 결과
|
||||
"""
|
||||
url = self._get_url(container_name, object_name)
|
||||
headers = self._get_headers()
|
||||
if content_type:
|
||||
headers["Content-Type"] = content_type
|
||||
|
||||
try:
|
||||
response = self._session.put(
|
||||
url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"오브젝트 업로드: {container_name}/{object_name}")
|
||||
return {"status": response.status_code, "object": object_name}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 업로드 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 업로드 실패: {e}")
|
||||
|
||||
def download_object(self, container_name: str, object_name: str) -> bytes:
|
||||
"""
|
||||
오브젝트 다운로드
|
||||
|
||||
Args:
|
||||
container_name: 컨테이너 이름
|
||||
object_name: 오브젝트 이름
|
||||
|
||||
Returns:
|
||||
bytes: 오브젝트 데이터
|
||||
"""
|
||||
url = self._get_url(container_name, object_name)
|
||||
|
||||
try:
|
||||
response = self._session.get(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
self._handle_error(response)
|
||||
return response.content
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 다운로드 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 다운로드 실패: {e}")
|
||||
|
||||
def delete_object(self, container_name: str, object_name: str) -> dict:
|
||||
"""오브젝트 삭제"""
|
||||
url = self._get_url(container_name, object_name)
|
||||
|
||||
try:
|
||||
response = self._session.delete(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
logger.info(f"오브젝트 삭제: {container_name}/{object_name}")
|
||||
return {"status": response.status_code, "object": object_name}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 삭제 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 삭제 실패: {e}")
|
||||
|
||||
def get_object_info(self, container_name: str, object_name: str) -> dict:
|
||||
"""오브젝트 메타데이터 조회"""
|
||||
url = self._get_url(container_name, object_name)
|
||||
|
||||
try:
|
||||
response = self._session.head(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
timeout=self.DEFAULT_TIMEOUT,
|
||||
)
|
||||
return dict(response.headers)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"오브젝트 정보 조회 실패: {e}")
|
||||
raise NHNCloudAPIError(f"오브젝트 정보 조회 실패: {e}")
|
||||
123
nhn/packages/token.py
Normal file
123
nhn/packages/token.py
Normal file
@ -0,0 +1,123 @@
|
||||
"""
|
||||
NHN Cloud Token Management Module
|
||||
|
||||
NHN Cloud API 인증 토큰 관리
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
import requests
|
||||
|
||||
from .base import NHNCloudEndpoints, NHNCloudAPIError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenResponse:
|
||||
"""토큰 응답 데이터"""
|
||||
token: str
|
||||
tenant_id: str
|
||||
expires_at: Optional[str] = None
|
||||
|
||||
|
||||
class NHNCloudToken:
|
||||
"""NHN Cloud 토큰 관리 클래스"""
|
||||
|
||||
TOKEN_URL = f"{NHNCloudEndpoints.IDENTITY}/v2.0/tokens"
|
||||
|
||||
def __init__(self, tenant_id: str, username: str, password: str):
|
||||
"""
|
||||
Args:
|
||||
tenant_id: NHN Cloud 테넌트 ID
|
||||
username: NHN Cloud 사용자 이메일
|
||||
password: NHN Cloud API 비밀번호
|
||||
"""
|
||||
self.tenant_id = tenant_id
|
||||
self.username = username
|
||||
self.password = password
|
||||
self._token: Optional[str] = None
|
||||
|
||||
def create_token(self) -> TokenResponse:
|
||||
"""
|
||||
새 토큰 생성
|
||||
|
||||
Returns:
|
||||
TokenResponse: 생성된 토큰 정보
|
||||
|
||||
Raises:
|
||||
NHNCloudAPIError: 토큰 생성 실패 시
|
||||
"""
|
||||
payload = {
|
||||
"auth": {
|
||||
"tenantId": self.tenant_id,
|
||||
"passwordCredentials": {
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"[NHN API] 토큰 생성 요청 - URL={self.TOKEN_URL}, tenant_id={self.tenant_id}, username={self.username}")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
self.TOKEN_URL,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
logger.info(f"[NHN API] 토큰 생성 응답 - status_code={response.status_code}")
|
||||
|
||||
data = response.json()
|
||||
|
||||
if "error" in data:
|
||||
error = data["error"]
|
||||
logger.error(f"[NHN API] 토큰 생성 실패 - code={error.get('code')}, message={error.get('message')}, details={error}")
|
||||
raise NHNCloudAPIError(
|
||||
message=error.get("message", "토큰 생성 실패"),
|
||||
code=error.get("code"),
|
||||
details=error,
|
||||
)
|
||||
|
||||
access = data.get("access", {})
|
||||
token_info = access.get("token", {})
|
||||
self._token = token_info.get("id")
|
||||
|
||||
token_preview = self._token[:8] + "..." if self._token else "None"
|
||||
logger.info(f"[NHN API] 토큰 생성 성공 - tenant_id={self.tenant_id}, token={token_preview}, expires={token_info.get('expires')}")
|
||||
|
||||
return TokenResponse(
|
||||
token=self._token,
|
||||
tenant_id=self.tenant_id,
|
||||
expires_at=token_info.get("expires"),
|
||||
)
|
||||
|
||||
except requests.exceptions.Timeout as e:
|
||||
logger.error(f"[NHN API] 토큰 생성 타임아웃 - URL={self.TOKEN_URL}, error={e}")
|
||||
raise NHNCloudAPIError(f"토큰 생성 요청 타임아웃: {e}")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.error(f"[NHN API] 토큰 생성 연결 실패 - URL={self.TOKEN_URL}, error={e}")
|
||||
raise NHNCloudAPIError(f"토큰 생성 서버 연결 실패: {e}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"[NHN API] 토큰 생성 요청 실패 - URL={self.TOKEN_URL}, error={e}")
|
||||
raise NHNCloudAPIError(f"토큰 생성 요청 실패: {e}")
|
||||
|
||||
@property
|
||||
def token(self) -> Optional[str]:
|
||||
"""현재 토큰 반환"""
|
||||
return self._token
|
||||
|
||||
def get_token(self) -> str:
|
||||
"""
|
||||
토큰 반환 (없으면 생성)
|
||||
|
||||
Returns:
|
||||
str: API 토큰
|
||||
"""
|
||||
if not self._token:
|
||||
self.create_token()
|
||||
return self._token
|
||||
272
nhn/packages/vpc.py
Normal file
272
nhn/packages/vpc.py
Normal file
@ -0,0 +1,272 @@
|
||||
"""
|
||||
NHN Cloud VPC API Module
|
||||
|
||||
VPC, Subnet, Routing Table, Floating IP 관리
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .base import BaseAPI, NHNCloudEndpoints, Region
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiVpc(BaseAPI):
|
||||
"""NHN Cloud VPC API 클래스"""
|
||||
|
||||
def __init__(self, region: str, token: str):
|
||||
"""
|
||||
Args:
|
||||
region: 리전 (kr1: 판교, kr2: 평촌)
|
||||
token: API 인증 토큰
|
||||
"""
|
||||
super().__init__(region, token)
|
||||
|
||||
if self.region == Region.KR1:
|
||||
self.vpc_url = NHNCloudEndpoints.NETWORK_KR1
|
||||
else:
|
||||
self.vpc_url = NHNCloudEndpoints.NETWORK_KR2
|
||||
|
||||
# ==================== VPC ====================
|
||||
|
||||
def get_vpc_list(self) -> dict:
|
||||
"""VPC 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcs"
|
||||
logger.info(f"[NHN API] VPC 목록 조회 요청 - URL={url}")
|
||||
result = self._get(url)
|
||||
vpc_count = len(result.get("vpcs", []))
|
||||
logger.info(f"[NHN API] VPC 목록 조회 완료 - count={vpc_count}")
|
||||
return result
|
||||
|
||||
def get_vpc_info(self, vpc_id: str) -> dict:
|
||||
"""VPC 상세 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcs/{vpc_id}"
|
||||
logger.info(f"[NHN API] VPC 상세 조회 요청 - URL={url}, vpc_id={vpc_id}")
|
||||
result = self._get(url)
|
||||
logger.info(f"[NHN API] VPC 상세 조회 완료 - vpc_id={vpc_id}")
|
||||
return result
|
||||
|
||||
def get_vpc_id_by_name(self, vpc_name: str) -> Optional[str]:
|
||||
"""VPC 이름으로 ID 조회"""
|
||||
data = self.get_vpc_list()
|
||||
vpcs = data.get("vpcs", [])
|
||||
|
||||
for vpc in vpcs:
|
||||
if vpc.get("name", "").startswith(vpc_name):
|
||||
return vpc.get("id")
|
||||
return None
|
||||
|
||||
def create_vpc(self, name: str, cidr: str) -> dict:
|
||||
"""
|
||||
VPC 생성
|
||||
|
||||
Args:
|
||||
name: VPC 이름
|
||||
cidr: CIDR 블록 (예: 10.0.0.0/16)
|
||||
|
||||
Returns:
|
||||
dict: 생성된 VPC 정보
|
||||
"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcs"
|
||||
payload = {"vpc": {"name": name, "cidrv4": cidr}}
|
||||
logger.info(f"VPC 생성 요청: {name} ({cidr})")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_vpc(self, vpc_id: str) -> dict:
|
||||
"""VPC 삭제"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcs/{vpc_id}"
|
||||
logger.info(f"VPC 삭제 요청: {vpc_id}")
|
||||
return self._delete(url)
|
||||
|
||||
# ==================== Subnet ====================
|
||||
|
||||
def get_subnet_list(self) -> dict:
|
||||
"""서브넷 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcsubnets"
|
||||
return self._get(url)
|
||||
|
||||
def get_subnet_info(self, subnet_id: str) -> dict:
|
||||
"""서브넷 상세 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcsubnets/{subnet_id}"
|
||||
return self._get(url)
|
||||
|
||||
def get_subnet_id_by_name(self, subnet_name: str) -> Optional[str]:
|
||||
"""서브넷 이름으로 ID 조회"""
|
||||
data = self.get_subnet_list()
|
||||
subnets = data.get("vpcsubnets", [])
|
||||
|
||||
for subnet in subnets:
|
||||
name = subnet.get("name", "")
|
||||
if name.startswith(subnet_name) and "rt" not in name:
|
||||
return subnet.get("id")
|
||||
return None
|
||||
|
||||
def create_subnet(self, vpc_id: str, cidr: str, name: str) -> dict:
|
||||
"""
|
||||
서브넷 생성
|
||||
|
||||
Args:
|
||||
vpc_id: VPC ID
|
||||
cidr: CIDR 블록 (예: 10.0.1.0/24)
|
||||
name: 서브넷 이름
|
||||
|
||||
Returns:
|
||||
dict: 생성된 서브넷 정보
|
||||
"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcsubnets"
|
||||
payload = {"vpcsubnet": {"vpc_id": vpc_id, "cidr": cidr, "name": name}}
|
||||
logger.info(f"서브넷 생성 요청: {name} ({cidr})")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_subnet(self, subnet_id: str) -> dict:
|
||||
"""서브넷 삭제"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcsubnets/{subnet_id}"
|
||||
logger.info(f"서브넷 삭제 요청: {subnet_id}")
|
||||
return self._delete(url)
|
||||
|
||||
# ==================== Routing Table ====================
|
||||
|
||||
def get_routing_table_list(self, detail: bool = True) -> dict:
|
||||
"""라우팅 테이블 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables"
|
||||
params = {"detail": "true"} if detail else None
|
||||
return self._get(url, params=params)
|
||||
|
||||
def get_routing_table_info(self, routingtable_id: str) -> dict:
|
||||
"""라우팅 테이블 상세 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables/{routingtable_id}"
|
||||
return self._get(url)
|
||||
|
||||
def get_default_routing_table_id(self) -> Optional[str]:
|
||||
"""기본 라우팅 테이블 ID 조회"""
|
||||
data = self.get_routing_table_list(detail=False)
|
||||
tables = data.get("routingtables", [])
|
||||
return tables[0].get("id") if tables else None
|
||||
|
||||
def create_routing_table(self, name: str, vpc_id: str, distributed: bool = True) -> dict:
|
||||
"""
|
||||
라우팅 테이블 생성
|
||||
|
||||
Args:
|
||||
name: 라우팅 테이블 이름
|
||||
vpc_id: VPC ID
|
||||
distributed: 분산 라우팅 여부 (기본 True)
|
||||
|
||||
Returns:
|
||||
dict: 생성된 라우팅 테이블 정보
|
||||
"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables"
|
||||
payload = {
|
||||
"routingtable": {
|
||||
"name": name,
|
||||
"vpc_id": vpc_id,
|
||||
"distributed": str(distributed).lower(),
|
||||
}
|
||||
}
|
||||
logger.info(f"라우팅 테이블 생성 요청: {name}")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_routing_table(self, routingtable_id: str) -> dict:
|
||||
"""라우팅 테이블 삭제"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables/{routingtable_id}"
|
||||
logger.info(f"라우팅 테이블 삭제 요청: {routingtable_id}")
|
||||
return self._delete(url)
|
||||
|
||||
def set_default_routing_table(self, routingtable_id: str) -> dict:
|
||||
"""라우팅 테이블을 기본으로 설정"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables/{routingtable_id}/set_as_default"
|
||||
return self._put(url)
|
||||
|
||||
def attach_gateway_to_routing_table(self, routingtable_id: str, gateway_id: str) -> dict:
|
||||
"""라우팅 테이블에 인터넷 게이트웨이 연결"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables/{routingtable_id}/attach_gateway"
|
||||
payload = {"gateway_id": gateway_id}
|
||||
logger.info(f"게이트웨이 연결 요청: {routingtable_id} -> {gateway_id}")
|
||||
return self._put(url, payload)
|
||||
|
||||
def detach_gateway_from_routing_table(self, routingtable_id: str) -> dict:
|
||||
"""라우팅 테이블에서 인터넷 게이트웨이 분리"""
|
||||
url = f"{self.vpc_url}/v2.0/routingtables/{routingtable_id}/detach_gateway"
|
||||
return self._put(url)
|
||||
|
||||
def attach_routing_table_to_subnet(self, subnet_id: str, routingtable_id: str) -> dict:
|
||||
"""서브넷에 라우팅 테이블 연결"""
|
||||
url = f"{self.vpc_url}/v2.0/vpcsubnets/{subnet_id}/attach_routingtable"
|
||||
payload = {"routingtable_id": routingtable_id}
|
||||
return self._put(url, payload)
|
||||
|
||||
# ==================== Floating IP ====================
|
||||
|
||||
def get_external_network_id(self) -> dict:
|
||||
"""외부 네트워크 ID 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/networks"
|
||||
params = {"router:external": "True"}
|
||||
return self._get(url, params=params)
|
||||
|
||||
def get_floating_ip_list(self) -> dict:
|
||||
"""Floating IP 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/floatingips"
|
||||
return self._get(url)
|
||||
|
||||
def create_floating_ip(self, external_network_id: str) -> dict:
|
||||
"""
|
||||
Floating IP 생성
|
||||
|
||||
Args:
|
||||
external_network_id: 외부 네트워크 ID
|
||||
|
||||
Returns:
|
||||
dict: 생성된 Floating IP 정보
|
||||
"""
|
||||
url = f"{self.vpc_url}/v2.0/floatingips"
|
||||
payload = {"floatingip": {"floating_network_id": external_network_id}}
|
||||
logger.info("Floating IP 생성 요청")
|
||||
return self._post(url, payload)
|
||||
|
||||
def delete_floating_ip(self, floating_ip_id: str) -> dict:
|
||||
"""Floating IP 삭제"""
|
||||
url = f"{self.vpc_url}/v2.0/floatingips/{floating_ip_id}"
|
||||
logger.info(f"Floating IP 삭제 요청: {floating_ip_id}")
|
||||
return self._delete(url)
|
||||
|
||||
def attach_floating_ip(self, floating_ip_id: str, port_id: str) -> dict:
|
||||
"""Floating IP를 포트에 연결"""
|
||||
url = f"{self.vpc_url}/v2.0/floatingips/{floating_ip_id}"
|
||||
payload = {"floatingip": {"port_id": port_id}}
|
||||
return self._put(url, payload)
|
||||
|
||||
def detach_floating_ip(self, floating_ip_id: str) -> dict:
|
||||
"""Floating IP 연결 해제"""
|
||||
url = f"{self.vpc_url}/v2.0/floatingips/{floating_ip_id}"
|
||||
payload = {"floatingip": {"port_id": None}}
|
||||
return self._put(url, payload)
|
||||
|
||||
# ==================== Security Group ====================
|
||||
|
||||
def get_security_group_list(self) -> dict:
|
||||
"""보안 그룹 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/security-groups"
|
||||
return self._get(url)
|
||||
|
||||
def get_security_group_info(self, security_group_id: str) -> dict:
|
||||
"""보안 그룹 상세 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/security-groups/{security_group_id}"
|
||||
return self._get(url)
|
||||
|
||||
# ==================== Port (NIC) ====================
|
||||
|
||||
def get_port_list(self) -> dict:
|
||||
"""포트 목록 조회"""
|
||||
url = f"{self.vpc_url}/v2.0/ports"
|
||||
return self._get(url)
|
||||
|
||||
def get_port_id_by_device(self, device_id: str) -> Optional[str]:
|
||||
"""디바이스 ID로 포트 ID 조회"""
|
||||
data = self.get_port_list()
|
||||
ports = data.get("ports", [])
|
||||
|
||||
for port in ports:
|
||||
if port.get("device_id", "").startswith(device_id):
|
||||
return port.get("id")
|
||||
return None
|
||||
Reference in New Issue
Block a user