v0.0.25 | KVM 서버 관리 API 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 2m7s
All checks were successful
Build And Test / build-and-push (push) Successful in 2m7s
- KVMServer 모델 추가 (멀티 서버 지원) - 서버별 SSH 키 암호화 저장 - msa-django-libvirt 연동용 SSH 정보 조회 API Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
293
users/views.py
293
users/views.py
@ -7,8 +7,8 @@ from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, BasePermission
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
from rest_framework import generics
|
||||
from .serializers import RegisterSerializer, CustomTokenObtainPairSerializer, UserListSerializer
|
||||
from .models import CustomUser, NHNCloudProject
|
||||
from .serializers import RegisterSerializer, CustomTokenObtainPairSerializer, UserListSerializer, KVMServerSerializer
|
||||
from .models import CustomUser, NHNCloudProject, KVMServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
tracer = trace.get_tracer(__name__) # ✅ 트레이서 생성
|
||||
@ -740,3 +740,292 @@ class NHNCloudProjectPasswordView(APIView):
|
||||
f"[NHN PROJECT PASSWORD] user={email} | status=fail | reason=exception | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response({"error": f"복호화 실패: {str(e)}"}, status=500)
|
||||
|
||||
|
||||
# ============================================
|
||||
# KVM 서버 관리 API (멀티 서버 지원)
|
||||
# ============================================
|
||||
|
||||
class KVMServerListView(APIView):
|
||||
"""KVM 서버 목록 조회 및 추가"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
"""서버 목록 조회"""
|
||||
with tracer.start_as_current_span("KVMServerListView GET") as span:
|
||||
email, ip, ua = get_request_info(request)
|
||||
servers = KVMServer.objects.filter(user=request.user)
|
||||
|
||||
logger.debug(f"[KVM SERVER LIST] user={email} | count={servers.count()} | IP={ip} | UA={ua}")
|
||||
span.add_event("KVM servers listed", attributes={"email": email, "count": servers.count()})
|
||||
|
||||
serializer = KVMServerSerializer(servers, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
"""서버 추가"""
|
||||
with tracer.start_as_current_span("KVMServerListView POST") as span:
|
||||
email, ip, ua = get_request_info(request)
|
||||
|
||||
# 중복 체크
|
||||
host = request.data.get("host")
|
||||
port = request.data.get("port", 22)
|
||||
if KVMServer.objects.filter(user=request.user, host=host, port=port).exists():
|
||||
logger.warning(
|
||||
f"[KVM SERVER CREATE] user={email} | status=fail | reason=duplicate | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response(
|
||||
{"error": "이미 등록된 서버입니다 (동일한 호스트:포트)."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = KVMServerSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
server = serializer.save(user=request.user)
|
||||
logger.info(
|
||||
f"[KVM SERVER CREATE] user={email} | server={server.name} | host={server.host} | IP={ip} | UA={ua}"
|
||||
)
|
||||
span.add_event("KVM server created", attributes={"email": email, "server": server.name})
|
||||
return Response(KVMServerSerializer(server).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
logger.warning(
|
||||
f"[KVM SERVER CREATE] user={email} | status=fail | reason=validation | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class KVMServerDetailView(APIView):
|
||||
"""KVM 서버 상세 (조회/수정/삭제)"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_server(self, request, server_id):
|
||||
"""서버 조회 (본인 것만)"""
|
||||
try:
|
||||
return KVMServer.objects.get(id=server_id, user=request.user)
|
||||
except KVMServer.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get(self, request, server_id):
|
||||
"""서버 상세 조회"""
|
||||
with tracer.start_as_current_span("KVMServerDetailView GET") as span:
|
||||
email, ip, ua = get_request_info(request)
|
||||
|
||||
server = self.get_server(request, server_id)
|
||||
if not server:
|
||||
return Response({"error": "서버를 찾을 수 없습니다."}, status=404)
|
||||
|
||||
span.add_event("KVM server retrieved", attributes={"email": email, "server": server.name})
|
||||
return Response(KVMServerSerializer(server).data)
|
||||
|
||||
def put(self, request, server_id):
|
||||
"""서버 수정"""
|
||||
with tracer.start_as_current_span("KVMServerDetailView PUT") as span:
|
||||
email, ip, ua = get_request_info(request)
|
||||
|
||||
server = self.get_server(request, server_id)
|
||||
if not server:
|
||||
logger.warning(
|
||||
f"[KVM SERVER UPDATE] user={email} | status=fail | reason=not_found | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response({"error": "서버를 찾을 수 없습니다."}, status=404)
|
||||
|
||||
# 호스트:포트 중복 체크 (자기 자신 제외)
|
||||
host = request.data.get("host", server.host)
|
||||
port = request.data.get("port", server.port)
|
||||
if KVMServer.objects.filter(
|
||||
user=request.user, host=host, port=port
|
||||
).exclude(id=server_id).exists():
|
||||
return Response(
|
||||
{"error": "이미 등록된 서버입니다 (동일한 호스트:포트)."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = KVMServerSerializer(server, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
server = serializer.save()
|
||||
logger.info(
|
||||
f"[KVM SERVER UPDATE] user={email} | server={server.name} | IP={ip} | UA={ua}"
|
||||
)
|
||||
span.add_event("KVM server updated", attributes={"email": email, "server": server.name})
|
||||
return Response(KVMServerSerializer(server).data)
|
||||
|
||||
logger.warning(
|
||||
f"[KVM SERVER UPDATE] user={email} | status=fail | reason=validation | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, server_id):
|
||||
"""서버 삭제"""
|
||||
with tracer.start_as_current_span("KVMServerDetailView DELETE") as span:
|
||||
email, ip, ua = get_request_info(request)
|
||||
|
||||
server = self.get_server(request, server_id)
|
||||
if not server:
|
||||
logger.warning(
|
||||
f"[KVM SERVER DELETE] user={email} | status=fail | reason=not_found | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response({"error": "서버를 찾을 수 없습니다."}, status=404)
|
||||
|
||||
server_name = server.name
|
||||
server.delete()
|
||||
|
||||
logger.info(
|
||||
f"[KVM SERVER DELETE] user={email} | server={server_name} | IP={ip} | UA={ua}"
|
||||
)
|
||||
span.add_event("KVM server deleted", attributes={"email": email, "server": server_name})
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class KVMServerActivateView(APIView):
|
||||
"""KVM 서버 활성화/비활성화"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def patch(self, request, server_id):
|
||||
"""서버 활성화 상태 토글"""
|
||||
with tracer.start_as_current_span("KVMServerActivateView PATCH") as span:
|
||||
email, ip, ua = get_request_info(request)
|
||||
|
||||
try:
|
||||
server = KVMServer.objects.get(id=server_id, user=request.user)
|
||||
except KVMServer.DoesNotExist:
|
||||
logger.warning(
|
||||
f"[KVM SERVER ACTIVATE] user={email} | status=fail | reason=not_found | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response({"error": "서버를 찾을 수 없습니다."}, status=404)
|
||||
|
||||
# 요청에서 is_active 값 가져오기 (없으면 토글)
|
||||
is_active = request.data.get('is_active')
|
||||
if is_active is None:
|
||||
server.is_active = not server.is_active
|
||||
else:
|
||||
server.is_active = is_active
|
||||
|
||||
server.save(update_fields=['is_active'])
|
||||
|
||||
action = "activated" if server.is_active else "deactivated"
|
||||
logger.info(
|
||||
f"[KVM SERVER ACTIVATE] user={email} | server={server.name} | action={action} | IP={ip} | UA={ua}"
|
||||
)
|
||||
span.add_event(f"KVM server {action}", attributes={"email": email, "server": server.name})
|
||||
|
||||
return Response({
|
||||
"message": f"서버가 {'활성화' if server.is_active else '비활성화'}되었습니다.",
|
||||
"id": server.id,
|
||||
"name": server.name,
|
||||
"is_active": server.is_active,
|
||||
})
|
||||
|
||||
|
||||
class KVMServerSSHKeyView(APIView):
|
||||
"""KVM 서버 SSH 키 조회 (복호화) - msa-django-libvirt에서 사용"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, server_id):
|
||||
"""복호화된 SSH 키와 접속 정보 조회"""
|
||||
with tracer.start_as_current_span("KVMServerSSHKeyView GET") as span:
|
||||
email, ip, ua = get_request_info(request)
|
||||
|
||||
try:
|
||||
server = KVMServer.objects.get(id=server_id, user=request.user)
|
||||
except KVMServer.DoesNotExist:
|
||||
logger.warning(
|
||||
f"[KVM SERVER SSH KEY] user={email} | status=fail | reason=not_found | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response({"error": "서버를 찾을 수 없습니다."}, status=404)
|
||||
|
||||
if not server.encrypted_private_key:
|
||||
logger.warning(
|
||||
f"[KVM SERVER SSH KEY] user={email} | server={server.name} | status=fail | reason=no_key | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response(
|
||||
{"error": "SSH 키가 등록되어 있지 않습니다."},
|
||||
status=404
|
||||
)
|
||||
|
||||
try:
|
||||
decrypted_key = server.decrypt_private_key()
|
||||
logger.info(
|
||||
f"[KVM SERVER SSH KEY] user={email} | server={server.name} | IP={ip} | UA={ua}"
|
||||
)
|
||||
span.add_event("KVM server SSH key retrieved", attributes={"email": email, "server": server.name})
|
||||
|
||||
return Response({
|
||||
"id": server.id,
|
||||
"name": server.name,
|
||||
"host": server.host,
|
||||
"port": server.port,
|
||||
"username": server.username,
|
||||
"private_key": decrypted_key,
|
||||
"libvirt_uri": server.get_libvirt_uri(),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"[KVM SERVER SSH KEY] user={email} | server={server.name} | status=fail | reason=exception | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response({"error": f"복호화 실패: {str(e)}"}, status=500)
|
||||
|
||||
|
||||
class KVMServerSSHKeyUploadView(APIView):
|
||||
"""KVM 서버 SSH 키 업로드/삭제"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_server(self, request, server_id):
|
||||
try:
|
||||
return KVMServer.objects.get(id=server_id, user=request.user)
|
||||
except KVMServer.DoesNotExist:
|
||||
return None
|
||||
|
||||
def post(self, request, server_id):
|
||||
"""SSH 키 업로드"""
|
||||
with tracer.start_as_current_span("KVMServerSSHKeyUploadView POST") as span:
|
||||
email, ip, ua = get_request_info(request)
|
||||
|
||||
server = self.get_server(request, server_id)
|
||||
if not server:
|
||||
return Response({"error": "서버를 찾을 수 없습니다."}, status=404)
|
||||
|
||||
private_key = request.data.get("private_key")
|
||||
key_name = request.data.get("key_name")
|
||||
|
||||
if not private_key:
|
||||
logger.warning(
|
||||
f"[KVM SERVER SSH UPLOAD] user={email} | server={server.name} | status=fail | reason=missing_key | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response(
|
||||
{"error": "private_key는 필수입니다."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
try:
|
||||
server.save_ssh_key(private_key, key_name)
|
||||
logger.info(
|
||||
f"[KVM SERVER SSH UPLOAD] user={email} | server={server.name} | IP={ip} | UA={ua}"
|
||||
)
|
||||
span.add_event("KVM server SSH key uploaded", attributes={"email": email, "server": server.name})
|
||||
return Response({"message": "SSH 키가 저장되었습니다."}, status=201)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"[KVM SERVER SSH UPLOAD] user={email} | server={server.name} | status=fail | reason=exception | IP={ip} | UA={ua}"
|
||||
)
|
||||
return Response({"error": f"저장 실패: {str(e)}"}, status=500)
|
||||
|
||||
def delete(self, request, server_id):
|
||||
"""SSH 키 삭제"""
|
||||
with tracer.start_as_current_span("KVMServerSSHKeyUploadView DELETE") as span:
|
||||
email, ip, ua = get_request_info(request)
|
||||
|
||||
server = self.get_server(request, server_id)
|
||||
if not server:
|
||||
return Response({"error": "서버를 찾을 수 없습니다."}, status=404)
|
||||
|
||||
server.encrypted_private_key = None
|
||||
server.encrypted_private_key_name = None
|
||||
server.last_used_at = None
|
||||
server.save(update_fields=['encrypted_private_key', 'encrypted_private_key_name', 'last_used_at'])
|
||||
|
||||
logger.info(
|
||||
f"[KVM SERVER SSH DELETE] user={email} | server={server.name} | IP={ip} | UA={ua}"
|
||||
)
|
||||
span.add_event("KVM server SSH key deleted", attributes={"email": email, "server": server.name})
|
||||
return Response({"message": "SSH 키가 삭제되었습니다."})
|
||||
|
||||
Reference in New Issue
Block a user