omad一键部署脚本优化

阿凡达2018-07-10 14:43

一、背景介绍

    项目需要在Jenkins中调用omad进行Java应用的一键部署,每次执行可以部署一个环境:

    上面加速的主要思路是,把每个项目ci环境的部署写在一个单独的python文件里,然后在一个总体部署的脚本中,每批调用两个环境的脚本来实现并发构建,缺点是管理起来不太方便。
     最近子账号项目要增加多Region,需要在omad的一个环境里部署2个实例(之前都是1个)。于是按照上面批量执行的方式,又复制了一份py文件,改了下实例id,加到了批量执行的列表里。过了一天,突然发现该环境的两个实例版本一直是旧的,研究了一下,发现可能是因为两个脚本同时构建同一个环境冲突了。虽然把脚本简单的改造一下就可以解决这个问题,但考虑到之前的实现方式不太“优雅”,于是决定在上面两篇文章的基础上重新编写一个。

二、脚本实现

    下面将把代码从上往下贴一下并进行简单的介绍,各个代码块连在一起即是完整的代码:

import httplib
import json
import time
import threading
import logging
import logging.handlers

LOG_FILE = 'deploy.log'
handler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes = 1024*1024, backupCount = 5)
fmt = '[%(levelname)s]%(asctime)s: %(message)s'
formatter = logging.Formatter(fmt)
handler.setFormatter(formatter)
logger = logging.getLogger('deploy')
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)

    首先导入需要的package,并创建一个简单的日志模块。
    然后定义一个dict,保存各环境和实例的参数(已打码),新添加环境或实例时直接写在这里即可:

instances = {
    'passport':
        {
            'ci': {'moduleId': '802*', 'envId': '201**', 'instanceIds': ('589**', '589**')},
            'dev': {'moduleId': '802*', 'envId': '200**', 'instanceIds': ('526**', )}
        },
    'console':
        {
            'ci': {'moduleId': '786*', 'envId': '192**', 'instanceIds': ('580**', )},
        },
    'cloudcitc':
        {
            'ci': {'moduleId': '85**', 'envId': '215**', 'instanceIds': ('550**', )},
        },
    'cloudcreative':
        {
            'ci': {'moduleId': '84**', 'envId': '212**', 'instanceIds': ('580**', )},
        },
    'homesite':
        {
            'ci': {'moduleId': '74**', 'envId': '200**', 'instanceIds': ('513**', )},
        },
    'admin':
        {
            'ci': {'moduleId': '79**', 'envId': '194**', 'instanceIds': ('581**', )},
        },
    'pay':
        {
            'ci': {'moduleId': '77**', 'envId': '204**', 'instanceIds': ('524**', )},
        },
    'pay-admin':
        {
            'ci': {'moduleId': '78**', 'envId': '194**', 'instanceIds': ('498**', )},
        },
}

    以上面第一个passport为例,各参数对应omad里如下图所示:

   然后是一个封装了omad相关api的类,把整个流程分成了auth()、build()、deploy()三个步骤:

class DeployAssistant:
    def __init__(self, project, env='ci'):
        if project not in instances.keys():
            raise Exception('wrong project')
        if env not in instances.get(project).keys():
            raise Exception('wrong environment')

        logger.info("project=%s, env=%s" % (project, env))
        self.project = project
        self.env = env
        config = instances.get(project).get(env)
        self.moduleId = config.get('moduleId')  # omad-产品管理-实例管理, url里的两个参数
        self.envId = config.get('envId')
        self.instanceIds = config.get('instanceIds') # 实例管理页面中的实例Id

        self.host = 'omad.hz.netease.com'
        self.appId = '9a3ebca00e364fe6ab9de70b0177b***' # omad-用户设置-AccessKey
        self.appSecret = '86a3bb8b38c845719d1b26de2b131***' # omad-用户设置-AccessSecret

        self.version = ''
        self.token = ''
        self.authSuccess = False
        self.buildSuccess = 'skip'

def getWebContent(self, method, url):
# 封装一个发送http请求并读取omad返回参数的方法
conn = httplib.HTTPConnection(self.host)
conn.request(method, url)
params = json.loads(conn.getresponse().read())
conn.close()
return params

def auth(self):
# 首先从omad获取token
try:
params = self.getWebContent('GET', '/api/cli/login?appId=%s&appSecret=%s' %
(self.appId, self.appSecret))
self.token = params.get('params').get('token')
logger.info('token= %s'% self.token)
self.authSuccess = True
logger.info('auth succefully')
except Exception as e:
logger.info('auth failed:%s' % e)

def build(self):
# 然后开始构建
if not self.authSuccess:
return
logger.info('build start')
try:
self.getWebContent('GET', '/api/cli/build?token=%s&moduleId=%s&envId=%s&version=%s' %
(self.token, self.moduleId, self.envId, self.version))
except Exception as e:
logger.info('build start failed:%s' % e)
return

