deployer.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  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 flask_socketio import emit
  12. from walle.model.project import ProjectModel
  13. from walle.model.record import RecordModel
  14. from walle.model.task import TaskModel
  15. from walle.service.code import Code
  16. from walle.service.error import WalleError
  17. from walle.service.utils import color_clean
  18. from walle.service.waller import Waller
  19. class Deployer:
  20. '''
  21. 序列号
  22. '''
  23. stage = 'init'
  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. if command:
  106. current_app.logger.info(command)
  107. with self.local.cd(self.dir_codebase_project):
  108. result = self.local.run(command, wenv=self.config())
  109. def deploy(self):
  110. '''
  111. 2.检出代码
  112. :param project_name:
  113. :return:
  114. '''
  115. self.stage = self.stage_deploy
  116. self.sequence = 2
  117. current_app.logger.info('git dir: %s', self.dir_codebase_project + '/.git')
  118. # 如果项目底下有 .git 目录则认为项目完整,可以直接检出代码
  119. # TODO 不标准
  120. if os.path.exists(self.dir_codebase_project + '/.git'):
  121. with self.local.cd(self.dir_codebase_project):
  122. command = 'pwd && git pull'
  123. result = self.local.run(command, wenv=self.config())
  124. else:
  125. # 否则当作新项目检出完整代码
  126. with self.local.cd(self.dir_codebase_project):
  127. command = 'pwd && git clone %s .' % (self.project_info['repo_url'])
  128. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  129. result = self.local.run(command, wenv=self.config())
  130. # copy to a local version
  131. self.release_version = '%s_%s_%s' % (
  132. self.project_name, self.task_id, time.strftime('%Y%m%d_%H%M%S', time.localtime(time.time())))
  133. with self.local.cd(self.local_codebase):
  134. command = 'cp -rf %s %s' % (self.dir_codebase_project, self.release_version)
  135. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  136. result = self.local.run(command, wenv=self.config())
  137. # 更新到指定 commit_id
  138. with self.local.cd(self.local_codebase + self.release_version):
  139. command = 'git reset -q --hard %s' % (self.taskMdl.get('commit_id'))
  140. result = self.local.run(command, wenv=self.config())
  141. if result.exited != Code.Ok:
  142. raise WalleError(Code.shell_git_fail, message=result.stdout)
  143. def post_deploy(self):
  144. '''
  145. 3.检出代码后要做的任务
  146. - 用户自定义操作命令
  147. - 代码编译
  148. - 清除日志文件及无用文件
  149. -
  150. - 压缩打包
  151. - 传送到版本库 release
  152. :return:
  153. '''
  154. self.stage = self.stage_post_deploy
  155. self.sequence = 3
  156. # 用户自定义命令
  157. command = self.project_info['post_deploy']
  158. if command:
  159. with self.local.cd(self.local_codebase + self.release_version):
  160. result = self.local.run(command, wenv=self.config())
  161. # 压缩打包
  162. self.release_version_tar = '%s.tgz' % (self.release_version)
  163. with self.local.cd(self.local_codebase):
  164. excludes = [i for i in self.project_info['excludes'].split('\n') if i.strip()]
  165. excludes = ' --exclude='.join(excludes)
  166. command = 'tar zcf %s --exclude=%s %s' % (self.release_version_tar, excludes, self.release_version)
  167. result = self.local.run(command, wenv=self.config())
  168. def prev_release(self, waller):
  169. '''
  170. 4.部署代码到目标机器前做的任务
  171. - 检查 webroot 父目录是否存在
  172. :return:
  173. '''
  174. self.stage = self.stage_prev_release
  175. self.sequence = 4
  176. # 检查 target_releases 父目录是否存在
  177. command = 'mkdir -p %s' % (self.project_info['target_releases'])
  178. result = waller.run(command, wenv=self.config())
  179. # TODO 检查 webroot 父目录是否存在,是否为软链
  180. # command = 'mkdir -p %s' % (self.project_info['target_root'])
  181. # result = waller.run(command)
  182. # current_app.logger.info('command: %s', dir(result))
  183. # 用户自定义命令
  184. command = self.project_info['prev_release']
  185. if command:
  186. current_app.logger.info(command)
  187. with waller.cd(self.project_info['target_releases']):
  188. result = waller.run(command, wenv=self.config())
  189. # TODO md5
  190. # 传送到版本库 release
  191. current_app.logger.info('/tmp/walle/codebase/' + self.release_version_tar)
  192. result = waller.put('/tmp/walle/codebase/' + self.release_version_tar,
  193. remote=self.project_info['target_releases'], wenv=self.config())
  194. current_app.logger.info('command: %s', dir(result))
  195. # 解压
  196. self.release_untar(waller)
  197. def release(self, waller):
  198. '''
  199. 5.部署代码到目标机器做的任务
  200. - 打包代码 local
  201. - scp local => remote
  202. - 解压 remote
  203. :return:
  204. '''
  205. self.stage = self.stage_release
  206. self.sequence = 5
  207. with waller.cd(self.project_info['target_releases']):
  208. # 1. create a tmp link dir
  209. current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
  210. command = 'ln -sfn %s/%s %s' % (
  211. self.project_info['target_releases'], self.release_version, current_link_tmp_dir)
  212. result = waller.run(command, wenv=self.config())
  213. # 2. make a soft link from release to tmp link
  214. # 3. move tmp link to webroot
  215. current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
  216. command = 'mv -fT %s %s' % (current_link_tmp_dir, self.project_info['target_root'])
  217. result = waller.run(command, wenv=self.config())
  218. def release_untar(self, waller):
  219. '''
  220. 解压版本包
  221. :return:
  222. '''
  223. with waller.cd(self.project_info['target_releases']):
  224. command = 'tar zxf %s' % (self.release_version_tar)
  225. result = waller.run(command, wenv=self.config())
  226. def post_release(self, waller):
  227. '''
  228. 6.部署代码到目标机器后要做的任务
  229. - 切换软链
  230. - 重启 nginx
  231. :return:
  232. '''
  233. self.stage = self.stage_post_release
  234. self.sequence = 6
  235. # 用户自定义命令
  236. command = self.project_info['post_release']
  237. if not command:
  238. return None
  239. current_app.logger.info(command)
  240. with waller.cd(self.project_info['target_root']):
  241. result = waller.run(command, wenv=self.config())
  242. self.post_release_service(waller)
  243. def post_release_service(self, waller):
  244. '''
  245. 代码部署完成后,服务启动工作,如: nginx重启
  246. :param connection:
  247. :return:
  248. '''
  249. with waller.cd(self.project_info['target_root']):
  250. command = 'sudo service nginx restart'
  251. result = waller.run(command, wenv=self.config())
  252. def list_tag(self):
  253. with self.local.cd(self.dir_codebase_project):
  254. command = 'git tag -l'
  255. result = self.local.run(command, pty=False, wenv=self.config())
  256. tags = result.stdout.strip()
  257. tags = tags.split('\n')
  258. return [color_clean(tag.strip()) for tag in tags]
  259. return None
  260. def list_branch(self):
  261. with self.local.cd(self.dir_codebase_project):
  262. command = 'git pull'
  263. result = self.local.run(command, wenv=self.config())
  264. if result.exited != Code.Ok:
  265. raise WalleError(Code.shell_git_pull_fail, message=result.stdout)
  266. current_app.logger.info(self.dir_codebase_project)
  267. command = 'git branch -r'
  268. result = self.local.run(command, pty=False, wenv=self.config())
  269. # if result.exited != Code.Ok:
  270. # raise WalleError(Code.shell_run_fail)
  271. # TODO 三种可能: false, error, success
  272. branches = result.stdout.strip()
  273. branches = branches.split('\n')
  274. # 去除 origin/HEAD -> 当前指向
  275. # 去除远端前缀
  276. branches = [branch.strip().lstrip('origin/') for branch in branches if
  277. not branch.strip().startswith('origin/HEAD')]
  278. return branches
  279. return None
  280. def list_commit(self, branch):
  281. with self.local.cd(self.dir_codebase_project):
  282. command = 'git checkout %s && git pull' % (branch)
  283. self.local.run(command, wenv=self.config())
  284. command = 'git log -50 --pretty="%h #@_@# %an #@_@# %s"'
  285. result = self.local.run(command, pty=False, wenv=self.config())
  286. current_app.logger.info(result.stdout)
  287. commit_log = result.stdout.strip()
  288. current_app.logger.info(commit_log)
  289. commit_list = commit_log.split('\n')
  290. commits = []
  291. for commit in commit_list:
  292. if not re.search('^.+ #@_@# .+ #@_@# .*$', commit):
  293. continue
  294. commit_dict = commit.split(' #@_@# ')
  295. current_app.logger.info(commit_dict)
  296. commits.append({
  297. 'id': commit_dict[0],
  298. 'name': commit_dict[1],
  299. 'message': commit_dict[2],
  300. })
  301. return commits
  302. # TODO
  303. return None
  304. def init_repo(self):
  305. if not os.path.exists(self.dir_codebase_project):
  306. # 检查 目录是否存在
  307. command = 'mkdir -p %s' % (self.dir_codebase_project)
  308. # TODO remove
  309. current_app.logger.info(command)
  310. self.local.run(command, wenv=self.config())
  311. with self.local.cd(self.dir_codebase_project):
  312. is_git_dir = self.local.run('git status', exception=False, wenv=self.config())
  313. if is_git_dir.exited != Code.Ok:
  314. # 否则当作新项目检出完整代码
  315. # 检查 目录是否存在
  316. command = 'rm -rf %s' % (self.dir_codebase_project)
  317. self.local.run(command, wenv=self.config())
  318. command = 'git clone %s %s' % (self.project_info['repo_url'], self.dir_codebase_project)
  319. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  320. result = self.local.run(command, wenv=self.config())
  321. if result.exited != Code.Ok:
  322. raise WalleError(Code.shell_git_init_fail, message=result.stdout)
  323. def logs(self):
  324. return RecordModel().fetch(task_id=self.task_id)
  325. def end(self, success=True, update_status=True):
  326. if update_status:
  327. status = TaskModel.status_success if success else TaskModel.status_fail
  328. current_app.logger.info('success:%s, status:%s' % (success, status))
  329. TaskModel().get_by_id(self.task_id).update({'status': status})
  330. if success:
  331. emit('success', {'event': 'finish', 'data': {'message': '部署完成,辛苦了,为你的努力喝彩!'}}, room=self.task_id)
  332. else:
  333. emit('fail', {'event': 'finish', 'data': {'message': Code.code_msg[Code.deploy_fail]}}, room=self.task_id)
  334. def walle_deploy(self):
  335. self.start()
  336. try:
  337. self.prev_deploy()
  338. self.deploy()
  339. self.post_deploy()
  340. is_all_servers_success = True
  341. for server_info in self.servers:
  342. server = server_info['host']
  343. try:
  344. self.connections[server] = Waller(host=server, user=self.project_info['target_user'])
  345. self.prev_release(self.connections[server])
  346. self.release(self.connections[server])
  347. self.post_release(self.connections[server])
  348. except Exception as e:
  349. is_all_servers_success = False
  350. current_app.logger.error(e)
  351. self.errors[server] = e.message
  352. self.end(is_all_servers_success)
  353. except Exception as e:
  354. self.end(False)
  355. emit('fail', {'event': 'console', 'data': {'message': Code.code_msg[Code.deploy_fail]}}, room=self.task_id)
  356. return {'success': self.success, 'errors': self.errors}