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:
41
users/migrations/0012_add_kvm_server.py
Normal file
41
users/migrations/0012_add_kvm_server.py
Normal file
@ -0,0 +1,41 @@
|
||||
# Generated by Django 4.2.14 on 2026-01-15 14:49
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0011_add_dns_appkey_to_nhncloudproject'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='KVMServer',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='서버 별칭')),
|
||||
('host', models.CharField(max_length=255, verbose_name='호스트 (IP 또는 도메인)')),
|
||||
('port', models.IntegerField(default=22, verbose_name='SSH 포트')),
|
||||
('username', models.CharField(max_length=100, verbose_name='SSH 사용자명')),
|
||||
('encrypted_private_key_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='SSH 키 이름')),
|
||||
('encrypted_private_key', models.BinaryField(blank=True, null=True, verbose_name='SSH 개인키 (암호화)')),
|
||||
('libvirt_uri', models.CharField(blank=True, help_text='예: qemu+ssh://user@host/system', max_length=255, null=True, verbose_name='Libvirt URI')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='설명')),
|
||||
('tags', models.CharField(blank=True, help_text='쉼표로 구분된 태그 목록', max_length=500, null=True, verbose_name='태그')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='활성화 상태')),
|
||||
('last_used_at', models.DateTimeField(blank=True, null=True, verbose_name='마지막 사용 시각')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='kvm_servers', to=settings.AUTH_USER_MODEL, verbose_name='사용자')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'KVM 서버',
|
||||
'verbose_name_plural': 'KVM 서버',
|
||||
'ordering': ['-is_active', '-created_at'],
|
||||
'unique_together': {('user', 'host', 'port')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -198,3 +198,97 @@ class NHNCloudProject(models.Model):
|
||||
"""자격증명 저장 (비밀번호 암호화)"""
|
||||
self.encrypted_password = self.encrypt_password(password)
|
||||
self.save()
|
||||
|
||||
|
||||
# ============================================
|
||||
# KVM 서버 관리 (멀티 서버 지원)
|
||||
# ============================================
|
||||
|
||||
class KVMServer(models.Model):
|
||||
"""
|
||||
사용자별 KVM 서버 관리 (멀티 서버 지원)
|
||||
msa-django-libvirt에서 SSH 접속 정보를 요청할 때 사용
|
||||
"""
|
||||
user = models.ForeignKey(
|
||||
CustomUser,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='kvm_servers',
|
||||
verbose_name="사용자"
|
||||
)
|
||||
name = models.CharField(max_length=100, verbose_name="서버 별칭")
|
||||
host = models.CharField(max_length=255, verbose_name="호스트 (IP 또는 도메인)")
|
||||
port = models.IntegerField(default=22, verbose_name="SSH 포트")
|
||||
username = models.CharField(max_length=100, verbose_name="SSH 사용자명")
|
||||
encrypted_private_key_name = models.CharField(
|
||||
max_length=100, blank=True, null=True, verbose_name="SSH 키 이름"
|
||||
)
|
||||
encrypted_private_key = models.BinaryField(
|
||||
blank=True, null=True, verbose_name="SSH 개인키 (암호화)"
|
||||
)
|
||||
libvirt_uri = models.CharField(
|
||||
max_length=255, blank=True, null=True,
|
||||
verbose_name="Libvirt URI",
|
||||
help_text="예: qemu+ssh://user@host/system"
|
||||
)
|
||||
description = models.TextField(blank=True, null=True, verbose_name="설명")
|
||||
tags = models.CharField(
|
||||
max_length=500, blank=True, null=True,
|
||||
verbose_name="태그",
|
||||
help_text="쉼표로 구분된 태그 목록"
|
||||
)
|
||||
is_active = models.BooleanField(default=True, verbose_name="활성화 상태")
|
||||
last_used_at = models.DateTimeField(blank=True, null=True, verbose_name="마지막 사용 시각")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "KVM 서버"
|
||||
verbose_name_plural = "KVM 서버"
|
||||
unique_together = ['user', 'host', 'port'] # 동일 사용자가 같은 호스트:포트 중복 등록 방지
|
||||
ordering = ['-is_active', '-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.host}:{self.port})"
|
||||
|
||||
def get_encryption_key(self) -> bytes:
|
||||
"""SECRET_KEY 기반 Fernet 키 생성"""
|
||||
hashed = hashlib.sha256(settings.SECRET_KEY.encode()).digest()
|
||||
return base64.urlsafe_b64encode(hashed[:32])
|
||||
|
||||
def encrypt_private_key(self, private_key: str) -> bytes:
|
||||
"""SSH 개인키 암호화"""
|
||||
cipher = Fernet(self.get_encryption_key())
|
||||
return cipher.encrypt(private_key.encode())
|
||||
|
||||
def decrypt_private_key(self) -> str:
|
||||
"""SSH 개인키 복호화"""
|
||||
if self.encrypted_private_key:
|
||||
cipher = Fernet(self.get_encryption_key())
|
||||
decrypted = cipher.decrypt(self.encrypted_private_key).decode()
|
||||
self.last_used_at = timezone.now()
|
||||
self.save(update_fields=['last_used_at'])
|
||||
return decrypted
|
||||
return ""
|
||||
|
||||
def save_ssh_key(self, private_key: str, key_name: str = None):
|
||||
"""SSH 키 저장 (암호화)"""
|
||||
self.encrypted_private_key = self.encrypt_private_key(private_key)
|
||||
if key_name:
|
||||
self.encrypted_private_key_name = key_name
|
||||
self.save()
|
||||
|
||||
def get_tags_list(self) -> list:
|
||||
"""태그 문자열을 리스트로 반환"""
|
||||
if self.tags:
|
||||
return [tag.strip() for tag in self.tags.split(',') if tag.strip()]
|
||||
return []
|
||||
|
||||
def set_tags_list(self, tags_list: list):
|
||||
"""태그 리스트를 문자열로 저장"""
|
||||
self.tags = ', '.join(tags_list)
|
||||
|
||||
def get_libvirt_uri(self) -> str:
|
||||
"""Libvirt URI 반환 (없으면 기본값 생성)"""
|
||||
if self.libvirt_uri:
|
||||
return self.libvirt_uri
|
||||
return f"qemu+ssh://{self.username}@{self.host}:{self.port}/system"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from rest_framework import serializers
|
||||
from .models import CustomUser
|
||||
from .models import CustomUser, KVMServer
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
@ -86,3 +86,60 @@ class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
|
||||
data["email"] = user.email
|
||||
data["grade"] = user.grade
|
||||
return data
|
||||
|
||||
|
||||
class KVMServerSerializer(serializers.ModelSerializer):
|
||||
"""KVM 서버 시리얼라이저"""
|
||||
tags_list = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
required=False,
|
||||
write_only=True
|
||||
)
|
||||
private_key = serializers.CharField(write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = KVMServer
|
||||
fields = [
|
||||
'id', 'name', 'host', 'port', 'username',
|
||||
'encrypted_private_key_name', 'libvirt_uri',
|
||||
'description', 'tags', 'tags_list', 'is_active',
|
||||
'last_used_at', 'created_at', 'updated_at', 'private_key'
|
||||
]
|
||||
read_only_fields = ['id', 'last_used_at', 'created_at', 'updated_at']
|
||||
extra_kwargs = {
|
||||
'tags': {'required': False},
|
||||
}
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
data['has_ssh_key'] = bool(instance.encrypted_private_key)
|
||||
data['tags_list'] = instance.get_tags_list()
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
tags_list = validated_data.pop('tags_list', None)
|
||||
private_key = validated_data.pop('private_key', None)
|
||||
|
||||
instance = super().create(validated_data)
|
||||
|
||||
if tags_list is not None:
|
||||
instance.set_tags_list(tags_list)
|
||||
if private_key:
|
||||
instance.save_ssh_key(private_key, validated_data.get('encrypted_private_key_name'))
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
tags_list = validated_data.pop('tags_list', None)
|
||||
private_key = validated_data.pop('private_key', None)
|
||||
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
if tags_list is not None:
|
||||
instance.set_tags_list(tags_list)
|
||||
instance.save()
|
||||
if private_key:
|
||||
instance.save_ssh_key(private_key, validated_data.get('encrypted_private_key_name'))
|
||||
|
||||
return instance
|
||||
|
||||
@ -7,6 +7,9 @@ from .views import (
|
||||
# NHN Cloud 멀티 프로젝트 지원
|
||||
NHNCloudProjectListView, NHNCloudProjectDetailView,
|
||||
NHNCloudProjectActivateView, NHNCloudProjectPasswordView,
|
||||
# KVM 서버 관리
|
||||
KVMServerListView, KVMServerDetailView,
|
||||
KVMServerActivateView, KVMServerSSHKeyView, KVMServerSSHKeyUploadView,
|
||||
)
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView
|
||||
from .views_jwks import jwks_view # django-jwks
|
||||
@ -33,4 +36,10 @@ urlpatterns = [
|
||||
path('nhn-cloud/projects/<int:project_id>/', NHNCloudProjectDetailView.as_view(), name='nhn_cloud_project_detail'),
|
||||
path('nhn-cloud/projects/<int:project_id>/activate/', NHNCloudProjectActivateView.as_view(), name='nhn_cloud_project_activate'),
|
||||
path('nhn-cloud/projects/<int:project_id>/password/', NHNCloudProjectPasswordView.as_view(), name='nhn_cloud_project_password'),
|
||||
# KVM 서버 관리 API (멀티 서버 지원)
|
||||
path('kvm-servers/', KVMServerListView.as_view(), name='kvm_server_list'),
|
||||
path('kvm-servers/<int:server_id>/', KVMServerDetailView.as_view(), name='kvm_server_detail'),
|
||||
path('kvm-servers/<int:server_id>/activate/', KVMServerActivateView.as_view(), name='kvm_server_activate'),
|
||||
path('kvm-servers/<int:server_id>/ssh-key/', KVMServerSSHKeyView.as_view(), name='kvm_server_ssh_key'),
|
||||
path('kvm-servers/<int:server_id>/ssh-key/upload/', KVMServerSSHKeyUploadView.as_view(), name='kvm_server_ssh_key_upload'),
|
||||
]
|
||||
|
||||
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