deployer.py 16 KB

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