publish.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. from public import app
  2. from flask import Blueprint, g
  3. from apps.deploy.models import App, History
  4. from apps.assets.models import Host
  5. from apps.configuration.models import Environment
  6. from libs.tools import human_time, json_response, JsonParser, Argument
  7. from libs.decorators import with_app_context
  8. from libs.decorators import require_permission
  9. from libs.tools import QueuePool
  10. from apps.deploy.utils import *
  11. from threading import Thread
  12. from io import BytesIO
  13. from functools import partial
  14. import tarfile
  15. import uuid
  16. import time
  17. import os
  18. blueprint = Blueprint(__name__, __name__)
  19. @blueprint.route('/history/<int:app_id>', methods=['GET'])
  20. @require_permission('publish_app_publish_deploy')
  21. def history(app_id):
  22. histories = History.query.filter(
  23. History.app_id == app_id,
  24. History.deploy_message != '').order_by(History.created.desc()).limit(20).all()
  25. return json_response(histories)
  26. @blueprint.route('/update', methods=['POST'])
  27. @require_permission('publish_app_publish_deploy')
  28. def app_update():
  29. form, error = JsonParser(
  30. Argument('app_id', type=int),
  31. Argument('env_id', type=int),
  32. Argument('deploy_message', default=''),
  33. Argument('deploy_restart', type=bool),
  34. Argument('host_ids', type=list)
  35. ).parse()
  36. if error is None:
  37. if not g.user.check_deploy_permission(form.env_id, form.app_id):
  38. return json_response(message='Permission denied'), 403
  39. token = uuid.uuid4().hex
  40. q = QueuePool.make_queue(token, len(form.host_ids))
  41. for host_id in form.pop('host_ids'):
  42. Thread(target=do_update, args=(q, form, host_id)).start()
  43. return json_response(token)
  44. return json_response(message=error)
  45. @with_app_context
  46. def do_update(q, form, host_id):
  47. ctr, api_token, deploy_success = None, None, False
  48. history = History(host_id=host_id, created=human_time(), deploy_success=deploy_success, **form).save()
  49. send_message = partial(PublishMessage.make_message, q, host_id)
  50. try:
  51. send_message('正在检测环境 . . . ')
  52. pro = App.query.get_or_404(form.app_id)
  53. env = Environment.query.get_or_404(form.env_id)
  54. cli = Host.query.get_or_404(host_id)
  55. hooks = {x.name: x.command for x in DeployMenu.query.filter_by(app_id=form.app_id)}
  56. ctr = Container(cli.docker_uri, pro.identify + '.' + env.identify)
  57. image = app.config['DOCKER_REGISTRY_SERVER'] + '/' + pro.image.name
  58. image_tag = pro.image.latest
  59. ctr_info = ctr.info
  60. if ctr.api_version < '1.21':
  61. send_message('环境检测失败,docker版本过低,请升级至1.9.x以上!', level='error')
  62. return
  63. else:
  64. send_message('环境检测完成!', update=True)
  65. # 当容器不存在或镜像有更新时,需要获取新镜像并使用新镜像重新创建容器
  66. if ctr_info is None or ctr_info.Image != image + ':' + image_tag:
  67. send_message('正在更新镜像,版本{0} . . . '.format(image_tag))
  68. ctr.pull_image(image, image_tag)
  69. send_message('镜像更新完成!', update=True)
  70. if ctr_info:
  71. send_message('正在删除原有容器 . . .')
  72. ctr.remove()
  73. send_message('删除原有容器成功!', update=True)
  74. send_message('正在创建新容器 . . . ')
  75. api_token = create_container(ctr, pro, env, image='{0}:{1}'.format(image, image_tag))
  76. history.update(api_token=api_token)
  77. send_message('创建新容器成功!', update=True)
  78. # 复制文件
  79. send_message('正在初始化容器 . . . ')
  80. tar_buffer = BytesIO()
  81. with tarfile.open(fileobj=tar_buffer, mode='w') as tar:
  82. add_file_to_tar(tar, os.path.join(app.config['BASE_DIR'], 'libs', 'scripts', 'entrypoint.sh'))
  83. # add_file_to_tar(tar, os.path.join(app.config['BASE_DIR'], 'libs', 'scripts', 'proxy_execute.sh'))
  84. ctr.put_archive('/', tar_buffer.getvalue())
  85. send_message('初始化容器成功!', update=True)
  86. # 启动容器
  87. send_message('正在启动新容器 . . . ')
  88. ctr.start()
  89. send_message('启动新容器成功!', update=True)
  90. # 执行init钩子
  91. send_message('正在执行应用初始化 . . .')
  92. exec_code, exec_output = ctr.exec_command_with_base64(hooks['容器创建'], timeout=120, with_exit_code=True)
  93. if exec_code != 0:
  94. send_message('执行应用初始化失败,退出状态码:{0}'.format(exec_code), level='error')
  95. send_message(exec_output, level='console')
  96. return
  97. else:
  98. send_message('执行应用初始化成功!', update=True)
  99. # 清理无用镜像
  100. if ctr.client.api_version >= "1.25":
  101. ctr.prune_images()
  102. # 当前容器如果为退出状态,则启动容器
  103. elif not ctr_info.running:
  104. send_message('容器当前为停止状态,正在启动容器 . . . ')
  105. ctr.start()
  106. send_message('启动容器成功!', update=True)
  107. # 执行发布操作
  108. send_message('正在执行应用更新 . . . ')
  109. exec_code, exec_output = ctr.exec_command_with_base64(hooks['应用发布'], form.deploy_message, timeout=120,
  110. with_exit_code=True)
  111. if exec_code != 0:
  112. send_message('执行应用更新失败,退出状态码:{0}'.format(exec_code), level='error')
  113. send_message(exec_output, level='console')
  114. return
  115. else:
  116. send_message('执行应用更新成功!', update=True)
  117. # 根据选择执行重启容器操作
  118. if form.deploy_restart:
  119. send_message('正在重启容器 . . . ')
  120. ctr.restart(timeout=3)
  121. send_message('重启容器成功!', update=True)
  122. # 整个流程正常结束
  123. send_message('完成发布!', level='success')
  124. deploy_success = True
  125. except Exception as e:
  126. send_message('%s' % e, level='error')
  127. raise e
  128. finally:
  129. q.done()
  130. if deploy_success:
  131. history.update(deploy_success=True)
  132. class PublishMessage(object):
  133. start_time = time.time()
  134. @classmethod
  135. def make_message(cls, q, host_id, message, level='info', update=False):
  136. data = {
  137. 'hid': host_id,
  138. 'msg': message,
  139. 'level': level,
  140. 'update': update
  141. }
  142. if update:
  143. duration = time.time() - cls.start_time
  144. cls.start_time = time.time()
  145. data['duration'] = duration
  146. q.put(data)