# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug # Copyright: (c) # Released under the AGPL-3.0 License. from django.views.generic import View from django.db.models import F from django.http.response import HttpResponseBadRequest, HttpResponse from libs import json_response, JsonParser, Argument, AttrDict, auth from apps.setting.utils import AppSetting from apps.account.utils import get_host_perms from apps.host.models import Host, Group from apps.host.utils import batch_sync_host, _sync_host_extend from apps.app.models import Deploy from apps.schedule.models import Task from apps.monitor.models import Detection from apps.exec.models import ExecTemplate from libs.ssh import SSH, AuthenticationException from paramiko.ssh_exception import BadAuthenticationType from openpyxl import load_workbook, Workbook from threading import Thread import socket import uuid import json class HostView(View): def get(self, request): hosts = Host.objects.select_related('hostextend') if not request.user.is_supper: hosts = hosts.filter(id__in=get_host_perms(request.user)) hosts = {x.id: x.to_view() for x in hosts} for rel in Group.hosts.through.objects.filter(host_id__in=hosts.keys()): hosts[rel.host_id]['group_ids'].append(rel.group_id) return json_response(list(hosts.values())) @auth('host.host.add|host.host.edit') def post(self, request): form, error = JsonParser( Argument('id', type=int, required=False), Argument('group_ids', type=list, filter=lambda x: len(x), help='请选择主机分组'), Argument('name', help='请输主机名称'), Argument('username', handler=str.strip, help='请输入登录用户名'), Argument('hostname', handler=str.strip, help='请输入主机名或IP'), Argument('port', type=int, help='请输入SSH端口'), Argument('pkey', required=False), Argument('desc', required=False), Argument('password', required=False), ).parse(request.body) if error is None: if not _do_host_verify(form): return json_response('auth fail') group_ids = form.pop('group_ids') other = Host.objects.filter(name=form.name).first() if other and (not form.id or other.id != form.id): return json_response(error=f'已存在的主机名称【{form.name}】') if form.id: Host.objects.filter(pk=form.id).update(is_verified=True, **form) host = Host.objects.get(pk=form.id) else: host = Host.objects.create(created_by=request.user, is_verified=True, **form) host.groups.set(group_ids) response = host.to_view() response['group_ids'] = group_ids return json_response(response) return json_response(error=error) @auth('host.host.add|host.host.edit') def put(self, request): form, error = JsonParser( Argument('id', type=int, help='参数错误') ).parse(request.body) if error is None: host = Host.objects.get(pk=form.id) with host.get_ssh() as ssh: _sync_host_extend(host, ssh=ssh) return json_response(error=error) @auth('admin') def patch(self, request): form, error = JsonParser( Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择主机'), Argument('s_group_id', type=int, help='参数错误'), Argument('t_group_id', type=int, help='参数错误'), Argument('is_copy', type=bool, help='参数错误'), ).parse(request.body) if error is None: if form.t_group_id == form.s_group_id: return json_response(error='不能选择本分组的主机') s_group = Group.objects.get(pk=form.s_group_id) t_group = Group.objects.get(pk=form.t_group_id) t_group.hosts.add(*form.host_ids) if not form.is_copy: s_group.hosts.remove(*form.host_ids) return json_response(error=error) @auth('host.host.del') def delete(self, request): form, error = JsonParser( Argument('id', type=int, required=False), Argument('group_id', type=int, required=False), ).parse(request.GET) if error is None: if form.id: host_ids = [form.id] elif form.group_id: group = Group.objects.get(pk=form.group_id) host_ids = [x.id for x in group.hosts.all()] else: return json_response(error='参数错误') for host_id in host_ids: regex = fr'[^0-9]{host_id}[^0-9]' deploy = Deploy.objects.filter(host_ids__regex=regex) \ .annotate(app_name=F('app__name'), env_name=F('env__name')).first() if deploy: return json_response( error=f'应用【{deploy.app_name}】在【{deploy.env_name}】的发布配置关联了该主机,请解除关联后再尝试删除该主机') task = Task.objects.filter(targets__regex=regex).first() if task: return json_response( error=f'任务计划中的任务【{task.name}】关联了该主机,请解除关联后再尝试删除该主机') detection = Detection.objects.filter(type__in=('3', '4'), targets__regex=regex).first() if detection: return json_response( error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机') tpl = ExecTemplate.objects.filter(host_ids__regex=regex).first() if tpl: return json_response(error=f'执行模板【{tpl.name}】关联了该主机,请解除关联后再尝试删除该主机') Host.objects.filter(id__in=host_ids).delete() return json_response(error=error) @auth('host.host.add') def post_import(request): group_id = request.POST.get('group_id') file = request.FILES['file'] hosts = [] ws = load_workbook(file, read_only=True)['Sheet1'] summary = {'fail': 0, 'success': 0, 'invalid': [], 'skip': [], 'repeat': []} for i, row in enumerate(ws.rows, start=1): if i == 1: # 第1行是表头 略过 continue if not all([row[x].value for x in range(4)]): summary['invalid'].append(i) summary['fail'] += 1 continue data = AttrDict( name=row[0].value, hostname=row[1].value, port=row[2].value, username=row[3].value, desc=row[5].value ) if Host.objects.filter(hostname=data.hostname, port=data.port, username=data.username).exists(): summary['skip'].append(i) summary['fail'] += 1 continue if Host.objects.filter(name=data.name).exists(): summary['repeat'].append(i) summary['fail'] += 1 continue host = Host.objects.create(created_by=request.user, **data) host.groups.add(group_id) summary['success'] += 1 host.password = row[4].value hosts.append(host) token = uuid.uuid4().hex if hosts: Thread(target=batch_sync_host, args=(token, hosts)).start() return json_response({'summary': summary, 'token': token, 'hosts': {x.id: {'name': x.name} for x in hosts}}) @auth('host.host.view') def post_export(request): hosts = Host.objects.select_related('hostextend') if not request.user.is_supper: hosts = hosts.filter(id__in=get_host_perms(request.user)) wb = Workbook() ws = wb.active ws.append(('主机名称', 'SSH地址', 'SSH端口', 'SSH用户', 'SSH密码', '备注信息', '实例ID', '操作系统', 'CPU核心数', '内存GB', '磁盘GB', '内网IP' '公网IP', '实例计费方式', '网络计费方式', '创建时间', '到期时间')) for item in hosts: data = [item.name, item.hostname, item.port, item.username, '', item.desc] if hasattr(item, 'hostextend'): data.extend([ item.hostextend.instance_id, item.hostextend.os_name, item.hostextend.cpu, item.hostextend.memory, ','.join(str(x) for x in json.loads(item.hostextend.disk)), ','.join(json.loads(item.hostextend.private_ip_address)), ','.join(json.loads(item.hostextend.public_ip_address)), item.hostextend.get_instance_charge_type_display(), item.hostextend.get_internet_charge_type_display(), item.hostextend.created_time, item.hostextend.expired_time ]) ws.append(data) response = HttpResponse(content_type='application/octet-stream') wb.save(response) return response @auth('host.host.add') def post_parse(request): file = request.FILES['file'] if file: data = file.read() return json_response(data.decode()) else: return HttpResponseBadRequest() @auth('host.host.add') def batch_valid(request): form, error = JsonParser( Argument('password', required=False), Argument('range', filter=lambda x: x in ('1', '2'), help='参数错误') ).parse(request.body) if error is None: if form.range == '1': # all hosts hosts = Host.objects.all() else: hosts = Host.objects.filter(is_verified=False).all() token = uuid.uuid4().hex Thread(target=batch_sync_host, args=(token, hosts, form.password)).start() return json_response({'token': token, 'hosts': {x.id: {'name': x.name} for x in hosts}}) return json_response(error=error) def _do_host_verify(form): password = form.pop('password') if form.pkey: try: with SSH(form.hostname, form.port, form.username, form.pkey) as ssh: ssh.ping() return True except BadAuthenticationType: raise Exception('该主机不支持密钥认证,请参考官方文档,错误代码:E01') except AuthenticationException: raise Exception('上传的独立密钥认证失败,请检查该密钥是否能正常连接主机(推荐使用全局密钥)') except socket.timeout: raise Exception('连接主机超时,请检查网络') private_key, public_key = AppSetting.get_ssh_key() if password: try: with SSH(form.hostname, form.port, form.username, password=password) as ssh: ssh.add_public_key(public_key) except BadAuthenticationType: raise Exception('该主机不支持密码认证,请参考官方文档,错误代码:E00') except AuthenticationException: raise Exception('密码连接认证失败,请检查密码是否正确') except socket.timeout: raise Exception('连接主机超时,请检查网络') try: with SSH(form.hostname, form.port, form.username, private_key) as ssh: ssh.ping() except BadAuthenticationType: raise Exception('该主机不支持密钥认证,请参考官方文档,错误代码:E01') except AuthenticationException: if password: raise Exception('密钥认证失败,请参考官方文档,错误代码:E02') return False except socket.timeout: raise Exception('连接主机超时,请检查网络') return True