deployer.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  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 flask import current_app
  10. from walle.model.record import RecordModel
  11. from walle.service.waller import Waller
  12. from walle.model.project import ProjectModel
  13. from walle.model.task import TaskModel
  14. from flask_socketio import emit
  15. from walle.service.extensions import socketio
  16. from walle.service.utils import color_clean
  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. # 检查 git 版本
  93. command = 'git --version'
  94. result = self.local.run(command, wenv=self.config())
  95. # 检查 目录是否存在
  96. self.init_repo()
  97. # TODO to be removed
  98. command = 'mkdir -p %s' % (self.dir_codebase_project)
  99. result = self.local.run(command, wenv=self.config())
  100. # 用户自定义命令
  101. command = self.project_info['prev_deploy']
  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. # TODO
  112. socketio.sleep(0.001)
  113. self.stage = self.stage_deploy
  114. self.sequence = 2
  115. current_app.logger.info('git dir: %s', self.dir_codebase_project + '/.git')
  116. # 如果项目底下有 .git 目录则认为项目完整,可以直接检出代码
  117. # TODO 不标准
  118. if os.path.exists(self.dir_codebase_project + '/.git'):
  119. with self.local.cd(self.dir_codebase_project):
  120. command = 'pwd && git pull'
  121. result = self.local.run(command, wenv=self.config())
  122. else:
  123. # 否则当作新项目检出完整代码
  124. with self.local.cd(self.dir_codebase_project):
  125. command = 'pwd && git clone %s .' % (self.project_info['repo_url'])
  126. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  127. result = self.local.run(command, wenv=self.config())
  128. # copy to a local version
  129. self.release_version = '%s_%s_%s' % (
  130. self.project_name, self.task_id, time.strftime('%Y%m%d_%H%M%S', time.localtime(time.time())))
  131. with self.local.cd(self.dir_codebase):
  132. command = 'cp -rf %s %s' % (self.dir_codebase_project, self.release_version)
  133. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  134. result = self.local.run(command, wenv=self.config())
  135. # 更新到指定 commit_id
  136. with self.local.cd(self.dir_codebase + self.release_version):
  137. command = 'git reset -q --hard %s' % (self.taskMdl.get('commit_id'))
  138. result = self.local.run(command, wenv=self.config())
  139. pass
  140. def post_deploy(self):
  141. '''
  142. 3.检出代码后要做的任务
  143. - 用户自定义操作命令
  144. - 代码编译
  145. - 清除日志文件及无用文件
  146. -
  147. - 压缩打包
  148. - 传送到版本库 release
  149. :return:
  150. '''
  151. # TODO
  152. socketio.sleep(0.001)
  153. self.stage = self.stage_post_deploy
  154. self.sequence = 3
  155. # 用户自定义命令
  156. command = self.project_info['post_deploy']
  157. with self.local.cd(self.dir_codebase + self.release_version):
  158. result = self.local.run(command, wenv=self.config())
  159. # 压缩打包
  160. self.release_version_tar = '%s.tgz' % (self.release_version)
  161. with self.local.cd(self.dir_codebase):
  162. command = 'tar zcvf %s %s' % (self.release_version_tar, 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. # TODO
  171. socketio.sleep(0.001)
  172. self.stage = self.stage_prev_release
  173. self.sequence = 4
  174. # 检查 target_releases 父目录是否存在
  175. command = 'mkdir -p %s' % (self.project_info['target_releases'])
  176. result = waller.run(command, wenv=self.config())
  177. # TODO 检查 webroot 父目录是否存在,是否为软链
  178. # command = 'mkdir -p %s' % (self.project_info['target_root'])
  179. # result = waller.run(command)
  180. # current_app.logger.info('command: %s', dir(result))
  181. # 用户自定义命令
  182. command = self.project_info['prev_release']
  183. current_app.logger.info(command)
  184. with waller.cd(self.project_info['target_releases']):
  185. result = waller.run(command, wenv=self.config())
  186. # TODO md5
  187. # 传送到版本库 release
  188. current_app.logger.info('/tmp/walle/codebase/' + self.release_version_tar)
  189. result = waller.put('/tmp/walle/codebase/' + self.release_version_tar,
  190. remote=self.project_info['target_releases'], wenv=self.config())
  191. current_app.logger.info('command: %s', dir(result))
  192. # 解压
  193. self.release_untar(waller)
  194. def release(self, waller):
  195. '''
  196. 5.部署代码到目标机器做的任务
  197. - 打包代码 local
  198. - scp local => remote
  199. - 解压 remote
  200. :return:
  201. '''
  202. # TODO
  203. socketio.sleep(0.001)
  204. self.stage = self.stage_release
  205. self.sequence = 5
  206. with waller.cd(self.project_info['target_releases']):
  207. # 1. create a tmp link dir
  208. current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
  209. command = 'ln -sfn %s/%s %s' % (
  210. self.project_info['target_releases'], self.release_version, current_link_tmp_dir)
  211. result = waller.run(command, wenv=self.config())
  212. # 2. make a soft link from release to tmp link
  213. # 3. move tmp link to webroot
  214. current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
  215. command = 'mv -fT %s %s' % (current_link_tmp_dir, self.project_info['target_root'])
  216. result = waller.run(command, wenv=self.config())
  217. def release_untar(self, waller):
  218. '''
  219. 解压版本包
  220. :return:
  221. '''
  222. # TODO
  223. socketio.sleep(0.001)
  224. with waller.cd(self.project_info['target_releases']):
  225. command = 'tar zxf %s' % (self.release_version_tar)
  226. result = waller.run(command, wenv=self.config())
  227. def post_release(self, waller):
  228. '''
  229. 6.部署代码到目标机器后要做的任务
  230. - 切换软链
  231. - 重启 nginx
  232. :return:
  233. '''
  234. self.stage = self.stage_post_release
  235. self.sequence = 6
  236. # 用户自定义命令
  237. command = self.project_info['post_release']
  238. current_app.logger.info(command)
  239. with waller.cd(self.project_info['target_root']):
  240. result = waller.run(command, wenv=self.config())
  241. self.post_release_service(waller)
  242. def post_release_service(self, waller):
  243. '''
  244. 代码部署完成后,服务启动工作,如: nginx重启
  245. :param connection:
  246. :return:
  247. '''
  248. with waller.cd(self.project_info['target_root']):
  249. command = 'sudo service nginx restart'
  250. result = waller.run(command, wenv=self.config())
  251. def list_tag(self):
  252. self.init_repo()
  253. with self.local.cd(self.dir_codebase_project):
  254. command = 'git tag -l'
  255. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  256. result = self.local.run(command, wenv=self.config())
  257. current_app.logger.info(dir(result))
  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. from flask import current_app
  265. from walle.service import utils
  266. current_app.logger.info(utils.detailtrace())
  267. result = self.local.run(command, wenv=self.config())
  268. current_app.logger.info(self.dir_codebase_project)
  269. command = 'git branch -r'
  270. result = self.local.run(command, wenv=self.config())
  271. # TODO 三种可能: false, error, success
  272. branches = color_clean(result.stdout.strip())
  273. branches = branches.split('\n')
  274. # 去除 origin/HEAD -> 当前指向
  275. # 去除远端前缀
  276. branches = [branch.strip().lstrip('origin/') for branch in branches if not branch.strip().startswith('origin/HEAD')]
  277. return branches
  278. return None
  279. def list_commit(self, branch):
  280. self.init_repo()
  281. with self.local.cd(self.dir_codebase_project):
  282. command = 'git checkout %s && git pull' % (branch)
  283. # result = self.local.run(command, wenv=self.config())
  284. # TODO 10是需要前端传的
  285. command = 'git log -10 --pretty="%h #_# %an #_# %s"'
  286. result = self.local.run(command, wenv=self.config())
  287. current_app.logger.info(result.stdout.strip())
  288. commit_log = color_clean(result.stdout.strip())
  289. current_app.logger.info(commit_log)
  290. commit_list = commit_log.split('\n')
  291. commits = []
  292. for commit in commit_list:
  293. commit_dict = commit.split(' #_# ')
  294. current_app.logger.info(commit_dict)
  295. commits.append({
  296. 'id': commit_dict[0],
  297. 'name': commit_dict[1],
  298. 'message': commit_dict[2],
  299. })
  300. return commits
  301. return None
  302. def init_repo(self):
  303. current_app.logger.info('git dir: %s', self.dir_codebase_project + '/.git')
  304. # 如果项目底下有 .git 目录则认为项目完整,可以直接检出代码
  305. # TODO 不标准
  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. result = self.local.run(command, wenv=self.config())
  312. if not os.path.exists(self.dir_codebase_project + '/.git'):
  313. # 否则当作新项目检出完整代码
  314. with self.local.cd(self.dir_codebase_project):
  315. command = 'pwd && git clone %s .' % (self.project_info['repo_url'])
  316. current_app.logger.info('cd %s command: %s ', self.dir_codebase_project, command)
  317. result = self.local.run(command, wenv=self.config())
  318. def end(self, success=True):
  319. status = TaskModel.status_success if success else TaskModel.status_fail
  320. TaskModel().get_by_id(self.task_id).update({'status': status})
  321. def walle_deploy(self):
  322. self.start()
  323. self.prev_deploy()
  324. self.deploy()
  325. self.post_deploy()
  326. all_servers_success = True
  327. for server_info in self.servers:
  328. server = server_info['host']
  329. try:
  330. self.connections[server] = Waller(host=server, user=self.project_info['target_user'])
  331. self.prev_release(self.connections[server])
  332. self.release(self.connections[server])
  333. self.post_release(self.connections[server])
  334. except Exception as e:
  335. all_servers_success = False
  336. self.errors[server] = e.message
  337. self.end(all_servers_success)
  338. return {'success': self.success, 'errors': self.errors}
  339. def test(self):
  340. # server = '172.20.95.43'
  341. server = '172.16.0.231'
  342. ws_dict = {
  343. 'user': 'xx',
  344. 'host': 'ip',
  345. 'cmd': 'Going to sleep !!!!!',
  346. 'status': 0,
  347. 'stage': 1,
  348. 'sequence': 1,
  349. 'success': 'Going to sleep !!!!!',
  350. 'error': '',
  351. }
  352. emit('console', {'event': 'task:console', 'data': ws_dict}, room=self.task_id)
  353. socketio.sleep(60)
  354. ws_dict = {
  355. 'user': 'xx',
  356. 'host': 'ip',
  357. 'cmd': 'sleep 60',
  358. 'status': 0,
  359. 'stage': 1,
  360. 'sequence': 1,
  361. 'success': 'sleep 60....',
  362. 'error': '',
  363. }
  364. emit('console', {'event': 'task:console', 'data': ws_dict}, room=self.task_id)
  365. socketio.sleep(10)
  366. ws_dict = {
  367. 'user': 'xx',
  368. 'host': 'ip',
  369. 'cmd': 'sleep 10',
  370. 'status': 0,
  371. 'stage': 1,
  372. 'sequence': 1,
  373. 'success': 'sleep 10....',
  374. 'error': '',
  375. }
  376. emit('console', {'event': 'task:console', 'data': ws_dict}, room=self.task_id)
  377. try:
  378. self.connections[server] = Waller(host=server, user='work')
  379. self.post_release_service(self.connections[server])
  380. except Exception as e:
  381. current_app.logger.exception(e)
  382. self.errors[server] = e.message