views.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
  2. # Copyright: (c) <spug.dev@gmail.com>
  3. # Released under the AGPL-3.0 License.
  4. from django.views.generic import View
  5. from django.db.models import F
  6. from django.http.response import HttpResponseBadRequest
  7. from libs import json_response, JsonParser, Argument, AttrDict
  8. from apps.setting.utils import AppSetting
  9. from apps.account.utils import get_host_perms
  10. from apps.host.models import Host, Group
  11. from apps.host.utils import batch_sync_host, _sync_host_extend
  12. from apps.app.models import Deploy
  13. from apps.schedule.models import Task
  14. from apps.monitor.models import Detection
  15. from libs.ssh import SSH, AuthenticationException
  16. from paramiko.ssh_exception import BadAuthenticationType
  17. from openpyxl import load_workbook
  18. from threading import Thread
  19. import uuid
  20. class HostView(View):
  21. def get(self, request):
  22. hosts = Host.objects.select_related('hostextend')
  23. if not request.user.is_supper:
  24. hosts = hosts.filter(id__in=get_host_perms(request.user))
  25. hosts = {x.id: x.to_view() for x in hosts}
  26. for rel in Group.hosts.through.objects.filter(host_id__in=hosts.keys()):
  27. hosts[rel.host_id]['group_ids'].append(rel.group_id)
  28. return json_response(list(hosts.values()))
  29. def post(self, request):
  30. form, error = JsonParser(
  31. Argument('id', type=int, required=False),
  32. Argument('group_ids', type=list, filter=lambda x: len(x), help='请选择主机分组'),
  33. Argument('name', help='请输主机名称'),
  34. Argument('username', handler=str.strip, help='请输入登录用户名'),
  35. Argument('hostname', handler=str.strip, help='请输入主机名或IP'),
  36. Argument('port', type=int, help='请输入SSH端口'),
  37. Argument('pkey', required=False),
  38. Argument('desc', required=False),
  39. Argument('password', required=False),
  40. ).parse(request.body)
  41. if error is None:
  42. password = form.pop('password')
  43. private_key, public_key = AppSetting.get_ssh_key()
  44. try:
  45. if form.pkey:
  46. private_key = form.pkey
  47. elif password:
  48. with SSH(form.hostname, form.port, form.username, password=password) as ssh:
  49. ssh.add_public_key(public_key)
  50. with SSH(form.hostname, form.port, form.username, private_key) as ssh:
  51. ssh.ping()
  52. except BadAuthenticationType:
  53. return json_response(error='该主机不支持密钥认证,请参考官方文档,错误代码:E01')
  54. except AuthenticationException:
  55. if password:
  56. return json_response(error='密钥认证失败,请参考官方文档,错误代码:E02')
  57. return json_response('auth fail')
  58. group_ids = form.pop('group_ids')
  59. other = Host.objects.filter(name=form.name).first()
  60. if other and (not form.id or other.id != form.id):
  61. return json_response(error=f'已存在的主机名称【{form.name}】')
  62. if form.id:
  63. Host.objects.filter(pk=form.id).update(is_verified=True, **form)
  64. host = Host.objects.get(pk=form.id)
  65. else:
  66. host = Host.objects.create(created_by=request.user, is_verified=True, **form)
  67. _sync_host_extend(host, ssh=ssh)
  68. host.groups.set(group_ids)
  69. response = host.to_view()
  70. response['group_ids'] = group_ids
  71. return json_response(response)
  72. return json_response(error=error)
  73. def patch(self, request):
  74. form, error = JsonParser(
  75. Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择主机'),
  76. Argument('s_group_id', type=int, help='参数错误'),
  77. Argument('t_group_id', type=int, help='参数错误'),
  78. Argument('is_copy', type=bool, help='参数错误'),
  79. ).parse(request.body)
  80. if error is None:
  81. if form.t_group_id == form.s_group_id:
  82. return json_response(error='不能选择本分组的主机')
  83. s_group = Group.objects.get(pk=form.s_group_id)
  84. t_group = Group.objects.get(pk=form.t_group_id)
  85. t_group.hosts.add(*form.host_ids)
  86. if not form.is_copy:
  87. s_group.hosts.remove(*form.host_ids)
  88. return json_response(error=error)
  89. def delete(self, request):
  90. form, error = JsonParser(
  91. Argument('id', type=int, required=False),
  92. Argument('group_id', type=int, required=False),
  93. ).parse(request.GET)
  94. if error is None:
  95. if form.id:
  96. host_ids = [form.id]
  97. elif form.group_id:
  98. group = Group.objects.get(pk=form.group_id)
  99. host_ids = [x.id for x in group.hosts.all()]
  100. else:
  101. return json_response(error='参数错误')
  102. for host_id in host_ids:
  103. regex = fr'[^0-9]{host_id}[^0-9]'
  104. deploy = Deploy.objects.filter(host_ids__regex=regex) \
  105. .annotate(app_name=F('app__name'), env_name=F('env__name')).first()
  106. if deploy:
  107. return json_response(error=f'应用【{deploy.app_name}】在【{deploy.env_name}】的发布配置关联了该主机,请解除关联后再尝试删除该主机')
  108. task = Task.objects.filter(targets__regex=fr'[^0-9]{form.id}[^0-9]').first()
  109. if task:
  110. return json_response(error=f'任务计划中的任务【{task.name}】关联了该主机,请解除关联后再尝试删除该主机')
  111. detection = Detection.objects.filter(type__in=('3', '4'), targets__regex=regex).first()
  112. if detection:
  113. return json_response(error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机')
  114. Host.objects.filter(id__in=host_ids).delete()
  115. return json_response(error=error)
  116. def post_import(request):
  117. group_id = request.POST.get('group_id')
  118. file = request.FILES['file']
  119. ws = load_workbook(file, read_only=True)['Sheet1']
  120. summary = {'invalid': [], 'skip': [], 'repeat': [], 'success': []}
  121. for i, row in enumerate(ws.rows):
  122. if i == 0: # 第1行是表头 略过
  123. continue
  124. if not all([row[x].value for x in range(4)]):
  125. summary['invalid'].append(i)
  126. continue
  127. data = AttrDict(
  128. name=row[0].value,
  129. hostname=row[1].value,
  130. port=row[2].value,
  131. username=row[3].value,
  132. desc=row[4].value
  133. )
  134. if Host.objects.filter(hostname=data.hostname, port=data.port, username=data.username).exists():
  135. summary['skip'].append(i)
  136. continue
  137. if Host.objects.filter(name=data.name).exists():
  138. summary['repeat'].append(i)
  139. continue
  140. host = Host.objects.create(created_by=request.user, **data)
  141. host.groups.add(group_id)
  142. summary['success'].append(i)
  143. return json_response(summary)
  144. def post_parse(request):
  145. file = request.FILES['file']
  146. if file:
  147. data = file.read()
  148. return json_response(data.decode())
  149. else:
  150. return HttpResponseBadRequest()
  151. def batch_valid(request):
  152. form, error = JsonParser(
  153. Argument('password', required=False),
  154. Argument('range', filter=lambda x: x in ('1', '2'), help='参数错误')
  155. ).parse(request.body)
  156. if error is None:
  157. if form.range == '1': # all hosts
  158. hosts = Host.objects.all()
  159. else:
  160. hosts = Host.objects.filter(is_verified=False).all()
  161. token = uuid.uuid4().hex
  162. Thread(target=batch_sync_host, args=(token, hosts, form.password)).start()
  163. return json_response({'token': token, 'hosts': {x.id: {'name': x.name} for x in hosts}})
  164. return json_response(error=error)