failed_count = 0
while True:
time.sleep(2)
try:
params = self.getWebContent('GET','/api/cli/estatus?token=%s&envId=%s'%
(self.token, self.envId))
status = params.get('status')
if status != 'building':
logger.info('build result=%s' % status)
self.buildSuccess = True if status == 'build_succ' else False
break
else:
logger.info('%s %s %s'% (status, self.project, self.env))
except Exception as e:
logger.debug(e)
failed_count += 1
if failed_count > 5:
raise Exception('build failed')

def deploy(self):
# 部署这个环境的所有实例
if not self.authSuccess:
return
if not self.buildSuccess:
return
for instanceId in self.instanceIds:
# 这里也可以改成多线程部署,不过这个操作速度较快,提升不会很明显
logger.info('start deploy instance:%s' % instanceId)
self.getWebContent('GET', '/api/cli/deploy?token=%s&moduleId=%s&envId=%s&version=%s&instanceId=%s'
% (self.token, self.moduleId, self.envId, self.version, instanceId))
failed_count = 0
while True:
time.sleep(2)
try:
params = self.getWebContent('GET', '/api/cli/istatus?token=%s&envId=%s&instanceId=%s' %
(self.token, self.envId, instanceId))
status = params.get('deployStatus')
if status!= 'deploying':
logger.info('deploy result=%s' % status)
break
else:
logger.info('%s %s %s'% (status, self.project, self.env))
except Exception as e:
logger.debug(e)
failed_count += 1
if failed_count > 5:
raise Exception('build failed')
logger.info('done deploy instance:%s' % instanceId)
logger.info('deploy finished')

 对于单个项目,上面的代码可以这样调用:

if __name__ == '__main__':
    omad = DeployAssistant("passport")    # 第二个参数可选,默认为'ci'
    omad.auth()    # 获取token
    omad.build()   # 构建(可以跳过这步)
    omad.deploy()  # 部署

    为了减少各项目构建和部署花费的总时间,还需要支持多个项目并发操作:

class JobManager:
    def __init__(self, parallel= 2):
        """
        @param parallel 并发构建的数量,默认为2
        """
        self.suite = []
        self.status = []
        self.parallel = parallel
        self.mutex = threading.Lock()

    def add(self, project, env= 'ci'):
        """添加一个项目的一个环境"""
        job = DeployAssistant(project, env)
        self.suite.append(job)

    def addAll(self, env= 'ci'):
        """添加instances字典中项目的所有指定环境"""
        for project, config in instances.iteritems():
            if config.get(env):
                job = DeployAssistant(project, env)
                self.suite.append(job)

    def getJob(self):
        #从列表里获取一个还没执行的项目(的序号)
        for i in range(len(self.status)):
            if self.status[i] == -1:  # 没有执行过的项目
                self.status[i] = 0    # 执行中的项目
                return i   
        return -1    # 返回-1表示所有项目都执行过了

    def jobHandler(self):
        thread = threading.current_thread()
        while True:
            self.mutex.acquire()
            jobIndex = self.getJob()
            self.mutex.release()
            if jobIndex == -1:
                break
            job = self.suite[jobIndex]
            logger.info('%s start run job %s: %s %s'%
                 (thread.getName(), jobIndex, job.project, job.env))
            job.auth()
            job.build()
            job.deploy()
            self.status[jobIndex] = 1
            logger.info(self.status)

    def run(self):
        self.status = [-1 for x in range(len(self.suite))]
        start = time.time()
        logger.info('%s jobs to run' % len(self.status))

        for i in range(self.parallel):
            t = threading.Thread(target= self.jobHandler)
            t.setDaemon(True)
            t.start()

        while True:
            time.sleep(0.5)
            if time.time() - start > 3600:
                # 超过1小时认为失败,强制退出
                logger.info('timeout, force exit')
                break
            if self.status.count(1) == len(self.status):
                # 全部执行完毕,退出
                logger.info('all done, used %.3f second'% (time.time() - start))
                break

    上面的代码可以这样调用:

if __name__ == '__main__':
    # 参数2表示最大同时构建的项目数量,默认为2
    omad = JobManager(2)   
    # 添加所有项目,参数默认为'ci'
    omad.addAll()   
    # 或依次添加一批指定项目(此时就不要再加上addAll()了),第二个参数默认为'ci'
    omad.add("passport")
    omad.add("console")
    # 最后是执行
    omad.run()

    默认开启两个线程,每个线程执行时都会从列表里读取一个环境,执行完毕之后再尝试读取下一个。相对于之前在for循环里每批执行执行两个的方式,这种方式下两个线程不会互相阻塞,总部署时间相应减少了90s左右。执行日志默认会输出到同目录下的deploy.log文件中:
 
    Jenkins里这样填写即可:


    最后提供一个完整的代码文件(去掉.txt):deployAssistant.py.txt

本文来自网易实践者社区,经作者hzsunzhengyu授权发布。