任务需求
- 上一步:Python解析程序参数,生成一组Jenkins Job的参数。这些Job覆盖不同操作系统并占用各自的Lockable Resource。
- 本任务的功能:使用Python代码启动一组Jenkins Job,并监控这些Job的完成情况。
- 下一步:拉取这组Job的运行结果,进行统计分析和汇总。记录有问题的Job,以备重跑。
其中,Lockable Resource这个技术点已经研究过了(见以前写的3篇文档):
- Jenkins的Lockable Resource插件 - 1:基本使用
- Jenkins的Lockable Resource插件 - 2:解决最多只能创建一个等待任务的问题
- http://t.csdnimg.cn/jt22wJenkins的Lockable Resource插件 - 3:在Freestyle Job中实现根据参数动态绑定资源
本文主要描述如何使用Python代码启动一组Jenkins Job,并监控这些Job的运行状态。
流程步骤
- 首先,通过Python代码连接到Jenkins服务器。
- 选择要启动的Job,触发新的构建过程。
- 监控构建过程的完成情况:获取Job的构建状态和结果。
- 通过轮询获取构建状态,直到构建完成。
- 获取构建日志和其他相关信息(可以在步骤4的轮询中作为事件)
- 对于有问题的Job,可以记录下来以备重跑或进行进一步分析。
Jenkins的Job、Build、Queue
Job类
Jenkins中的Job
对象(文档:https://javadoc.jenkins.io/hudson/model/Job.html),是在Jenkins中定义的任务,可以由Jenkins服务器执行和管理。Job
的配置信息都保存在一个XML文件中,定义了一系列的构建步骤和配置选项。
Job的REST API是:http://<jenkins的地址>:8080/job/<job的name>/api
Build类
Jenkins中的Build
对象(文档:https://javadoc.jenkins.io/hudson/model/Build.html),是Job
的一个具体实例,代表了一次构建的过程和结果。每次触发构建,都会生成一个新的Build
实例。可以通过Build
获取构建的状态、结果和日志信息。
Queue类
Jenkins中的Queue
对象(文档:https://javadoc.jenkins-ci.org/hudson/model/Queue.html),任务队列,用于管理待执行的Job
。当触发构建时,Job
会被添加到Queue
中等待执行,生成QueueItem
对象。
Queue的REST API是:http://<jenkins的地址>:8080/queue/api
QueueItem接口
在Jenkins中,QueueItem
是一个接口,用于表示Jenkins队列中的对象。当启动一个Build
时,Jenkins会首先在队列中创建一个QueueItem
,其中包含了Job
的配置信息、启动Parameters
以及发起Job的Actions
。当QueueItem
被分配给一个executor
执行时,它会与一个Build
对象关联。可以通过执行cancel
操作来取消QueueItem
,一旦QueueItem
被取消或者关联的Build
开始执行后几分钟后它会被删除。据说StackOverflow上某个非官方的帖子说是5分钟:“once a build job has started, the queue item has 5 minutes before it is deleted”。
Queue的REST API是:http://<jenkins的地址>:8080/queue/item/<queue_item_id>/api
选择Python下的Jenkins库
常用于Jenkins管理的Python库包括:
- jenkinsapi:提供高级接口,将信息封装为对象,可用于启动、停止和管理Jenkins任务。
- python-jenkins:实际上是一个Restful API的封装器,以简单直接的方式管理Jenkins任务。返回的JSON数据未封装为对象,而是直接转换为字典供用户使用。
在封装对象时,jenkinsapi对Restful API返回的数据进行了筛选,舍弃了一些不常用的信息。然而,对于QueueItem的封装无法体现QueueItem ID和对应的Build ID之间的对应关系。python-jenkins库的返回值就是API原始的返回值,包含了所有的信息,因此选择python-jenkins库。
实现代码
JenkinsJob类的实现:
import jenkins
class JenkinsJob:
def __init__(self, job_url: str):
self.queue_id = None
self.queue_item = None
self.executable = None
self.params = None
self.status = None
# TODO:省略
# self.server = “获取根据地址缓存的jenkins.Jenkins实例”
# ...
def build_job(self, params: dict):
self.queue_id = self.server.build_job(self.job_name, params)
self.params = params
logging.info(f'{self} entered the queue')
def monitor_status(self) -> bool:
# The job has not been started: ignore to monitor
if self.queue_id is None:
self.status = 'NOT_BUILD'
return True
# The job is finished with status: ignore to monitor
if self.status:
return True
# The job is in queue: check if it is cancelled
if not self.executable:
self.queue_item = self.server.get_queue_item(self.queue_id)
if self.queue_item.get('cancelled'):
logging.info(f'{self} has been cancelled.')
self.status = 'QUEUE_CANCELLED'
return True
# The job is in queue: check if it is started
if self.executable is None and 'executable' in self.queue_item:
self.executable = self.queue_item['executable']
logging.info(f'{self} left queue, building started...')
# The job is started: check if it is finished with status
if self.executable:
self.build = self.server.get_build_info(self.job_name, self.executable['number'])
if status := self.build.get('result'):
logging.info(f'{self} set build status: {status}')
self.status = status
return True
return False
@classmethod
def wait_for_all(cls, jobs: List['JenkinsJob'], delay: int = 10):
while any(job.status is None for job in jobs):
time.sleep(delay)
for job in jobs:
job.monitor_status()
logger.debug('all jobs done')
调用代码:
def start_jenkins_job(job_url, test_params, timestamp):
jenkins_job = JenkinsJob(job_url)
params = {
'OS_NAME': test_params.os.upper(),
'MIXING': test_params.params['mixing'],
'TIMESTAMP': timestamp,
'PARAMS': str(test_params.params),
}
jenkins_job.build_job(params)
return jenkins_job
def run_all_jenkins_suite():
running_items = []
suite_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
for params in generate_parameters():
item = start_jenkins_job(job_urls[params.item], params, suite_timestamp)
if item is not None:
running_items.append(item)
JenkinsJob.wait_for_all(running_items)
return running_items
run_all_jenkins_suite()
函数调用start_jenkins_job()
函数,启动一个个的构建并将其添加到running_items
中。然后调用JenkinsJob.wait_for_all()
函数,等待所有的构建执行完毕。
JenkinsJob.wait_for_all()
函数每隔10
秒(使用delay
参数)检查所有Job的执行状态(通过monitor_status()
函数)。如果所有的Job的执行状态都不为None
,则退出等待。
monitor_status()
函数的执行逻辑如下:
- 首先检查
queue_id
:如果Job没有queue_id
,则表示尚未进行构建,将status
设置为NOT_BUILD
,然后停止状态检查。 - 检查
status
字段,如果不为None
,表示构建已经完成,停止状态检查。 - 检查
executable
字段,这个字段只有在QueueItem
获取到executor
后才会被设置。 - 当发现
executable
字段时,保存其中构建的url
和number
信息。 - 有了构建的
url
和number
信息后,可以调用RESTful API检查构建的状态。 - 如果构建的字段中有
result
字段,表示构建已结束,将其值用于更新status
字段。
参考文档:
- https://stackoverflow.com/questions/45472604/get-jenkins-job-build-id-from-queue-id
- https://python-jenkins.readthedocs.io/
- https://jenkinsapi.readthedocs.io/en/latest/index.html