deployer.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # @Author: wushuiyong
  4. # @Created Time : 日 1/ 1 23:43:12 2017
  5. # @Description:
  6. import time
  7. from datetime import datetime
  8. import os
  9. import re
  10. from flask import current_app
  11. from walle.model.project import ProjectModel
  12. from walle.model.record import RecordModel
  13. from walle.model.task import TaskModel
  14. from walle.service.code import Code
  15. from walle.service.error import WalleError
  16. from walle.service.utils import color_clean
  17. from walle.service.waller import Waller
  18. from flask_socketio import emit
  19. class Deployer:
  20. '''
  21. 序列号
  22. '''
  23. stage = '0'
  24. sequence = 0
  25. stage_prev_deploy = 'prev_deploy'
  26. stage_deploy = 'deploy'
  27. stage_post_deploy = 'post_deploy'
  28. stage_prev_release = 'prev_release'
  29. stage_release = 'release'
  30. stage_post_release = 'post_release'
  31. task_id = '0'
  32. user_id = '0'
  33. taskMdl = None
  34. TaskRecord = None
  35. console = False
  36. version = datetime.now().strftime('%Y%m%d%H%M%s')
  37. local_codebase, dir_codebase_project, project_name = None, None, None
  38. dir_release, dir_webroot = None, None
  39. connections, success, errors = {}, {}, {}
  40. release_version_tar, release_version = None, None
  41. local = None
  42. def __init__(self, task_id=None, project_id=None, console=False):
  43. self.local_codebase = current_app.config.get('CODE_BASE')
  44. self.local = Waller(host=current_app.config.get('LOCAL_SERVER_HOST'),
  45. user=current_app.config.get('LOCAL_SERVER_USER'),
  46. port=current_app.config.get('LOCAL_SERVER_PORT'),
  47. )
  48. self.TaskRecord = RecordModel()
  49. if task_id:
  50. self.task_id = task_id
  51. # task start
  52. current_app.logger.info(self.task_id)
  53. self.taskMdl = TaskModel().item(self.task_id)
  54. self.user_id = self.taskMdl.get('user_id')
  55. self.servers = self.taskMdl.get('servers_info')
  56. self.task = self.taskMdl.get('target_user')
  57. self.project_info = self.taskMdl.get('project_info')
  58. if project_id:
  59. self.project_id = project_id
  60. self.project_info = ProjectModel(id=project_id).item()
  61. self.project_name = self.project_info['id']
  62. self.dir_codebase_project = self.local_codebase + str(self.project_name)
  63. self.init_repo()
  64. # start to deploy
  65. self.console = console
  66. def config(self):
  67. return {'task_id': self.task_id, 'user_id': self.user_id, 'stage': self.stage, 'sequence': self.sequence,
  68. 'console': self.console}
  69. def start(self):
  70. TaskModel().get_by_id(self.task_id).update({'status': TaskModel.status_doing})
  71. self.taskMdl = TaskModel().item(self.task_id)
  72. # ===================== fabric ================
  73. # SocketHandler
  74. def prev_deploy(self):
  75. '''
  76. # TODO
  77. socketio.sleep(0.001)
  78. 1.代码检出前要做的基础工作
  79. - 检查 当前用户
  80. - 检查 python 版本
  81. - 检查 git 版本
  82. - 检查 目录是否存在
  83. - 用户自定义命令
  84. :return:
  85. '''
  86. self.stage = self.stage_prev_deploy
  87. self.sequence = 1
  88. # 检查 当前用户
  89. command = 'whoami'
  90. current_app.logger.info(command)
  91. result = self.local.run(command, wenv=self.config())
  92. # 检查 python 版本
  93. command = 'python --version'
  94. result = self.local.run(command, wenv=self.config())
  95. # 检查 git 版本
  96. command = 'git --version'
  97. result = self.local.run(command, wenv=self.config())
  98. # 检查 目录是否存在
  99. self.init_repo()
  100. # TODO to be removed
  101. command = 'mkdir -p %s' % (self.dir_codebase_project)
  102. result = self.local.run(command, wenv=self.config())
  103. # 用户自定义命令
  104. command = self.project_info['prev_deploy']
  105. current_app.logger.info(command)
  106. with self.local.cd(self.dir_codebase_project):
  107. result = self.local.run(command, wenv=self.config())
  108. def deploy(self):
  109. '''
  110. 2.检出代码
  111. :param project_name:
  112. :return:
  113. '''
  114. self.stage = self.stage_deploy
  115. self.sequence = 2
  116. current_app.logger.info('git dir: %s', self.dir_codebase_project + '/.git')
  117. # 如果项目底下有 .git 目录则认为项目完整,可以直接检出代码
  118. # TODO 不标准
  119. if os.path.exists(self.dir_codebase_project + '/.git'):
  120. with self.local.cd(self.dir_codebase_project):
  121. command = 'pwd && git pull'
  122. result = self.local.run(command, wenv=self.config())
  123. else:
  124. # 否则当作新项目检出完整代码
  125. with self.local.cd(self.dir_codebase_project):
  126. command = 'pwd && git clone %s .' % (self.project_info['repo_url'])
  127. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  128. result = self.local.run(command, wenv=self.config())
  129. # copy to a local version
  130. self.release_version = '%s_%s_%s' % (
  131. self.project_name, self.task_id, time.strftime('%Y%m%d_%H%M%S', time.localtime(time.time())))
  132. with self.local.cd(self.local_codebase):
  133. command = 'cp -rf %s %s' % (self.dir_codebase_project, self.release_version)
  134. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  135. result = self.local.run(command, wenv=self.config())
  136. # 更新到指定 commit_id
  137. with self.local.cd(self.local_codebase + self.release_version):
  138. command = 'git reset -q --hard %s' % (self.taskMdl.get('commit_id'))
  139. result = self.local.run(command, wenv=self.config())
  140. if result.exited != Code.Ok:
  141. raise WalleError(Code.shell_git_fail, message=result.stdout)
  142. def post_deploy(self):
  143. '''
  144. 3.检出代码后要做的任务
  145. - 用户自定义操作命令
  146. - 代码编译
  147. - 清除日志文件及无用文件
  148. -
  149. - 压缩打包
  150. - 传送到版本库 release
  151. :return:
  152. '''
  153. self.stage = self.stage_post_deploy
  154. self.sequence = 3
  155. # 用户自定义命令
  156. command = self.project_info['post_deploy']
  157. with self.local.cd(self.local_codebase + self.release_version):
  158. result = self.local.run(command, wenv=self.config())
  159. # 压缩打包
  160. self.release_version_tar = '%s.tgz' % (self.release_version)
  161. with self.local.cd(self.local_codebase):
  162. command = 'tar zcf %s %s' % (self.release_version_tar, self.release_version)
  163. result = self.local.run(command, wenv=self.config())
  164. def prev_release(self, waller):
  165. '''
  166. 4.部署代码到目标机器前做的任务
  167. - 检查 webroot 父目录是否存在
  168. :return:
  169. '''
  170. self.stage = self.stage_prev_release
  171. self.sequence = 4
  172. # 检查 target_releases 父目录是否存在
  173. command = 'mkdir -p %s' % (self.project_info['target_releases'])
  174. result = waller.run(command, wenv=self.config())
  175. # TODO 检查 webroot 父目录是否存在,是否为软链
  176. # command = 'mkdir -p %s' % (self.project_info['target_root'])
  177. # result = waller.run(command)
  178. # current_app.logger.info('command: %s', dir(result))
  179. # 用户自定义命令
  180. command = self.project_info['prev_release']
  181. current_app.logger.info(command)
  182. with waller.cd(self.project_info['target_releases']):
  183. result = waller.run(command, wenv=self.config())
  184. # TODO md5
  185. # 传送到版本库 release
  186. current_app.logger.info('/tmp/walle/codebase/' + self.release_version_tar)
  187. result = waller.put('/tmp/walle/codebase/' + self.release_version_tar,
  188. remote=self.project_info['target_releases'], wenv=self.config())
  189. current_app.logger.info('command: %s', dir(result))
  190. # 解压
  191. self.release_untar(waller)
  192. def release(self, waller):
  193. '''
  194. 5.部署代码到目标机器做的任务
  195. - 打包代码 local
  196. - scp local => remote
  197. - 解压 remote
  198. :return:
  199. '''
  200. self.stage = self.stage_release
  201. self.sequence = 5
  202. with waller.cd(self.project_info['target_releases']):
  203. # 1. create a tmp link dir
  204. current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
  205. command = 'ln -sfn %s/%s %s' % (
  206. self.project_info['target_releases'], self.release_version, current_link_tmp_dir)
  207. result = waller.run(command, wenv=self.config())
  208. # 2. make a soft link from release to tmp link
  209. # 3. move tmp link to webroot
  210. current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
  211. command = 'mv -fT %s %s' % (current_link_tmp_dir, self.project_info['target_root'])
  212. result = waller.run(command, wenv=self.config())
  213. def release_untar(self, waller):
  214. '''
  215. 解压版本包
  216. :return:
  217. '''
  218. with waller.cd(self.project_info['target_releases']):
  219. command = 'tar zxf %s' % (self.release_version_tar)
  220. result = waller.run(command, wenv=self.config())
  221. def post_release(self, waller):
  222. '''
  223. 6.部署代码到目标机器后要做的任务
  224. - 切换软链
  225. - 重启 nginx
  226. :return:
  227. '''
  228. self.stage = self.stage_post_release
  229. self.sequence = 6
  230. # 用户自定义命令
  231. command = self.project_info['post_release']
  232. current_app.logger.info(command)
  233. with waller.cd(self.project_info['target_root']):
  234. result = waller.run(command, wenv=self.config())
  235. self.post_release_service(waller)
  236. def post_release_service(self, waller):
  237. '''
  238. 代码部署完成后,服务启动工作,如: nginx重启
  239. :param connection:
  240. :return:
  241. '''
  242. with waller.cd(self.project_info['target_root']):
  243. command = 'sudo service nginx restart'
  244. result = waller.run(command, wenv=self.config())
  245. def list_tag(self):
  246. with self.local.cd(self.dir_codebase_project):
  247. command = 'git tag -l'
  248. result = self.local.run(command, pty=False, wenv=self.config())
  249. tags = result.stdout.strip()
  250. tags = tags.split('\n')
  251. return [color_clean(tag.strip()) for tag in tags]
  252. return None
  253. def list_branch(self):
  254. with self.local.cd(self.dir_codebase_project):
  255. command = 'git pull'
  256. result = self.local.run(command, wenv=self.config())
  257. if result.exited != Code.Ok:
  258. raise WalleError(Code.shell_git_pull_fail, message=result.stdout)
  259. current_app.logger.info(self.dir_codebase_project)
  260. command = 'git branch -r'
  261. result = self.local.run(command, pty=False, wenv=self.config())
  262. # if result.exited != Code.Ok:
  263. # raise WalleError(Code.shell_run_fail)
  264. # TODO 三种可能: false, error, success
  265. branches = result.stdout.strip()
  266. branches = branches.split('\n')
  267. # 去除 origin/HEAD -> 当前指向
  268. # 去除远端前缀
  269. branches = [branch.strip().lstrip('origin/') for branch in branches if
  270. not branch.strip().startswith('origin/HEAD')]
  271. return branches
  272. return None
  273. def list_commit(self, branch):
  274. with self.local.cd(self.dir_codebase_project):
  275. command = 'git checkout %s && git pull' % (branch)
  276. self.local.run(command, wenv=self.config())
  277. command = 'git log -50 --pretty="%h #@_@# %an #@_@# %s"'
  278. result = self.local.run(command, pty=False, wenv=self.config())
  279. current_app.logger.info(result.stdout)
  280. commit_log = result.stdout.strip()
  281. current_app.logger.info(commit_log)
  282. commit_list = commit_log.split('\n')
  283. commits = []
  284. for commit in commit_list:
  285. if not re.search('^.+ #@_@# .+ #@_@# .*$', commit):
  286. continue
  287. commit_dict = commit.split(' #@_@# ')
  288. current_app.logger.info(commit_dict)
  289. commits.append({
  290. 'id': commit_dict[0],
  291. 'name': commit_dict[1],
  292. 'message': commit_dict[2],
  293. })
  294. return commits
  295. # TODO
  296. return None
  297. def init_repo(self):
  298. if not os.path.exists(self.dir_codebase_project):
  299. # 检查 目录是否存在
  300. command = 'mkdir -p %s' % (self.dir_codebase_project)
  301. # TODO remove
  302. current_app.logger.info(command)
  303. self.local.run(command, wenv=self.config())
  304. with self.local.cd(self.dir_codebase_project):
  305. is_git_dir = self.local.run('git status', exception=False, wenv=self.config())
  306. if is_git_dir.exited != Code.Ok:
  307. # 否则当作新项目检出完整代码
  308. # 检查 目录是否存在
  309. command = 'rm -rf %s' % (self.dir_codebase_project)
  310. self.local.run(command, wenv=self.config())
  311. command = 'git clone %s %s' % (self.project_info['repo_url'], self.dir_codebase_project)
  312. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  313. result = self.local.run(command, wenv=self.config())
  314. if result.exited != Code.Ok:
  315. raise WalleError(Code.shell_git_init_fail, message=result.stdout)
  316. def end(self, success=True):
  317. status = TaskModel.status_success if success else TaskModel.status_fail
  318. current_app.logger.info('success:%s, status:%s' % (success, status))
  319. TaskModel().get_by_id(self.task_id).update({'status': status})
  320. def walle_deploy(self):
  321. self.start()
  322. try:
  323. self.prev_deploy()
  324. self.deploy()
  325. self.post_deploy()
  326. is_all_servers_success = True
  327. for server_info in self.servers:
  328. server = server_info['host']
  329. try:
  330. self.connections[server] = Waller(host=server, user=self.project_info['target_user'])
  331. self.prev_release(self.connections[server])
  332. self.release(self.connections[server])
  333. self.post_release(self.connections[server])
  334. except Exception as e:
  335. is_all_servers_success = False
  336. current_app.logger.error(e)
  337. self.errors[server] = e.message
  338. self.end(is_all_servers_success)
  339. except Exception as e:
  340. self.end(False)
  341. emit('fail', {'event': 'console', 'data': {'message': Code.code_msg[Code.deploy_fail]}}, room=self.task_id)
  342. return {'success': self.success, 'errors': self.errors}