Files
LiteOps/backend/apps/views/build_history.py
2025-06-12 16:48:37 +08:00

549 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import logging
from datetime import datetime, timedelta
from django.http import JsonResponse, HttpResponse
from django.views import View
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.db.models import Q
from ..models import BuildHistory, BuildTask, Project, Environment
from ..utils.auth import jwt_auth_required
from ..utils.permissions import get_user_permissions
logger = logging.getLogger('apps')
@method_decorator(csrf_exempt, name='dispatch')
class BuildHistoryView(View):
def _get_stage_status_from_log(self, log: str, stage_name: str, overall_status: str = None) -> str:
"""从日志中获取指定阶段的状态"""
if not log:
return 'failed'
# 处理特殊阶段Git Clone
if stage_name == 'Git Clone':
# Git Clone 阶段使用 [Git Clone] 格式
if '[Git Clone]' not in log:
return 'failed'
# 检查是否有完成标记
if '[Git Clone] 代码克隆完成' in log:
return 'success'
elif overall_status == 'terminated':
return 'terminated'
else:
return 'failed'
# 处理普通构建阶段
stage_start_pattern = f'[Build Stages] 开始执行阶段: {stage_name}'
stage_complete_pattern = f'[Build Stages] 阶段 {stage_name} 执行完成'
# 如果整体构建状态是terminated检查阶段是否有开始执行
if overall_status == 'terminated':
if stage_start_pattern in log:
# 检查阶段是否完成
if stage_complete_pattern in log:
return 'success'
else:
return 'terminated'
else:
# 没有该阶段的日志,说明还没开始执行就被终止了
return 'terminated'
if stage_start_pattern not in log:
return 'failed' # 阶段未开始执行
# 检查阶段是否完成
if stage_complete_pattern in log:
return 'success' # 阶段成功完成
else:
return 'failed' # 阶段开始了但没有完成
@method_decorator(jwt_auth_required)
def get(self, request):
"""获取构建历史列表"""
try:
# 获取当前用户的权限信息
user_permissions = get_user_permissions(request.user_id)
data_permissions = user_permissions.get('data', {})
# 检查用户是否有构建历史查看权限
function_permissions = user_permissions.get('function', {})
build_history_permissions = function_permissions.get('build_history', [])
if 'view' not in build_history_permissions:
logger.warning(f'用户[{request.user_id}]没有构建历史查看权限')
return JsonResponse({
'code': 403,
'message': '没有权限查看构建历史'
}, status=403)
# 获取查询参数
project_id = request.GET.get('project_id')
environment_id = request.GET.get('environment_id')
task_id = request.GET.get('task_id') # 添加task_id参数
task_name = request.GET.get('task_name')
page = int(request.GET.get('page', 1))
page_size = int(request.GET.get('page_size', 10))
# 构建查询条件
query = Q()
# 应用项目权限过滤
project_scope = data_permissions.get('project_scope', 'all')
if project_scope == 'custom':
permitted_project_ids = data_permissions.get('project_ids', [])
if not permitted_project_ids:
logger.info(f'用户[{request.user_id}]没有权限查看任何项目的构建历史')
return JsonResponse({
'code': 200,
'message': '获取构建历史列表成功',
'data': [],
'total': 0,
'page': page,
'page_size': page_size
})
# 用户只能查看有权限的项目
if project_id and project_id != 'all':
# 如果指定了项目,检查是否有该项目的权限
if project_id not in permitted_project_ids:
logger.warning(f'用户[{request.user_id}]尝试查看无权限的项目[{project_id}]的构建历史')
return JsonResponse({
'code': 403,
'message': '没有权限查看该项目的构建历史'
}, status=403)
query &= Q(task__project__project_id=project_id)
else:
query &= Q(task__project__project_id__in=permitted_project_ids)
else:
# 如果有全部项目权限并且指定了项目ID
if project_id and project_id != 'all':
query &= Q(task__project__project_id=project_id)
# 应用环境权限过滤
environment_scope = data_permissions.get('environment_scope', 'all')
if environment_scope == 'custom':
permitted_environment_types = data_permissions.get('environment_types', [])
if not permitted_environment_types:
# 如果设置了自定义环境权限但列表为空,意味着没有权限查看任何环境
logger.info(f'用户[{request.user_id}]没有权限查看任何环境的构建历史')
return JsonResponse({
'code': 200,
'message': '获取构建历史列表成功',
'data': [],
'total': 0,
'page': page,
'page_size': page_size
})
if environment_id and environment_id != 'all':
# 如果指定了环境,需要检查是否在有权限的环境类型中
try:
env = Environment.objects.get(environment_id=environment_id)
if env.type not in permitted_environment_types:
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境[{environment_id}]的构建历史')
return JsonResponse({
'code': 403,
'message': '没有权限查看该环境的构建历史'
}, status=403)
query &= Q(task__environment__environment_id=environment_id)
except Environment.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '环境不存在'
}, status=404)
else:
# 如果没有指定环境或选择了全部,则限制为有权限的环境类型
query &= Q(task__environment__type__in=permitted_environment_types)
else:
# 如果有全部环境权限并且指定了环境ID
if environment_id and environment_id != 'all':
query &= Q(task__environment__environment_id=environment_id)
# 添加其他查询条件
if task_id:
query &= Q(task__task_id=task_id)
if task_name:
query &= Q(task__name__icontains=task_name)
# 查询构建历史
histories = BuildHistory.objects.select_related(
'task',
'task__project',
'task__environment',
'operator'
).filter(query).order_by('-create_time')
# 计算总数
total = histories.count()
# 分页
start = (page - 1) * page_size
end = start + page_size
histories = histories[start:end]
# 构建返回数据
history_list = []
for history in histories:
# 计算构建耗时
duration = '未完成'
if history.build_time and 'total_duration' in history.build_time:
duration_seconds = int(history.build_time['total_duration'])
if duration_seconds < 60:
duration = f"{duration_seconds}"
else:
minutes = duration_seconds // 60
seconds = duration_seconds % 60
duration = f"{minutes}{seconds}"
# 处理构建阶段信息
stages = []
# 添加 Git Clone 阶段
git_clone_stage = next(
(t for t in history.build_time.get('stages_time', []) if t['name'] == 'Git Clone'),
None
) if history.build_time else None
if git_clone_stage:
git_clone_status = self._get_stage_status_from_log(history.build_log, 'Git Clone', history.status)
stages.append({
'name': 'Git Clone',
'status': git_clone_status,
'startTime': git_clone_stage['start_time'],
'duration': git_clone_stage['duration'] + ''
})
# 添加其他阶段
for stage in history.stages:
stage_time = next(
(t for t in history.build_time.get('stages_time', []) if t['name'] == stage['name']),
None
) if history.build_time else None
stage_status = self._get_stage_status_from_log(history.build_log, stage['name'], history.status)
stages.append({
'name': stage['name'],
'status': stage_status,
'startTime': stage_time['start_time'] if stage_time else None,
'duration': stage_time['duration'] + '' if stage_time else '未知'
})
# 检查是否有回滚权限
can_rollback = history.status == 'success'
history_list.append({
'id': history.history_id,
'build_number': history.build_number,
'status': history.status,
'branch': history.branch,
'commit': history.commit_id[:8] if history.commit_id else None,
'version': history.version,
'environment': history.task.environment.name if history.task.environment else None,
'startTime': history.build_time.get('start_time') if history.build_time else history.create_time.strftime('%Y-%m-%d %H:%M:%S'),
'duration': duration,
'operator': history.operator.name if history.operator else None,
'requirement': history.requirement,
'stages': stages,
'canRollback': can_rollback,
'task': {
'id': history.task.task_id,
'name': history.task.name,
'description': history.task.description
}
})
return JsonResponse({
'code': 200,
'message': '获取构建历史列表成功',
'data': history_list,
'total': total,
'page': page,
'page_size': page_size
})
except Exception as e:
logger.error(f'获取构建历史列表失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})
@method_decorator(jwt_auth_required)
def post(self, request):
"""回滚到指定版本"""
try:
# 获取当前用户的权限信息
user_permissions = get_user_permissions(request.user_id)
function_permissions = user_permissions.get('function', {})
build_history_permissions = function_permissions.get('build_history', [])
# 检查是否有回滚权限
if 'rollback' not in build_history_permissions:
logger.warning(f'用户[{request.user_id}]没有构建历史回滚权限')
return JsonResponse({
'code': 403,
'message': '没有权限执行回滚操作'
}, status=403)
data = json.loads(request.body)
history_id = data.get('history_id')
if not history_id:
return JsonResponse({
'code': 400,
'message': '历史ID不能为空'
})
try:
history = BuildHistory.objects.select_related('task', 'task__project', 'task__environment').get(history_id=history_id)
except BuildHistory.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '构建历史不存在'
})
# 检查项目和环境权限
data_permissions = user_permissions.get('data', {})
# 项目权限检查
project_scope = data_permissions.get('project_scope', 'all')
if project_scope == 'custom':
permitted_project_ids = data_permissions.get('project_ids', [])
if history.task.project.project_id not in permitted_project_ids:
logger.warning(f'用户[{request.user_id}]尝试回滚无权限的项目[{history.task.project.project_id}]的构建')
return JsonResponse({
'code': 403,
'message': '没有权限回滚该项目的构建'
}, status=403)
# 环境权限检查
environment_scope = data_permissions.get('environment_scope', 'all')
if environment_scope == 'custom':
permitted_environment_types = data_permissions.get('environment_types', [])
if history.task.environment.type not in permitted_environment_types:
logger.warning(f'用户[{request.user_id}]尝试回滚无权限的环境类型[{history.task.environment.type}]的构建')
return JsonResponse({
'code': 403,
'message': '没有权限回滚该环境的构建'
}, status=403)
if history.status != 'success':
return JsonResponse({
'code': 400,
'message': '只能回滚到构建成功的版本'
})
# TODO: 实现回滚逻辑
return JsonResponse({
'code': 200,
'message': '开始回滚',
'data': {
'version': history.version
}
})
except Exception as e:
logger.error(f'回滚失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})
@method_decorator(csrf_exempt, name='dispatch')
class BuildLogView(View):
@method_decorator(jwt_auth_required)
def get(self, request, history_id):
"""获取构建日志"""
try:
# 获取当前用户的权限信息
user_permissions = get_user_permissions(request.user_id)
data_permissions = user_permissions.get('data', {})
# 检查用户是否有构建历史查看日志权限
function_permissions = user_permissions.get('function', {})
build_task_permissions = function_permissions.get('build_task', [])
build_history_permissions = function_permissions.get('build_history', [])
has_log_permission = 'view_log' in build_task_permissions or 'view_log' in build_history_permissions
if not has_log_permission:
logger.warning(f'用户[{request.user_id}]没有构建历史查看日志权限')
return JsonResponse({
'code': 403,
'message': '没有权限查看构建日志'
}, status=403)
try:
history = BuildHistory.objects.select_related('task', 'task__project', 'task__environment').get(history_id=history_id)
except BuildHistory.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '构建历史不存在'
})
# 项目权限检查
project_scope = data_permissions.get('project_scope', 'all')
if project_scope == 'custom':
permitted_project_ids = data_permissions.get('project_ids', [])
if history.task.project and history.task.project.project_id not in permitted_project_ids:
logger.warning(f'用户[{request.user_id}]尝试查看无权限的项目[{history.task.project.project_id}]的构建日志')
return JsonResponse({
'code': 403,
'message': '没有权限查看该项目的构建日志'
}, status=403)
# 环境权限检查
environment_scope = data_permissions.get('environment_scope', 'all')
if environment_scope == 'custom':
permitted_environment_types = data_permissions.get('environment_types', [])
if history.task.environment and history.task.environment.type not in permitted_environment_types:
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境类型[{history.task.environment.type}]的构建日志')
return JsonResponse({
'code': 403,
'message': '没有权限查看该环境的构建日志'
}, status=403)
# 检查是否为下载请求
is_download = request.GET.get('download') == 'true'
if is_download:
# 生成日志文件名
filename = f"build_log_{history.task.name}_{history.build_number}.txt"
# 准备日志内容
log_content = history.build_log or '暂无日志'
# 创建响应对象
response = HttpResponse(log_content, content_type='text/plain')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
return JsonResponse({
'code': 200,
'message': '获取构建日志成功',
'data': {
'log': history.build_log or '暂无日志'
}
})
except Exception as e:
logger.error(f'获取构建日志失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})
@method_decorator(csrf_exempt, name='dispatch')
class BuildStageLogView(View):
@method_decorator(jwt_auth_required)
def get(self, request, history_id, stage_name):
"""获取构建阶段日志"""
try:
# 获取当前用户的权限信息
user_permissions = get_user_permissions(request.user_id)
data_permissions = user_permissions.get('data', {})
# 检查用户是否有构建历史查看日志权限
function_permissions = user_permissions.get('function', {})
build_task_permissions = function_permissions.get('build_task', [])
build_history_permissions = function_permissions.get('build_history', [])
# 只要有任何一方的view_log权限即可
has_log_permission = 'view_log' in build_task_permissions or 'view_log' in build_history_permissions
if not has_log_permission:
logger.warning(f'用户[{request.user_id}]没有构建历史查看日志权限')
return JsonResponse({
'code': 403,
'message': '没有权限查看构建日志'
}, status=403)
try:
history = BuildHistory.objects.select_related('task', 'task__project', 'task__environment').get(history_id=history_id)
except BuildHistory.DoesNotExist:
return JsonResponse({
'code': 404,
'message': '构建历史不存在'
})
# 项目权限检查
project_scope = data_permissions.get('project_scope', 'all')
if project_scope == 'custom':
permitted_project_ids = data_permissions.get('project_ids', [])
if history.task.project and history.task.project.project_id not in permitted_project_ids:
logger.warning(f'用户[{request.user_id}]尝试查看无权限的项目[{history.task.project.project_id}]的构建阶段日志')
return JsonResponse({
'code': 403,
'message': '没有权限查看该项目的构建日志'
}, status=403)
# 环境权限检查
environment_scope = data_permissions.get('environment_scope', 'all')
if environment_scope == 'custom':
permitted_environment_types = data_permissions.get('environment_types', [])
if history.task.environment and history.task.environment.type not in permitted_environment_types:
logger.warning(f'用户[{request.user_id}]尝试查看无权限的环境类型[{history.task.environment.type}]的构建阶段日志')
return JsonResponse({
'code': 403,
'message': '没有权限查看该环境的构建日志'
}, status=403)
# 在完整日志中查找指定阶段的日志
if not history.build_log:
return JsonResponse({
'code': 200,
'message': '获取阶段日志成功',
'data': {
'log': '暂无日志'
}
})
# 适配Jenkins风格日志格式的阶段日志解析
stage_logs = []
lines = history.build_log.split('\n')
in_stage = False
# 处理特殊阶段Git Clone
if stage_name == 'Git Clone':
# Git Clone 阶段使用 [Git Clone] 格式
for line in lines:
if '[Git Clone]' in line:
stage_logs.append(line)
else:
# 普通构建阶段使用 [Build Stages] 格式
stage_start_pattern = f'[Build Stages] 开始执行阶段: {stage_name}'
stage_complete_pattern = f'[Build Stages] 阶段 {stage_name} 执行完成'
for line in lines:
if stage_start_pattern in line:
in_stage = True
stage_logs.append(line)
elif in_stage and stage_complete_pattern in line:
stage_logs.append(line)
break # 阶段结束
elif in_stage:
# 在阶段执行期间的所有日志都属于该阶段
# 排除其他阶段的开始标记
if '[Build Stages] 开始执行阶段:' not in line:
stage_logs.append(line)
else:
# 遇到其他阶段开始,当前阶段结束
break
return JsonResponse({
'code': 200,
'message': '获取阶段日志成功',
'data': {
'log': '\n'.join(stage_logs) if stage_logs else '暂无该阶段日志'
}
})
except Exception as e:
logger.error(f'获取阶段日志失败: {str(e)}', exc_info=True)
return JsonResponse({
'code': 500,
'message': f'服务器错误: {str(e)}'
})