Backend (Django 4.2 + DRF): - Admin auth with SimpleJWT - Volcengine API client with HMAC-SHA256 signing - IAM user management (create/sync/import/disable/enable) - Billing query with pagination - Feishu webhook notifications (async) - APScheduler for periodic spending checks - AES-256 encrypted credential storage - API key auth for external system integration Frontend (Vue 3 + Element Plus): - Login page - Dashboard with stats overview - IAM user list with per-user threshold config - Billing view with spending progress bars - Alert history with type filtering - Settings page for global config and Volcengine account management Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
420 lines
14 KiB
Python
420 lines
14 KiB
Python
"""AirGate 核心 API 视图"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
|
|
from django.db.models import Sum
|
|
from rest_framework import status
|
|
from rest_framework.decorators import api_view, permission_classes
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
|
|
from utils.crypto import encrypt, decrypt, make_hint
|
|
from utils.iam_service import IAMService
|
|
from utils.billing_service import BillingService
|
|
from utils.volcengine_client import VolcengineAPIError
|
|
|
|
from .models import VolcAccount, IAMUser, GlobalConfig, AlertRecord, SpendingRecord
|
|
from .serializers import (
|
|
VolcAccountSerializer, VolcAccountCreateSerializer,
|
|
IAMUserSerializer, IAMUserCreateSerializer, IAMUserImportSerializer,
|
|
IAMUserThresholdSerializer,
|
|
GlobalConfigSerializer,
|
|
AlertRecordSerializer,
|
|
DashboardSerializer,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _get_volc_account(volc_id=None):
|
|
"""获取主账号,解密密钥"""
|
|
if volc_id:
|
|
account = VolcAccount.objects.get(pk=volc_id)
|
|
else:
|
|
account = VolcAccount.objects.filter(is_active=True).first()
|
|
if not account:
|
|
return None, '', ''
|
|
ak = decrypt(account.access_key_enc)
|
|
sk = decrypt(account.secret_key_enc)
|
|
return account, ak, sk
|
|
|
|
|
|
# ==================== Dashboard ====================
|
|
|
|
@api_view(['GET'])
|
|
def dashboard_view(request):
|
|
total = IAMUser.objects.count()
|
|
active = IAMUser.objects.filter(status=IAMUser.Status.ACTIVE).count()
|
|
disabled = IAMUser.objects.filter(status=IAMUser.Status.DISABLED).count()
|
|
monitored = IAMUser.objects.filter(monitor_enabled=True).count()
|
|
total_spending = IAMUser.objects.aggregate(
|
|
total=Sum('current_month_spending'))['total'] or Decimal('0')
|
|
recent_alerts = AlertRecord.objects.all()[:10]
|
|
|
|
data = {
|
|
'total_users': total,
|
|
'active_users': active,
|
|
'disabled_users': disabled,
|
|
'monitored_users': monitored,
|
|
'total_spending': total_spending,
|
|
'recent_alerts': AlertRecordSerializer(recent_alerts, many=True).data,
|
|
}
|
|
return Response(data)
|
|
|
|
|
|
# ==================== Volcengine Account ====================
|
|
|
|
@api_view(['GET', 'POST'])
|
|
def volc_account_view(request):
|
|
if request.method == 'GET':
|
|
accounts = VolcAccount.objects.all()
|
|
return Response(VolcAccountSerializer(accounts, many=True).data)
|
|
|
|
serializer = VolcAccountCreateSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
d = serializer.validated_data
|
|
|
|
account = VolcAccount.objects.create(
|
|
name=d['name'],
|
|
access_key_enc=encrypt(d['access_key']),
|
|
secret_key_enc=encrypt(d['secret_key']),
|
|
access_key_hint=make_hint(d['access_key']),
|
|
)
|
|
return Response(VolcAccountSerializer(account).data, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
@api_view(['PUT', 'DELETE'])
|
|
def volc_account_detail_view(request, pk):
|
|
try:
|
|
account = VolcAccount.objects.get(pk=pk)
|
|
except VolcAccount.DoesNotExist:
|
|
return Response({'error': 'not_found', 'message': '主账号不存在'},
|
|
status=status.HTTP_404_NOT_FOUND)
|
|
|
|
if request.method == 'DELETE':
|
|
account.delete()
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
# PUT: update
|
|
name = request.data.get('name')
|
|
if name:
|
|
account.name = name
|
|
ak = request.data.get('access_key')
|
|
sk = request.data.get('secret_key')
|
|
if ak:
|
|
account.access_key_enc = encrypt(ak)
|
|
account.access_key_hint = make_hint(ak)
|
|
if sk:
|
|
account.secret_key_enc = encrypt(sk)
|
|
account.save()
|
|
return Response(VolcAccountSerializer(account).data)
|
|
|
|
|
|
@api_view(['POST'])
|
|
def volc_account_test_view(request, pk):
|
|
"""测试主账号密钥是否有效"""
|
|
try:
|
|
account = VolcAccount.objects.get(pk=pk)
|
|
except VolcAccount.DoesNotExist:
|
|
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
ak = decrypt(account.access_key_enc)
|
|
sk = decrypt(account.secret_key_enc)
|
|
try:
|
|
svc = IAMService(ak, sk)
|
|
svc.list_users(limit=1)
|
|
return Response({'status': 'ok', 'message': '密钥验证成功'})
|
|
except VolcengineAPIError as e:
|
|
return Response({'status': 'error', 'message': str(e)},
|
|
status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
# ==================== IAM Users ====================
|
|
|
|
@api_view(['GET'])
|
|
def iam_user_list_view(request):
|
|
users = IAMUser.objects.select_related('volc_account').all()
|
|
status_filter = request.query_params.get('status')
|
|
if status_filter:
|
|
users = users.filter(status=status_filter)
|
|
return Response(IAMUserSerializer(users, many=True).data)
|
|
|
|
|
|
@api_view(['POST'])
|
|
def iam_user_sync_view(request):
|
|
"""从火山引擎同步所有已有 IAM 用户"""
|
|
account, ak, sk = _get_volc_account()
|
|
if not account:
|
|
return Response({'error': 'no_account', 'message': '请先配置火山主账号'},
|
|
status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
svc = IAMService(ak, sk)
|
|
imported = []
|
|
offset = 0
|
|
|
|
while True:
|
|
try:
|
|
resp = svc.list_users(limit=100, offset=offset)
|
|
except VolcengineAPIError as e:
|
|
return Response({'error': 'api_error', 'message': str(e)},
|
|
status=status.HTTP_502_BAD_GATEWAY)
|
|
|
|
users = resp.get("Result", {}).get("UserMetadata", [])
|
|
if not users:
|
|
break
|
|
|
|
for u in users:
|
|
username = u.get("UserName", "")
|
|
obj, created = IAMUser.objects.update_or_create(
|
|
volc_account=account,
|
|
username=username,
|
|
defaults={
|
|
'display_name': u.get("DisplayName", ""),
|
|
'user_id': u.get("UserId", ""),
|
|
'email': u.get("Email", ""),
|
|
'phone': u.get("MobilePhone", ""),
|
|
},
|
|
)
|
|
if created:
|
|
imported.append(username)
|
|
|
|
# Sync access keys
|
|
try:
|
|
keys = svc.list_access_keys(username)
|
|
obj.access_key_ids = [k["AccessKeyId"] for k in keys]
|
|
except Exception:
|
|
pass
|
|
|
|
# Sync login status
|
|
try:
|
|
profile = svc.get_login_profile(username)
|
|
login_allowed = profile.get("Result", {}).get("LoginProfile", {}).get("LoginAllowed", True)
|
|
obj.status = IAMUser.Status.ACTIVE if login_allowed else IAMUser.Status.DISABLED
|
|
except Exception:
|
|
obj.status = IAMUser.Status.UNKNOWN
|
|
|
|
obj.save()
|
|
|
|
offset += 100
|
|
total = resp.get("Result", {}).get("Total", 0)
|
|
if offset >= total:
|
|
break
|
|
|
|
total_count = IAMUser.objects.filter(volc_account=account).count()
|
|
return Response({
|
|
'message': f'同步完成,共 {total_count} 个用户,新导入 {len(imported)} 个',
|
|
'imported': imported,
|
|
'total': total_count,
|
|
})
|
|
|
|
|
|
@api_view(['POST'])
|
|
def iam_user_import_view(request):
|
|
"""导入指定的已有 IAM 用户"""
|
|
serializer = IAMUserImportSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
username = serializer.validated_data['username']
|
|
|
|
account, ak, sk = _get_volc_account()
|
|
if not account:
|
|
return Response({'error': 'no_account', 'message': '请先配置火山主账号'},
|
|
status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
svc = IAMService(ak, sk)
|
|
try:
|
|
resp = svc.get_user(username)
|
|
except VolcengineAPIError as e:
|
|
return Response({'error': 'user_not_found', 'message': f'火山引擎未找到用户: {e}'},
|
|
status=status.HTTP_404_NOT_FOUND)
|
|
|
|
u = resp.get("Result", {}).get("User", {})
|
|
obj, created = IAMUser.objects.update_or_create(
|
|
volc_account=account,
|
|
username=username,
|
|
defaults={
|
|
'display_name': u.get("DisplayName", ""),
|
|
'user_id': u.get("UserId", ""),
|
|
'email': u.get("Email", ""),
|
|
'phone': u.get("MobilePhone", ""),
|
|
},
|
|
)
|
|
return Response({
|
|
'message': '导入成功' if created else '用户已存在,已更新信息',
|
|
'user': IAMUserSerializer(obj).data,
|
|
})
|
|
|
|
|
|
@api_view(['GET'])
|
|
def iam_user_detail_view(request, pk):
|
|
try:
|
|
user = IAMUser.objects.get(pk=pk)
|
|
except IAMUser.DoesNotExist:
|
|
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
|
|
return Response(IAMUserSerializer(user).data)
|
|
|
|
|
|
@api_view(['PUT'])
|
|
def iam_user_update_view(request, pk):
|
|
"""更新子账号的本地配置(阈值、开关等)"""
|
|
try:
|
|
user = IAMUser.objects.get(pk=pk)
|
|
except IAMUser.DoesNotExist:
|
|
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
serializer = IAMUserThresholdSerializer(data=request.data, partial=True)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
for field, value in serializer.validated_data.items():
|
|
setattr(user, field, value)
|
|
user.save()
|
|
return Response(IAMUserSerializer(user).data)
|
|
|
|
|
|
@api_view(['POST'])
|
|
def iam_user_disable_view(request, pk):
|
|
"""停用子账号"""
|
|
try:
|
|
user = IAMUser.objects.get(pk=pk)
|
|
except IAMUser.DoesNotExist:
|
|
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
account, ak, sk = _get_volc_account(user.volc_account_id)
|
|
if not ak:
|
|
return Response({'error': 'no_credentials'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
svc = IAMService(ak, sk)
|
|
try:
|
|
svc.disable_user(user.username)
|
|
user.status = IAMUser.Status.DISABLED
|
|
user.save(update_fields=['status'])
|
|
AlertRecord.objects.create(
|
|
iam_user=user,
|
|
alert_type=AlertRecord.AlertType.MANUAL,
|
|
title=f"手动停用子账号 {user.username}",
|
|
content=f"操作人: {request.user.username}",
|
|
)
|
|
return Response({'message': f'用户 {user.username} 已停用'})
|
|
except VolcengineAPIError as e:
|
|
return Response({'error': 'api_error', 'message': str(e)},
|
|
status=status.HTTP_502_BAD_GATEWAY)
|
|
|
|
|
|
@api_view(['POST'])
|
|
def iam_user_enable_view(request, pk):
|
|
"""恢复子账号"""
|
|
try:
|
|
user = IAMUser.objects.get(pk=pk)
|
|
except IAMUser.DoesNotExist:
|
|
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
account, ak, sk = _get_volc_account(user.volc_account_id)
|
|
if not ak:
|
|
return Response({'error': 'no_credentials'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
svc = IAMService(ak, sk)
|
|
try:
|
|
svc.enable_user(user.username)
|
|
user.status = IAMUser.Status.ACTIVE
|
|
user.save(update_fields=['status'])
|
|
AlertRecord.objects.create(
|
|
iam_user=user,
|
|
alert_type=AlertRecord.AlertType.MANUAL,
|
|
title=f"手动恢复子账号 {user.username}",
|
|
content=f"操作人: {request.user.username}",
|
|
)
|
|
return Response({'message': f'用户 {user.username} 已恢复'})
|
|
except VolcengineAPIError as e:
|
|
return Response({'error': 'api_error', 'message': str(e)},
|
|
status=status.HTTP_502_BAD_GATEWAY)
|
|
|
|
|
|
@api_view(['GET'])
|
|
def iam_user_policies_view(request, pk):
|
|
"""查看子账号的权限策略"""
|
|
try:
|
|
user = IAMUser.objects.get(pk=pk)
|
|
except IAMUser.DoesNotExist:
|
|
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
account, ak, sk = _get_volc_account(user.volc_account_id)
|
|
if not ak:
|
|
return Response({'error': 'no_credentials'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
svc = IAMService(ak, sk)
|
|
try:
|
|
resp = svc.list_attached_user_policies(user.username)
|
|
policies = resp.get("Result", {}).get("AttachedPolicyMetadata", [])
|
|
return Response({'policies': policies})
|
|
except VolcengineAPIError as e:
|
|
return Response({'error': 'api_error', 'message': str(e)},
|
|
status=status.HTTP_502_BAD_GATEWAY)
|
|
|
|
|
|
# ==================== Billing ====================
|
|
|
|
@api_view(['GET'])
|
|
def spending_overview_view(request):
|
|
"""消费总览"""
|
|
bill_period = request.query_params.get('period', datetime.now().strftime("%Y-%m"))
|
|
users = IAMUser.objects.all().order_by('-current_month_spending')
|
|
return Response({
|
|
'period': bill_period,
|
|
'users': IAMUserSerializer(users, many=True).data,
|
|
})
|
|
|
|
|
|
@api_view(['POST'])
|
|
def spending_refresh_view(request):
|
|
"""手动刷新消费数据"""
|
|
from utils.scheduler import check_spending
|
|
try:
|
|
check_spending()
|
|
return Response({'message': '消费数据刷新完成'})
|
|
except Exception as e:
|
|
return Response({'error': 'refresh_failed', 'message': str(e)},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
@api_view(['GET'])
|
|
def balance_view(request):
|
|
"""查询主账号余额"""
|
|
account, ak, sk = _get_volc_account()
|
|
if not ak:
|
|
return Response({'error': 'no_account', 'message': '请先配置火山主账号'},
|
|
status=status.HTTP_400_BAD_REQUEST)
|
|
try:
|
|
billing = BillingService(ak, sk)
|
|
result = billing.get_balance()
|
|
return Response(result)
|
|
except VolcengineAPIError as e:
|
|
return Response({'error': 'api_error', 'message': str(e)},
|
|
status=status.HTTP_502_BAD_GATEWAY)
|
|
|
|
|
|
# ==================== Global Config ====================
|
|
|
|
@api_view(['GET', 'PUT'])
|
|
def global_config_view(request):
|
|
config = GlobalConfig.get_solo()
|
|
if request.method == 'GET':
|
|
return Response(GlobalConfigSerializer(config).data)
|
|
|
|
serializer = GlobalConfigSerializer(config, data=request.data, partial=True)
|
|
serializer.is_valid(raise_exception=True)
|
|
serializer.save()
|
|
return Response(serializer.data)
|
|
|
|
|
|
# ==================== Alerts ====================
|
|
|
|
@api_view(['GET'])
|
|
def alert_list_view(request):
|
|
alerts = AlertRecord.objects.select_related('iam_user').all()
|
|
alert_type = request.query_params.get('type')
|
|
if alert_type:
|
|
alerts = alerts.filter(alert_type=alert_type)
|
|
limit = int(request.query_params.get('limit', 50))
|
|
return Response(AlertRecordSerializer(alerts[:limit], many=True).data)
|