deployer.py 14 KB

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