需求来源
测试过程中,业务的同事经常会需要临时执行一些批量程序去处理联机准备的业务测试数据。批量程序一般部署在服务器或数据库上,由应用维护人员登录服务器执行,这样就存在沟通与执行上的成本。我们的目标是通过简单易行的方法,实现业务同事自主灵活调度批量程序的需求。
这个需求早在几年前就有了,我们当时使用vbs脚本这种最简单的方法,写了一个自动登录服务器执行shell的自动化脚本。这种方法用了好几年,还算稳定,也基本能满足业务需求。直到最近,我们发现批量作业调度经常出现执行顺序的错误,最后定位问题原因是批量调度框架逻辑更新,但是业务使用的vbs脚本没有及时更新,这就暴露了vbs调度的最大的弱点:每个需求都是一个vbs脚本,难于集中管理和更新。
上述问题是自动化运维中一个非常简单的需求,解决方案基本都是登陆服务器执行脚本这种方法,只不过用到的工具不同而已。我们这次的设想是能够将批量执行封装成一个服务,业务不需要直接去执行批量程序,而是去触发批量执行的服务。这样做的好处主要有三点,一、业务使用的客户端只是一个服务调用的程序,不涉及批量执行逻辑,无需随版本更新,不同业务需求使用相同的客户端,省去了客户端管理和更新的工作;二、技术人员可以在后台对批量服务执行的目标环境、执行逻辑顺序进行定制,并且对于业务是透明的;三、跳过了登录服务器和切换用户的步骤,获取程序回显更加简单稳定。基于上述需求,我们决定采用jenkins作为远程调度服务的框架,使用python调用jenkins的api接口作为业务使用的客户端。
为什么使用jenkins
使用jenkins出于以下考虑:
1、jenkins安装部署简单,windows平台基本是一键安装。
2、有丰富的插件支持,可以满足用户管理、服务管理、作业控制及历史执行统计等功能
3、jenkins的分布式设计简单稳定,只需要在从节点加载jar包就能够实现主从节点的管理
4、jenkins本身就是以web服务作为载体,提供api的接口调用方式,这样可以在任意客户端环境调用jenkins的服务
具体实现
这里略过jenkins的安装和配置等细节,大家有兴趣可以自行百度。
处理流程如下:
客户端 --> 数据库(保存用户与服务关系数据、服务调用逻辑元数据信息)--> 调用jenkins api调用对应服务 --> jenkins去服务所在节点执行shell脚本
1、服务端:
我们选择了一台windows服务器作为jenkins的主节点,在从节点加载slave.jar包,实现分布式框架。在任务定义是,选择只在选择的从节点执行任务,任务执行输入批量调度程序的shell(如果涉及多个批量,可以通过一个shell进行批量编排和输出的控制,简化在jenkins中任务定义过程)
2、数据库:
数据库记录了一些元数据信息,主要设计了两张表:用户与服务对照关系表,定义了业务同事姓名与jenkins上定义的服务名。服务元数据信息表,定义了服务的输入参数、简单的逻辑关系信息等,客户端根据这些信息控制用户的输入。
3、jenkins api
根据客户端输入,使用python加载jenkins的api包,通过buidjob和get output两个方法去调用jenkins的作业并获得结果数据。具体代码如下:
import jenkins
import cx_Oracle
import time
import sys
import urllib2
reload(sys)
sys.setdefaultencoding('utf-8')
#修改了_init_.py文件的438行,解决中文回显得问题
if __name__=='__main__':
conn = cx_Oracle.connect('test/test@1.1.1.1/db')
username = raw_input("请输入您的姓名:".encode("GBK"))
#根据传入的用户名,获取用户下注册的服务信息
str0 = "SELECT M.SERVICE_NAME,M.JENKINS_NAME,N.PARAM1 FROM JENKINS_SERVICE N , JENKINS_SERVICE_PARAM M" \
" WHERE M.SERVICE_NAME=N.SERVICE_NAME AND N.USER_NAME = '"+username+"' GROUP BY M.SERVICE_NAME,M.JENKINS_NAME,N.PARAM1 ORDER BY N.PARAM1"
cursor0 = conn.cursor()
cursor0.execute(str0)
res0 = cursor0.fetchall()
rowcount0 = len(res0)
x = 0
while x < rowcount0:
service_name = str(res0[x][0])
param1 = str(res0[x][2])
print (param1+" "+service_name+"\n")
x=x+1
#获取需要调用的服务,并根据服务进一步获取输入参数
tmp1=raw_input("请选择需要执行的服务:".encode("GBK"))
job=str(res0[int(tmp1)-1][1])
str1="SELECT PARAM2,PARAM3 FROM JENKINS_SERVICE_PARAM WHERE PARAM1='INPUT' AND JENKINS_NAME='"+job+"'"
cursor0.execute(str1)
res1 = cursor0.fetchall()
rowcount1 = len(res1)
y=0
#构建jenkins api的传入参数字典
param_dict={}
while y<rowcount1:
input_param=res1[y][0]
input_desc=str(res1[y][1])
tmp_input=""
tmp_input=raw_input("请输入 ".encode("GBK")+input_desc+" 的传入参数值:".encode("GBK"))
param_dict[input_param]=tmp_input
y=y+1
#连接jenkins api
server_ip='http://2.2.2.2:8080/login?from=%2F'
user_id='admin'
api_token='2544a1af98ba9677ae7f041d0959df3d'
server=jenkins.Jenkins(server_ip,username=user_id,password=api_token)
#检查上一次服务调用是否已经完成
build_no = server.get_job_info(job)["lastBuild"]["number"]
while True:
status = server.get_build_info(job, build_no)["building"]
if status is True:
print "案例执行中,请稍等".encode("GBK")
time.sleep(5)
else:
break
print "上一次执行结束,可以开始执行".encode("GBK")
#调用服务
server.build_job(job,param_dict)
time.sleep(5)
build_no = build_no + 1
#获取调用结果的Output信息
while True:
try:
status = server.get_build_info(job, build_no)["building"]
if status is True:
print "案例执行中,请稍等".encode("GBK")
time.sleep(5)
else:
break
except Exception,e:
print "任务正在初始化".encode("GBK")
time.sleep(5)
continue
print server.get_build_console_output(job, build_no).decode("GBK").encode("GBK")
print "执行完毕".encode("GBK")