deployer.py 15 KB

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