From b30cebb603eed49b66cc5442c006cb087b7ffd88 Mon Sep 17 00:00:00 2001 From: icurfer Date: Fri, 24 Apr 2026 17:17:39 +0900 Subject: [PATCH] =?UTF-8?q?v0.0.19=20|=20Internet=20Gateway=20/=20Routing?= =?UTF-8?q?=20Table=20=EA=B4=80=EB=A6=AC=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ApiVpc에 IGW CRUD 추가 (/v2.0/internetgateways, external_network_id 사용) - IGW/RoutingTable/Subnet-attach View 및 URL 신규 (모두 동기) - API_SPEC.md 3.7.1~3.7.8 네트워크 섹션 추가 Co-Authored-By: Claude Opus 4.7 (1M context) --- API_SPEC.md | 101 ++++++++++++++++ nhn/packages/vpc.py | 40 +++++++ nhn/serializers.py | 52 +++++++++ nhn/urls.py | 14 +++ nhn/views.py | 273 ++++++++++++++++++++++++++++++++++++++++++++ version | 2 +- 6 files changed, 481 insertions(+), 1 deletion(-) diff --git a/API_SPEC.md b/API_SPEC.md index 3171358..8e98ec6 100644 --- a/API_SPEC.md +++ b/API_SPEC.md @@ -387,6 +387,107 @@ DELETE /api/nhn/subnet/{subnet_id}/ --- +### 3.7.1 서브넷에 라우팅 테이블 연결 + +``` +PUT /api/nhn/subnet/{subnet_id}/attach-routing-table/ +``` + +**Request Body** +```json +{ + "routingtable_id": "rt-id-123" +} +``` + +--- + +### 3.7.2 인터넷 게이트웨이 목록 조회 + +``` +GET /api/nhn/internet-gateway/ +``` + +**Response (200 OK)** NHN Cloud Internet Gateway Public API `/v2.0/internetgateways` 통과 응답. + +--- + +### 3.7.3 인터넷 게이트웨이 생성 + +``` +POST /api/nhn/internet-gateway/create/ +``` + +**Request Body** +```json +{ + "name": "igw-1", + "external_network_id": "ext-net-id (선택, 생략 시 자동 조회)" +} +``` + +--- + +### 3.7.4 인터넷 게이트웨이 상세/삭제 + +``` +GET /api/nhn/internet-gateway/{internetgateway_id}/ +DELETE /api/nhn/internet-gateway/{internetgateway_id}/ +``` + +--- + +### 3.7.5 라우팅 테이블 목록 조회 + +``` +GET /api/nhn/routing-table/ +``` + +--- + +### 3.7.6 라우팅 테이블 생성 + +``` +POST /api/nhn/routing-table/create/ +``` + +**Request Body** +```json +{ + "name": "rt-1", + "vpc_id": "vpc-id-123", + "distributed": true +} +``` + +--- + +### 3.7.7 라우팅 테이블 상세/삭제 + +``` +GET /api/nhn/routing-table/{routingtable_id}/ +DELETE /api/nhn/routing-table/{routingtable_id}/ +``` + +--- + +### 3.7.8 라우팅 테이블 기본 설정 / 게이트웨이 연결·해제 + +``` +PUT /api/nhn/routing-table/{routingtable_id}/set-default/ +PUT /api/nhn/routing-table/{routingtable_id}/attach-gateway/ +PUT /api/nhn/routing-table/{routingtable_id}/detach-gateway/ +``` + +**attach-gateway Request Body** +```json +{ + "gateway_id": "igw-id-123" +} +``` + +--- + ### 3.8 Floating IP 목록 조회 ``` diff --git a/nhn/packages/vpc.py b/nhn/packages/vpc.py index ab1f513..56d7fc2 100644 --- a/nhn/packages/vpc.py +++ b/nhn/packages/vpc.py @@ -196,6 +196,46 @@ class ApiVpc(BaseAPI): payload = {"routingtable_id": routingtable_id} return self._put(url, payload) + # ==================== Internet Gateway ==================== + + def get_internet_gateway_list(self) -> dict: + """인터넷 게이트웨이 목록 조회""" + url = f"{self.vpc_url}/v2.0/internetgateways" + logger.info(f"[NHN API] IGW 목록 조회 요청 - URL={url}") + result = self._get(url) + count = len(result.get("internetgateways", [])) + logger.info(f"[NHN API] IGW 목록 조회 완료 - count={count}") + return result + + def get_internet_gateway_info(self, internetgateway_id: str) -> dict: + """인터넷 게이트웨이 상세 조회""" + url = f"{self.vpc_url}/v2.0/internetgateways/{internetgateway_id}" + return self._get(url) + + def create_internet_gateway(self, name: str, external_network_id: str) -> dict: + """ + 인터넷 게이트웨이 생성 + + Args: + name: IGW 이름 + external_network_id: 외부 네트워크 ID (get_external_network_id 로 조회) + """ + url = f"{self.vpc_url}/v2.0/internetgateways" + payload = { + "internetgateway": { + "name": name, + "external_network_id": external_network_id, + } + } + logger.info(f"IGW 생성 요청: {name} (external_network={external_network_id})") + return self._post(url, payload) + + def delete_internet_gateway(self, internetgateway_id: str) -> dict: + """인터넷 게이트웨이 삭제""" + url = f"{self.vpc_url}/v2.0/internetgateways/{internetgateway_id}" + logger.info(f"IGW 삭제 요청: {internetgateway_id}") + return self._delete(url) + # ==================== Floating IP ==================== def get_external_network_id(self) -> dict: diff --git a/nhn/serializers.py b/nhn/serializers.py index 1f13612..3febefb 100644 --- a/nhn/serializers.py +++ b/nhn/serializers.py @@ -119,6 +119,58 @@ class SubnetSerializer(serializers.Serializer): ) +class SubnetAttachRoutingTableSerializer(serializers.Serializer): + """서브넷에 라우팅 테이블 연결 요청""" + + routingtable_id = serializers.CharField( + help_text="라우팅 테이블 ID", + ) + + +# ==================== Routing Table ==================== + + +class RoutingTableSerializer(serializers.Serializer): + """라우팅 테이블 생성 요청""" + + name = serializers.CharField( + help_text="라우팅 테이블 이름", + max_length=255, + ) + vpc_id = serializers.CharField( + help_text="VPC ID", + ) + distributed = serializers.BooleanField( + help_text="분산 라우팅 여부 (기본 True)", + default=True, + ) + + +class RoutingTableAttachGatewaySerializer(serializers.Serializer): + """라우팅 테이블에 인터넷 게이트웨이 연결 요청""" + + gateway_id = serializers.CharField( + help_text="인터넷 게이트웨이 ID", + ) + + +# ==================== Internet Gateway ==================== + + +class InternetGatewaySerializer(serializers.Serializer): + """인터넷 게이트웨이 생성 요청""" + + name = serializers.CharField( + help_text="인터넷 게이트웨이 이름", + max_length=255, + ) + external_network_id = serializers.CharField( + help_text="외부 네트워크 ID (생략 시 자동 조회)", + required=False, + allow_blank=True, + ) + + # ==================== Security Group ==================== diff --git a/nhn/urls.py b/nhn/urls.py index cbfa656..5c022e0 100644 --- a/nhn/urls.py +++ b/nhn/urls.py @@ -28,6 +28,20 @@ urlpatterns = [ path("subnet/", views.SubnetListView.as_view(), name="subnet-list"), path("subnet/create/", views.SubnetCreateView.as_view(), name="subnet-create"), path("subnet//", views.SubnetDetailView.as_view(), name="subnet-detail"), + path("subnet//attach-routing-table/", views.SubnetAttachRoutingTableView.as_view(), name="subnet-attach-routing-table"), + + # ==================== Internet Gateway ==================== + path("internet-gateway/", views.InternetGatewayListView.as_view(), name="internet-gateway-list"), + path("internet-gateway/create/", views.InternetGatewayCreateView.as_view(), name="internet-gateway-create"), + path("internet-gateway//", views.InternetGatewayDetailView.as_view(), name="internet-gateway-detail"), + + # ==================== Routing Table ==================== + path("routing-table/", views.RoutingTableListView.as_view(), name="routing-table-list"), + path("routing-table/create/", views.RoutingTableCreateView.as_view(), name="routing-table-create"), + path("routing-table//", views.RoutingTableDetailView.as_view(), name="routing-table-detail"), + path("routing-table//set-default/", views.RoutingTableSetDefaultView.as_view(), name="routing-table-set-default"), + path("routing-table//attach-gateway/", views.RoutingTableAttachGatewayView.as_view(), name="routing-table-attach-gateway"), + path("routing-table//detach-gateway/", views.RoutingTableDetachGatewayView.as_view(), name="routing-table-detach-gateway"), # ==================== Floating IP ==================== path("floatingip/", views.FloatingIpListView.as_view(), name="floatingip-list"), diff --git a/nhn/views.py b/nhn/views.py index ff7b4cf..16b9e43 100644 --- a/nhn/views.py +++ b/nhn/views.py @@ -19,6 +19,10 @@ from .serializers import ( ComputeInstanceSerializer, VpcSerializer, SubnetSerializer, + SubnetAttachRoutingTableSerializer, + RoutingTableSerializer, + RoutingTableAttachGatewaySerializer, + InternetGatewaySerializer, SecurityGroupSerializer, SecurityGroupUpdateSerializer, SecurityGroupRuleSerializer, @@ -654,6 +658,275 @@ class FloatingIpDetachView(NHNBaseView): return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) +# ==================== Internet Gateway API ==================== + + +class InternetGatewayListView(NHNBaseView): + """인터넷 게이트웨이 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="인터넷 게이트웨이 목록 조회", + manual_parameters=[region_header, token_header], + responses={200: "IGW 목록"}, + ) + def get(self, request): + headers = get_nhn_headers(request) + try: + api = ApiVpc(headers["region"], headers["token"]) + return Response(api.get_internet_gateway_list()) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class InternetGatewayCreateView(NHNBaseView): + """인터넷 게이트웨이 생성 API""" + + @swagger_auto_schema( + operation_summary="인터넷 게이트웨이 생성", + manual_parameters=[region_header, token_header], + request_body=InternetGatewaySerializer, + responses={201: "생성된 IGW 정보"}, + ) + def post(self, request): + headers = get_nhn_headers(request) + serializer = InternetGatewaySerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiVpc(headers["region"], headers["token"]) + external_network_id = serializer.validated_data.get("external_network_id", "") + if not external_network_id: + networks = api.get_external_network_id().get("networks", []) + if not networks: + return Response( + {"error": "외부 네트워크를 찾을 수 없습니다."}, + status=status.HTTP_400_BAD_REQUEST, + ) + external_network_id = networks[0].get("id") + + result = api.create_internet_gateway( + name=serializer.validated_data["name"], + external_network_id=external_network_id, + ) + logger.info(f"[IGW] 생성 성공 - name={serializer.validated_data['name']}") + return Response(result, status=status.HTTP_201_CREATED) + except NHNCloudAPIError as e: + logger.error(f"[IGW] 생성 실패 - error={e.message}") + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class InternetGatewayDetailView(NHNBaseView): + """인터넷 게이트웨이 상세/삭제 API""" + + @swagger_auto_schema( + operation_summary="인터넷 게이트웨이 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "IGW 상세 정보"}, + ) + def get(self, request, internetgateway_id): + headers = get_nhn_headers(request) + try: + api = ApiVpc(headers["region"], headers["token"]) + return Response(api.get_internet_gateway_info(internetgateway_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="인터넷 게이트웨이 삭제", + manual_parameters=[region_header, token_header], + responses={204: "삭제 완료"}, + ) + def delete(self, request, internetgateway_id): + headers = get_nhn_headers(request) + try: + api = ApiVpc(headers["region"], headers["token"]) + api.delete_internet_gateway(internetgateway_id) + logger.info(f"[IGW] 삭제 성공 - id={internetgateway_id}") + return Response(status=status.HTTP_204_NO_CONTENT) + except NHNCloudAPIError as e: + logger.error(f"[IGW] 삭제 실패 - id={internetgateway_id}, error={e.message}") + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +# ==================== Routing Table API ==================== + + +class RoutingTableListView(NHNBaseView): + """라우팅 테이블 목록 조회 API""" + + @swagger_auto_schema( + operation_summary="라우팅 테이블 목록 조회", + manual_parameters=[region_header, token_header], + responses={200: "라우팅 테이블 목록"}, + ) + def get(self, request): + headers = get_nhn_headers(request) + try: + api = ApiVpc(headers["region"], headers["token"]) + return Response(api.get_routing_table_list()) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class RoutingTableCreateView(NHNBaseView): + """라우팅 테이블 생성 API""" + + @swagger_auto_schema( + operation_summary="라우팅 테이블 생성", + manual_parameters=[region_header, token_header], + request_body=RoutingTableSerializer, + responses={201: "생성된 라우팅 테이블 정보"}, + ) + def post(self, request): + headers = get_nhn_headers(request) + serializer = RoutingTableSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiVpc(headers["region"], headers["token"]) + result = api.create_routing_table( + name=serializer.validated_data["name"], + vpc_id=serializer.validated_data["vpc_id"], + distributed=serializer.validated_data.get("distributed", True), + ) + logger.info(f"[RT] 생성 성공 - name={serializer.validated_data['name']}") + return Response(result, status=status.HTTP_201_CREATED) + except NHNCloudAPIError as e: + logger.error(f"[RT] 생성 실패 - error={e.message}") + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class RoutingTableDetailView(NHNBaseView): + """라우팅 테이블 상세/삭제 API""" + + @swagger_auto_schema( + operation_summary="라우팅 테이블 상세 조회", + manual_parameters=[region_header, token_header], + responses={200: "라우팅 테이블 상세"}, + ) + def get(self, request, routingtable_id): + headers = get_nhn_headers(request) + try: + api = ApiVpc(headers["region"], headers["token"]) + return Response(api.get_routing_table_info(routingtable_id)) + except NHNCloudAPIError as e: + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_summary="라우팅 테이블 삭제", + manual_parameters=[region_header, token_header], + responses={204: "삭제 완료"}, + ) + def delete(self, request, routingtable_id): + headers = get_nhn_headers(request) + try: + api = ApiVpc(headers["region"], headers["token"]) + api.delete_routing_table(routingtable_id) + logger.info(f"[RT] 삭제 성공 - id={routingtable_id}") + return Response(status=status.HTTP_204_NO_CONTENT) + except NHNCloudAPIError as e: + logger.error(f"[RT] 삭제 실패 - id={routingtable_id}, error={e.message}") + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class RoutingTableSetDefaultView(NHNBaseView): + """라우팅 테이블을 기본으로 설정""" + + @swagger_auto_schema( + operation_summary="라우팅 테이블 기본 설정", + manual_parameters=[region_header, token_header], + responses={200: "설정 결과"}, + ) + def put(self, request, routingtable_id): + headers = get_nhn_headers(request) + try: + api = ApiVpc(headers["region"], headers["token"]) + result = api.set_default_routing_table(routingtable_id) + logger.info(f"[RT] 기본 설정 성공 - id={routingtable_id}") + return Response(result) + except NHNCloudAPIError as e: + logger.error(f"[RT] 기본 설정 실패 - id={routingtable_id}, error={e.message}") + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class RoutingTableAttachGatewayView(NHNBaseView): + """라우팅 테이블에 인터넷 게이트웨이 연결""" + + @swagger_auto_schema( + operation_summary="라우팅 테이블에 인터넷 게이트웨이 연결", + manual_parameters=[region_header, token_header], + request_body=RoutingTableAttachGatewaySerializer, + responses={200: "연결 결과"}, + ) + def put(self, request, routingtable_id): + headers = get_nhn_headers(request) + serializer = RoutingTableAttachGatewaySerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiVpc(headers["region"], headers["token"]) + result = api.attach_gateway_to_routing_table( + routingtable_id=routingtable_id, + gateway_id=serializer.validated_data["gateway_id"], + ) + logger.info(f"[RT] 게이트웨이 연결 성공 - rt={routingtable_id}") + return Response(result) + except NHNCloudAPIError as e: + logger.error(f"[RT] 게이트웨이 연결 실패 - rt={routingtable_id}, error={e.message}") + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class RoutingTableDetachGatewayView(NHNBaseView): + """라우팅 테이블에서 인터넷 게이트웨이 분리""" + + @swagger_auto_schema( + operation_summary="라우팅 테이블에서 인터넷 게이트웨이 분리", + manual_parameters=[region_header, token_header], + responses={200: "분리 결과"}, + ) + def put(self, request, routingtable_id): + headers = get_nhn_headers(request) + try: + api = ApiVpc(headers["region"], headers["token"]) + result = api.detach_gateway_from_routing_table(routingtable_id) + logger.info(f"[RT] 게이트웨이 분리 성공 - rt={routingtable_id}") + return Response(result) + except NHNCloudAPIError as e: + logger.error(f"[RT] 게이트웨이 분리 실패 - rt={routingtable_id}, error={e.message}") + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + +class SubnetAttachRoutingTableView(NHNBaseView): + """서브넷에 라우팅 테이블 연결""" + + @swagger_auto_schema( + operation_summary="서브넷에 라우팅 테이블 연결", + manual_parameters=[region_header, token_header], + request_body=SubnetAttachRoutingTableSerializer, + responses={200: "연결 결과"}, + ) + def put(self, request, subnet_id): + headers = get_nhn_headers(request) + serializer = SubnetAttachRoutingTableSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + try: + api = ApiVpc(headers["region"], headers["token"]) + result = api.attach_routing_table_to_subnet( + subnet_id=subnet_id, + routingtable_id=serializer.validated_data["routingtable_id"], + ) + logger.info(f"[Subnet] 라우팅 테이블 연결 성공 - subnet={subnet_id}") + return Response(result) + except NHNCloudAPIError as e: + logger.error(f"[Subnet] 라우팅 테이블 연결 실패 - subnet={subnet_id}, error={e.message}") + return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) + + # ==================== Security Group API ==================== diff --git a/version b/version index 4f4d7f3..4c36eb1 100644 --- a/version +++ b/version @@ -1 +1 @@ -v0.0.18 \ No newline at end of file +v0.0.19 \ No newline at end of file