302 lines
17 KiB
Python
302 lines
17 KiB
Python
# encoding: UTF-8
|
|
import json
|
|
import time
|
|
from datetime import datetime
|
|
|
|
import requests
|
|
from requests.auth import HTTPBasicAuth
|
|
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
|
|
from const import JENKINS_BASE_URL, JENKINS_USER, JENKINS_TOKEN
|
|
from logger import logger
|
|
from app.api.model.automationModel import AutoExecution, AutoExecutionCase
|
|
|
|
|
|
class JenkinsPollService(object):
|
|
STATUS_QUEUED = 2
|
|
STATUS_RUNNING = 3
|
|
STATUS_SUCCESS = 4
|
|
STATUS_FAILED = 5
|
|
|
|
@staticmethod
|
|
def poll_jenkins_build_status(session, execution_id):
|
|
execution = session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).first()
|
|
if not execution:
|
|
logger.error(f'执行记录不存在: execution_id={execution_id}')
|
|
return False, '执行记录不存在'
|
|
|
|
if execution.status not in [JenkinsPollService.STATUS_QUEUED, JenkinsPollService.STATUS_RUNNING]:
|
|
logger.info(f'执行状态不需要轮询: execution_id={execution_id}, status={execution.status}')
|
|
return True, ''
|
|
|
|
base_url = JENKINS_BASE_URL.rstrip('/')
|
|
job_name = execution.jenkins_job_name
|
|
build_number = execution.jenkins_build_number
|
|
|
|
if not job_name:
|
|
if execution.jenkins_build_url:
|
|
import re
|
|
match = re.search(r'/job/([^/]+(?:/job/[^/]+)*)/\d+/', execution.jenkins_build_url)
|
|
if match:
|
|
job_name = match.group(1).replace('/job/', '/')
|
|
logger.info(f'从构建URL中提取job_name: {job_name}')
|
|
else:
|
|
logger.error(f'无法从构建URL中提取job_name: {execution.jenkins_build_url}')
|
|
return False, 'Jenkins job 名称为空'
|
|
else:
|
|
logger.error(f'Jenkins job 名称为空: execution_id={execution_id}')
|
|
return False, 'Jenkins job 名称为空'
|
|
|
|
auth = HTTPBasicAuth(JENKINS_USER, JENKINS_TOKEN) if JENKINS_USER and JENKINS_TOKEN else None
|
|
|
|
try:
|
|
if not build_number:
|
|
if execution.jenkins_build_url:
|
|
import re
|
|
match = re.search(r'/job/([^/]+(?:/job/[^/]+)*)/(\d+)/', execution.jenkins_build_url)
|
|
if match:
|
|
job_name = match.group(1).replace('/job/', '/')
|
|
build_number = match.group(2)
|
|
logger.info(f'从构建URL中提取: job_name={job_name}, build_number={build_number}')
|
|
else:
|
|
logger.error(f'无法从构建URL中提取信息: {execution.jenkins_build_url}')
|
|
|
|
queue_id = execution.jenkins_queue_id
|
|
if queue_id:
|
|
queue_url = f'{base_url}/queue/item/{queue_id}/api/json'
|
|
response = requests.get(queue_url, auth=auth, timeout=30)
|
|
if response.status_code == 200:
|
|
queue_data = response.json()
|
|
logger.debug(f'队列数据: execution_id={execution_id}, queue_data={json.dumps(queue_data, ensure_ascii=False)[:500]}')
|
|
|
|
if queue_data.get('executable'):
|
|
build_number = queue_data['executable'].get('number')
|
|
logger.info(f'队列任务已开始执行: execution_id={execution_id}, build_number={build_number}')
|
|
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update({
|
|
'jenkins_build_number': build_number,
|
|
'status': JenkinsPollService.STATUS_RUNNING,
|
|
'start_time': datetime.now()
|
|
})
|
|
session.done(close=False)
|
|
elif queue_data.get('cancelled') or queue_data.get('blocked'):
|
|
logger.error(f'队列任务已取消或阻塞: execution_id={execution_id}, cancelled={queue_data.get("cancelled")}, blocked={queue_data.get("blocked")}')
|
|
end_time = datetime.now()
|
|
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update({
|
|
'status': JenkinsPollService.STATUS_FAILED,
|
|
'end_time': end_time,
|
|
'trigger_message': queue_data.get('why', '队列任务已取消或阻塞')
|
|
})
|
|
session.done(close=False)
|
|
JenkinsPollService.refresh_execution_summary(session, execution_id, force_finish=True)
|
|
if execution.plan_id:
|
|
JenkinsPollService.refresh_plan_status(session, execution.plan_id)
|
|
return True, '队列任务已取消或阻塞'
|
|
elif queue_data.get('why'):
|
|
logger.info(f'队列任务等待中: execution_id={execution_id}, reason={queue_data.get("why")}')
|
|
return True, f'队列等待中: {queue_data.get("why")}'
|
|
else:
|
|
logger.info(f'队列任务等待中: execution_id={execution_id}, queue_id={queue_id}')
|
|
return True, '队列等待中'
|
|
else:
|
|
logger.warning(f'获取队列状态失败: execution_id={execution_id}, status_code={response.status_code}')
|
|
|
|
if response.status_code == 404:
|
|
logger.info(f'队列项已不存在,尝试查询执行状态: execution_id={execution_id}')
|
|
builds_url = f'{base_url}/job/{job_name}/builds/api/json?limit=10'
|
|
try:
|
|
builds_response = requests.get(builds_url, auth=auth, timeout=30)
|
|
logger.info(f'构建历史查询: url={builds_url}, status_code={builds_response.status_code}')
|
|
|
|
if builds_response.status_code == 200:
|
|
builds_data = builds_response.json()
|
|
logger.info(f'构建历史数据: count={len(builds_data) if builds_data else 0}')
|
|
|
|
if builds_data:
|
|
latest_build = builds_data[0]
|
|
build_number = latest_build.get('number')
|
|
is_building = latest_build.get('building', False)
|
|
result = latest_build.get('result')
|
|
timestamp = latest_build.get('timestamp', 0)
|
|
|
|
logger.info(f'最新构建信息: build_number={build_number}, is_building={is_building}, result={result}')
|
|
|
|
if is_building:
|
|
status = JenkinsPollService.STATUS_RUNNING
|
|
elif result == 'SUCCESS':
|
|
status = JenkinsPollService.STATUS_SUCCESS
|
|
else:
|
|
status = JenkinsPollService.STATUS_FAILED
|
|
|
|
logger.info(f'更新执行状态: execution_id={execution_id}, build_number={build_number}, status={status}')
|
|
update_info = {
|
|
'jenkins_build_number': build_number,
|
|
'status': status,
|
|
'start_time': datetime.fromtimestamp(timestamp/1000) if timestamp else datetime.now()
|
|
}
|
|
|
|
if not is_building and result:
|
|
update_info['end_time'] = datetime.now()
|
|
update_info['jenkins_build_url'] = f'{base_url}/job/{job_name}/{build_number}/'
|
|
update_info['console_url'] = f'{base_url}/job/{job_name}/{build_number}/console'
|
|
update_info['report_url'] = f'{base_url}/job/{job_name}/{build_number}/allure/'
|
|
|
|
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update(update_info)
|
|
session.done(close=False)
|
|
|
|
if not is_building:
|
|
JenkinsPollService.refresh_execution_summary(session, execution_id, force_finish=True)
|
|
if execution.plan_id:
|
|
JenkinsPollService.refresh_plan_status(session, execution.plan_id)
|
|
|
|
return True, f'队列不存在,使用最新构建: {build_number}'
|
|
else:
|
|
logger.error(f'获取构建历史失败: status_code={builds_response.status_code}, body={builds_response.text[:200]}')
|
|
except Exception as err:
|
|
logger.error(f'查询构建历史异常: {err}')
|
|
|
|
return True, '获取队列状态失败'
|
|
else:
|
|
logger.warning(f'缺少 queue_id 和 build_number: execution_id={execution_id}')
|
|
return False, '无法轮询,缺少构建信息'
|
|
|
|
if build_number:
|
|
build_url = f'{base_url}/job/{job_name}/{build_number}/api/json'
|
|
response = requests.get(build_url, auth=auth, timeout=30)
|
|
if response.status_code == 200:
|
|
build_data = response.json()
|
|
is_running = build_data.get('building', False)
|
|
result = build_data.get('result')
|
|
|
|
console_url = f'{base_url}/job/{job_name}/{build_number}/console'
|
|
build_url_full = f'{base_url}/job/{job_name}/{build_number}/'
|
|
|
|
if is_running:
|
|
logger.info(f'构建执行中: execution_id={execution_id}, build_number={build_number}')
|
|
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update({
|
|
'status': JenkinsPollService.STATUS_RUNNING,
|
|
'jenkins_build_url': build_url_full,
|
|
'console_url': console_url
|
|
})
|
|
session.done(close=False)
|
|
return True, '执行中'
|
|
else:
|
|
logger.info(f'构建完成: execution_id={execution_id}, result={result}')
|
|
end_time = datetime.now()
|
|
report_url = f'{base_url}/job/{job_name}/{build_number}/allure/'
|
|
update_info = {
|
|
'status': JenkinsPollService.STATUS_SUCCESS if result == 'SUCCESS' else JenkinsPollService.STATUS_FAILED,
|
|
'jenkins_build_url': build_url_full,
|
|
'console_url': console_url,
|
|
'report_url': report_url,
|
|
'end_time': end_time
|
|
}
|
|
if execution.start_time:
|
|
update_info['duration_seconds'] = int((end_time - execution.start_time).total_seconds())
|
|
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update(update_info)
|
|
session.done(close=False)
|
|
|
|
JenkinsPollService.refresh_execution_summary(session, execution_id, force_finish=True)
|
|
if execution.plan_id:
|
|
JenkinsPollService.refresh_plan_status(session, execution.plan_id)
|
|
|
|
return True, f'构建完成: {result}'
|
|
|
|
except Exception as err:
|
|
logger.error(f'轮询 Jenkins 状态失败: execution_id={execution_id}, error={err}')
|
|
return False, str(err)
|
|
|
|
return True, ''
|
|
|
|
@staticmethod
|
|
def refresh_execution_summary(session, execution_id, force_finish=False):
|
|
from sqlalchemy import func
|
|
|
|
rows = session.query(AutoExecutionCase.status, func.count(AutoExecutionCase.id)).filter(
|
|
AutoExecutionCase.execution_id == int(execution_id)
|
|
).group_by(AutoExecutionCase.status).all()
|
|
|
|
summary = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0}
|
|
for status, count in rows:
|
|
summary[int(status)] = int(count)
|
|
total = sum(summary.values())
|
|
|
|
execution = session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).first()
|
|
if execution:
|
|
update_info = {
|
|
'total_count': total,
|
|
'pending_count': summary.get(0, 0),
|
|
'running_count': summary.get(1, 0),
|
|
'passed_count': summary.get(2, 0),
|
|
'failed_count': summary.get(3, 0),
|
|
'blocked_count': summary.get(4, 0),
|
|
'skipped_count': summary.get(5, 0),
|
|
'not_found_count': summary.get(6, 0)
|
|
}
|
|
|
|
running_count = summary.get(1, 0)
|
|
finished_count = summary.get(2, 0) + summary.get(3, 0) + summary.get(4, 0) + summary.get(5, 0) + summary.get(6, 0) + summary.get(7, 0)
|
|
|
|
if running_count > 0:
|
|
update_info['status'] = JenkinsPollService.STATUS_RUNNING
|
|
elif total > 0 and finished_count == total:
|
|
if summary.get(3, 0) + summary.get(4, 0) + summary.get(6, 0) > 0:
|
|
update_info['status'] = JenkinsPollService.STATUS_FAILED
|
|
else:
|
|
update_info['status'] = JenkinsPollService.STATUS_SUCCESS
|
|
|
|
if force_finish or (total > 0 and finished_count == total):
|
|
end_time = execution.end_time or datetime.now()
|
|
update_info['end_time'] = end_time
|
|
if execution.start_time:
|
|
update_info['duration_seconds'] = int((end_time - execution.start_time).total_seconds())
|
|
|
|
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update(update_info)
|
|
session.done(close=False)
|
|
|
|
@staticmethod
|
|
def refresh_plan_status(session, plan_id):
|
|
from sqlalchemy import func
|
|
|
|
rows = session.query(
|
|
AutoExecution.status, func.count(AutoExecution.id)
|
|
).filter(
|
|
AutoExecution.plan_id == int(plan_id),
|
|
AutoExecution.status.in_([JenkinsPollService.STATUS_RUNNING, JenkinsPollService.STATUS_SUCCESS, JenkinsPollService.STATUS_FAILED])
|
|
).group_by(AutoExecution.status).all()
|
|
|
|
status_counts = {}
|
|
for status, count in rows:
|
|
status_counts[status] = count
|
|
|
|
running_count = status_counts.get(JenkinsPollService.STATUS_RUNNING, 0)
|
|
success_count = status_counts.get(JenkinsPollService.STATUS_SUCCESS, 0)
|
|
failed_count = status_counts.get(JenkinsPollService.STATUS_FAILED, 0)
|
|
|
|
from app.api.model.planModel import TestPlan
|
|
|
|
if running_count > 0:
|
|
session.query(TestPlan).filter(TestPlan.id == int(plan_id)).update({'status': 1})
|
|
elif success_count > 0 and failed_count == 0:
|
|
session.query(TestPlan).filter(TestPlan.id == int(plan_id)).update({'status': 4})
|
|
elif success_count + failed_count > 0:
|
|
session.query(TestPlan).filter(TestPlan.id == int(plan_id)).update({'status': 2})
|
|
|
|
session.done(close=False)
|
|
|
|
@staticmethod
|
|
def poll_all_pending_executions(session):
|
|
pending_executions = session.query(AutoExecution).filter(
|
|
AutoExecution.status.in_([JenkinsPollService.STATUS_QUEUED, JenkinsPollService.STATUS_RUNNING])
|
|
).all()
|
|
|
|
for execution in pending_executions:
|
|
try:
|
|
success, msg = JenkinsPollService.poll_jenkins_build_status(session, execution.id)
|
|
logger.info(f'轮询执行 {execution.id}: success={success}, msg={msg}')
|
|
except Exception as err:
|
|
logger.error(f'轮询执行 {execution.id} 异常: {err}')
|
|
|
|
session.done(close=False) |