app.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. from flask import Blueprint, request, g
  2. from apps.deploy.models import App, AppHostRel, DeployMenu
  3. from apps.configuration.models import ConfigKey, AppConfigRel, Environment
  4. from apps.assets.models import Host
  5. from libs.tools import json_response, JsonParser, Argument, AttrDict
  6. from libs.utils import Container
  7. from docker.errors import DockerException
  8. from datetime import datetime
  9. from public import db
  10. from libs.decorators import require_permission
  11. from apps.deploy.utils import get_built_in_menus
  12. blueprint = Blueprint(__name__, __name__)
  13. args = AttrDict(
  14. group=Argument('group', help='请选择分组名称!'),
  15. name=Argument('name', help='请输入应用名称!'),
  16. desc=Argument('desc', help='请输入应用描述!'),
  17. notify_way_id=Argument('notify_way_id', type=int, help='请选择通知方式!'),
  18. identify=Argument('identify', help='请输入应用标识!'),
  19. image_id=Argument('image_id', type=int, help='请选择应用使用的Docker镜像!')
  20. )
  21. @blueprint.route('/', methods=['GET'])
  22. @require_permission('publish_app_view | config_app_view | publish_field_rel_view')
  23. def get():
  24. group = request.args.get('group')
  25. query = App.query
  26. if group:
  27. query = query.filter_by(group=group)
  28. if g.user.is_supper:
  29. apps = query.all()
  30. else:
  31. app_ids = g.user.role.app_ids
  32. if app_ids:
  33. apps = query.filter(App.id.in_(app_ids.split(','))).all()
  34. else:
  35. apps = []
  36. data_list = []
  37. for i in apps:
  38. data = i.to_json()
  39. data['notify_way_name'] = i.notify_way.name if i.notify_way else ''
  40. data['images'] = i.image.name
  41. data_list.append(data)
  42. return json_response(data_list)
  43. @blueprint.route('/', methods=['POST'])
  44. @require_permission('publish_app_add')
  45. def post():
  46. form, error = JsonParser(*args.values()).parse()
  47. if error is None:
  48. if App.query.filter_by(identify=form.identify).first():
  49. return json_response(message='应用标识不能重复!')
  50. app = App(**form)
  51. app.create_date = datetime.now()
  52. app.edit_date = datetime.now()
  53. app.save()
  54. if not g.user.is_supper:
  55. g.user.role.update(app_ids=g.user.role.app_ids + ',%d' % app.id)
  56. return json_response(app)
  57. return json_response(message=error)
  58. @blueprint.route('/<int:app_id>', methods=['DELETE'])
  59. @require_permission('publish_app_del')
  60. def delete(app_id):
  61. app = App.query.get_or_404(app_id)
  62. if AppHostRel.query.filter_by(app_id=app_id).first():
  63. return json_response(message='请先取消与已发布主机的关联后再尝试删除应用!')
  64. rel = AppConfigRel.query.filter_by(d_id=app_id, d_type='app').first()
  65. if rel:
  66. rel_app = App.query.get_or_404(rel.s_id)
  67. return json_response(message='应用 <%s> 引用了该应用,请解除关联后再尝试删除该应用!' % rel_app.name)
  68. app_keys = ConfigKey.query.filter_by(owner_id=app_id, owner_type='app')
  69. if [x for x in app_keys.all() if x.type != 'system']:
  70. return json_response(message='为了安全,请删除该应用下的所有配置后再尝试删除该应用!')
  71. app_keys.delete()
  72. app.delete()
  73. return json_response()
  74. @blueprint.route('/<int:app_id>', methods=['PUT'])
  75. @require_permission('publish_app_edit')
  76. def put(app_id):
  77. form, error = JsonParser(*args.values()).parse()
  78. if error is None:
  79. exists_record = App.query.filter_by(identify=form.identify).first()
  80. if exists_record and exists_record.id != app_id:
  81. return json_response(message='应用标识不能重复!')
  82. app = App.query.get_or_404(app_id)
  83. app.update(**form)
  84. app.save()
  85. return json_response(app)
  86. return json_response(message=error)
  87. @blueprint.route('/<int:app_id>/bind/hosts', methods=['POST'])
  88. @require_permission('publish_app_publish_view')
  89. def bind_hosts(app_id):
  90. form, error = JsonParser('ids', 'env_id').parse()
  91. if error is None:
  92. pro = App.query.get_or_404(app_id)
  93. env = Environment.query.get_or_404(form.env_id)
  94. old_relationships = AppHostRel.query.filter_by(env_id=form.env_id, app_id=app_id).all()[:]
  95. for host_id in form.ids:
  96. rel = AppHostRel(env_id=form.env_id, app_id=app_id, host_id=host_id)
  97. if rel in old_relationships:
  98. old_relationships.remove(rel)
  99. else:
  100. rel.add()
  101. for old_rel in old_relationships:
  102. host = Host.query.get_or_404(old_rel.host_id)
  103. try:
  104. app_process = Container(host.docker_uri, '%s.%s' % (pro.identify, env.identify)).info
  105. if app_process:
  106. return json_response(message='在主机 <%s> 上已经部署了该应用,请删除已部署的容器后再尝试解除关联!' % host.name)
  107. except DockerException:
  108. pass
  109. old_rel.delete(commit=False)
  110. db.session.commit()
  111. return json_response()
  112. return json_response(message=error)
  113. @blueprint.route('/groups/', methods=['GET'])
  114. @require_permission('publish_app_view')
  115. def fetch_groups():
  116. apps = db.session.query(App.group.distinct().label('group')).all()
  117. return json_response([x.group for x in apps])
  118. # 用户在发布详情页获取自定义的展示字段,需具有应用发布权限
  119. @blueprint.route('/<int:app_id>/fields', methods=['GET'])
  120. @require_permission('publish_app_publish_view')
  121. def fetch_fields(app_id):
  122. pro = App.query.get_or_404(app_id)
  123. return json_response(pro.fields)
  124. # 用于在发布详情页面获取启用的自定义菜单,需具有执行自定义菜单的权限
  125. @blueprint.route('/<int:app_id>/menus', methods=['GET'])
  126. @require_permission('publish_app_publish_menu_exec|publish_app_menu_view')
  127. def fetch_menus(app_id):
  128. q_type = request.args.get('type')
  129. if q_type == 'built-in':
  130. menus = DeployMenu.query.filter_by(app_id=app_id).all()
  131. built_in_menus = get_built_in_menus()
  132. for item in menus:
  133. built_in_menus[item.name]['command'] = item.command
  134. return json_response(list(built_in_menus.values()))
  135. pro = App.query.get_or_404(app_id)
  136. if q_type == 'all':
  137. menus = pro.menus[:]
  138. menus.extend(DeployMenu.query.filter_by(app_id=app_id).all()[:])
  139. else:
  140. menus = pro.menus
  141. return json_response(menus)
  142. # 用于编辑内置发布用菜单,需要具有发布菜单编辑权限
  143. @blueprint.route('/<int:app_id>/bind/menus', methods=['POST'])
  144. @require_permission('publish_app_menu_edit')
  145. def bind_menus(app_id):
  146. all_valid_menus = get_built_in_menus()
  147. form, error = JsonParser(
  148. Argument('name', filter=lambda x: x in all_valid_menus, help='无效的菜单名称!', default=''),
  149. Argument('command', default='')
  150. ).parse()
  151. if error is None:
  152. if form.name is '': # 可能通过发布页提供的添加预定义菜单的请求,格式[{name: 'x', desc: 'x', command: 'x'} ...]
  153. post_data = request.get_json()
  154. if isinstance(post_data, list) and all([isinstance(x, dict) and x['name'] in all_valid_menus for x in post_data]):
  155. for item in post_data:
  156. tmp = all_valid_menus[item['name']]
  157. tmp['command'] = item.get('command')
  158. tmp['app_id'] = app_id
  159. DeployMenu.upsert({'app_id': app_id, 'name': item['name']}, **tmp)
  160. return json_response()
  161. else:
  162. return json_response(message='错误的参数!')
  163. form.desc = get_built_in_menus()[form.name]['desc']
  164. form.app_id = app_id
  165. DeployMenu.upsert({'app_id': app_id, 'name': form.name}, **form)
  166. return json_response(message=error)