deployer.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  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 pwd
  10. import re
  11. from flask import current_app
  12. from flask_socketio import emit
  13. from walle.model.project import ProjectModel
  14. from walle.model.record import RecordModel
  15. from walle.model.task import TaskModel
  16. from walle.service.code import Code
  17. from walle.service.error import WalleError
  18. from walle.service.utils import color_clean
  19. from walle.service.utils import excludes_format
  20. from walle.service.notice import Notice
  21. from walle.service.waller import Waller
  22. from flask_login import current_user
  23. class Deployer:
  24. '''
  25. 序列号
  26. '''
  27. stage = 'init'
  28. sequence = 0
  29. stage_prev_deploy = 'prev_deploy'
  30. stage_deploy = 'deploy'
  31. stage_post_deploy = 'post_deploy'
  32. stage_prev_release = 'prev_release'
  33. stage_release = 'release'
  34. stage_post_release = 'post_release'
  35. task_id = '0'
  36. user_id = '0'
  37. taskMdl = None
  38. TaskRecord = None
  39. console = False
  40. version = datetime.now().strftime('%Y%m%d%H%M%s')
  41. local_codebase, dir_codebase_project, project_name = None, None, None
  42. dir_release, dir_webroot = None, None
  43. connections, success, errors = {}, {}, {}
  44. release_version_tar, previous_release_version, release_version = None, None, None
  45. local = None
  46. def __init__(self, task_id=None, project_id=None, console=False):
  47. self.local_codebase = current_app.config.get('CODE_BASE')
  48. self.localhost = Waller(host='127.0.0.1')
  49. self.TaskRecord = RecordModel()
  50. if task_id:
  51. self.task_id = task_id
  52. # task start
  53. current_app.logger.info(self.task_id)
  54. self.taskMdl = TaskModel().item(self.task_id)
  55. self.user_id = self.taskMdl.get('user_id')
  56. self.servers = self.taskMdl.get('servers_info')
  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.servers = self.project_info['servers_info']
  62. self.project_name = self.project_info['id']
  63. self.dir_codebase_project = self.local_codebase + str(self.project_name)
  64. # self.init_repo()
  65. # start to deploy
  66. self.console = console
  67. def config(self, console=None):
  68. return {
  69. 'task_id': self.task_id,
  70. 'user_id': self.user_id,
  71. 'stage': self.stage,
  72. 'sequence': self.sequence,
  73. 'console': console if console is not None else self.console
  74. }
  75. def start(self):
  76. RecordModel().query.filter_by(task_id=self.task_id).delete()
  77. TaskModel().get_by_id(self.task_id).update({'status': TaskModel.status_doing})
  78. self.taskMdl = TaskModel().item(self.task_id)
  79. # ===================== fabric ================
  80. # SocketHandler
  81. def prev_deploy(self):
  82. '''
  83. # TODO
  84. socketio.sleep(0.001)
  85. 1.代码检出前要做的基础工作
  86. - 检查 当前用户
  87. - 检查 python 版本
  88. - 检查 git 版本
  89. - 检查 目录是否存在
  90. - 用户自定义命令
  91. :return:
  92. '''
  93. self.stage = self.stage_prev_deploy
  94. self.sequence = 1
  95. # # 检查 python 版本
  96. # command = 'python --version'
  97. # result = self.localhost.local(command, wenv=self.config())
  98. #
  99. # # 检查 git 版本
  100. # command = 'git --version'
  101. # result = self.localhost.local(command, wenv=self.config())
  102. # 检查 目录是否存在
  103. self.init_repo()
  104. # 用户自定义命令
  105. command = self.project_info['prev_deploy']
  106. if command:
  107. current_app.logger.info(command)
  108. with self.localhost.cd(self.dir_codebase_project):
  109. result = self.localhost.local(command, wenv=self.config())
  110. def deploy(self):
  111. '''
  112. 2.检出代码
  113. :param project_name:
  114. :return:
  115. '''
  116. self.stage = self.stage_deploy
  117. self.sequence = 2
  118. # copy to a local version
  119. self.release_version = '%s_%s_%s' % (
  120. self.project_name, self.task_id, time.strftime('%Y%m%d_%H%M%S', time.localtime(time.time())))
  121. with self.localhost.cd(self.local_codebase):
  122. command = 'cp -rf %s %s' % (self.dir_codebase_project, self.release_version)
  123. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  124. result = self.localhost.local(command, wenv=self.config())
  125. # 更新到指定 commit_id
  126. with self.localhost.cd(self.local_codebase + self.release_version):
  127. command = 'git reset -q --hard %s' % (self.taskMdl.get('commit_id'))
  128. result = self.localhost.local(command, wenv=self.config())
  129. if result.exited != Code.Ok:
  130. raise WalleError(Code.shell_git_fail, message=result.stdout)
  131. def post_deploy(self):
  132. '''
  133. 3.检出代码后要做的任务
  134. - 用户自定义操作命令
  135. - 代码编译
  136. - 清除日志文件及无用文件
  137. -
  138. - 压缩打包
  139. - 传送到版本库 release
  140. :return:
  141. '''
  142. self.stage = self.stage_post_deploy
  143. self.sequence = 3
  144. # 用户自定义命令
  145. command = self.project_info['post_deploy']
  146. if command:
  147. with self.localhost.cd(self.local_codebase + self.release_version):
  148. result = self.localhost.local(command, wenv=self.config())
  149. # 压缩打包
  150. self.release_version_tar = '%s.tgz' % (self.release_version)
  151. with self.localhost.cd(self.local_codebase):
  152. excludes = excludes_format(self.project_info['excludes'])
  153. command = 'tar zcf %s %s %s' % (self.release_version_tar, excludes, self.release_version)
  154. result = self.localhost.local(command, wenv=self.config())
  155. def prev_release(self, waller):
  156. '''
  157. 4.部署代码到目标机器前做的任务
  158. - 检查 webroot 父目录是否存在
  159. :return:
  160. '''
  161. self.stage = self.stage_prev_release
  162. self.sequence = 4
  163. # 检查 target_releases 父目录是否存在
  164. command = 'mkdir -p %s' % (self.project_info['target_releases'])
  165. result = waller.run(command, wenv=self.config())
  166. # TODO md5
  167. # 传送到版本库 release
  168. result = waller.put(self.local_codebase + self.release_version_tar,
  169. remote=self.project_info['target_releases'], wenv=self.config())
  170. current_app.logger.info('command: %s', dir(result))
  171. # 解压
  172. self.release_untar(waller)
  173. # 用户自定义命令
  174. self.prev_release_custom(waller)
  175. def prev_release_custom(self, waller):
  176. # 用户自定义命令
  177. command = self.project_info['prev_release']
  178. if command:
  179. current_app.logger.info(command)
  180. # TODO
  181. target_release_version = "%s/%s" % (self.project_info['target_releases'], self.release_version)
  182. with waller.cd(target_release_version):
  183. result = waller.run(command, wenv=self.config())
  184. def release(self, waller):
  185. '''
  186. 5.部署代码到目标机器做的任务
  187. - 打包代码 local
  188. - scp local => remote
  189. - 解压 remote
  190. :return:
  191. '''
  192. self.stage = self.stage_release
  193. self.sequence = 5
  194. with waller.cd(self.project_info['target_releases']):
  195. # 0. get previous link
  196. command = 'readlink ' + self.project_info['target_root']
  197. result = waller.run(command, wenv=self.config(console=False))
  198. self.previous_release_version = os.path.basename(result.stdout).strip()
  199. # 1. create a tmp link dir
  200. current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
  201. command = 'ln -sfn %s/%s %s' % (
  202. self.project_info['target_releases'], self.release_version, current_link_tmp_dir)
  203. result = waller.run(command, wenv=self.config())
  204. # 2. make a soft link from release to tmp link
  205. # 3. move tmp link to webroot
  206. current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
  207. command = 'mv -fT %s %s' % (current_link_tmp_dir, self.project_info['target_root'])
  208. result = waller.run(command, wenv=self.config())
  209. def rollback(self, waller):
  210. '''
  211. 5.部署代码到目标机器做的任务
  212. - 恢复旧版本
  213. :return:
  214. '''
  215. self.stage = self.stage_release
  216. self.sequence = 5
  217. with waller.cd(self.project_info['target_releases']):
  218. # 0. get previous link
  219. command = 'readlink ' + self.project_info['target_root']
  220. result = waller.run(command, wenv=self.config(console=False))
  221. self.previous_release_version = os.path.basename(result.stdout)
  222. # 1. create a tmp link dir
  223. current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
  224. command = 'ln -sfn %s/%s %s' % (
  225. self.project_info['target_releases'], self.release_version, current_link_tmp_dir)
  226. result = waller.run(command, wenv=self.config())
  227. # 2. make a soft link from release to tmp link
  228. # 3. move tmp link to webroot
  229. current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
  230. command = 'mv -fT %s %s' % (current_link_tmp_dir, self.project_info['target_root'])
  231. result = waller.run(command, wenv=self.config())
  232. def release_untar(self, waller):
  233. '''
  234. 解压版本包
  235. :return:
  236. '''
  237. with waller.cd(self.project_info['target_releases']):
  238. command = 'tar zxf %s' % (self.release_version_tar)
  239. result = waller.run(command, wenv=self.config())
  240. def post_release(self, waller):
  241. '''
  242. 6.部署代码到目标机器后要做的任务
  243. - 切换软链
  244. - 重启 nginx
  245. :return:
  246. '''
  247. self.stage = self.stage_post_release
  248. self.sequence = 6
  249. # 用户自定义命令
  250. command = self.project_info['post_release']
  251. if command:
  252. current_app.logger.info(command)
  253. with waller.cd(self.project_info['target_root']):
  254. result = waller.run(command, wenv=self.config())
  255. # self.post_release_service(waller)
  256. def post_release_service(self, waller):
  257. '''
  258. 代码部署完成后,服务启动工作,如: nginx重启
  259. :param connection:
  260. :return:
  261. '''
  262. with waller.cd(self.project_info['target_root']):
  263. command = 'sudo service nginx restart'
  264. result = waller.run(command, wenv=self.config())
  265. def project_detection(self):
  266. errors = []
  267. # LOCAL_SERVER_USER => git
  268. # LOCAL_SERVER_USER => target_servers
  269. for server_info in self.servers:
  270. waller = Waller(host=server_info['host'], user=server_info['user'], port=server_info['port'])
  271. result = waller.run('id', exception=False, wenv=self.config())
  272. if result.failed:
  273. errors.append({
  274. 'title': '远程目标机器免密码登录失败',
  275. 'why': '远程目标机器:%s 错误:%s' % (server_info['host'], result.stdout),
  276. 'how': '在宿主机中配置免密码登录,把宿主机用户%s的~/.ssh/ssh_rsa.pub添加到远程目标机器用户%s的~/.ssh/authorized_keys。了解更多:http://walle-web.io/docs/troubleshooting.html' % (
  277. pwd.getpwuid(os.getuid())[0], server_info['host']),
  278. })
  279. # 检查 webroot 父目录是否存在,是否为软链
  280. command = '[ -L "%s" ] && echo "true" || echo "false"' % (self.project_info['target_root'])
  281. result = waller.run(command, exception=False, wenv=self.config())
  282. if result.stdout == 'false':
  283. errors.append({
  284. 'title': '远程目标机器webroot不能是已建好的目录',
  285. 'why': '远程目标机器%s webroot不能是已存在的目录,必须为软链接,你不必新建,walle会自行创建。' % (server_info['host']),
  286. 'how': '手工删除远程目标机器:%s webroot目录:%s' % (server_info['host'], self.project_info['target_root']),
  287. })
  288. # remote release directory
  289. return errors
  290. def list_tag(self):
  291. self.init_repo()
  292. with self.localhost.cd(self.dir_codebase_project):
  293. command = 'git tag -l'
  294. result = self.localhost.local(command, pty=False, wenv=self.config())
  295. tags = result.stdout.strip()
  296. tags = tags.split('\n')
  297. return [color_clean(tag.strip()) for tag in tags]
  298. return None
  299. def list_branch(self):
  300. self.init_repo()
  301. with self.localhost.cd(self.dir_codebase_project):
  302. command = 'git pull'
  303. result = self.localhost.local(command, wenv=self.config())
  304. if result.exited != Code.Ok:
  305. raise WalleError(Code.shell_git_pull_fail, message=result.stdout)
  306. current_app.logger.info(self.dir_codebase_project)
  307. command = 'git branch -r'
  308. result = self.localhost.local(command, pty=False, wenv=self.config())
  309. # if result.exited != Code.Ok:
  310. # raise WalleError(Code.shell_run_fail)
  311. # TODO 三种可能: false, error, success
  312. branches = result.stdout.strip()
  313. branches = branches.split('\n')
  314. # 去除 origin/HEAD -> 当前指向
  315. # 去除远端前缀
  316. branches = [branch.strip().lstrip('origin/') for branch in branches if
  317. not branch.strip().startswith('origin/HEAD')]
  318. return branches
  319. return None
  320. def list_commit(self, branch):
  321. self.init_repo()
  322. with self.localhost.cd(self.dir_codebase_project):
  323. command = 'git checkout %s && git pull' % (branch)
  324. self.localhost.local(command, wenv=self.config())
  325. command = 'git log -50 --pretty="%h #@_@# %an #@_@# %s"'
  326. result = self.localhost.local(command, pty=False, wenv=self.config())
  327. current_app.logger.info(result.stdout)
  328. commit_log = result.stdout.strip()
  329. current_app.logger.info(commit_log)
  330. commit_list = commit_log.split('\n')
  331. commits = []
  332. for commit in commit_list:
  333. if not re.search('^.+ #@_@# .+ #@_@# .*$', commit):
  334. continue
  335. commit_dict = commit.split(' #@_@# ')
  336. current_app.logger.info(commit_dict)
  337. commits.append({
  338. 'id': commit_dict[0],
  339. 'name': commit_dict[1],
  340. 'message': commit_dict[2],
  341. })
  342. return commits
  343. # TODO
  344. return None
  345. def init_repo(self):
  346. if not os.path.exists(self.dir_codebase_project):
  347. # 检查 目录是否存在
  348. command = 'mkdir -p %s' % (self.dir_codebase_project)
  349. self.localhost.local(command, wenv=self.config())
  350. with self.localhost.cd(self.dir_codebase_project):
  351. is_git_dir = self.localhost.local('[ -d ".git" ] && git status', exception=False, wenv=self.config())
  352. if is_git_dir.exited != Code.Ok:
  353. # 否则当作新项目检出完整代码
  354. # 检查 目录是否存在
  355. command = 'rm -rf %s' % (self.dir_codebase_project)
  356. self.localhost.local(command, wenv=self.config())
  357. command = 'git clone %s %s' % (self.project_info['repo_url'], self.dir_codebase_project)
  358. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  359. result = self.localhost.local(command, wenv=self.config())
  360. if result.exited != Code.Ok:
  361. raise WalleError(Code.shell_git_init_fail, message=result.stdout)
  362. def logs(self):
  363. return RecordModel().fetch(task_id=self.task_id)
  364. def end(self, success=True, update_status=True):
  365. if update_status:
  366. status = TaskModel.status_success if success else TaskModel.status_fail
  367. current_app.logger.info('success:%s, status:%s' % (success, status))
  368. TaskModel().get_by_id(self.task_id).update({
  369. 'status': status,
  370. 'link_id': self.release_version,
  371. 'ex_link_id': self.previous_release_version,
  372. })
  373. notice_info = {
  374. 'title': '',
  375. 'username': current_user.username,
  376. 'project_name': self.project_info['name'],
  377. 'task_name': '%s ([%s](%s))' % (self.taskMdl.get('name'), self.task_id, Notice.task_url(project_name=self.project_info['name'], task_id=self.task_id)),
  378. 'branch': self.taskMdl.get('branch'),
  379. 'commit': self.taskMdl.get('commit_id'),
  380. 'is_branch': self.project_info['repo_mode'],
  381. }
  382. notice = Notice.create(self.project_info['notice_type'])
  383. if success:
  384. emit('success', {'event': 'finish', 'data': {'message': '部署完成,辛苦了,为你的努力喝彩!'}}, room=self.task_id)
  385. notice_info['title'] = '上线部署成功'
  386. notice.deploy_task(project_info=self.project_info, notice_info=notice_info)
  387. else:
  388. emit('fail', {'event': 'finish', 'data': {'message': Code.code_msg[Code.deploy_fail]}}, room=self.task_id)
  389. notice_info['title'] = '上线部署失败'
  390. notice.deploy_task(project_info=self.project_info, notice_info=notice_info)
  391. def walle_deploy(self):
  392. self.start()
  393. try:
  394. self.prev_deploy()
  395. self.deploy()
  396. self.post_deploy()
  397. is_all_servers_success = True
  398. for server_info in self.servers:
  399. host = server_info['host']
  400. try:
  401. self.connections[host] = Waller(host=host, user=server_info['user'], port=server_info['port'])
  402. self.prev_release(self.connections[host])
  403. self.release(self.connections[host])
  404. self.post_release(self.connections[host])
  405. except Exception as e:
  406. is_all_servers_success = False
  407. current_app.logger.error(e)
  408. self.errors[host] = e.message
  409. self.end(is_all_servers_success)
  410. except Exception as e:
  411. self.end(False)
  412. return {'success': self.success, 'errors': self.errors}
  413. def walle_rollback(self):
  414. self.start()
  415. try:
  416. is_all_servers_success = True
  417. self.release_version = self.taskMdl.get('link_id')
  418. for server_info in self.servers:
  419. host = server_info['host']
  420. try:
  421. self.connections[host] = Waller(host=host, user=server_info['user'], port=server_info['port'])
  422. self.prev_release_custom(self.connections[host])
  423. self.release(self.connections[host])
  424. self.post_release(self.connections[host])
  425. except Exception as e:
  426. is_all_servers_success = False
  427. current_app.logger.error(e)
  428. self.errors[host] = e.message
  429. self.end(is_all_servers_success)
  430. except Exception as e:
  431. self.end(False)
  432. return {'success': self.success, 'errors': self.errors}