提交所有代码到 qiaoxinjiu 分支
This commit is contained in:
302
app/api/service/jenkinsPollService.py
Normal file
302
app/api/service/jenkinsPollService.py
Normal file
@@ -0,0 +1,302 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user