"""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)