Exemplo n.º 1
0
def query(request):
    """
    获取SQL查询结果
    :param request:
    :return:
    """
    instance_name = request.POST.get('instance_name')
    sql_content = request.POST.get('sql_content')
    db_name = request.POST.get('db_name')
    limit_num = int(request.POST.get('limit_num', 0))
    user = request.user

    result = {'status': 0, 'msg': 'ok', 'data': {}}
    try:
        instance = Instance.objects.get(instance_name=instance_name)
    except Instance.DoesNotExist:
        result['status'] = 1
        result['msg'] = '实例不存在'
        return result

    # 服务器端参数验证
    if None in [sql_content, db_name, instance_name, limit_num]:
        result['status'] = 1
        result['msg'] = '页面提交参数可能为空'
        return HttpResponse(json.dumps(result),
                            content_type='application/json')

    try:
        config = SysConfig()
        # 查询前的检查,禁用语句检查,语句切分
        query_engine = get_engine(instance=instance)
        query_check_info = query_engine.query_check(db_name=db_name,
                                                    sql=sql_content)
        if query_check_info.get('bad_query'):
            # 引擎内部判断为 bad_query
            result['status'] = 1
            result['msg'] = query_check_info.get('msg')
            return HttpResponse(json.dumps(result),
                                content_type='application/json')
        if query_check_info.get(
                'has_star') and config.get('disable_star') is True:
            # 引擎内部判断为有 * 且禁止 * 选项打开
            result['status'] = 1
            result['msg'] = query_check_info.get('msg')
            return HttpResponse(json.dumps(result),
                                content_type='application/json')
        sql_content = query_check_info['filtered_sql']

        # 查询权限校验,并且获取limit_num
        priv_check_info = query_priv_check(user, instance, db_name,
                                           sql_content, limit_num)
        if priv_check_info['status'] == 0:
            limit_num = priv_check_info['data']['limit_num']
            priv_check = priv_check_info['data']['priv_check']
        else:
            result['status'] = 1
            result['msg'] = priv_check_info['msg']
            return HttpResponse(json.dumps(result),
                                content_type='application/json')
        # explain的limit_num设置为0
        limit_num = 0 if re.match(r"^explain",
                                  sql_content.lower()) else limit_num

        # 对查询sql增加limit限制或者改写语句
        sql_content = query_engine.filter_sql(sql=sql_content,
                                              limit_num=limit_num)

        # 执行查询语句,timeout=max_execution_time
        max_execution_time = int(config.get('max_execution_time', 60))
        query_task_id = async_task(query_engine.query,
                                   db_name=str(db_name),
                                   sql=sql_content,
                                   limit_num=limit_num,
                                   timeout=max_execution_time,
                                   cached=60)
        # 等待执行结果,max_execution_time后还没有返回结果代表将会被终止
        query_task = fetch(query_task_id,
                           wait=max_execution_time * 1000,
                           cached=True)
        # 在max_execution_time内执行结束
        if query_task:
            if query_task.success:
                query_result = query_task.result
                query_result.query_time = query_task.time_taken()
            else:
                query_result = ResultSet(full_sql=sql_content)
                query_result.error = query_task.result
        # 等待超时,async_task主动关闭连接
        else:
            query_result = ResultSet(full_sql=sql_content)
            query_result.error = f'查询时间超过 {max_execution_time} 秒,已被主动终止,请优化语句或者联系管理员。'

        # 查询异常
        if query_result.error:
            result['status'] = 1
            result['msg'] = query_result.error
        # 数据脱敏,仅对查询无错误的结果集进行脱敏,并且按照query_check配置是否返回
        elif config.get('data_masking'):
            query_masking_task_id = async_task(query_engine.query_masking,
                                               db_name=db_name,
                                               sql=sql_content,
                                               resultset=query_result,
                                               cached=60)
            query_masking_task = fetch(query_masking_task_id,
                                       wait=60 * 1000,
                                       cached=True)
            if query_masking_task.success:
                masking_result = query_masking_task.result
                masking_result.mask_time = query_masking_task.time_taken()
                # 脱敏出错
                if masking_result.error:
                    # 开启query_check,直接返回异常,禁止执行
                    if config.get('query_check'):
                        result['status'] = 1
                        result['msg'] = masking_result.error
                    # 关闭query_check,忽略错误信息,返回未脱敏数据,权限校验标记为跳过
                    else:
                        query_result.error = None
                        priv_check = False
                        result['data'] = query_result.__dict__
                # 正常脱敏
                else:
                    result['data'] = masking_result.__dict__
            else:
                logger.error(
                    f'数据脱敏异常,查询语句:{sql_content}\n,错误信息:{traceback.format_exc()}'
                )
                # 抛出未定义异常,并且开启query_check,直接返回异常,禁止执行
                if config.get('query_check'):
                    result['status'] = 1
                    result[
                        'msg'] = f'数据脱敏异常,请联系管理员,错误信息:{query_masking_task.result}'
                # 关闭query_check,忽略错误信息,返回未脱敏数据,权限校验标记为跳过
                else:
                    query_result.error = None
                    priv_check = False
                    result['data'] = query_result.__dict__
        # 无需脱敏的语句
        else:
            result['data'] = query_result.__dict__

        # 仅将成功的查询语句记录存入数据库
        if not query_result.error:
            if int(limit_num) == 0:
                limit_num = int(query_result.affected_rows)
            else:
                limit_num = min(int(limit_num),
                                int(query_result.affected_rows))
            query_log = QueryLog(username=user.username,
                                 user_display=user.display,
                                 db_name=db_name,
                                 instance_name=instance.instance_name,
                                 sqllog=sql_content,
                                 effect_row=limit_num,
                                 cost_time=query_result.query_time,
                                 priv_check=priv_check,
                                 hit_rule=query_result.mask_rule_hit,
                                 masking=query_result.is_masked)
            # 防止查询超时
            try:
                query_log.save()
            except OperationalError:
                connection.close()
                query_log.save()
    except Exception as e:
        logger.error(
            f'查询异常报错,查询语句:{sql_content}\n,错误信息:{traceback.format_exc()}')
        result['status'] = 1
        result['msg'] = f'查询异常报错,错误信息:{e}'
        return HttpResponse(json.dumps(result),
                            content_type='application/json')
    # 返回查询结果
    try:
        return HttpResponse(json.dumps(result,
                                       cls=ExtendJSONEncoder,
                                       bigint_as_string=True),
                            content_type='application/json')
    # 虽然能正常返回,但是依然会乱码
    except UnicodeDecodeError:
        return HttpResponse(json.dumps(result,
                                       default=str,
                                       bigint_as_string=True,
                                       encoding='latin1'),
                            content_type='application/json')
Exemplo n.º 2
0
    def execute_check(self, db_name=None, sql=''):
        """上线单执行前的检查, 返回Review set"""
        config = SysConfig()
        # 进行Inception检查,获取检测结果
        if not config.get('inception'):
            try:
                inception_engine = GoInceptionEngine()
                inc_check_result = inception_engine.execute_check(
                    instance=self.instance, db_name=db_name, sql=sql)
            except Exception as e:
                logger.debug(f"goInception检测语句报错:错误信息{traceback.format_exc()}")
                raise RuntimeError(
                    f"goInception检测语句报错,请注意检查系统配置中goInception配置,错误信息:\n{e}")
        else:
            try:
                inception_engine = InceptionEngine()
                inc_check_result = inception_engine.execute_check(
                    instance=self.instance, db_name=db_name, sql=sql)
            except Exception as e:
                logger.debug(f"Inception检测语句报错:错误信息{traceback.format_exc()}")
                raise RuntimeError(
                    f"Inception检测语句报错,请注意检查系统配置中Inception配置,错误信息:\n{e}")
        # 判断Inception检测结果
        if inc_check_result.error:
            logger.debug(f"Inception检测语句报错:错误信息{inc_check_result.error}")
            raise RuntimeError(
                f"Inception检测语句报错,错误信息:\n{inc_check_result.error}")

        # 禁用/高危语句检查
        check_critical_result = ReviewSet(full_sql=sql)
        line = 1
        critical_ddl_regex = config.get('critical_ddl_regex', '')
        p = re.compile(critical_ddl_regex)
        check_critical_result.syntax_type = 2  # TODO 工单类型 0、其他 1、DDL,2、DML

        for row in inc_check_result.rows:
            statement = row.sql
            # 去除注释
            statement = remove_comments(statement, db_type='mysql')
            # 禁用语句
            if re.match(r"^select", statement.lower()):
                check_critical_result.is_critical = True
                result = ReviewResult(
                    id=line,
                    errlevel=2,
                    stagestatus='驳回不支持语句',
                    errormessage='仅支持DML和DDL语句,查询语句请使用SQL查询功能!',
                    sql=statement)
            # 高危语句
            elif critical_ddl_regex and p.match(statement.strip().lower()):
                check_critical_result.is_critical = True
                result = ReviewResult(id=line,
                                      errlevel=2,
                                      stagestatus='驳回高危SQL',
                                      errormessage='禁止提交匹配' +
                                      critical_ddl_regex + '条件的语句!',
                                      sql=statement)
            # 正常语句
            else:
                result = ReviewResult(
                    id=line,
                    errlevel=0,
                    stagestatus='Audit completed',
                    errormessage='None',
                    sql=statement,
                    affected_rows=0,
                    execute_time=0,
                )

            # 没有找出DDL语句的才继续执行此判断
            if check_critical_result.syntax_type == 2:
                if get_syntax_type(statement, parser=False,
                                   db_type='mysql') == 'DDL':
                    check_critical_result.syntax_type = 1
            check_critical_result.rows += [result]

            # 遇到禁用和高危语句直接返回
            if check_critical_result.is_critical:
                check_critical_result.error_count += 1
                return check_critical_result
            line += 1
        return inc_check_result
Exemplo n.º 3
0
def notify_for_audit(audit_id, msg_type=0, **kwargs):
    """
    工作流消息通知,不包含工单执行结束的通知
    :param audit_id:
    :param msg_type: 0.all,1.email,2.dingding
    :param kwargs:
    :return:
    """
    # 判断是否开启消息通知,未开启直接返回
    sys_config = SysConfig()
    if not sys_config.get('mail') and not sys_config.get('ding'):
        logger.info('未开启消息通知,可在系统设置中开启')
        return None
    # 获取审核信息
    audit_detail = Audit.detail(audit_id=audit_id)
    audit_id = audit_detail.audit_id
    workflow_audit_remark = kwargs.get('audit_remark', '')
    base_url = sys_config.get('archery_base_url',
                              'http://127.0.0.1:8000').rstrip('/')
    workflow_url = "{base_url}/workflow/{audit_id}".format(
        base_url=base_url, audit_id=audit_detail.audit_id)
    msg_email_cc = kwargs.get('email_cc', [])
    workflow_id = audit_detail.workflow_id
    workflow_type = audit_detail.workflow_type
    status = audit_detail.current_status
    workflow_title = audit_detail.workflow_title
    workflow_from = audit_detail.create_user_display
    group_name = audit_detail.group_name
    webhook_url = ResourceGroup.objects.get(
        group_id=audit_detail.group_id).ding_webhook

    # 获取当前审批和审批流程
    workflow_auditors, current_workflow_auditors = Audit.review_info(
        audit_detail.workflow_id, audit_detail.workflow_type)

    # 准备消息内容
    if workflow_type == WorkflowDict.workflow_type['query']:
        workflow_type_display = WorkflowDict.workflow_type['query_display']
        workflow_detail = QueryPrivilegesApply.objects.get(
            apply_id=workflow_id)
        if workflow_detail.priv_type == 1:
            workflow_content = '''数据库清单:{}\n授权截止时间:{}\n结果集:{}\n'''.format(
                workflow_detail.db_list,
                datetime.datetime.strftime(workflow_detail.valid_date,
                                           '%Y-%m-%d %H:%M:%S'),
                workflow_detail.limit_num)
        elif workflow_detail.priv_type == 2:
            workflow_content = '''数据库:{}\n表清单:{}\n授权截止时间:{}\n结果集:{}\n'''.format(
                workflow_detail.db_list, workflow_detail.table_list,
                datetime.datetime.strftime(workflow_detail.valid_date,
                                           '%Y-%m-%d %H:%M:%S'),
                workflow_detail.limit_num)
        else:
            workflow_content = ''
    elif workflow_type == WorkflowDict.workflow_type['sqlreview']:
        workflow_type_display = WorkflowDict.workflow_type['sqlreview_display']
        workflow_detail = SqlWorkflow.objects.get(pk=workflow_id)
        workflow_content = re.sub(
            '[\r\n\f]{2,}', '\n',
            workflow_detail.sqlworkflowcontent.sql_content[0:500].replace(
                '\r', ''))
    else:
        raise Exception('工单类型不正确')

    # 准备消息格式
    if status == WorkflowDict.workflow_status['audit_wait']:  # 申请阶段
        msg_title = "[{}]新的工单申请#{}".format(workflow_type_display, audit_id)
        # 接收人,发送给该资源组内对应权限组所有的用户
        auth_group_names = Group.objects.get(
            id=audit_detail.current_audit).name
        msg_email_reciver = [
            user.email for user in auth_group_users([auth_group_names],
                                                    audit_detail.group_id)
        ]
        # 消息内容
        msg_content = '''发起人:{}\n组:{}\n审批流程:{}\n当前审批:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
            workflow_from, group_name, workflow_auditors,
            current_workflow_auditors, workflow_title, workflow_url,
            workflow_content)
    elif status == WorkflowDict.workflow_status['audit_success']:  # 审核通过
        msg_title = "[{}]工单审核通过#{}".format(workflow_type_display, audit_id)
        # 接收人,仅发送给申请人
        msg_email_reciver = [
            Users.objects.get(username=audit_detail.create_user).email
        ]
        # 消息内容
        msg_content = '''发起人:{}\n组:{}\n审批流程:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
            workflow_from, group_name, workflow_auditors, workflow_title,
            workflow_url, workflow_content)
    elif status == WorkflowDict.workflow_status['audit_reject']:  # 审核驳回
        msg_title = "[{}]工单被驳回#{}".format(workflow_type_display, audit_id)
        # 接收人,仅发送给申请人
        msg_email_reciver = [
            Users.objects.get(username=audit_detail.create_user).email
        ]
        # 消息内容
        msg_content = '''工单名称:{}\n工单地址:{}\n驳回原因:{}\n提醒:此工单被审核不通过,请按照驳回原因进行修改!'''.format(
            workflow_title, workflow_url, workflow_audit_remark)
    elif status == WorkflowDict.workflow_status['audit_abort']:  # 审核取消,通知所有审核人
        msg_title = "[{}]提交人主动终止工单#{}".format(workflow_type_display, audit_id)
        # 接收人,发送给该资源组内对应权限组所有的用户
        auth_group_names = [
            Group.objects.get(id=auth_group_id).name
            for auth_group_id in audit_detail.audit_auth_groups.split(',')
        ]
        msg_email_reciver = [
            user.email for user in auth_group_users(auth_group_names,
                                                    audit_detail.group_id)
        ]
        # 消息内容
        msg_content = '''发起人:{}\n组:{}\n工单名称:{}\n工单地址:{}\n提醒:提交人主动终止流程'''.format(
            workflow_from, group_name, workflow_title, workflow_url)
    else:
        raise Exception('工单状态不正确')

    # 判断是发送钉钉还是发送邮件
    msg_sender = MsgSender()
    logger.info('发送消息通知,消息audit_id={}'.format(audit_id))
    logger.info('消息标题:{}\n通知对象:{}\n消息内容:{}'.format(msg_title,
                                                   msg_email_reciver,
                                                   msg_content))
    if msg_type == 0:
        if sys_config.get('mail'):
            msg_sender.send_email(msg_title,
                                  msg_content,
                                  msg_email_reciver,
                                  list_cc_addr=msg_email_cc)
        if sys_config.get('ding'):
            msg_sender.send_ding(webhook_url, msg_title + '\n' + msg_content)
    elif msg_type == 1:
        msg_sender.send_email(msg_title,
                              msg_content,
                              msg_email_reciver,
                              list_cc_addr=msg_email_cc)
    elif msg_type == 2:
        msg_sender.send_ding(webhook_url, msg_title + '\n' + msg_content)
Exemplo n.º 4
0
def notify_for_audit(audit_id, **kwargs):
    """
    工作流消息通知,不包含工单执行结束的通知
    :param audit_id:
    :param kwargs:
    :return:
    """
    # 判断是否开启消息通知,未开启直接返回
    if not __notify_cnf_status():
        return None
    sys_config = SysConfig()

    # 获取审核信息
    audit_detail = Audit.detail(audit_id=audit_id)
    audit_id = audit_detail.audit_id
    workflow_audit_remark = kwargs.get('audit_remark', '')
    base_url = sys_config.get('archery_base_url',
                              'http://127.0.0.1:8000').rstrip('/')
    workflow_url = "{base_url}/workflow/{audit_id}".format(
        base_url=base_url, audit_id=audit_detail.audit_id)
    workflow_id = audit_detail.workflow_id
    workflow_type = audit_detail.workflow_type
    status = audit_detail.current_status
    workflow_title = audit_detail.workflow_title
    workflow_from = audit_detail.create_user_display
    group_name = audit_detail.group_name
    dingding_webhook = ResourceGroup.objects.get(
        group_id=audit_detail.group_id).ding_webhook
    feishu_webhook = ResourceGroup.objects.get(
        group_id=audit_detail.group_id).feishu_webhook
    qywx_webhook = ResourceGroup.objects.get(
        group_id=audit_detail.group_id).qywx_webhook
    # 获取当前审批和审批流程
    workflow_auditors, current_workflow_auditors = Audit.review_info(
        audit_detail.workflow_id, audit_detail.workflow_type)

    # 准备消息内容
    if workflow_type == WorkflowDict.workflow_type['query']:
        workflow_type_display = WorkflowDict.workflow_type['query_display']
        workflow_detail = QueryPrivilegesApply.objects.get(
            apply_id=workflow_id)
        instance = workflow_detail.instance.instance_name
        db_name = ' '
        if workflow_detail.priv_type == 1:
            workflow_content = '''数据库清单:{}\n授权截止时间:{}\n结果集:{}\n'''.format(
                workflow_detail.db_list,
                datetime.datetime.strftime(workflow_detail.valid_date,
                                           '%Y-%m-%d %H:%M:%S'),
                workflow_detail.limit_num)
        elif workflow_detail.priv_type == 2:
            db_name = workflow_detail.db_list
            workflow_content = '''数据库:{}\n表清单:{}\n授权截止时间:{}\n结果集:{}\n'''.format(
                workflow_detail.db_list, workflow_detail.table_list,
                datetime.datetime.strftime(workflow_detail.valid_date,
                                           '%Y-%m-%d %H:%M:%S'),
                workflow_detail.limit_num)
        else:
            workflow_content = ''
    elif workflow_type == WorkflowDict.workflow_type['sqlreview']:
        workflow_type_display = WorkflowDict.workflow_type['sqlreview_display']
        workflow_detail = SqlWorkflow.objects.get(pk=workflow_id)
        instance = workflow_detail.instance.instance_name
        db_name = workflow_detail.db_name
        workflow_content = re.sub(
            '[\r\n\f]{2,}', '\n',
            workflow_detail.sqlworkflowcontent.sql_content[0:500].replace(
                '\r', ''))
    elif workflow_type == WorkflowDict.workflow_type['archive']:
        workflow_type_display = WorkflowDict.workflow_type['archive_display']
        workflow_detail = ArchiveConfig.objects.get(pk=workflow_id)
        instance = workflow_detail.src_instance.instance_name
        db_name = workflow_detail.src_db_name
        workflow_content = '''归档表:{}\n归档模式:{}\n归档条件:{}\n'''.format(
            workflow_detail.src_table_name, workflow_detail.mode,
            workflow_detail.condition)
    else:
        raise Exception('工单类型不正确')

    # 准备消息格式
    if status == WorkflowDict.workflow_status['audit_wait']:  # 申请阶段
        msg_title = "[{}]新的工单申请#{}".format(workflow_type_display, audit_id)
        # 接收人,发送给该资源组内对应权限组所有的用户
        auth_group_names = Group.objects.get(
            id=audit_detail.current_audit).name
        msg_to = auth_group_users([auth_group_names], audit_detail.group_id)
        msg_cc = Users.objects.filter(username__in=kwargs.get('cc_users', []))
        # 消息内容
        msg_content = '''发起时间:{}\n发起人:{}\n组:{}\n目标实例:{}\n数据库:{}\n审批流程:{}\n当前审批:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
            workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
            workflow_from, group_name, instance, db_name, workflow_auditors,
            current_workflow_auditors, workflow_title, workflow_url,
            workflow_content)
    elif status == WorkflowDict.workflow_status['audit_success']:  # 审核通过
        msg_title = "[{}]工单审核通过#{}".format(workflow_type_display, audit_id)
        # 接收人,仅发送给申请人
        msg_to = [Users.objects.get(username=audit_detail.create_user)]
        msg_cc = Users.objects.filter(username__in=kwargs.get('cc_users', []))
        # 消息内容
        msg_content = '''发起时间:{}\n发起人:{}\n组:{}\n目标实例:{}\n数据库:{}\n审批流程:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
            workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
            workflow_from, group_name, instance, db_name, workflow_auditors,
            workflow_title, workflow_url, workflow_content)
    elif status == WorkflowDict.workflow_status['audit_reject']:  # 审核驳回
        msg_title = "[{}]工单被驳回#{}".format(workflow_type_display, audit_id)
        # 接收人,仅发送给申请人
        msg_to = [Users.objects.get(username=audit_detail.create_user)]
        msg_cc = Users.objects.filter(username__in=kwargs.get('cc_users', []))
        # 消息内容
        msg_content = '''发起时间:{}\n目标实例:{}\n数据库:{}\n工单名称:{}\n工单地址:{}\n驳回原因:{}\n提醒:此工单被审核不通过,请按照驳回原因进行修改!'''.format(
            workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
            instance, db_name, workflow_title, workflow_url,
            re.sub('[\r\n\f]{2,}', '\n', workflow_audit_remark))
    elif status == WorkflowDict.workflow_status['audit_abort']:  # 审核取消,通知所有审核人
        msg_title = "[{}]提交人主动终止工单#{}".format(workflow_type_display, audit_id)
        # 接收人,发送给该资源组内对应权限组所有的用户
        auth_group_names = [
            Group.objects.get(id=auth_group_id).name
            for auth_group_id in audit_detail.audit_auth_groups.split(',')
        ]
        msg_to = auth_group_users(auth_group_names, audit_detail.group_id)
        msg_cc = Users.objects.filter(username__in=kwargs.get('cc_users', []))
        # 消息内容
        msg_content = '''发起时间:{}\n发起人:{}\n组:{}\n目标实例:{}\n数据库:{}\n工单名称:{}\n工单地址:{}\n终止原因:{}'''.format(
            workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
            workflow_from, group_name, instance, db_name, workflow_title,
            workflow_url, re.sub('[\r\n\f]{2,}', '\n', workflow_audit_remark))
    else:
        raise Exception('工单状态不正确')
    logger.info(f"通知Debug{msg_to}{msg_cc}")
    # 发送通知
    __send(msg_title,
           msg_content,
           msg_to,
           msg_cc,
           feishu_webhook=feishu_webhook,
           dingding_webhook=dingding_webhook,
           qywx_webhook=qywx_webhook)
Exemplo n.º 5
0
def execute_callback(task):
    """异步任务的回调, 将结果填入数据库等等
    使用django-q的hook, 传入参数为整个task
    task.result 是真正的结果
    """
    # https://stackoverflow.com/questions/7835272/django-operationalerror-2006-mysql-server-has-gone-away
    if connection.connection and not connection.is_usable():
        close_old_connections()
    workflow_id = task.args[0]
    # 判断工单状态,如果不是执行中的,不允许更新信息,直接抛错记录日志
    with transaction.atomic():
        workflow = SqlWorkflow.objects.get(id=workflow_id)
        if workflow.status != 'workflow_executing':
            raise Exception(f'工单{workflow.id}状态不正确,禁止重复更新执行结果!')

    workflow.finish_time = task.stopped

    if not task.success:
        # 不成功会返回错误堆栈信息,构造一个错误信息
        workflow.status = 'workflow_exception'
        execute_result = ReviewSet(
            full_sql=workflow.sqlworkflowcontent.sql_content)
        execute_result.rows = [
            ReviewResult(stage='Execute failed',
                         errlevel=2,
                         stagestatus='异常终止',
                         errormessage=task.result,
                         sql=workflow.sqlworkflowcontent.sql_content)
        ]
    elif task.result.warning or task.result.error:
        execute_result = task.result
        workflow.status = 'workflow_exception'
    else:
        execute_result = task.result
        workflow.status = 'workflow_finish'
    try:
        # 保存执行结果
        workflow.sqlworkflowcontent.execute_result = execute_result.json()
        workflow.sqlworkflowcontent.save()
        workflow.save()
    except Exception as e:
        logger.error(f'SQL工单回调异常: {workflow_id} {traceback.format_exc()}')
        SqlWorkflow.objects.filter(id=workflow_id).update(
            finish_time=task.stopped,
            status='workflow_exception',
        )
        workflow.sqlworkflowcontent.execute_result = {f'{e}'}
        workflow.sqlworkflowcontent.save()
    # 增加工单日志
    audit_id = Audit.detail_by_workflow_id(
        workflow_id=workflow_id,
        workflow_type=WorkflowDict.workflow_type['sqlreview']).audit_id
    Audit.add_log(audit_id=audit_id,
                  operation_type=6,
                  operation_type_desc='执行结束',
                  operation_info='执行结果:{}'.format(
                      workflow.get_status_display()),
                  operator='',
                  operator_display='系统')

    # DDL工单结束后清空实例资源缓存
    if workflow.syntax_type == 1:
        r = get_redis_connection("default")
        for key in r.scan_iter(match='*insRes*', count=2000):
            r.delete(key)

    # 开启了Execute阶段通知参数才发送消息通知
    sys_config = SysConfig()
    is_notified = 'Execute' in sys_config.get('notify_phase_control').split(',') \
        if sys_config.get('notify_phase_control') else True
    if is_notified:
        notify_for_execute(workflow)
Exemplo n.º 6
0
def query(request):
    """
    获取SQL查询结果
    :param request:
    :return:
    """
    instance_name = request.POST.get('instance_name')
    sql_content = request.POST.get('sql_content')
    db_name = request.POST.get('db_name')
    limit_num = int(request.POST.get('limit_num', 0))
    user = request.user

    result = {'status': 0, 'msg': 'ok', 'data': {}}
    try:
        instance = Instance.objects.get(instance_name=instance_name)
    except Instance.DoesNotExist:
        result['status'] = 1
        result['msg'] = '实例不存在'
        return result

    # 服务器端参数验证
    if None in [sql_content, db_name, instance_name, limit_num]:
        result['status'] = 1
        result['msg'] = '页面提交参数可能为空'
        return HttpResponse(json.dumps(result),
                            content_type='application/json')

    try:
        config = SysConfig()
        # 查询前的检查,禁用语句检查,语句切分
        query_engine = get_engine(instance=instance)
        query_check_info = query_engine.query_check(db_name=db_name,
                                                    sql=sql_content)
        if query_check_info.get('bad_query'):
            # 引擎内部判断为 bad_query
            result['status'] = 1
            result['msg'] = query_check_info.get('msg')
            return HttpResponse(json.dumps(result),
                                content_type='application/json')
        if query_check_info.get(
                'has_star') and config.get('disable_star') is True:
            # 引擎内部判断为有 * 且禁止 * 选项打开
            result['status'] = 1
            result['msg'] = query_check_info.get('msg')
            return HttpResponse(json.dumps(result),
                                content_type='application/json')
        sql_content = query_check_info['filtered_sql']

        # 查询权限校验,并且获取limit_num
        priv_check_info = query_priv_check(user, instance, db_name,
                                           sql_content, limit_num)
        if priv_check_info['status'] == 0:
            limit_num = priv_check_info['data']['limit_num']
            priv_check = priv_check_info['data']['priv_check']
        else:
            result['status'] = 1
            result['msg'] = priv_check_info['msg']
            return HttpResponse(json.dumps(result),
                                content_type='application/json')
        # explain的limit_num设置为0
        limit_num = 0 if re.match(r"^explain",
                                  sql_content.lower()) else limit_num

        # 对查询sql增加limit限制或者改写语句
        sql_content = query_engine.filter_sql(sql=sql_content,
                                              limit_num=limit_num)

        # 先获取查询连接,用于后面查询复用连接以及终止会话
        query_engine.get_connection(db_name=db_name)
        thread_id = query_engine.thread_id
        max_execution_time = int(config.get('max_execution_time', 60))
        # 执行查询语句,并增加一个定时终止语句的schedule,timeout=max_execution_time
        if thread_id:
            schedule_name = f'query-{time.time()}'
            run_date = (datetime.datetime.now() +
                        datetime.timedelta(seconds=max_execution_time))
            add_kill_conn_schedule(schedule_name, run_date, instance.id,
                                   thread_id)
        with FuncTimer() as t:
            query_result = query_engine.query(db_name, sql_content, limit_num)
        query_result.query_time = t.cost
        # 返回查询结果后删除schedule
        if thread_id:
            del_schedule(schedule_name)

        # 查询异常
        if query_result.error:
            result['status'] = 1
            result['msg'] = query_result.error
        # 数据脱敏,仅对查询无错误的结果集进行脱敏,并且按照query_check配置是否返回
        elif config.get('data_masking'):
            try:
                with FuncTimer() as t:
                    masking_result = query_engine.query_masking(
                        db_name, sql_content, query_result)
                masking_result.mask_time = t.cost
                # 脱敏出错
                if masking_result.error:
                    # 开启query_check,直接返回异常,禁止执行
                    if config.get('query_check'):
                        result['status'] = 1
                        result['msg'] = f'数据脱敏异常:{masking_result.error}'
                    # 关闭query_check,忽略错误信息,返回未脱敏数据,权限校验标记为跳过
                    else:
                        query_result.error = None
                        priv_check = False
                        result['data'] = query_result.__dict__
                    logger.error(
                        f'数据脱敏异常,查询语句:{sql_content}\n,错误信息:{masking_result.error}'
                    )
                # 正常脱敏
                else:
                    result['data'] = masking_result.__dict__
            except Exception as msg:
                logger.error(f'数据脱敏异常,查询语句:{sql_content}\n,错误信息:{msg}')
                # 抛出未定义异常,并且开启query_check,直接返回异常,禁止执行
                if config.get('query_check'):
                    result['status'] = 1
                    result['msg'] = f'数据脱敏异常,请联系管理员,错误信息:{msg}'
                # 关闭query_check,忽略错误信息,返回未脱敏数据,权限校验标记为跳过
                else:
                    query_result.error = None
                    priv_check = False
                    result['data'] = query_result.__dict__
        # 无需脱敏的语句
        else:
            result['data'] = query_result.__dict__

        # 仅将成功的查询语句记录存入数据库
        if not query_result.error:
            if hasattr(query_engine, 'seconds_behind_master'):
                result['data'][
                    'seconds_behind_master'] = query_engine.seconds_behind_master
            if int(limit_num) == 0:
                limit_num = int(query_result.affected_rows)
            else:
                limit_num = min(int(limit_num),
                                int(query_result.affected_rows))
            query_log = QueryLog(username=user.username,
                                 user_display=user.display,
                                 db_name=db_name,
                                 instance_name=instance.instance_name,
                                 sqllog=sql_content,
                                 effect_row=limit_num,
                                 cost_time=query_result.query_time,
                                 priv_check=priv_check,
                                 hit_rule=query_result.mask_rule_hit,
                                 masking=query_result.is_masked)
            # 防止查询超时
            try:
                query_log.save()
            except OperationalError:
                connection.close()
                query_log.save()
    except Exception as e:
        logger.error(
            f'查询异常报错,查询语句:{sql_content}\n,错误信息:{traceback.format_exc()}')
        result['status'] = 1
        result['msg'] = f'查询异常报错,错误信息:{e}'
        return HttpResponse(json.dumps(result),
                            content_type='application/json')
    # 返回查询结果
    try:
        return HttpResponse(json.dumps(result,
                                       cls=ExtendJSONEncoder,
                                       bigint_as_string=True),
                            content_type='application/json')
    # 虽然能正常返回,但是依然会乱码
    except UnicodeDecodeError:
        return HttpResponse(json.dumps(result,
                                       default=str,
                                       bigint_as_string=True,
                                       encoding='latin1'),
                            content_type='application/json')
Exemplo n.º 7
0
def submit(request):
    """正式提交SQL, 此处生成工单"""
    sql_content = request.POST.get('sql_content').strip()
    workflow_title = request.POST.get('workflow_name')
    # 检查用户是否有权限涉及到资源组等, 比较复杂, 可以把检查权限改成一个独立的方法
    group_name = request.POST.get('group_name')
    group_id = ResourceGroup.objects.get(group_name=group_name).group_id
    instance_name = request.POST.get('instance_name')
    instance = Instance.objects.get(instance_name=instance_name)
    db_name = request.POST.get('db_name')
    is_backup = True if request.POST.get('is_backup') == 'True' else False
    notify_users = request.POST.getlist('notify_users')
    list_cc_addr = [email['email'] for email in Users.objects.filter(username__in=notify_users).values('email')]
    run_date_start = request.POST.get('run_date_start')
    run_date_end = request.POST.get('run_date_end')

    # 服务器端参数验证
    if None in [sql_content, db_name, instance_name, db_name, is_backup]:
        context = {'errMsg': '页面提交参数可能为空'}
        return render(request, 'error.html', context)

    # 未开启备份选项,强制设置备份
    sys_config = SysConfig()
    if not sys_config.get('enable_backup_switch'):
        is_backup = True

    # 验证组权限(用户是否在该组、该组是否有指定实例)
    try:
        user_instances(request.user, tag_codes=['can_write']).get(instance_name=instance_name)
    except instance.DoesNotExist:
        context = {'errMsg': '你所在组未关联该实例!'}
        return render(request, 'error.html', context)

    # 再次交给engine进行检测,防止绕过
    try:
        check_engine = get_engine(instance=instance)
        check_result = check_engine.execute_check(db_name=db_name, sql=sql_content.strip())
    except Exception as e:
        context = {'errMsg': str(e)}
        return render(request, 'error.html', context)

    # 按照系统配置确定是自动驳回还是放行
    auto_review_wrong = sys_config.get('auto_review_wrong', '')  # 1表示出现警告就驳回,2和空表示出现错误才驳回
    workflow_status = 'workflow_manreviewing'
    if check_result.warning_count > 0 and auto_review_wrong == '1':
        workflow_status = 'workflow_autoreviewwrong'
    elif check_result.error_count > 0 and auto_review_wrong in ('', '1', '2'):
        workflow_status = 'workflow_autoreviewwrong'

    # 调用工作流生成工单
    # 使用事务保持数据一致性
    try:
        with transaction.atomic():
            # 存进数据库里
            sql_workflow = SqlWorkflow.objects.create(
                workflow_name=workflow_title,
                group_id=group_id,
                group_name=group_name,
                engineer=request.user.username,
                engineer_display=request.user.display,
                audit_auth_groups=Audit.settings(group_id, WorkflowDict.workflow_type['sqlreview']),
                status=workflow_status,
                is_backup=is_backup,
                instance=instance,
                db_name=db_name,
                is_manual=0,
                syntax_type=check_result.syntax_type,
                create_time=timezone.now(),
                run_date_start=run_date_start or None,
                run_date_end=run_date_end or None
            )
            SqlWorkflowContent.objects.create(workflow=sql_workflow,
                                              sql_content=sql_content,
                                              review_content=check_result.json(),
                                              execute_result=''
                                              )
            workflow_id = sql_workflow.id
            # 自动审核通过了,才调用工作流
            if workflow_status == 'workflow_manreviewing':
                # 调用工作流插入审核信息, 查询权限申请workflow_type=2
                Audit.add(WorkflowDict.workflow_type['sqlreview'], workflow_id)
    except Exception as msg:
        logger.error(f"提交工单报错,错误信息:{traceback.format_exc()}")
        context = {'errMsg': msg}
        return render(request, 'error.html', context)
    else:
        # 自动审核通过才进行消息通知
        if workflow_status == 'workflow_manreviewing':
            # 获取审核信息
            audit_id = Audit.detail_by_workflow_id(workflow_id=workflow_id,
                                                   workflow_type=WorkflowDict.workflow_type['sqlreview']).audit_id
            async_task(notify_for_audit, audit_id=audit_id, email_cc=list_cc_addr, timeout=60)

    return HttpResponseRedirect(reverse('sql:detail', args=(workflow_id,)))
Exemplo n.º 8
0
class InceptionDao(object):
    def __init__(self, instance_name=None):
        self.sys_config = SysConfig().sys_config
        self.inception_host = self.sys_config.get('inception_host')
        self.inception_port = int(self.sys_config.get('inception_port')) if self.sys_config.get(
            'inception_port') else 6669
        self.inception_remote_backup_host = self.sys_config.get('inception_remote_backup_host')
        self.inception_remote_backup_port = int(
            self.sys_config.get('inception_remote_backup_port')) if self.sys_config.get(
            'inception_remote_backup_port') else 3306
        self.inception_remote_backup_user = self.sys_config.get('inception_remote_backup_user')
        self.inception_remote_backup_password = self.sys_config.get('inception_remote_backup_password')
        if instance_name:
            try:
                instance_info = Instance.objects.get(instance_name=instance_name)
                self.host = instance_info.host
                self.port = int(instance_info.port)
                self.user = instance_info.user
                self.password = Prpcrypt().decrypt(instance_info.password)
            except Exception:
                raise Exception('找不到对应的实例配置信息,请配置')

    def criticalDDL(self, sql_content):
        '''
        识别DROP DATABASE, DROP TABLE, TRUNCATE PARTITION, TRUNCATE TABLE等高危DDL操作,因为对于这些操作,inception在备份时只能备份METADATA,而不会备份数据!
        如果识别到包含高危操作,则返回“审核不通过”
        '''
        resultList = []
        criticalSqlFound = 0
        critical_ddl_regex = self.sys_config.get('critical_ddl_regex')
        p = re.compile(critical_ddl_regex)
        # 删除注释语句
        sql_content = ''.join(
            map(lambda x: re.compile(r'(^--\s+.*|^/\*.*\*/;\s*$)').sub('', x, count=1),
                sql_content.splitlines(1))).strip()

        for row in sql_content.rstrip(';').split(';'):
            if p.match(row.strip().lower()):
                result = (
                    '', '', 2, '驳回高危SQL', '禁止提交匹配' + critical_ddl_regex + '条件的语句!',
                    row,
                    '', '', '', '')
                criticalSqlFound = 1
            else:
                result = ('', '', 0, '', 'None', row, '', '', '', '')
            resultList.append(result)
        if criticalSqlFound == 1:
            return resultList
        else:
            return None

    def preCheck(self, sql_content):
        '''
        在提交给inception之前,预先识别一些Inception不能正确审核的SQL,比如"alter table t1;"或"alter table test.t1;" 以免导致inception core dump
        '''
        resultList = []
        syntaxErrorSqlFound = 0
        for row in sql_content.rstrip(';').split(';'):
            if re.match(r"(\s*)alter(\s+)table(\s+)(\S+)(\s*);|(\s*)alter(\s+)table(\s+)(\S+)\.(\S+)(\s*);",
                        row.lower() + ";"):
                result = ('', '', 2, 'SQL语法错误', 'ALTER TABLE 必须带有选项', row, '', '', '', '')
                syntaxErrorSqlFound = 1
            else:
                result = ('', '', 0, '', 'None', row, '', '', '', '')
            resultList.append(result)
        if syntaxErrorSqlFound == 1:
            return resultList
        else:
            return None

    def sqlautoReview(self, sql_content, db_name, isSplit="no"):
        '''
        将sql交给inception进行自动审核,并返回审核结果。
        '''
        # 高危SQL检查
        if self.sys_config.get('critical_ddl_regex', '') != '':
            criticalDDL_check = self.criticalDDL(sql_content)
        else:
            criticalDDL_check = None

        if criticalDDL_check is not None:
            result = criticalDDL_check
        else:
            preCheckResult = self.preCheck(sql_content)
            if preCheckResult is not None:
                result = preCheckResult
            else:
                if isSplit == "yes":
                    # 这种场景只给osc进度功能使用
                    # 如果一个工单中同时包含DML和DDL,那么执行时被split后的SQL与提交的SQL会不一样(会在每条语句前面加use database;),导致osc进度更新取不到正确的SHA1值。
                    # 请参考inception文档中--enable-split参数的说明

                    sqlSplit = "/*--user=%s; --password=%s; --host=%s; --enable-execute;--port=%d; --enable-ignore-warnings;--enable-split;*/\
                         inception_magic_start;\
                         use %s;\
                         %s\
                         inception_magic_commit;" % (
                        self.user,
                        self.password,
                        self.host,
                        self.port,
                        db_name,
                        sql_content)
                    splitResult = self._fetchall(sqlSplit, self.inception_host, self.inception_port, '', '', '')
                    tmpList = []
                    for splitRow in splitResult:
                        sqlTmp = splitRow[1]
                        sql = "/*--user=%s;--password=%s;--host=%s;--enable-check;--port=%d; --enable-ignore-warnings;*/\
                                inception_magic_start;\
                                %s\
                                inception_magic_commit;" % (
                            self.user,
                            self.password,
                            self.host,
                            self.port,
                            sqlTmp)
                        reviewResult = self._fetchall(sql, self.inception_host, self.inception_port, '', '', '')
                        tmpList.append(reviewResult)

                    # 二次加工一下
                    finalList = []
                    for splitRow in tmpList:
                        for sqlRow in splitRow:
                            finalList.append(list(sqlRow))
                    result = finalList
                else:
                    # 工单审核使用
                    sql = "/*--user=%s;--password=%s;--host=%s;--enable-check=1;--port=%d;*/\
                      inception_magic_start;\
                      use %s;\
                      %s\
                      inception_magic_commit;" % (
                        self.user,
                        self.password,
                        self.host,
                        self.port,
                        db_name,
                        sql_content)
                    result = self._fetchall(sql, self.inception_host, self.inception_port, '', '', '')
        return result

    def executeFinal(self, workflow_detail):
        '''
        将sql交给inception进行最终执行,并返回执行结果。
        '''
        if workflow_detail.is_backup == '是':
            strBackup = "--enable-remote-backup;"
        else:
            strBackup = "--disable-remote-backup;"

        # 根据inception的要求,执行之前最好先split一下
        sqlSplit = "/*--user=%s; --password=%s; --host=%s; --enable-execute;--port=%d; --enable-ignore-warnings;--enable-split;*/\
             inception_magic_start;\
             use %s;\
             %s\
             inception_magic_commit;" % (
            self.user,
            self.password,
            self.host,
            self.port,
            workflow_detail.db_name, workflow_detail.sql_content)
        splitResult = self._fetchall(sqlSplit, self.inception_host, self.inception_port, '', '', '')

        tmpList = []
        # 对于split好的结果,再次交给inception执行.这里无需保持在长连接里执行,短连接即可.
        for splitRow in splitResult:
            sqlTmp = splitRow[1]
            sqlExecute = "/*--user=%s;--password=%s;--host=%s;--enable-execute;--port=%d; --enable-ignore-warnings;%s*/\
                    inception_magic_start;\
                    %s\
                    inception_magic_commit;" % (
                self.user,
                self.password,
                self.host,
                self.port,
                strBackup,
                sqlTmp)

            executeResult = self._fetchall(sqlExecute, self.inception_host, self.inception_port, '', '', '')
            for sqlRow in executeResult:
                tmpList.append(sqlRow)
            # 每执行一次,就将执行结果更新到工单的execute_result,便于获取osc进度时对比
            workflow_detail.execute_result = json.dumps(tmpList)
            try:
                workflow_detail.save()
            except Exception:
                # 关闭后重新获取连接,防止超时
                connection.close()
                workflow_detail.save()

        # 二次加工一下,目的是为了和sqlautoReview()函数的return保持格式一致,便于在detail页面渲染.
        finalStatus = "已正常结束"
        finalList = []
        for sqlRow in tmpList:
            # 如果发现任何一个行执行结果里有errLevel为1或2,并且stagestatus列没有包含Execute Successfully字样,则判断最终执行结果为有异常.
            if (sqlRow[2] == 1 or sqlRow[2] == 2) and re.match(r"\w*Execute Successfully\w*", sqlRow[3]) is None:
                finalStatus = "执行有异常"
            finalList.append(list(sqlRow))

        return (finalStatus, finalList)

    def getRollbackSqlList(self, workflow_id):
        workflow_detail = SqlWorkflow.objects.get(id=workflow_id)
        listExecuteResult = json.loads(workflow_detail.execute_result)
        # 回滚数据倒序展示
        listExecuteResult.reverse()
        listBackupSql = []
        # 创建连接
        conn = MySQLdb.connect(host=self.inception_remote_backup_host,
                               user=self.inception_remote_backup_user,
                               passwd=self.inception_remote_backup_password,
                               port=self.inception_remote_backup_port,
                               charset='utf8')
        cur = conn.cursor()
        for row in listExecuteResult:
            try:
                # 获取backup_dbname
                if row[8] == 'None':
                    continue
                backupDbName = row[8]
                sequence = row[7]
                sql = row[5]
                opidTime = sequence.replace("'", "")
                sqlTable = "select tablename from %s.$_$Inception_backup_information$_$ where opid_time='%s';" % (
                    backupDbName, opidTime)
                cur.execute(sqlTable)
                listTables = cur.fetchall()
                if listTables:
                    tableName = listTables[0][0]
                    sqlBack = "select rollback_statement from %s.%s where opid_time='%s'" % (
                        backupDbName, tableName, opidTime)
                    cur.execute(sqlBack)
                    listBackup = cur.fetchall()
                    block_rollback_sql_list = [sql]
                    block_rollback_sql = '\n'.join([back_info[0] for back_info in listBackup])
                    block_rollback_sql_list.append(block_rollback_sql)
                    listBackupSql.append(block_rollback_sql_list)
            except Exception as e:
                logger.error(traceback.format_exc())
                raise Exception(e)
        return listBackupSql

    def _fetchall(self, sql, paramHost, paramPort, paramUser, paramPasswd, paramDb):
        '''
        封装mysql连接和获取结果集方法
        '''
        try:
            conn = MySQLdb.connect(host=paramHost, user=paramUser, passwd=paramPasswd, db=paramDb, port=paramPort,
                                   charset='utf8')
            cur = conn.cursor()
            ret = cur.execute(sql)
            result = cur.fetchall()
        except Exception as e:
            logger.error(traceback.format_exc())
            raise Exception(e)
        else:
            cur.close()
            conn.close()
        return result

    def getOscPercent(self, sqlSHA1):
        """已知SHA1值,去inception里查看OSC进度"""
        sqlStr = "inception get osc_percent '%s'" % sqlSHA1
        result = self._fetchall(sqlStr, self.inception_host, self.inception_port, '', '', '')
        if len(result) > 0:
            percent = result[0][3]
            timeRemained = result[0][4]
            pctResult = {"status": 0, "msg": "ok", "data": {"percent": percent, "timeRemained": timeRemained}}
        else:
            pctResult = {"status": 1, "msg": "没找到该SQL的进度信息,是否已经执行完毕?", "data": {"percent": -100, "timeRemained": -100}}
        return pctResult

    def stopOscProgress(self, sqlSHA1):
        """已知SHA1值,调用inception命令停止OSC进程,涉及的Inception命令和注意事项,请参考http://mysql-inception.github.io/inception-document/osc/"""
        sqlStr = "inception stop alter '%s'" % sqlSHA1
        result = self._fetchall(sqlStr, self.inception_host, self.inception_port, '', '', '')
        if result is not None:
            optResult = {"status": 0, "msg": "已成功停止OSC进程,请注意清理触发器和临时表,先清理触发器再删除临时表", "data": ""}
        else:
            optResult = {"status": 1, "msg": "ERROR 2624 (HY000):未找到OSC执行进程,可能已经执行完成", "data": ""}
        return optResult

    def query_print(self, sql_content, db_name):
        '''
        将sql交给inception打印语法树。
        '''
        # 工单审核使用
        sql = "/*--user=%s;--password=%s;--host=%s;--port=%d;--enable-query-print;*/\
                          inception_magic_start;\
                          use %s;\
                          %s\
                          inception_magic_commit;" % (
            self.user,
            self.password,
            self.host,
            self.port,
            db_name,
            sql_content)
        result = self._fetchall(sql, self.inception_host, self.inception_port, '', '', '')
        return result

    # inception执行情况统计
    def statistic(self):
        sql = '''
             select
                 sum(deleting)     deleting,
                 sum(inserting)    inserting,
                 sum(updating)     updating,
                 sum(selecting)    selecting,
                 sum(altertable)   altertable,
                 sum(renaming)     renaming,
                 sum(createindex)  createindex,
                 sum(dropindex)    dropindex,
                 sum(addcolumn)    addcolumn,
                 sum(dropcolumn)   dropcolumn,
                 sum(changecolumn) changecolumn,
                 sum(alteroption)  alteroption,
                 sum(alterconvert) alterconvert,
                 sum(createtable)  createtable,
                 sum(droptable)    droptable,
                 sum(createdb)     createdb,
                 sum(truncating)   truncating
               from statistic;'''
        try:
            return Dao(host=self.inception_remote_backup_host,
                       user=self.inception_remote_backup_user,
                       port=self.inception_remote_backup_port,
                       password=self.inception_remote_backup_password).mysql_query('inception', sql)
        except Exception:
            logger.error(traceback.format_exc())
            return {'column_list': [], 'rows': [], 'effect_row': 0}
Exemplo n.º 9
0
def autoreview(request):
    """正式提交SQL, 此处生成工单"""
    workflow_id = request.POST.get('workflow_id')
    sql_content = request.POST['sql_content']
    workflow_title = request.POST['workflow_name']
    group_name = request.POST['group_name']
    group_id = ResourceGroup.objects.get(group_name=group_name).group_id
    instance_name = request.POST['instance_name']
    instance = Instance.objects.get(instance_name=instance_name)
    db_name = request.POST.get('db_name')
    is_backup = request.POST['is_backup']
    notify_users = request.POST.getlist('notify_users')
    list_cc_addr = [
        email['email'] for email in Users.objects.filter(
            username__in=notify_users).values('email')
    ]

    # 服务器端参数验证
    if sql_content is None or workflow_title is None or instance_name is None or db_name is None or is_backup is None:
        context = {'errMsg': '页面提交参数可能为空'}
        return render(request, 'error.html', context)

    # 验证组权限(用户是否在该组、该组是否有指定实例)
    try:
        user_instances(request.user, 'master').get(instance_name=instance_name)
    except Exception:
        context = {'errMsg': '你所在组未关联该实例!'}
        return render(request, 'error.html', context)

    # # 删除注释语句
    # sql_content = ''.join(
    #     map(lambda x: re.compile(r'(^--.*|^/\*.*\*/;\s*$)').sub('', x, count=1),
    #         sql_content.splitlines(1))).strip()
    # # 去除空行
    # sql_content = re.sub('[\r\n\f]{2,}', '\n', sql_content)

    sql_content = sql_content.strip()

    # 审核
    try:
        check_engine = get_engine(instance)
        check_result = check_engine.execute_check(db_name=db_name,
                                                  sql=sql_content)
    except Exception as msg:
        context = {'errMsg': msg}
        return render(request, 'error.html', context)

    if not check_result:
        context = {'errMsg': 'inception返回的结果集为空!可能是SQL语句有语法错误'}
        return render(request, 'error.html', context)
    # 要把result转成JSON存进数据库里,方便SQL单子详细信息展示

    # 遍历result,看是否有任何自动审核不通过的地方,并且按配置确定是标记审核不通过还是放行,放行的可以在工单内跳过inception直接执行
    sys_config = SysConfig()
    is_manual = 0
    workflow_status = 'workflow_manreviewing'
    for row in check_result.rows:
        # 1表示警告,不影响执行
        if row.errlevel == 1 and sys_config.get('auto_review_wrong',
                                                '') == '1':
            workflow_status = 'workflow_autoreviewwrong'
            break
        # 2表示严重错误,或者inception不支持的语法,标记手工执行,可以跳过inception直接执行
        elif row.errlevel == 2:
            is_manual = 1
            if sys_config.get('auto_review_wrong', '') in ('', '1', '2'):
                workflow_status = 'workflow_autoreviewwrong'
            break
        elif re.match(r"\w*comments\w*", row.errormessage):
            is_manual = 1
            if sys_config.get('auto_review_wrong', '') in ('', '1', '2'):
                workflow_status = 'workflow_autoreviewwrong'
            break

    # 判断SQL是否包含DDL语句,SQL语法 1、DDL,2、DML
    sql_syntax = 2
    for stmt in sqlparse.split(sql_content):
        statement = sqlparse.parse(stmt)[0]
        syntax_type = statement.token_first(skip_cm=True).ttype.__str__()
        if syntax_type == 'Token.Keyword.DDL':
            sql_syntax = 1
            break

    # 调用工作流生成工单
    # 使用事务保持数据一致性
    try:
        with transaction.atomic():
            # 存进数据库里
            engineer = request.user.username
            if not workflow_id:
                sql_workflow = SqlWorkflow()
                sql_workflow.create_time = timezone.now()
            else:
                sql_workflow = SqlWorkflow.objects.get(id=int(workflow_id))
            sql_workflow.workflow_name = workflow_title
            sql_workflow.group_id = group_id
            sql_workflow.group_name = group_name
            sql_workflow.engineer = engineer
            sql_workflow.engineer_display = request.user.display
            sql_workflow.audit_auth_groups = Audit.settings(
                group_id, WorkflowDict.workflow_type['sqlreview'])
            sql_workflow.status = workflow_status
            sql_workflow.is_backup = is_backup
            sql_workflow.review_content = check_result.json()
            sql_workflow.instance_name = instance_name
            sql_workflow.db_name = db_name
            sql_workflow.sql_content = sql_content
            sql_workflow.execute_result = ''
            sql_workflow.is_manual = is_manual
            sql_workflow.audit_remark = ''
            sql_workflow.sql_syntax = sql_syntax
            sql_workflow.save()
            workflow_id = sql_workflow.id
            # 自动审核通过了,才调用工作流
            if workflow_status == 'workflow_manreviewing':
                # 调用工作流插入审核信息, 查询权限申请workflow_type=2
                Audit.add(WorkflowDict.workflow_type['sqlreview'], workflow_id)
    except Exception as msg:
        logger.error(traceback.format_exc())
        context = {'errMsg': msg}
        return render(request, 'error.html', context)
    else:
        # 自动审核通过了,才调用工作流,进行消息通知
        if workflow_status == 'workflow_manreviewing':
            sys_config = SysConfig()
            if sys_config.get('mail') or sys_config.get('ding'):
                # 再次获取审核信息
                audit_detail = Audit.detail_by_workflow_id(
                    workflow_id=workflow_id,
                    workflow_type=WorkflowDict.workflow_type['sqlreview'])
                base_url = sys_config.get('archery_base_url',
                                          'http://127.0.0.1:8000').rstrip('/')
                workflow_url = "{base_url}/workflow/{audit_id}".format(
                    base_url=base_url, audit_id=audit_detail.audit_id)
                async_task(notify,
                           audit_info=audit_detail,
                           workflow_url=workflow_url,
                           email_cc=list_cc_addr,
                           timeout=60)

    return HttpResponseRedirect(reverse('sql:detail', args=(workflow_id, )))
Exemplo n.º 10
0
    def execute_check(self, db_name=None, sql=''):
        """上线单执行前的检查, 返回Review set"""
        config = SysConfig()
        check_result = ReviewSet(full_sql=sql)
        # 禁用/高危语句检查
        line = 1
        critical_ddl_regex = config.get('critical_ddl_regex', '')
        p = re.compile(critical_ddl_regex)
        check_result.syntax_type = 2  # TODO 工单类型 0、其他 1、DDL,2、DML

        # 把所有SQL转换成SqlItem List。 如有多行(内部有多个;)执行块,约定以delimiter $$作为开始, 以$$结束
        # 需要在函数里实现单条SQL做sqlparse.format(sql, strip_comments=True)
        sqlitemList = get_full_sqlitem_list(sql, db_name)

        for sqlitem in sqlitemList:
            # 禁用语句
            if re.match(r"^\s*select", sqlitem.statement.lower(), re.I):
                check_result.is_critical = True
                result = ReviewResult(
                    id=line,
                    errlevel=2,
                    stagestatus='驳回不支持语句',
                    errormessage='仅支持DML和DDL语句,查询语句请使用SQL查询功能!',
                    sql=sqlitem.statement)
            # 高危语句
            elif critical_ddl_regex and p.match(
                    sqlitem.statement.strip().lower()):
                check_result.is_critical = True
                result = ReviewResult(id=line,
                                      errlevel=2,
                                      stagestatus='驳回高危SQL',
                                      errormessage='禁止提交匹配' +
                                      critical_ddl_regex + '条件的语句!',
                                      sql=sqlitem.statement)

            # 正常语句
            else:
                result = ReviewResult(
                    id=line,
                    errlevel=0,
                    stagestatus='Audit completed',
                    errormessage='None',
                    sql=sqlitem.statement,
                    stmt_type=sqlitem.stmt_type,
                    object_owner=sqlitem.object_owner,
                    object_type=sqlitem.object_type,
                    object_name=sqlitem.object_name,
                    affected_rows=0,
                    execute_time=0,
                )
            # 判断工单类型
            if get_syntax_type(sqlitem.statement) == 'DDL':
                check_result.syntax_type = 1
            check_result.rows += [result]

            # 遇到禁用和高危语句直接返回,提高效率
            if check_result.is_critical:
                check_result.error_count += 1
                return check_result
            line += 1
        return check_result
Exemplo n.º 11
0
    def execute_check(self, db_name=None, sql=''):
        """上线单执行前的检查, 返回Review set"""
        config = SysConfig()
        check_result = ReviewSet(full_sql=sql)
        # 禁用/高危语句检查
        line = 1
        critical_ddl_regex = config.get('critical_ddl_regex', '')
        p = re.compile(critical_ddl_regex)
        check_result.syntax_type = 2  # TODO 工单类型 0、其他 1、DDL,2、DML
        for statement in sqlparse.split(sql):
            statement = sqlparse.format(statement, strip_comments=True)
            # 禁用语句
            if re.match(r"^select", statement.lower()):
                check_result.is_critical = False
                result = ReviewResult(id=line,
                                      errlevel=0,
                                      stagestatus='Select statements',
                                      errormessage='None',
                                      sql=statement)
                check_result.rows += [result]
                check_result.syntax_type = 0
                # check_result.error_count += 1
            # 高危语句
            elif critical_ddl_regex and p.match(statement.strip().lower()):
                check_result.is_critical = True
                result = ReviewResult(id=line,
                                      errlevel=2,
                                      stagestatus='驳回高危SQL',
                                      errormessage='禁止提交匹配' +
                                      critical_ddl_regex + '条件的语句!',
                                      sql=statement)
            # 正常语句
            else:
                result = ReviewResult(
                    id=line,
                    errlevel=0,
                    stagestatus='Audit completed',
                    errormessage='None',
                    sql=statement,
                    affected_rows=0,
                    execute_time=0,
                )
            # 判断工单类型
            # 没有找出DDL语句的才继续执行此判断
            if check_result.syntax_type == 2:
                if get_syntax_type(statement) == 'DDL':
                    check_result.syntax_type = 1
            check_result.rows += [result]

            # 遇到禁用和高危语句直接返回,提高效率
            if check_result.is_critical:
                check_result.error_count += 1
                return check_result
            line += 1
        # 通过检测的再进行inception检查
        if config.get('go_inception'):
            try:
                inception_engine = GoInceptionEngine()
                check_result = inception_engine.execute_check(
                    instance=self.instance, db_name=db_name, sql=sql)
            except Exception as e:
                logger.debug(f"Inception检测语句报错:错误信息{traceback.format_exc()}")
                raise RuntimeError(
                    f"Inception检测语句报错,请注意检查系统配置中Inception配置,错误信息:\n{e}")
        else:
            try:
                inception_engine = InceptionEngine()
                check_result = inception_engine.execute_check(
                    instance=self.instance, db_name=db_name, sql=sql)
            except Exception as e:
                logger.debug(f"Inception检测语句报错:错误信息{traceback.format_exc()}")
                raise RuntimeError(
                    f"Inception检测语句报错,请注意检查系统配置中Inception配置,错误信息:\n{e}")
        return check_result
Exemplo n.º 12
0
    def addworkflowaudit(self, request, workflow_type, workflow_id, **kwargs):
        result = {'status': 0, 'msg': '', 'data': []}

        # 检查是否已存在待审核数据
        workflowInfo = WorkflowAudit.objects.filter(workflow_type=workflow_type, workflow_id=workflow_id,
                                                    current_status=WorkflowDict.workflow_status['audit_wait'])
        if len(workflowInfo) >= 1:
            result['msg'] = '该工单当前状态为待审核,请勿重复提交'
            raise Exception(result['msg'])

        # 获取工单信息
        if workflow_type == WorkflowDict.workflow_type['query']:
            workflow_detail = QueryPrivilegesApply.objects.get(apply_id=workflow_id)
            workflow_title = workflow_detail.title
            group_id = workflow_detail.group_id
            group_name = workflow_detail.group_name
            create_user = workflow_detail.user_name
            audit_auth_groups = workflow_detail.audit_auth_groups
            workflow_remark = ''
        elif workflow_type == WorkflowDict.workflow_type['sqlreview']:
            workflow_detail = SqlWorkflow.objects.get(pk=workflow_id)
            workflow_title = workflow_detail.workflow_name
            group_id = workflow_detail.group_id
            group_name = workflow_detail.group_name
            create_user = workflow_detail.engineer
            audit_auth_groups = workflow_detail.audit_auth_groups
            workflow_remark = ''
        else:
            result['msg'] = '工单类型不存在'
            raise Exception(result['msg'])

        # 校验是否配置审批流程
        if audit_auth_groups is None:
            result['msg'] = '审批流程不能为空,请先配置审批流程'
            raise Exception(result['msg'])
        else:
            audit_auth_groups_list = audit_auth_groups.split(',')

        # 判断是否无需审核,并且修改审批人为空
        if SysConfig().sys_config.get('auto_review', False):
            if workflow_type == WorkflowDict.workflow_type['sqlreview']:
                if is_autoreview(workflow_id):
                    Workflow = SqlWorkflow.objects.get(id=int(workflow_id))
                    Workflow.audit_auth_groups = '无需审批'
                    Workflow.status = '审核通过'
                    Workflow.save()
                    audit_auth_groups_list = None

        # 无审核配置则无需审核,直接通过
        if audit_auth_groups_list is None:
            # 向审核主表插入审核通过的数据
            audit_detail = WorkflowAudit()
            audit_detail.group_id = group_id
            audit_detail.group_name = group_name
            audit_detail.workflow_id = workflow_id
            audit_detail.workflow_type = workflow_type
            audit_detail.workflow_title = workflow_title
            audit_detail.workflow_remark = workflow_remark
            audit_detail.audit_auth_groups = ''
            audit_detail.current_audit = '-1'
            audit_detail.next_audit = '-1'
            audit_detail.current_status = WorkflowDict.workflow_status['audit_success']  # 审核通过
            audit_detail.create_user = create_user
            audit_detail.create_user_display = request.user.display
            audit_detail.save()
            result['data'] = {'workflow_status': WorkflowDict.workflow_status['audit_success']}
            result['msg'] = '无审核配置,直接审核通过'
            # 增加工单日志
            self.add_workflow_log(audit_id=audit_detail.audit_id,
                                  operation_type=0,
                                  operation_type_desc='提交',
                                  operation_info='无需审批,系统直接审核通过',
                                  operator=audit_detail.create_user,
                                  operator_display=audit_detail.create_user_display
                                  )
        else:
            # 向审核主表插入待审核数据
            audit_detail = WorkflowAudit()
            audit_detail.group_id = group_id
            audit_detail.group_name = group_name
            audit_detail.workflow_id = workflow_id
            audit_detail.workflow_type = workflow_type
            audit_detail.workflow_title = workflow_title
            audit_detail.workflow_remark = workflow_remark
            audit_detail.audit_auth_groups = ','.join(audit_auth_groups_list)
            audit_detail.current_audit = audit_auth_groups_list[0]
            # 判断有无下级审核
            if len(audit_auth_groups_list) == 1:
                audit_detail.next_audit = '-1'
            else:
                audit_detail.next_audit = audit_auth_groups_list[1]

            audit_detail.current_status = WorkflowDict.workflow_status['audit_wait']
            audit_detail.create_user = create_user
            audit_detail.create_user_display = request.user.display
            audit_detail.save()
            result['data'] = {'workflow_status': WorkflowDict.workflow_status['audit_wait']}
            # 增加工单日志
            audit_auth_group, current_audit_auth_group = self.review_info(workflow_id, workflow_type)
            self.add_workflow_log(audit_id=audit_detail.audit_id,
                                  operation_type=0,
                                  operation_type_desc='提交',
                                  operation_info='等待审批,审批流程:{}'.format(audit_auth_group),
                                  operator=audit_detail.create_user,
                                  operator_display=audit_detail.create_user_display
                                  )

        # 消息通知
        sys_config = SysConfig().sys_config
        if sys_config.get('mail') or sys_config.get('ding'):
            # 再次获取审核信息
            audit_info = WorkflowAudit.objects.get(audit_id=audit_detail.audit_id)
            workflow_url = "{}://{}/workflow/{}".format(request.scheme, request.get_host(), audit_detail.audit_id)
            email_cc = kwargs.get('list_cc_addr', [])
            send_msg(audit_info=audit_info, workflow_url=workflow_url, email_cc=email_cc)

        # 返回添加结果
        return result
Exemplo n.º 13
0
    def auditworkflow(self, request, audit_id, audit_status, audit_user, audit_remark):
        result = {'status': 0, 'msg': 'ok', 'data': 0}
        audit_detail = WorkflowAudit.objects.get(audit_id=audit_id)

        # 不同审核状态
        if audit_status == WorkflowDict.workflow_status['audit_success']:
            # 判断当前工单是否为待审核状态
            if audit_detail.current_status != WorkflowDict.workflow_status['audit_wait']:
                result['msg'] = '工单不是待审核状态,请返回刷新'
                raise Exception(result['msg'])

            # 判断是否还有下一级审核
            if audit_detail.next_audit == '-1':
                # 更新主表审核状态为审核通过
                audit_result = WorkflowAudit()
                audit_result.audit_id = audit_id
                audit_result.current_audit = '-1'
                audit_result.current_status = WorkflowDict.workflow_status['audit_success']
                audit_result.save(update_fields=['current_audit', 'current_status'])
            else:
                # 更新主表审核下级审核组和当前审核组
                audit_result = WorkflowAudit()
                audit_result.audit_id = audit_id
                audit_result.current_status = WorkflowDict.workflow_status['audit_wait']
                audit_result.current_audit = audit_detail.next_audit
                # 判断后续是否还有下下一级审核组
                audit_auth_groups_list = audit_detail.audit_auth_groups.split(',')
                for index, auth_group in enumerate(audit_auth_groups_list):
                    if auth_group == audit_detail.next_audit:
                        # 无下下级审核组
                        if index == len(audit_auth_groups_list) - 1:
                            audit_result.next_audit = '-1'
                            break
                        # 存在下下级审核组
                        else:
                            audit_result.next_audit = audit_auth_groups_list[index + 1]
                audit_result.save(update_fields=['current_audit', 'next_audit', 'current_status'])

            # 插入审核明细数据
            audit_detail_result = WorkflowAuditDetail()
            audit_detail_result.audit_id = audit_id
            audit_detail_result.audit_user = audit_user
            audit_detail_result.audit_status = WorkflowDict.workflow_status['audit_success']
            audit_detail_result.audit_time = timezone.now()
            audit_detail_result.remark = audit_remark
            audit_detail_result.save()
            # 增加工单日志
            audit_auth_group, current_audit_auth_group = self.review_info(audit_detail.workflow_id,
                                                                          audit_detail.workflow_type)
            self.add_workflow_log(audit_id=audit_id,
                                  operation_type=1,
                                  operation_type_desc='审批通过',
                                  operation_info="审批备注:{},下级审批:{}".format(audit_remark, current_audit_auth_group),
                                  operator=request.user.username,
                                  operator_display=request.user.display
                                  )
        elif audit_status == WorkflowDict.workflow_status['audit_reject']:
            # 判断当前工单是否为待审核状态
            if audit_detail.current_status != WorkflowDict.workflow_status['audit_wait']:
                result['msg'] = '工单不是待审核状态,请返回刷新'
                raise Exception(result['msg'])

            # 更新主表审核状态
            audit_result = WorkflowAudit()
            audit_result.audit_id = audit_id
            audit_result.current_audit = '-1'
            audit_result.next_audit = '-1'
            audit_result.current_status = WorkflowDict.workflow_status['audit_reject']
            audit_result.save(update_fields=['current_audit', 'next_audit', 'current_status'])

            # 插入审核明细数据
            audit_detail_result = WorkflowAuditDetail()
            audit_detail_result.audit_id = audit_id
            audit_detail_result.audit_user = audit_user
            audit_detail_result.audit_status = WorkflowDict.workflow_status['audit_reject']
            audit_detail_result.audit_time = timezone.now()
            audit_detail_result.remark = audit_remark
            audit_detail_result.save()
            # 增加工单日志
            self.add_workflow_log(audit_id=audit_id,
                                  operation_type=2,
                                  operation_type_desc='审批不通过',
                                  operation_info="审批备注:{}".format(audit_remark),
                                  operator=request.user.username,
                                  operator_display=request.user.display
                                  )
        elif audit_status == WorkflowDict.workflow_status['audit_abort']:
            # 判断当前工单是否为待审核/审核通过状态
            if audit_detail.current_status != WorkflowDict.workflow_status['audit_wait'] and \
                    audit_detail.current_status != WorkflowDict.workflow_status['audit_success']:
                result['msg'] = '工单不是待审核态/审核通过状态,请返回刷新'
                raise Exception(result['msg'])

            # 更新主表审核状态
            audit_result = WorkflowAudit()
            audit_result.audit_id = audit_id
            audit_result.current_status = WorkflowDict.workflow_status['audit_abort']
            audit_result.save(update_fields=['current_status'])

            # 插入审核明细数据
            audit_detail_result = WorkflowAuditDetail()
            audit_detail_result.audit_id = audit_id
            audit_detail_result.audit_user = audit_user
            audit_detail_result.audit_status = WorkflowDict.workflow_status['audit_abort']
            audit_detail_result.audit_time = timezone.now()
            audit_detail_result.remark = audit_remark
            audit_detail_result.save()

            # 增加工单日志
            self.add_workflow_log(audit_id=audit_id,
                                  operation_type=3,
                                  operation_type_desc='审批取消',
                                  operation_info="取消原因:{}".format(audit_remark),
                                  operator=request.user.username,
                                  operator_display=request.user.display
                                  )
        else:
            result['msg'] = '审核异常'
            raise Exception(result['msg'])

        # 消息通知
        sys_config = SysConfig().sys_config
        if sys_config.get('mail') or sys_config.get('ding'):
            # 再次获取审核信息
            audit_info = WorkflowAudit.objects.get(audit_id=audit_id)
            workflow_url = "{}://{}/workflow/{}".format(request.scheme, request.get_host(), audit_detail.audit_id)
            send_msg(audit_info=audit_info, workflow_url=workflow_url, audit_remark=audit_remark)
        # 返回审核结果
        result['data'] = {'workflow_status': audit_result.current_status}
        return result
Exemplo n.º 14
0
class MysqlEngine(EngineBase):
    def __init__(self, instance=None):
        super().__init__(instance=instance)
        self.config = SysConfig()
        self.inc_engine = InceptionEngine() if self.config.get(
            'inception') else GoInceptionEngine()

    def get_connection(self, db_name=None):
        # https://stackoverflow.com/questions/19256155/python-mysqldb-returning-x01-for-bit-values
        conversions = MySQLdb.converters.conversions
        conversions[FIELD_TYPE.BIT] = lambda data: data == b'\x01'
        if self.conn:
            self.thread_id = self.conn.thread_id()
            return self.conn
        if db_name:
            self.conn = MySQLdb.connect(host=self.host,
                                        port=self.port,
                                        user=self.user,
                                        passwd=self.password,
                                        db=db_name,
                                        charset=self.instance.charset
                                        or 'utf8mb4',
                                        conv=conversions,
                                        connect_timeout=10)
        else:
            self.conn = MySQLdb.connect(host=self.host,
                                        port=self.port,
                                        user=self.user,
                                        passwd=self.password,
                                        charset=self.instance.charset
                                        or 'utf8mb4',
                                        conv=conversions,
                                        connect_timeout=10)
        self.thread_id = self.conn.thread_id()
        return self.conn

    @property
    def name(self):
        return 'MySQL'

    @property
    def info(self):
        return 'MySQL engine'

    @property
    def auto_backup(self):
        """是否支持备份"""
        return True

    @property
    def seconds_behind_master(self):
        slave_status = self.query(sql='show slave status',
                                  close_conn=False,
                                  cursorclass=MySQLdb.cursors.DictCursor)
        return slave_status.rows[0].get(
            'Seconds_Behind_Master') if slave_status.rows else None

    @property
    def server_version(self):
        def numeric_part(s):
            """Returns the leading numeric part of a string.
            """
            re_numeric_part = re.compile(r"^(\d+)")
            m = re_numeric_part.match(s)
            if m:
                return int(m.group(1))
            return None

        self.get_connection()
        version = self.conn.get_server_info()
        return tuple([numeric_part(n) for n in version.split('.')[:3]])

    @property
    def schema_object(self):
        """获取实例对象信息"""
        url = build_database_url(host=self.host,
                                 username=self.user,
                                 password=self.password,
                                 port=self.port)
        return schemaobject.SchemaObject(url,
                                         charset=self.instance.charset
                                         or 'utf8mb4')

    def kill_connection(self, thread_id):
        """终止数据库连接"""
        self.query(sql=f'kill {thread_id}')

    def get_all_databases(self):
        """获取数据库列表, 返回一个ResultSet"""
        sql = "show databases"
        result = self.query(sql=sql)
        db_list = [
            row[0] for row in result.rows
            if row[0] not in ('information_schema', 'performance_schema',
                              'mysql', 'test', 'sys')
        ]
        result.rows = db_list
        return result

    def get_all_tables(self, db_name, **kwargs):
        """获取table 列表, 返回一个ResultSet"""
        sql = "show tables"
        result = self.query(db_name=db_name, sql=sql)
        tb_list = [row[0] for row in result.rows if row[0] not in ['test']]
        result.rows = tb_list
        return result

    def get_all_columns_by_tb(self, db_name, tb_name, **kwargs):
        """获取所有字段, 返回一个ResultSet"""
        sql = f"""SELECT 
            COLUMN_NAME,
            COLUMN_TYPE,
            CHARACTER_SET_NAME,
            IS_NULLABLE,
            COLUMN_KEY,
            EXTRA,
            COLUMN_COMMENT
        FROM
            information_schema.COLUMNS
        WHERE
            TABLE_SCHEMA = '{db_name}'
                AND TABLE_NAME = '{tb_name}'
        ORDER BY ORDINAL_POSITION;"""
        result = self.query(db_name=db_name, sql=sql)
        column_list = [row[0] for row in result.rows]
        result.rows = column_list
        return result

    def describe_table(self, db_name, tb_name, **kwargs):
        """return ResultSet 类似查询"""
        sql = f"show create table `{tb_name}`;"
        result = self.query(db_name=db_name, sql=sql)
        return result

    def query(self,
              db_name=None,
              sql='',
              limit_num=0,
              close_conn=True,
              **kwargs):
        """返回 ResultSet """
        result_set = ResultSet(full_sql=sql)
        max_execution_time = kwargs.get('max_execution_time', 0)
        cursorclass = kwargs.get('cursorclass') or MySQLdb.cursors.Cursor
        try:
            conn = self.get_connection(db_name=db_name)
            conn.autocommit(True)
            cursor = conn.cursor(cursorclass)
            try:
                cursor.execute(
                    f"set session max_execution_time={max_execution_time};")
            except MySQLdb.OperationalError:
                pass
            effect_row = cursor.execute(sql)
            if int(limit_num) > 0:
                rows = cursor.fetchmany(size=int(limit_num))
            else:
                rows = cursor.fetchall()
            fields = cursor.description

            result_set.column_list = [i[0] for i in fields] if fields else []
            result_set.rows = rows
            result_set.affected_rows = effect_row
        except Exception as e:
            logger.warning(
                f"MySQL语句执行报错,语句:{sql},错误信息{traceback.format_exc()}")
            result_set.error = str(e)
        finally:
            if close_conn:
                self.close()
        return result_set

    def query_check(self, db_name=None, sql=''):
        # 查询语句的检查、注释去除、切分
        result = {
            'msg': '',
            'bad_query': False,
            'filtered_sql': sql,
            'has_star': False
        }
        # 删除注释语句,进行语法判断,执行第一条有效sql
        try:
            sql = sqlparse.format(sql, strip_comments=True)
            sql = sqlparse.split(sql)[0]
            result['filtered_sql'] = sql.strip()
        except IndexError:
            result['bad_query'] = True
            result['msg'] = '没有有效的SQL语句'
        if re.match(r"^select|^show|^explain", sql, re.I) is None:
            result['bad_query'] = True
            result['msg'] = '不支持的查询语法类型!'
        if '*' in sql:
            result['has_star'] = True
            result['msg'] = 'SQL语句中含有 * '
        # select语句先使用Explain判断语法是否正确
        if re.match(r"^select", sql, re.I):
            explain_result = self.query(db_name=db_name, sql=f"explain {sql}")
            if explain_result.error:
                result['bad_query'] = True
                result['msg'] = explain_result.error
        return result

    def filter_sql(self, sql='', limit_num=0):
        # 对查询sql增加limit限制,limit n 或 limit n,n 或 limit n offset n统一改写成limit n
        sql = sql.rstrip(';').strip()
        if re.match(r"^select", sql, re.I):
            # LIMIT N
            limit_n = re.compile(r'limit([\s]*\d+[\s]*)$', re.I)
            # LIMIT N, N 或LIMIT N OFFSET N
            limit_offset = re.compile(
                r'limit([\s]*\d+[\s]*)(,|offset)([\s]*\d+[\s]*)$', re.I)
            if limit_n.search(sql):
                sql_limit = limit_n.search(sql).group(1)
                limit_num = min(int(limit_num), int(sql_limit))
                sql = limit_n.sub(f'limit {limit_num};', sql)
            elif limit_offset.search(sql):
                sql_limit = limit_offset.search(sql).group(3)
                limit_num = min(int(limit_num), int(sql_limit))
                sql = limit_offset.sub(f'limit {limit_num};', sql)
            else:
                sql = f'{sql} limit {limit_num};'
        else:
            sql = f'{sql};'
        return sql

    def query_masking(self, db_name=None, sql='', resultset=None):
        """传入 sql语句, db名, 结果集,
        返回一个脱敏后的结果集"""
        # 仅对select语句脱敏
        if re.match(r"^select", sql, re.I):
            mask_result = data_masking(self.instance, db_name, sql, resultset)
        else:
            mask_result = resultset
        return mask_result

    def execute_check(self, db_name=None, sql=''):
        """上线单执行前的检查, 返回Review set"""
        # 进行Inception检查,获取检测结果
        try:
            inc_check_result = self.inc_engine.execute_check(
                instance=self.instance, db_name=db_name, sql=sql)
        except Exception as e:
            logger.debug(
                f"{self.inc_engine.name}检测语句报错:错误信息{traceback.format_exc()}")
            raise RuntimeError(
                f"{self.inc_engine.name}检测语句报错,请注意检查系统配置中{self.inc_engine.name}配置,错误信息:\n{e}"
            )

        # 判断Inception检测结果
        if inc_check_result.error:
            logger.debug(
                f"{self.inc_engine.name}检测语句报错:错误信息{inc_check_result.error}")
            raise RuntimeError(
                f"{self.inc_engine.name}检测语句报错,错误信息:\n{inc_check_result.error}"
            )

        # 禁用/高危语句检查
        check_critical_result = ReviewSet(full_sql=sql)
        line = 1
        critical_ddl_regex = self.config.get('critical_ddl_regex', '')
        p = re.compile(critical_ddl_regex)
        check_critical_result.syntax_type = 2  # TODO 工单类型 0、其他 1、DDL,2、DML

        for row in inc_check_result.rows:
            statement = row.sql
            # 去除注释
            statement = remove_comments(statement, db_type='mysql')
            # 禁用语句
            if re.match(r"^select", statement.lower()):
                check_critical_result.is_critical = True
                result = ReviewResult(
                    id=line,
                    errlevel=2,
                    stagestatus='驳回不支持语句',
                    errormessage='仅支持DML和DDL语句,查询语句请使用SQL查询功能!',
                    sql=statement)
            # 高危语句
            elif critical_ddl_regex and p.match(statement.strip().lower()):
                check_critical_result.is_critical = True
                result = ReviewResult(id=line,
                                      errlevel=2,
                                      stagestatus='驳回高危SQL',
                                      errormessage='禁止提交匹配' +
                                      critical_ddl_regex + '条件的语句!',
                                      sql=statement)
            # 正常语句
            else:
                result = ReviewResult(
                    id=line,
                    errlevel=0,
                    stagestatus='Audit completed',
                    errormessage='None',
                    sql=statement,
                    affected_rows=0,
                    execute_time=0,
                )

            # 没有找出DDL语句的才继续执行此判断
            if check_critical_result.syntax_type == 2:
                if get_syntax_type(statement, parser=False,
                                   db_type='mysql') == 'DDL':
                    check_critical_result.syntax_type = 1
            check_critical_result.rows += [result]

            # 遇到禁用和高危语句直接返回
            if check_critical_result.is_critical:
                check_critical_result.error_count += 1
                return check_critical_result
            line += 1
        return inc_check_result

    def execute_workflow(self, workflow):
        """执行上线单,返回Review set"""
        # 判断实例是否只读
        read_only = self.query(sql='SELECT @@global.read_only;').rows[0][0]
        if read_only in (1, 'ON'):
            result = ReviewSet(
                full_sql=workflow.sqlworkflowcontent.sql_content,
                rows=[
                    ReviewResult(id=1,
                                 errlevel=2,
                                 stagestatus='Execute Failed',
                                 errormessage='实例read_only=1,禁止执行变更语句!',
                                 sql=workflow.sqlworkflowcontent.sql_content)
                ])
            result.error = '实例read_only=1,禁止执行变更语句!',
            return result
        # TODO 原生执行
        # if workflow.is_manual == 1:
        #     return self.execute(db_name=workflow.db_name, sql=workflow.sqlworkflowcontent.sql_content)
        # inception执行
        return self.inc_engine.execute(workflow)

    def execute(self, db_name=None, sql='', close_conn=True):
        """原生执行语句"""
        result = ResultSet(full_sql=sql)
        conn = self.get_connection(db_name=db_name)
        try:
            cursor = conn.cursor()
            for statement in sqlparse.split(sql):
                cursor.execute(statement)
            conn.commit()
            cursor.close()
        except Exception as e:
            logger.warning(
                f"MySQL语句执行报错,语句:{sql},错误信息{traceback.format_exc()}")
            result.error = str(e)
        if close_conn:
            self.close()
        return result

    def get_rollback(self, workflow):
        """通过inception获取回滚语句列表"""
        inception_engine = InceptionEngine()
        return inception_engine.get_rollback(workflow)

    def get_variables(self, variables=None):
        """获取实例参数"""
        if variables:
            variables = "','".join(variables) if isinstance(
                variables, list) else "','".join(list(variables))
            db = 'performance_schema' if self.server_version > (
                5, 7) else 'information_schema'
            sql = f"""select * from {db}.global_variables where variable_name in ('{variables}');"""
        else:
            sql = "show global variables;"
        return self.query(sql=sql)

    def set_variable(self, variable_name, variable_value):
        """修改实例参数值"""
        sql = f"""set global {variable_name}={variable_value};"""
        return self.query(sql=sql)

    def osc_control(self, **kwargs):
        """控制osc执行,获取进度、终止、暂停、恢复等
            get、kill、pause、resume
        """
        return self.inc_engine.osc_control(**kwargs)

    def close(self):
        if self.conn:
            self.conn.close()
            self.conn = None
Exemplo n.º 15
0
class ClickHouseEngine(EngineBase):
    def __init__(self, instance=None):
        super(ClickHouseEngine, self).__init__(instance=instance)
        self.config = SysConfig()

    def get_connection(self, db_name=None):
        if self.conn:
            return self.conn
        if db_name:
            self.conn = connect(host=self.host,
                                port=self.port,
                                user=self.user,
                                password=self.password,
                                database=db_name,
                                connect_timeout=10)
        else:
            self.conn = connect(host=self.host,
                                port=self.port,
                                user=self.user,
                                password=self.password,
                                connect_timeout=10)
        return self.conn

    @property
    def name(self):
        return 'ClickHouse'

    @property
    def info(self):
        return 'ClickHouse engine'

    @property
    def auto_backup(self):
        """是否支持备份"""
        return False

    @property
    def server_version(self):
        sql = "select value from system.build_options where name = 'VERSION_FULL';"
        result = self.query(sql=sql)
        version = result.rows[0][0].split(' ')[1]
        return tuple([int(n) for n in version.split('.')[:3]])

    def get_table_engine(self, tb_name):
        """获取某个table的engine type"""
        sql = f"""select engine 
                    from system.tables 
                   where database='{tb_name.split('.')[0]}' 
                     and name='{tb_name.split('.')[1]}'"""
        query_result = self.query(sql=sql)
        if query_result.rows:
            result = {'status': 1, 'engine': query_result.rows[0][0]}
        else:
            result = {'status': 0, 'engine': 'None'}
        return result

    def get_all_databases(self):
        """获取数据库列表, 返回一个ResultSet"""
        sql = "show databases"
        result = self.query(sql=sql)
        db_list = [
            row[0] for row in result.rows
            if row[0] not in ('system', 'INFORMATION_SCHEMA',
                              'information_schema', 'datasets')
        ]
        result.rows = db_list
        return result

    def get_all_tables(self, db_name, **kwargs):
        """获取table 列表, 返回一个ResultSet"""
        sql = "show tables"
        result = self.query(db_name=db_name, sql=sql)
        tb_list = [row[0] for row in result.rows]
        result.rows = tb_list
        return result

    def get_all_columns_by_tb(self, db_name, tb_name, **kwargs):
        """获取所有字段, 返回一个ResultSet"""
        sql = f"""select
            name,
            type,
            comment
        from
            system.columns
        where
            database = '{db_name}'
        and table = '{tb_name}';"""
        result = self.query(db_name=db_name, sql=sql)
        column_list = [row[0] for row in result.rows]
        result.rows = column_list
        return result

    def describe_table(self, db_name, tb_name, **kwargs):
        """return ResultSet 类似查询"""
        sql = f"show create table `{tb_name}`;"
        result = self.query(db_name=db_name, sql=sql)

        result.rows[0] = (tb_name, ) + (result.rows[0][0].replace(
            '(', '(\n ').replace(',', ',\n '), )
        return result

    def query(self,
              db_name=None,
              sql='',
              limit_num=0,
              close_conn=True,
              **kwargs):
        """返回 ResultSet """
        result_set = ResultSet(full_sql=sql)
        try:
            conn = self.get_connection(db_name=db_name)
            cursor = conn.cursor()
            cursor.execute(sql)
            if int(limit_num) > 0:
                rows = cursor.fetchmany(size=int(limit_num))
            else:
                rows = cursor.fetchall()
            fields = cursor.description

            result_set.column_list = [i[0] for i in fields] if fields else []
            result_set.rows = rows
            result_set.affected_rows = len(rows)
        except Exception as e:
            logger.warning(f"ClickHouse语句执行报错,语句:{sql},错误信息{e}")
            result_set.error = str(e).split('Stack trace')[0]
        finally:
            if close_conn:
                self.close()
        return result_set

    def query_check(self, db_name=None, sql=''):
        # 查询语句的检查、注释去除、切分
        result = {
            'msg': '',
            'bad_query': False,
            'filtered_sql': sql,
            'has_star': False
        }
        # 删除注释语句,进行语法判断,执行第一条有效sql
        try:
            sql = sqlparse.format(sql, strip_comments=True)
            sql = sqlparse.split(sql)[0]
            result['filtered_sql'] = sql.strip()
        except IndexError:
            result['bad_query'] = True
            result['msg'] = '没有有效的SQL语句'
        if re.match(r"^select|^show|^explain", sql, re.I) is None:
            result['bad_query'] = True
            result['msg'] = '不支持的查询语法类型!'
        if '*' in sql:
            result['has_star'] = True
            result['msg'] = 'SQL语句中含有 * '
        # clickhouse 20.6.3版本开始正式支持explain语法
        if re.match(r"^explain", sql,
                    re.I) and self.server_version < (20, 6, 3):
            result['bad_query'] = True
            result['msg'] = f"当前ClickHouse实例版本低于20.6.3,不支持explain!"
        # select语句先使用Explain判断语法是否正确
        if re.match(r"^select", sql,
                    re.I) and self.server_version >= (20, 6, 3):
            explain_result = self.query(db_name=db_name, sql=f"explain {sql}")
            if explain_result.error:
                result['bad_query'] = True
                result['msg'] = explain_result.error

        return result

    def filter_sql(self, sql='', limit_num=0):
        # 对查询sql增加limit限制,limit n 或 limit n,n 或 limit n offset n统一改写成limit n
        sql = sql.rstrip(';').strip()
        if re.match(r"^select", sql, re.I):
            # LIMIT N
            limit_n = re.compile(r'limit\s+(\d+)\s*$', re.I)
            # LIMIT M OFFSET N
            limit_offset = re.compile(r'limit\s+(\d+)\s+offset\s+(\d+)\s*$',
                                      re.I)
            # LIMIT M,N
            offset_comma_limit = re.compile(r'limit\s+(\d+)\s*,\s*(\d+)\s*$',
                                            re.I)
            if limit_n.search(sql):
                sql_limit = limit_n.search(sql).group(1)
                limit_num = min(int(limit_num), int(sql_limit))
                sql = limit_n.sub(f'limit {limit_num};', sql)
            elif limit_offset.search(sql):
                sql_limit = limit_offset.search(sql).group(1)
                sql_offset = limit_offset.search(sql).group(2)
                limit_num = min(int(limit_num), int(sql_limit))
                sql = limit_offset.sub(
                    f'limit {limit_num} offset {sql_offset};', sql)
            elif offset_comma_limit.search(sql):
                sql_offset = offset_comma_limit.search(sql).group(1)
                sql_limit = offset_comma_limit.search(sql).group(2)
                limit_num = min(int(limit_num), int(sql_limit))
                sql = offset_comma_limit.sub(
                    f'limit {sql_offset},{limit_num};', sql)
            else:
                sql = f'{sql} limit {limit_num};'
        else:
            sql = f'{sql};'
        return sql

    def explain_check(self, check_result, db_name=None, line=0, statement=''):
        """使用explain ast检查sql语法, 返回Review set"""
        result = ReviewResult(
            id=line,
            errlevel=0,
            stagestatus='Audit completed',
            errormessage='None',
            sql=statement,
            affected_rows=0,
            execute_time=0,
        )
        # clickhouse版本>=21.1.2 explain ast才支持非select语句检查
        if self.server_version >= (21, 1, 2):
            explain_result = self.query(db_name=db_name,
                                        sql=f"explain ast {statement}")
            if explain_result.error:
                check_result.is_critical = True
                result = ReviewResult(
                    id=line,
                    errlevel=2,
                    stagestatus='驳回未通过检查SQL',
                    errormessage=f'explain语法检查错误:{explain_result.error}',
                    sql=statement)
        return result

    def execute_check(self, db_name=None, sql=''):
        """上线单执行前的检查, 返回Review set"""
        sql = sqlparse.format(sql, strip_comments=True)
        sql_list = sqlparse.split(sql)

        # 禁用/高危语句检查
        check_result = ReviewSet(full_sql=sql)
        line = 1
        critical_ddl_regex = self.config.get('critical_ddl_regex', '')
        p = re.compile(critical_ddl_regex)
        check_result.syntax_type = 2  # TODO 工单类型 0、其他 1、DDL,2、DML

        for statement in sql_list:
            statement = statement.rstrip(';')
            # 禁用语句
            if re.match(r"^select|^show", statement.lower()):
                check_result.is_critical = True
                result = ReviewResult(
                    id=line,
                    errlevel=2,
                    stagestatus='驳回不支持语句',
                    errormessage='仅支持DML和DDL语句,查询语句请使用SQL查询功能!',
                    sql=statement)
            # 高危语句
            elif critical_ddl_regex and p.match(statement.strip().lower()):
                check_result.is_critical = True
                result = ReviewResult(id=line,
                                      errlevel=2,
                                      stagestatus='驳回高危SQL',
                                      errormessage='禁止提交匹配' +
                                      critical_ddl_regex + '条件的语句!',
                                      sql=statement)
            # alter语句
            elif re.match(r"^alter", statement.lower()):
                # alter table语句
                if re.match(r"^alter\s+table\s+(.+?)\s+", statement.lower()):
                    table_name = re.match(r"^alter\s+table\s+(.+?)\s+",
                                          statement.lower(), re.M).group(1)
                    if '.' not in table_name:
                        table_name = f"{db_name}.{table_name}"
                    table_engine = self.get_table_engine(table_name)['engine']
                    table_exist = self.get_table_engine(table_name)['status']
                    if table_exist == 1:
                        if not table_engine.endswith(
                                'MergeTree') and table_engine not in (
                                    'Merge', 'Distributed'):
                            check_result.is_critical = True
                            result = ReviewResult(
                                id=line,
                                errlevel=2,
                                stagestatus='驳回不支持SQL',
                                errormessage=
                                'ALTER TABLE仅支持*MergeTree,Merge以及Distributed等引擎表!',
                                sql=statement)
                        else:
                            # delete与update语句,实际是alter语句的变种
                            if re.match(
                                    r"^alter\s+table\s+(.+?)\s+(delete|update)\s+",
                                    statement.lower()):
                                if not table_engine.endswith('MergeTree'):
                                    check_result.is_critical = True
                                    result = ReviewResult(
                                        id=line,
                                        errlevel=2,
                                        stagestatus='驳回不支持SQL',
                                        errormessage=
                                        'DELETE与UPDATE仅支持*MergeTree引擎表!',
                                        sql=statement)
                                else:
                                    result = self.explain_check(
                                        check_result, db_name, line, statement)
                            else:
                                result = self.explain_check(
                                    check_result, db_name, line, statement)
                    else:
                        check_result.is_critical = True
                        result = ReviewResult(
                            id=line,
                            errlevel=2,
                            stagestatus='表不存在',
                            errormessage=f'表 {table_name} 不存在!',
                            sql=statement)
                # 其他alter语句
                else:
                    result = self.explain_check(check_result, db_name, line,
                                                statement)
            # truncate语句
            elif re.match(r"^truncate\s+table\s+(.+?)(\s|$)",
                          statement.lower()):
                table_name = re.match(r"^truncate\s+table\s+(.+?)(\s|$)",
                                      statement.lower(), re.M).group(1)
                if '.' not in table_name:
                    table_name = f"{db_name}.{table_name}"
                table_engine = self.get_table_engine(table_name)['engine']
                table_exist = self.get_table_engine(table_name)['status']
                if table_exist == 1:
                    if table_engine in ('View', 'File,', 'URL', 'Buffer',
                                        'Null'):
                        check_result.is_critical = True
                        result = ReviewResult(
                            id=line,
                            errlevel=2,
                            stagestatus='驳回不支持SQL',
                            errormessage=
                            'TRUNCATE不支持View,File,URL,Buffer和Null表引擎!',
                            sql=statement)
                    else:
                        result = self.explain_check(check_result, db_name,
                                                    line, statement)
                else:
                    check_result.is_critical = True
                    result = ReviewResult(id=line,
                                          errlevel=2,
                                          stagestatus='表不存在',
                                          errormessage=f'表 {table_name} 不存在!',
                                          sql=statement)
            # insert语句,explain无法正确判断,暂时只做表存在性检查与简单关键字匹配
            elif re.match(r"^insert", statement.lower()):
                if re.match(
                        r"^insert\s+into\s+(.+?)(\s+|\s*\(.+?)(values|format|select)(\s+|\()",
                        statement.lower()):
                    table_name = re.match(
                        r"^insert\s+into\s+(.+?)(\s+|\s*\(.+?)(values|format|select)(\s+|\()",
                        statement.lower(), re.M).group(1)
                    if '.' not in table_name:
                        table_name = f"{db_name}.{table_name}"
                    table_exist = self.get_table_engine(table_name)['status']
                    if table_exist == 1:
                        result = ReviewResult(
                            id=line,
                            errlevel=0,
                            stagestatus='Audit completed',
                            errormessage='None',
                            sql=statement,
                            affected_rows=0,
                            execute_time=0,
                        )
                    else:
                        check_result.is_critical = True
                        result = ReviewResult(
                            id=line,
                            errlevel=2,
                            stagestatus='表不存在',
                            errormessage=f'表 {table_name} 不存在!',
                            sql=statement)
                else:
                    check_result.is_critical = True
                    result = ReviewResult(id=line,
                                          errlevel=2,
                                          stagestatus='驳回不支持SQL',
                                          errormessage='INSERT语法不正确!',
                                          sql=statement)
            # 其他语句使用explain ast简单检查
            else:
                result = self.explain_check(check_result, db_name, line,
                                            statement)

            # 没有找出DDL语句的才继续执行此判断
            if check_result.syntax_type == 2:
                if get_syntax_type(statement, parser=False,
                                   db_type='mysql') == 'DDL':
                    check_result.syntax_type = 1
            check_result.rows += [result]

            # 遇到禁用和高危语句直接返回
            if check_result.is_critical:
                check_result.error_count += 1
                return check_result
            line += 1
        return check_result

    def execute_workflow(self, workflow):
        """执行上线单,返回Review set"""
        sql = workflow.sqlworkflowcontent.sql_content
        execute_result = ReviewSet(full_sql=sql)
        sqls = sqlparse.format(sql, strip_comments=True)
        sql_list = sqlparse.split(sqls)

        line = 1
        for statement in sql_list:
            with FuncTimer() as t:
                result = self.execute(db_name=workflow.db_name,
                                      sql=statement,
                                      close_conn=True)
            if not result.error:
                execute_result.rows.append(
                    ReviewResult(
                        id=line,
                        errlevel=0,
                        stagestatus='Execute Successfully',
                        errormessage='None',
                        sql=statement,
                        affected_rows=0,
                        execute_time=t.cost,
                    ))
                line += 1
            else:
                # 追加当前报错语句信息到执行结果中
                execute_result.error = result.error
                execute_result.rows.append(
                    ReviewResult(
                        id=line,
                        errlevel=2,
                        stagestatus='Execute Failed',
                        errormessage=f'异常信息:{result.error}',
                        sql=statement,
                        affected_rows=0,
                        execute_time=0,
                    ))
                line += 1
                # 报错语句后面的语句标记为审核通过、未执行,追加到执行结果中
                for statement in sql_list[line - 1:]:
                    execute_result.rows.append(
                        ReviewResult(
                            id=line,
                            errlevel=0,
                            stagestatus='Audit completed',
                            errormessage=f'前序语句失败, 未执行',
                            sql=statement,
                            affected_rows=0,
                            execute_time=0,
                        ))
                    line += 1
                break
        return execute_result

    def execute(self, db_name=None, sql='', close_conn=True):
        """原生执行语句"""
        result = ResultSet(full_sql=sql)
        conn = self.get_connection(db_name=db_name)
        try:
            cursor = conn.cursor()
            for statement in sqlparse.split(sql):
                cursor.execute(statement)
            cursor.close()
        except Exception as e:
            logger.warning(f"ClickHouse语句执行报错,语句:{sql},错误信息{e}")
            result.error = str(e).split('Stack trace')[0]
        if close_conn:
            self.close()
        return result

    def close(self):
        if self.conn:
            self.conn.close()
            self.conn = None
Exemplo n.º 16
0
def cancel(request):
    workflow_id = request.POST.get('workflow_id')
    if workflow_id == '' or workflow_id is None:
        context = {'errMsg': 'workflow_id参数为空.'}
        return render(request, 'error.html', context)

    workflow_id = int(workflow_id)
    workflow_detail = SqlWorkflow.objects.get(id=workflow_id)
    audit_remark = request.POST.get('cancel_remark')
    if audit_remark is None:
        context = {'errMsg': '终止原因不能为空'}
        return render(request, 'error.html', context)

    user = request.user
    if can_cancel(request.user, workflow_id) is False:
        context = {'errMsg': '你无权操作当前工单!'}
        return render(request, 'error.html', context)

    # 使用事务保持数据一致性
    try:
        with transaction.atomic():
            # 调用工作流接口取消或者驳回
            audit_id = Audit.detail_by_workflow_id(
                workflow_id=workflow_id,
                workflow_type=WorkflowDict.workflow_type['sqlreview']).audit_id
            # 仅待审核的需要调用工作流,审核通过的不需要
            if workflow_detail.status != 'workflow_manreviewing':
                # 增加工单日志
                if user.username == workflow_detail.engineer:
                    Audit.add_log(
                        audit_id=audit_id,
                        operation_type=3,
                        operation_type_desc='取消执行',
                        operation_info="取消原因:{}".format(audit_remark),
                        operator=request.user.username,
                        operator_display=request.user.display)
                else:
                    Audit.add_log(
                        audit_id=audit_id,
                        operation_type=2,
                        operation_type_desc='审批不通过',
                        operation_info="审批备注:{}".format(audit_remark),
                        operator=request.user.username,
                        operator_display=request.user.display)
            else:
                if user.username == workflow_detail.engineer:
                    Audit.audit(audit_id,
                                WorkflowDict.workflow_status['audit_abort'],
                                user.username, audit_remark)
                # 非提交人需要校验审核权限
                elif user.has_perm('sql.sql_review'):
                    Audit.audit(audit_id,
                                WorkflowDict.workflow_status['audit_reject'],
                                user.username, audit_remark)
                else:
                    raise PermissionDenied

            # 删除定时执行job
            if workflow_detail.status == 'workflow_timingtask':
                job_id = Const.workflowJobprefix['sqlreview'] + '-' + str(
                    workflow_id)
                del_sqlcronjob(job_id)
            # 将流程状态修改为人工终止流程
            workflow_detail.status = 'workflow_abort'
            workflow_detail.audit_remark = audit_remark
            workflow_detail.save()
    except Exception as msg:
        logger.error(traceback.format_exc())
        context = {'errMsg': msg}
        return render(request, 'error.html', context)
    else:
        # 消息通知
        sys_config = SysConfig()
        if sys_config.get('mail') or sys_config.get('ding'):
            # 再次获取审核信息
            audit_detail = Audit.detail_by_workflow_id(
                workflow_id=workflow_id,
                workflow_type=WorkflowDict.workflow_type['sqlreview'])
            base_url = sys_config.get('archery_base_url',
                                      'http://127.0.0.1:8000').rstrip('/')
            workflow_url = "{base_url}/workflow/{audit_id}".format(
                base_url=base_url, audit_id=audit_detail.audit_id)
            async_task(notify,
                       audit_info=audit_detail,
                       workflow_url=workflow_url,
                       audit_remark=audit_remark,
                       timeout=60)
    return HttpResponseRedirect(reverse('sql:detail', args=(workflow_id, )))
Exemplo n.º 17
0
def query(request):
    instance_name = request.POST.get('instance_name')
    sql_content = request.POST.get('sql_content')
    db_name = request.POST.get('db_name')
    limit_num = request.POST.get('limit_num')

    result = {'status': 0, 'msg': 'ok', 'data': {}}
    try:
        instance = Instance.objects.get(instance_name=instance_name)
    except Instance.DoesNotExist:
        result['status'] = 1
        result['msg'] = '实例不存在'
        return result
    # 服务器端参数验证
    if sql_content is None or db_name is None or instance_name is None or limit_num is None:
        result['status'] = 1
        result['msg'] = '页面提交参数可能为空'
        return HttpResponse(json.dumps(result),
                            content_type='application/json')

    sql_content = sql_content.strip()
    archer_config = SysConfig()
    if archer_config.get('disable_star'):
        if '*' in sql_content:
            result['status'] = 1
            result['msg'] = '不允许 * 标记, 请指定具体字段名.'
            return HttpResponse(json.dumps(result),
                                content_type='application/json')
    # 获取用户信息
    user = request.user

    # 过滤注释语句和非查询的语句
    sql_content = ''.join(
        map(
            lambda x: re.compile(r'(^--\s+.*|^/\*.*\*/;\s*$)').sub(
                '', x, count=1), sql_content.splitlines(1))).strip()
    # 去除空行
    sql_content = re.sub('[\r\n\f]{2,}', '\n', sql_content)

    sql_list = sql_content.strip().split('\n')
    for sql in sql_list:
        if re.match(r"^select|^show|^explain", sql.lower()):
            break
        else:
            result['status'] = 1
            result['msg'] = '仅支持^select|^show|^explain语法,请联系管理员!'
            return HttpResponse(json.dumps(result),
                                content_type='application/json')

    # 执行第一条有效sql
    sql_content = sqlparse.split(sql_content)[0].rstrip(';')

    try:
        # 查询权限校验
        priv_check_info = query_priv_check(user, instance_name, db_name,
                                           sql_content, limit_num)

        if priv_check_info['status'] == 0:
            limit_num = priv_check_info['data']['limit_num']
            priv_check = priv_check_info['data']['priv_check']
        else:
            return HttpResponse(json.dumps(priv_check_info),
                                content_type='application/json')

        if re.match(r"^explain", sql_content.lower()):
            limit_num = 0
        query_engine = get_engine(instance=instance)
        filter_result = query_engine.query_check(db_name=db_name,
                                                 sql=sql_content,
                                                 limit_num=limit_num)
        if filter_result.get('bad_query'):
            result['status'] = 1
            result['msg'] = filter_result.get('msg')
            return HttpResponse(json.dumps(result),
                                content_type='application/json')
        else:
            sql_content = filter_result['filtered_sql']
        sql_content = sql_content + ';'

        # 执行查询语句,统计执行时间
        t_start = time.time()
        query_result = query_engine.query(db_name=str(db_name),
                                          sql=sql_content,
                                          limit_num=limit_num)
        t_end = time.time()
        query_result.query_time = "%5s" % "{:.4f}".format(t_end - t_start)

        # 数据脱敏,同样需要检查配置,是否开启脱敏,语法树解析是否允许出错继续执行
        hit_rule = 0 if re.match(r"^select", sql_content.lower(
        )) else 2  # 查询是否命中脱敏规则,0, '未知', 1, '命中', 2, '未命中'
        masking = 2  # 查询结果是否正常脱敏,1, '是', 2, '否'
        t_start = time.time()
        # 仅对查询语句进行脱敏

        if SysConfig().sys_config.get('data_masking') and re.match(
                r"^select", sql_content.lower()):
            try:
                query_result = query_engine.query_masking(
                    db_name=db_name, sql=sql_content, resultset=query_result)
                if SysConfig().sys_config.get(
                        'query_check') and query_result.is_critical == True:
                    return HttpResponse(json.dumps(masking_result),
                                        content_type='application/json')
                else:
                    # 实际未命中, 则显示为未做脱敏
                    if query_result.is_masked:
                        masking = 1
                        hit_rule = 1
            except Exception:
                logger.error(traceback.format_exc())
                # 报错, 未脱敏, 未命中
                hit_rule = 2
                masking = 2
                if SysConfig().sys_config.get('query_check'):
                    result['status'] = 1
                    result['msg'] = '脱敏数据报错,请联系管理员'
                    return HttpResponse(json.dumps(result),
                                        content_type='application/json')

        t_end = time.time()
        query_result.mask_time = "%5s" % "{:.4f}".format(t_end - t_start)
        sql_result = query_result.__dict__
        sql_result['masking_cost_time'] = query_result.mask_time
        sql_result['cost_time'] = query_result.query_time

        result['data'] = sql_result

        # 成功的查询语句记录存入数据库
        if sql_result.get('error'):
            pass
        else:
            query_log = QueryLog()
            query_log.username = user.username
            query_log.user_display = user.display
            query_log.db_name = db_name
            query_log.instance_name = instance_name
            query_log.sqllog = sql_content
            if int(limit_num) == 0:
                limit_num = int(sql_result['affected_rows'])
            else:
                limit_num = min(int(limit_num),
                                int(sql_result['affected_rows']))
            query_log.effect_row = limit_num
            query_log.cost_time = query_result.query_time
            query_log.priv_check = priv_check
            query_log.hit_rule = hit_rule
            query_log.masking = masking
            # 防止查询超时
            try:
                query_log.save()
            except:
                connection.close()
                query_log.save()
    except Exception as e:
        logger.error(traceback.format_exc())
        result['status'] = 1
        result['msg'] = str(e)

    # 返回查询结果
    try:
        return HttpResponse(json.dumps(result,
                                       cls=ExtendJSONEncoder,
                                       bigint_as_string=True),
                            content_type='application/json')
    except Exception:
        return HttpResponse(json.dumps(result,
                                       default=str,
                                       bigint_as_string=True,
                                       encoding='latin1'),
                            content_type='application/json')
Exemplo n.º 18
0
    def execute_check(self, db_name=None, sql='', close_conn=True):
        """
        上线单执行前的检查, 返回Review set
        update by Jan.song 20200302
        使用explain对数据修改预计进行检测
        """
        config = SysConfig()
        check_result = ReviewSet(full_sql=sql)
        # explain支持的语法
        explain_re = r"^merge|^update|^delete|^insert|^create\s+table|^create\s+index|^create\s+unique\s+index"
        # 禁用/高危语句检查
        line = 1
        # 保存SQL中的新建对象
        object_name_list = set()
        critical_ddl_regex = config.get('critical_ddl_regex', '')
        p = re.compile(critical_ddl_regex)
        check_result.syntax_type = 2  # TODO 工单类型 0、其他 1、DDL,2、DML
        try:
            sqlitemList = get_full_sqlitem_list(sql, db_name)
            for sqlitem in sqlitemList:
                sql_lower = sqlitem.statement.lower().rstrip(';')
                # 禁用语句
                if re.match(r"^select|^with|^explain", sql_lower):
                    check_result.is_critical = True
                    result = ReviewResult(
                        id=line,
                        errlevel=2,
                        stagestatus='驳回不支持语句',
                        errormessage='仅支持DML和DDL语句,查询语句请使用SQL查询功能!',
                        sql=sqlitem.statement)
                # 高危语句
                elif critical_ddl_regex and p.match(sql_lower.strip()):
                    check_result.is_critical = True
                    result = ReviewResult(id=line,
                                          errlevel=2,
                                          stagestatus='驳回高危SQL',
                                          errormessage='禁止提交匹配' +
                                          critical_ddl_regex + '条件的语句!',
                                          sql=sqlitem.statement)
                # 驳回未带where数据修改语句,如确实需做全部删除或更新,显示的带上where 1=1
                elif re.match(r"^update((?!where).)*$|^delete((?!where).)*$",
                              sql_lower):
                    check_result.is_critical = True
                    result = ReviewResult(id=line,
                                          errlevel=2,
                                          stagestatus='驳回未带where数据修改',
                                          errormessage='数据修改需带where条件!',
                                          sql=sqlitem.statement)
                # 驳回事务控制,会话控制SQL
                elif re.match(r"^set|^rollback|^exit", sql_lower):
                    check_result.is_critical = True
                    result = ReviewResult(
                        id=line,
                        errlevel=2,
                        stagestatus='SQL中不能包含^set|^rollback|^exit',
                        errormessage='SQL中不能包含^set|^rollback|^exit',
                        sql=sqlitem.statement)

                # 通过explain对SQL做语法语义检查
                elif re.match(explain_re,
                              sql_lower) and sqlitem.stmt_type == 'SQL':
                    if self.check_create_index_table(
                            db_name=db_name,
                            sql=sql_lower,
                            object_name_list=object_name_list):
                        object_name = self.get_sql_first_object_name(
                            sql=sql_lower)
                        if '.' in object_name:
                            object_name = object_name
                        else:
                            object_name = f"""{db_name}.{object_name}"""
                        object_name_list.add(object_name)
                        result = ReviewResult(
                            id=line,
                            errlevel=1,
                            stagestatus='WARNING:新建表的新建索引语句暂无法检测!',
                            errormessage='WARNING:新建表的新建索引语句暂无法检测!',
                            stmt_type=sqlitem.stmt_type,
                            object_owner=sqlitem.object_owner,
                            object_type=sqlitem.object_type,
                            object_name=sqlitem.object_name,
                            sql=sqlitem.statement)
                    elif len(object_name_list) > 0 and self.get_dml_table(
                            db_name=db_name,
                            sql=sql_lower,
                            object_name_list=object_name_list):
                        result = ReviewResult(
                            id=line,
                            errlevel=1,
                            stagestatus='WARNING:新建表的数据修改暂无法检测!',
                            errormessage='WARNING:新建表的数据修改暂无法检测!',
                            stmt_type=sqlitem.stmt_type,
                            object_owner=sqlitem.object_owner,
                            object_type=sqlitem.object_type,
                            object_name=sqlitem.object_name,
                            sql=sqlitem.statement)
                    else:
                        result_set = self.explain_check(db_name=db_name,
                                                        sql=sqlitem.statement,
                                                        close_conn=False)
                        if result_set['msg']:
                            check_result.is_critical = True
                            result = ReviewResult(
                                id=line,
                                errlevel=2,
                                stagestatus='explain语法检查未通过!',
                                errormessage=result_set['msg'],
                                sql=sqlitem.statement)
                        else:
                            # 对create table\create index\create unique index语法做对象存在性检测
                            if re.match(
                                    r"^create\s+table|^create\s+index|^create\s+unique\s+index",
                                    sql_lower):
                                object_name = self.get_sql_first_object_name(
                                    sql=sql_lower)
                                # 保存create对象对后续SQL做存在性判断
                                if '.' in object_name:
                                    object_name = object_name
                                else:
                                    object_name = f"""{db_name}.{object_name}"""
                                if self.object_name_check(
                                        db_name=db_name,
                                        object_name=object_name
                                ) or object_name in object_name_list:
                                    check_result.is_critical = True
                                    result = ReviewResult(
                                        id=line,
                                        errlevel=2,
                                        stagestatus=f"""{object_name}对象已经存在!""",
                                        errormessage=
                                        f"""{object_name}对象已经存在!""",
                                        sql=sqlitem.statement)
                                else:
                                    object_name_list.add(object_name)
                                    if result_set['rows'] > 1000:
                                        result = ReviewResult(
                                            id=line,
                                            errlevel=1,
                                            stagestatus='影响行数大于1000,请关注',
                                            errormessage='影响行数大于1000,请关注',
                                            sql=sqlitem.statement,
                                            stmt_type=sqlitem.stmt_type,
                                            object_owner=sqlitem.object_owner,
                                            object_type=sqlitem.object_type,
                                            object_name=sqlitem.object_name,
                                            affected_rows=result_set['rows'],
                                            execute_time=0,
                                        )
                                    else:
                                        result = ReviewResult(
                                            id=line,
                                            errlevel=0,
                                            stagestatus='Audit completed',
                                            errormessage='None',
                                            sql=sqlitem.statement,
                                            stmt_type=sqlitem.stmt_type,
                                            object_owner=sqlitem.object_owner,
                                            object_type=sqlitem.object_type,
                                            object_name=sqlitem.object_name,
                                            affected_rows=result_set['rows'],
                                            execute_time=0,
                                        )
                            else:
                                if result_set['rows'] > 1000:
                                    result = ReviewResult(
                                        id=line,
                                        errlevel=1,
                                        stagestatus='影响行数大于1000,请关注',
                                        errormessage='影响行数大于1000,请关注',
                                        sql=sqlitem.statement,
                                        stmt_type=sqlitem.stmt_type,
                                        object_owner=sqlitem.object_owner,
                                        object_type=sqlitem.object_type,
                                        object_name=sqlitem.object_name,
                                        affected_rows=result_set['rows'],
                                        execute_time=0,
                                    )
                                else:
                                    result = ReviewResult(
                                        id=line,
                                        errlevel=0,
                                        stagestatus='Audit completed',
                                        errormessage='None',
                                        sql=sqlitem.statement,
                                        stmt_type=sqlitem.stmt_type,
                                        object_owner=sqlitem.object_owner,
                                        object_type=sqlitem.object_type,
                                        object_name=sqlitem.object_name,
                                        affected_rows=result_set['rows'],
                                        execute_time=0,
                                    )
                # 其它无法用explain判断的语句
                else:
                    # 对alter table做对象存在性检查
                    if re.match(r"^alter\s+table\s", sql_lower):
                        object_name = self.get_sql_first_object_name(
                            sql=sql_lower)
                        if '.' in object_name:
                            object_name = object_name
                        else:
                            object_name = f"""{db_name}.{object_name}"""
                        if not self.object_name_check(
                                db_name=db_name, object_name=object_name
                        ) and object_name not in object_name_list:
                            check_result.is_critical = True
                            result = ReviewResult(
                                id=line,
                                errlevel=2,
                                stagestatus=f"""{object_name}对象不存在!""",
                                errormessage=f"""{object_name}对象不存在!""",
                                sql=sqlitem.statement)
                        else:
                            result = ReviewResult(
                                id=line,
                                errlevel=1,
                                stagestatus='当前平台,此语法不支持审核!',
                                errormessage='当前平台,此语法不支持审核!',
                                sql=sqlitem.statement,
                                stmt_type=sqlitem.stmt_type,
                                object_owner=sqlitem.object_owner,
                                object_type=sqlitem.object_type,
                                object_name=sqlitem.object_name,
                                affected_rows=0,
                                execute_time=0,
                            )
                    # 对create做对象存在性检查
                    elif re.match(r"^create", sql_lower):
                        object_name = self.get_sql_first_object_name(
                            sql=sql_lower)
                        if '.' in object_name:
                            object_name = object_name
                        else:
                            object_name = f"""{db_name}.{object_name}"""
                        if self.object_name_check(
                                db_name=db_name, object_name=object_name
                        ) or object_name in object_name_list:
                            check_result.is_critical = True
                            result = ReviewResult(
                                id=line,
                                errlevel=2,
                                stagestatus=f"""{object_name}对象已经存在!""",
                                errormessage=f"""{object_name}对象已经存在!""",
                                sql=sqlitem.statement)
                        else:
                            object_name_list.add(object_name)
                            result = ReviewResult(
                                id=line,
                                errlevel=1,
                                stagestatus='当前平台,此语法不支持审核!',
                                errormessage='当前平台,此语法不支持审核!',
                                sql=sqlitem.statement,
                                stmt_type=sqlitem.stmt_type,
                                object_owner=sqlitem.object_owner,
                                object_type=sqlitem.object_type,
                                object_name=sqlitem.object_name,
                                affected_rows=0,
                                execute_time=0,
                            )
                    else:
                        result = ReviewResult(
                            id=line,
                            errlevel=1,
                            stagestatus='当前平台,此语法不支持审核!',
                            errormessage='当前平台,此语法不支持审核!',
                            sql=sqlitem.statement,
                            stmt_type=sqlitem.stmt_type,
                            object_owner=sqlitem.object_owner,
                            object_type=sqlitem.object_type,
                            object_name=sqlitem.object_name,
                            affected_rows=0,
                            execute_time=0,
                        )
                # 判断工单类型
                if get_syntax_type(sql=sqlitem.statement,
                                   db_type='oracle') == 'DDL':
                    check_result.syntax_type = 1
                check_result.rows += [result]
                # 遇到禁用和高危语句直接返回,提高效率
                if check_result.is_critical:
                    check_result.error_count += 1
                    return check_result
                line += 1
        except Exception as e:
            logger.warning(
                f"Oracle 语句执行报错,第{line}个SQL:{sqlitem.statement},错误信息{traceback.format_exc()}"
            )
            check_result.error = str(e)
        finally:
            if close_conn:
                self.close()
        return check_result
Exemplo n.º 19
0
    def create(self, validated_data):
        """使用原工单submit流程创建工单"""
        workflow_data = validated_data.pop('workflow')
        instance = workflow_data['instance']
        sql_content = validated_data['sql_content'].strip()
        user = Users.objects.get(username=workflow_data['engineer'])
        group = ResourceGroup.objects.get(pk=workflow_data['group_id'])
        active_user = Users.objects.filter(is_active=1)

        # 验证组权限(用户是否在该组、该组是否有指定实例)
        try:
            user_instances(user, tag_codes=['can_write']).get(id=instance.id)
        except instance.DoesNotExist:
            raise serializers.ValidationError({'errors': '你所在组未关联该实例!'})

        # 再次交给engine进行检测,防止绕过
        try:
            check_engine = get_engine(instance=instance)
            check_result = check_engine.execute_check(db_name=workflow_data['db_name'],
                                                      sql=sql_content)
        except Exception as e:
            raise serializers.ValidationError({'errors': str(e)})

        # 未开启备份选项,并且engine支持备份,强制设置备份
        is_backup = workflow_data['is_backup'] if 'is_backup' in workflow_data.keys() else False
        sys_config = SysConfig()
        if not sys_config.get('enable_backup_switch') and check_engine.auto_backup:
            is_backup = True

        # 按照系统配置确定是自动驳回还是放行
        auto_review_wrong = sys_config.get('auto_review_wrong', '')  # 1表示出现警告就驳回,2和空表示出现错误才驳回
        workflow_status = 'workflow_manreviewing'
        if check_result.warning_count > 0 and auto_review_wrong == '1':
            workflow_status = 'workflow_autoreviewwrong'
        elif check_result.error_count > 0 and auto_review_wrong in ('', '1', '2'):
            workflow_status = 'workflow_autoreviewwrong'

        workflow_data.update(status=workflow_status,
                             is_backup=is_backup,
                             is_manual=0,
                             syntax_type=check_result.syntax_type,
                             engineer_display=user.display,
                             group_name=group.group_name,
                             audit_auth_groups=Audit.settings(workflow_data['group_id'],
                                                              WorkflowDict.workflow_type['sqlreview']))
        try:
            with transaction.atomic():
                workflow = SqlWorkflow.objects.create(**workflow_data)
                validated_data['review_content'] = check_result.json()
                workflow_content = SqlWorkflowContent.objects.create(workflow=workflow, **validated_data)
                # 自动审核通过了,才调用工作流
                if workflow_status == 'workflow_manreviewing':
                    # 调用工作流插入审核信息, SQL上线权限申请workflow_type=2
                    Audit.add(WorkflowDict.workflow_type['sqlreview'], workflow.id)
        except Exception as e:
            logger.error(f"提交工单报错,错误信息:{traceback.format_exc()}")
            raise serializers.ValidationError({'errors': str(e)})
        else:
            # 自动审核通过且开启了Apply阶段通知参数才发送消息通知
            is_notified = 'Apply' in sys_config.get('notify_phase_control').split(',') \
                if sys_config.get('notify_phase_control') else True
            if workflow_status == 'workflow_manreviewing' and is_notified:
                # 获取审核信息
                audit_id = Audit.detail_by_workflow_id(workflow_id=workflow.id,
                                                       workflow_type=WorkflowDict.workflow_type['sqlreview']).audit_id
                async_task(notify_for_audit, audit_id=audit_id, cc_users=active_user, timeout=60,
                           task_name=f'sqlreview-submit-{workflow.id}')
            return workflow_content
Exemplo n.º 20
0
def notify_for_audit(audit_id, **kwargs):
    """
    工作流消息通知,不包含工单执行结束的通知
    :param audit_id:
    :param kwargs:
    :return:
    """
    # 判断是否开启消息通知,未开启直接返回
    sys_config = SysConfig()
    wx_status = sys_config.get('wx')
    if not sys_config.get('mail') and not sys_config.get('ding') and not wx_status:
        logger.info('未开启消息通知,可在系统设置中开启')
        return None

    wx_msg_content = ''

    # 获取审核信息
    audit_detail = Audit.detail(audit_id=audit_id)
    audit_id = audit_detail.audit_id
    workflow_audit_remark = kwargs.get('audit_remark', '')
    base_url = sys_config.get('archery_base_url', 'http://127.0.0.1:8000').rstrip('/')
    workflow_url = "{base_url}/workflow/{audit_id}".format(base_url=base_url, audit_id=audit_detail.audit_id)
    msg_cc_email = kwargs.get('email_cc', [])
    workflow_id = audit_detail.workflow_id
    workflow_type = audit_detail.workflow_type
    status = audit_detail.current_status
    workflow_title = audit_detail.workflow_title
    workflow_from = audit_detail.create_user_display
    group_name = audit_detail.group_name
    webhook_url = ResourceGroup.objects.get(group_id=audit_detail.group_id).ding_webhook

    # 获取当前审批和审批流程
    workflow_auditors, current_workflow_auditors = Audit.review_info(audit_detail.workflow_id,
                                                                     audit_detail.workflow_type)

    # 准备消息内容
    if workflow_type == WorkflowDict.workflow_type['query']:
        workflow_type_display = WorkflowDict.workflow_type['query_display']
        workflow_detail = QueryPrivilegesApply.objects.get(apply_id=workflow_id)
        instance = workflow_detail.instance.instance_name
        db_name = ' '
        if workflow_detail.priv_type == 1:
            workflow_content = '''数据库清单:{}\n授权截止时间:{}\n结果集:{}\n'''.format(
                workflow_detail.db_list,
                datetime.datetime.strftime(workflow_detail.valid_date, '%Y-%m-%d %H:%M:%S'),
                workflow_detail.limit_num)
        elif workflow_detail.priv_type == 2:
            db_name = workflow_detail.db_list
            workflow_content = '''数据库:{}\n表清单:{}\n授权截止时间:{}\n结果集:{}\n'''.format(
                workflow_detail.db_list,
                workflow_detail.table_list,
                datetime.datetime.strftime(workflow_detail.valid_date, '%Y-%m-%d %H:%M:%S'),
                workflow_detail.limit_num)
        else:
            workflow_content = ''
    elif workflow_type == WorkflowDict.workflow_type['sqlreview']:
        workflow_type_display = WorkflowDict.workflow_type['sqlreview_display']
        workflow_detail = SqlWorkflow.objects.get(pk=workflow_id)
        instance = workflow_detail.instance.instance_name
        db_name = workflow_detail.db_name
        workflow_content = re.sub('[\r\n\f]{2,}', '\n',
                                  workflow_detail.sqlworkflowcontent.sql_content[0:500].replace('\r', ''))
    else:
        raise Exception('工单类型不正确')

    # 准备消息格式
    if status == WorkflowDict.workflow_status['audit_wait']:  # 申请阶段
        msg_title = "[{}]新的工单申请#{}".format(workflow_type_display, audit_id)
        # 接收人,发送给该资源组内对应权限组所有的用户
        auth_group_names = Group.objects.get(id=audit_detail.current_audit).name
        msg_to = auth_group_users([auth_group_names], audit_detail.group_id)
        # 消息内容
        msg_content = '''发起时间:{}\n发起人:{}\n组:{}\n目标实例:{}\n数据库:{}\n审批流程:{}\n当前审批:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
            workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
            workflow_from,
            group_name,
            instance,
            db_name,
            workflow_auditors,
            current_workflow_auditors,
            workflow_title,
            workflow_url,
            workflow_content)

        # 企业微信消息格式
        if wx_status:
            wx_msg_content = "[【{}】新的工单申请(点击查看)]({})\n" \
                             ">发起时间:<font color=\"comment\">{}</font>\n" \
                             ">发起人:<font color=\"comment\">{}</font>\n" \
                             ">组:<font color=\"comment\">{}</font>\n" \
                             ">目标实例:<font color=\"comment\">{}</font>\n"\
                             ">数据库:<font color=\"comment\">{}</font>\n" \
                             ">审批流程:<font color=\"comment\">{}</font>\n" \
                             ">当前审批:<font color=\"comment\">{}</font>\n" \
                             ">工单名称:<font color=\"comment\">{}</font>\n".format(
                workflow_type_display, workflow_url, workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
                workflow_from, group_name, instance, db_name, workflow_auditors, current_workflow_auditors,
                workflow_title
            )

    elif status == WorkflowDict.workflow_status['audit_success']:  # 审核通过
        msg_title = "[{}]工单审核通过#{}".format(workflow_type_display, audit_id)
        # 接收人,仅发送给申请人
        msg_to = [Users.objects.get(username=audit_detail.create_user)]
        # 消息内容
        msg_content = '''发起时间:{}\n发起人:{}\n组:{}\n目标实例:{}\n数据库:{}\n审批流程:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
            workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
            workflow_from,
            group_name,
            instance,
            db_name,
            workflow_auditors,
            workflow_title,
            workflow_url,
            workflow_content)

        if wx_status:
            wx_msg_content = "[【{}】工单审核通过(点击查看)]({})\n" \
                             ">发起时间:<font color=\"comment\">{}</font>\n" \
                             ">发起人:<font color=\"comment\">{}</font>\n" \
                             ">组:<font color=\"comment\">{}</font>\n" \
                             ">目标实例:<font color=\"comment\">{}</font>\n" \
                             ">数据库:<font color=\"comment\">{}</font>\n" \
                             ">审批流程:<font color=\"comment\">{}</font>\n" \
                             ">工单名称:<font color=\"comment\">{}</font>\n".format(
                workflow_type_display, workflow_url, workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
                workflow_from, group_name, instance, db_name, workflow_auditors, workflow_title
            )

    elif status == WorkflowDict.workflow_status['audit_reject']:  # 审核驳回
        msg_title = "[{}]工单被驳回#{}".format(workflow_type_display, audit_id)
        # 接收人,仅发送给申请人
        msg_to = [Users.objects.get(username=audit_detail.create_user)]
        # 消息内容
        msg_content = '''发起时间:{}\n目标实例:{}\n数据库:{}\n工单名称:{}\n工单地址:{}\n驳回原因:{}\n提醒:此工单被审核不通过,请按照驳回原因进行修改!'''.format(
            workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
            instance,
            db_name,
            workflow_title,
            workflow_url,
            workflow_audit_remark)

        if wx_status:
            wx_msg_content = "[【{}】工单被驳回(点击查看)]({})\n" \
                             ">发起时间:<font color=\"comment\">{}</font>\n" \
                             ">目标实例:<font color=\"comment\">{}</font>\n" \
                             ">数据库:<font color=\"comment\">{}</font>\n" \
                             ">工单名称:<font color=\"comment\">{}</font>\n" \
                             ">驳回原因:<font color=\"comment\">{}</font>\n" \
                             "提醒:此工单审核不通过,请按照驳回原因进行修改!".format(
                workflow_type_display, workflow_url, workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
                instance, db_name, workflow_title, workflow_audit_remark
            )

    elif status == WorkflowDict.workflow_status['audit_abort']:  # 审核取消,通知所有审核人
        msg_title = "[{}]提交人主动终止工单#{}".format(workflow_type_display, audit_id)
        # 接收人,发送给该资源组内对应权限组所有的用户
        auth_group_names = [Group.objects.get(id=auth_group_id).name for auth_group_id in
                            audit_detail.audit_auth_groups.split(',')]
        msg_to = auth_group_users(auth_group_names, audit_detail.group_id)
        # 消息内容
        msg_content = '''发起时间:{}\n发起人:{}\n组:{}\n目标实例:{}\n数据库:{}\n工单名称:{}\n工单地址:{}\n终止原因:{}'''.format(
            workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
            workflow_from,
            group_name,
            instance,
            db_name,
            workflow_title,
            workflow_url,
            workflow_audit_remark)

        if wx_status:
            wx_msg_content = "[【{}】提交人主动终止工单(点击查看)]({})\n" \
                             ">发起时间:<font color=\"comment\">{}</font>\n" \
                             ">发起人:<font color=\"comment\">{}</font>\n" \
                             ">组:<font color=\"comment\">{}</font>\n" \
                             ">目标实例:<font color=\"comment\">{}</font>\n" \
                             ">数据库:<font color=\"comment\">{}</font>\n" \
                             ">工单名称:<font color=\"comment\">{}</font>\n" \
                             ">终止原因:<font color=\"comment\">{}</font>\n".format(
                workflow_type_display, workflow_url, workflow_detail.create_time.strftime('%Y-%m-%d %H:%M:%S'),
                workflow_from, group_name, instance, db_name, workflow_title, workflow_audit_remark
            )
    else:
        raise Exception('工单状态不正确')

    # 处理接收人信息
    msg_to_email = [user.email for user in msg_to if user.email]
    msg_to_ding_user = [user.ding_user_id for user in msg_to if user.ding_user_id]
    # 发送通知
    msg_sender = MsgSender()
    if sys_config.get('mail'):
        msg_sender.send_email(msg_title, msg_content, msg_to_email, list_cc_addr=msg_cc_email)
    if sys_config.get('ding') and webhook_url:
        msg_sender.send_ding(webhook_url, msg_title + '\n' + msg_content)
    if sys_config.get('ding_to_person') and msg_to_ding_user:
        msg_sender.send_ding2user(msg_to_ding_user, msg_title + '\n' + msg_content)
    if wx_status:
        user_list = []
        for user in msg_to:
            if user.wx_user_id:
                user_list.append(user.wx_user_id)
            else:
                user_list.append(user.username)
        # user_list = [user.wx_user_id for user in msg_to if user.wx_user_id]
        msg_sender.send_wx2user(wx_msg_content, user_list)
Exemplo n.º 21
0
def notify_for_execute(workflow):
    """
    工单执行结束的通知
    :param workflow:
    :return:
    """
    # 判断是否开启消息通知,未开启直接返回
    if not __notify_cnf_status():
        return None
    sys_config = SysConfig()

    # 获取当前审批和审批流程
    base_url = sys_config.get('archery_base_url',
                              'http://127.0.0.1:8000').rstrip('/')
    audit_auth_group, current_audit_auth_group = Audit.review_info(
        workflow.id, 2)
    audit_id = Audit.detail_by_workflow_id(workflow.id, 2).audit_id
    url = "{base_url}/workflow/{audit_id}".format(base_url=base_url,
                                                  audit_id=audit_id)
    msg_title = "[{}]工单{}#{}".format(
        WorkflowDict.workflow_type['sqlreview_display'],
        workflow.get_status_display(), audit_id)
    msg_content = '''发起时间:{}\n发起人:{}\n组:{}\n目标实例:{}\n数据库:{}\n审批流程:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
        workflow.create_time.strftime('%Y-%m-%d %H:%M:%S'),
        workflow.engineer_display, workflow.group_name,
        workflow.instance.instance_name, workflow.db_name, audit_auth_group,
        workflow.workflow_name, url,
        re.sub(
            '[\r\n\f]{2,}', '\n',
            workflow.sqlworkflowcontent.sql_content[0:500].replace('\r', '')))
    # 邮件通知申请人,抄送DBA
    msg_to = Users.objects.filter(username=workflow.engineer)
    msg_cc = auth_group_users(auth_group_names=['DBA'],
                              group_id=workflow.group_id)

    # 处理接收人
    dingding_webhook = ResourceGroup.objects.get(
        group_id=workflow.group_id).ding_webhook
    feishu_webhook = ResourceGroup.objects.get(
        group_id=workflow.group_id).feishu_webhook
    qywx_webhook = ResourceGroup.objects.get(
        group_id=workflow.group_id).qywx_webhook
    # 发送通知
    __send(msg_title,
           msg_content,
           msg_to,
           msg_cc,
           dingding_webhook=dingding_webhook,
           feishu_webhook=feishu_webhook,
           qywx_webhook=qywx_webhook)

    # DDL通知
    if sys_config.get(
            'ddl_notify_auth_group') and workflow.status == 'workflow_finish':
        # 判断上线语句是否存在DDL,存在则通知相关人员
        if workflow.syntax_type == 1:
            # 消息内容通知
            msg_title = '[Archery]有新的DDL语句执行完成#{}'.format(audit_id)
            msg_content = '''发起人:{}\n变更组:{}\n变更实例:{}\n变更数据库:{}\n工单名称:{}\n工单地址:{}\n工单预览:{}\n'''.format(
                Users.objects.get(username=workflow.engineer).display,
                workflow.group_name, workflow.instance.instance_name,
                workflow.db_name, workflow.workflow_name, url,
                workflow.sqlworkflowcontent.sql_content[0:500])
            # 获取通知成员ddl_notify_auth_group
            ddl_notify_auth_group = sys_config.get('ddl_notify_auth_group',
                                                   '').split(',')
            msg_to = Users.objects.filter(
                groups__name__in=ddl_notify_auth_group)
            # 发送通知
            __send(msg_title, msg_content, msg_to, msg_cc)
Exemplo n.º 22
0
def notify_for_execute(workflow):
    """
    工单执行结束的通知
    :param workflow:
    :return:
    """
    # 判断是否开启消息通知,未开启直接返回
    sys_config = SysConfig()
    wx_status = sys_config.get('wx')
    wx_msg_content = ''

    if not sys_config.get('mail') and not sys_config.get('ding') and not sys_config.get('ding_to_person') \
            and not wx_status:
        logger.info('未开启消息通知,可在系统设置中开启')
        return None
    # 获取当前审批和审批流程
    base_url = sys_config.get('archery_base_url', 'http://127.0.0.1:8000').rstrip('/')
    audit_auth_group, current_audit_auth_group = Audit.review_info(workflow.id, 2)
    audit_id = Audit.detail_by_workflow_id(workflow.id, 2).audit_id
    url = "{base_url}/workflow/{audit_id}".format(base_url=base_url, audit_id=audit_id)
    msg_title = "[{}]工单{}#{}".format(WorkflowDict.workflow_type['sqlreview_display'],
                                     workflow.get_status_display(), audit_id)
    msg_content = '''发起人:{}\n组:{}\n审批流程:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
        workflow.engineer_display,
        workflow.group_name,
        audit_auth_group,
        workflow.workflow_name,
        url,
        re.sub('[\r\n\f]{2,}', '\n', workflow.sqlworkflowcontent.sql_content[0:500].replace('\r', '')))

    if wx_status:
        wx_msg_content = "[工单执行完毕(点击查看)]({})\n" \
                         ">发起人:<font color=\"comment\">{}</font>\n" \
                         ">组:<font color=\"comment\">{}</font>\n" \
                         ">审批流程:<font color=\"comment\">{}</font>\n" \
                         ">工单名称:<font color=\"comment\">{}</font>\n" \
                         ">审批流程:<font color=\"comment\">{}</font>\n".format(
            url, workflow.engineer_display, workflow.group_name, audit_auth_group, workflow.workflow_name,
            re.sub('[\r\n\f]{2,}', '\n', workflow.sqlworkflowcontent.sql_content[0:500].replace('\r', '')))

    # 邮件通知申请人,抄送DBA
    msg_to = Users.objects.filter(username=workflow.engineer)
    msg_cc = auth_group_users(auth_group_names=['DBA'], group_id=workflow.group_id)

    # 处理接收人信息
    msg_to_email = [user.email for user in msg_to if user.email]
    msg_cc_email = [user.email for user in msg_cc if user.email]
    msg_to_ding_user = [user.ding_user_id for user in msg_to if user.ding_user_id]

    # 判断是发送钉钉还是发送邮件
    msg_sender = MsgSender()
    if sys_config.get('mail'):
        msg_sender.send_email(msg_title, msg_content, msg_to_email, list_cc_addr=msg_cc_email)
    if sys_config.get('ding'):
        # 钉钉通知申请人,审核人,抄送DBA
        webhook_url = ResourceGroup.objects.get(group_id=workflow.group_id).ding_webhook
        if webhook_url:
            MsgSender.send_ding(webhook_url, msg_title + '\n' + msg_content)
    if sys_config.get('ding_to_person') and msg_to_ding_user:
        msg_sender.send_ding2user(msg_to_ding_user, msg_title + '\n' + msg_content)

    if wx_status:
        msg_to_wx_user = []
        for user in msg_to:
            if user.wx_user_id:
                msg_to_wx_user.append(user.wx_user_id)
            else:
                msg_to_wx_user.append(user.username)
        msg_sender.send_wx2user(wx_msg_content, msg_to_wx_user)

    # DDL通知
    if sys_config.get('mail') and sys_config.get('ddl_notify_auth_group') and workflow.status == 'workflow_finish':
        # 判断上线语句是否存在DDL,存在则通知相关人员
        if workflow.syntax_type == 1:
            # 消息内容通知
            msg_title = '[Archery]有新的DDL语句执行完成#{}'.format(audit_id)
            msg_content = '''发起人:{}\n变更组:{}\n变更实例:{}\n变更数据库:{}\n工单名称:{}\n工单地址:{}\n工单预览:{}\n'''.format(
                Users.objects.get(username=workflow.engineer).display,
                workflow.group_name,
                workflow.instance.instance_name,
                workflow.db_name,
                workflow.workflow_name,
                url,
                workflow.sqlworkflowcontent.sql_content[0:500])
            # 获取通知成员ddl_notify_auth_group
            msg_to = Users.objects.filter(groups__name=sys_config.get('ddl_notify_auth_group'))
            # 处理接收人信息
            msg_to_email = [user.email for user in msg_to]

            # 发送
            msg_sender.send_email(msg_title, msg_content, msg_to_email)
Exemplo n.º 23
0
def debug(request):
    # 获取完整信息
    full = request.GET.get('full')

    # 系统配置
    sys_config = SysConfig().sys_config
    # 敏感信息处理
    secret_keys = [
        'inception_remote_backup_password', 'ding_app_secret',
        'feishu_app_secret', 'mail_smtp_password'
    ]
    sys_config.update({k: "******" for k in secret_keys})

    # MySQL信息
    cursor = connection.cursor()
    mysql_info = {
        'mysql_server_info': cursor.db.mysql_server_info,
        'timezone_name': cursor.db.timezone_name
    }

    # Redis信息
    try:
        redis_conn = get_redis_connection("default")
        full_redis_info = redis_conn.info()
        redis_info = {
            'redis_version': full_redis_info.get('redis_version'),
            'redis_mode': full_redis_info.get('redis_mode'),
            'role': full_redis_info.get('role'),
            'maxmemory_human': full_redis_info.get('maxmemory_human'),
            'used_memory_human': full_redis_info.get('used_memory_human'),
        }
    except Exception as e:
        redis_info = f'获取Redis信息报错:{e}'
        full_redis_info = redis_info

    # django_q
    try:
        django_q_version = '.'.join(str(i) for i in django_q.VERSION)
        broker = get_broker()
        stats = Stat.get_all(broker=broker)
        queue_size = broker.queue_size()
        lock_size = broker.lock_size()
        if lock_size:
            queue_size = '{}({})'.format(queue_size, lock_size)
        q_broker_stats = {
            'info': broker.info(),
            'Queued': queue_size,
            'Success': Success.objects.count(),
            'Failures': Failure.objects.count(),
        }
        q_cluster_stats = []
        for stat in stats:
            # format uptime
            uptime = (timezone.now() - stat.tob).total_seconds()
            hours, remainder = divmod(uptime, 3600)
            minutes, seconds = divmod(remainder, 60)
            uptime = '%d:%02d:%02d' % (hours, minutes, seconds)
            q_cluster_stats.append({
                'host': stat.host,
                'cluster_id': stat.cluster_id,
                'state': stat.status,
                'pool': len(stat.workers),
                'tq': stat.task_q_size,
                'rq': stat.done_q_size,
                'rc': stat.reincarnations,
                'up': uptime
            })
        django_q_info = {
            'version': django_q_version,
            'conf': django_q.conf.Conf.conf,
            'q_cluster_stats': q_cluster_stats
            if q_cluster_stats else '没有正在运行的集群信息,请检查django_q状态',
            'q_broker_stats': q_broker_stats
        }
    except Exception as e:
        django_q_info = f'获取django_q信息报错:{e}'

    # Inception和goInception信息
    inception_host = sys_config.get('inception_host')
    inception_port = sys_config.get('inception_port', 0)
    go_inception_host = sys_config.get('go_inception_host')
    go_inception_port = sys_config.get('go_inception_port', 0)
    inception_remote_backup_host = sys_config.get(
        'inception_remote_backup_host', '')
    inception_remote_backup_port = sys_config.get(
        'inception_remote_backup_port', '')
    inception_remote_backup_user = sys_config.get(
        'inception_remote_backup_user', '')
    inception_remote_backup_password = sys_config.get(
        'inception_remote_backup_password', '')

    # inception
    try:
        inc_conn = MySQLdb.connect(host=inception_host,
                                   port=int(inception_port),
                                   connect_timeout=1,
                                   cursorclass=MySQLdb.cursors.DictCursor)
        cursor = inc_conn.cursor()
        cursor.execute('inception get variables')
        rows = cursor.fetchall()
        full_inception_info = dict()
        for row in rows:
            full_inception_info[row.get('Variable_name')] = row.get('Value')
        inception_info = {
            'version':
            full_inception_info.get('version'),
            'max_allowed_packet':
            full_inception_info.get('max_allowed_packet'),
            'inception_language_code':
            full_inception_info.get('inception_language_code'),
            'inception_osc_on':
            full_inception_info.get('inception_osc_on'),
            'inception_osc_bin_dir':
            full_inception_info.get('inception_osc_bin_dir'),
        }
    except Exception as e:
        inception_info = f'获取Inception信息报错:{e}'
        full_inception_info = inception_info

    # goInception
    try:
        goinc_conn = MySQLdb.connect(host=go_inception_host,
                                     port=int(go_inception_port),
                                     connect_timeout=1,
                                     cursorclass=MySQLdb.cursors.DictCursor)
        cursor = goinc_conn.cursor()
        cursor.execute('inception get variables')
        rows = cursor.fetchall()
        full_goinception_info = dict()
        for row in rows:
            full_goinception_info[row.get('Variable_name')] = row.get('Value')
        goinception_info = {
            'version': full_goinception_info.get('version'),
            'max_allowed_packet':
            full_goinception_info.get('max_allowed_packet'),
            'lang': full_goinception_info.get('lang'),
            'osc_on': full_goinception_info.get('osc_on'),
            'osc_bin_dir': full_goinception_info.get('osc_bin_dir'),
            'ghost_on': full_goinception_info.get('ghost_on'),
        }
    except Exception as e:
        goinception_info = f'获取goInception信息报错:{e}'
        full_goinception_info = goinception_info

    # 备份库
    try:
        bak_conn = MySQLdb.connect(host=inception_remote_backup_host,
                                   port=int(inception_remote_backup_port),
                                   user=inception_remote_backup_user,
                                   password=inception_remote_backup_password,
                                   connect_timeout=1)
        cursor = bak_conn.cursor()
        cursor.execute('select 1;')
        backup_info = 'normal'
    except Exception as e:
        backup_info = f'无法连接Inception备份库\n{e}'

    # PACKAGES
    installed_packages = pkg_resources.working_set
    installed_packages_list = sorted(
        ["%s==%s" % (i.key, i.version) for i in installed_packages])

    # 最终集合
    system_info = {
        'archery': {
            'version': archery.display_version
        },
        'django_q': django_q_info,
        'inception': {
            'enable_inception': sys_config.get('inception'),
            'inception_info': full_inception_info if full else inception_info,
            'goinception_info':
            full_goinception_info if full else goinception_info,
            'backup_info': backup_info
        },
        'runtime_info': {
            'python_version': platform.python_version(),
            'mysql_info': mysql_info,
            'redis_info': full_redis_info if full else redis_info,
            'sys_argv': sys.argv,
            'platform': platform.uname()
        },
        'sys_config': sys_config,
        'packages': installed_packages_list
    }
    return JsonResponse(system_info)
Exemplo n.º 24
0
class ArcheryAuth(object):
    def __init__(self, request):
        self.request = request
        self.sys_config = SysConfig()

    @staticmethod
    def challenge(username=None, password=None):
        # 仅验证密码, 验证成功返回 user 对象, 清空计数器
        user = authenticate(username=username, password=password)
        # 登录成功
        if user:
            # 如果登录成功, 登录失败次数重置为0
            user.failed_login_count = 0
            user.save()
            return user

    def authenticate(self):
        username = self.request.POST.get('username')
        password = self.request.POST.get('password')
        # 确认用户是否已经存在
        try:
            user = Users.objects.get(username=username)
        except Users.DoesNotExist:
            authenticated_user = self.challenge(username=username,
                                                password=password)
            if authenticated_user:
                # ldap 首次登录逻辑
                init_user(authenticated_user)
                login(self.request, authenticated_user)
                return {'status': 0, 'msg': 'ok', 'data': authenticated_user}
            else:
                return {'status': 1, 'msg': '用户名或密码错误,请重新输入!', 'data': ''}
        except:
            logger.error('验证用户密码时报错')
            logger.error(traceback.format_exc())
            return {
                'status': 1,
                'msg': f'服务器错误{traceback.format_exc()}',
                'data': ''
            }
        # 已存在用户, 验证是否在锁期间
        # 读取配置文件
        lock_count = int(self.sys_config.get('lock_cnt_threshold', 5))
        lock_time = int(self.sys_config.get('lock_time_threshold', 60 * 5))
        # 验证是否在锁, 分了几个if 防止代码太长
        if user.failed_login_count and user.last_login_failed_at:
            if user.failed_login_count >= lock_count:
                now = datetime.datetime.now()
                if user.last_login_failed_at + datetime.timedelta(
                        seconds=lock_time) > now:
                    return {
                        'status': 3,
                        'msg': f'登录失败超过限制,该账号已被锁定!请等候大约{lock_time}秒再试',
                        'data': ''
                    }
                else:
                    # 如果锁已超时, 重置失败次数
                    user.failed_login_count = 0
                    user.save()
        authenticated_user = self.challenge(username=username,
                                            password=password)
        if authenticated_user:
            if not authenticated_user.last_login:
                init_user(authenticated_user)
            login(self.request, authenticated_user)
            return {'status': 0, 'msg': 'ok', 'data': authenticated_user}
        user.failed_login_count += 1
        user.last_login_failed_at = datetime.datetime.now()
        user.save()
        return {'status': 1, 'msg': '用户名或密码错误,请重新输入!', 'data': ''}
Exemplo n.º 25
0
    def execute_check(self, db_name=None, sql=''):
        """上线单执行前的检查, 返回Review set"""
        archer_config = SysConfig()
        check_result = ReviewSet(full_sql=sql)
        if archer_config.get('critical_ddl_regex'):
            # 如果启用critical_ddl 的检查
            critical_ddl_regex = archer_config.get('critical_ddl_regex')
            p = re.compile(critical_ddl_regex)
            # 逐行匹配正则
            line = 1
            for statement in sqlparse.split(sql):
                # 删除注释语句
                statement = sqlparse.format(statement, strip_comments=True)
                if p.match(statement.strip().lower()):
                    result = ReviewResult(id=line,
                                          errlevel=2,
                                          stagestatus='驳回高危SQL',
                                          errormessage='禁止提交匹配' +
                                          critical_ddl_regex + '条件的语句!',
                                          sql=statement)
                    check_result.is_critical = True
                else:
                    result = ReviewResult(id=line, errlevel=0, sql=statement)
                check_result.rows += [result]
                line += 1
            if check_result.is_critical:
                return check_result

        # 检查 inception 不支持的函数
        check_result.rows = []
        line = 1
        for statement in sqlparse.split(sql):
            # 删除注释语句
            statement = sqlparse.format(statement, strip_comments=True)
            if re.match(
                    r"(\s*)alter(\s+)table(\s+)(\S+)(\s*);|(\s*)alter(\s+)table(\s+)(\S+)\.(\S+)(\s*);",
                    statement.lower() + ";"):
                result = ReviewSet(id=line,
                                   errlevel=2,
                                   stagestatus='SQL语法错误',
                                   errormessage='ALTER TABLE 必须带有选项',
                                   sql=statement)
                check_result.is_critical = True
            else:
                result = ReviewSet(id=line, errlevel=0, sql=statement)
            check_result.rows += [result]
            line += 1
        if check_result.is_critical:
            return check_result

        # inception 校验
        check_result.rows = []
        inception_sql = "/*--user=%s;--password=%s;--host=%s;--enable-check=1;--port=%d;*/\
            inception_magic_start;\
            use %s;\
            %s\
            inception_magic_commit;" % (self.user, self.password, self.host,
                                        self.port, db_name, sql)
        inception_engine = InceptionEngine()
        inception_result = inception_engine.query(sql=inception_sql)
        for r in inception_result.rows:
            check_result.rows += [ReviewResult(inception_result=r)]
        check_result.column_list = inception_result.column_list
        return check_result
Exemplo n.º 26
0
def send_msg(workflowDetail, url):
    mailSender = MailSender()
    sys_config = SysConfig().sys_config
    # 获取当前审批和审批流程
    audit_auth_group, current_audit_auth_group = Workflow.review_info(
        workflowDetail.id, 2)
    audit_id = Workflow.auditinfobyworkflow_id(workflowDetail.id, 2).audit_id
    # 如果执行完毕了,则根据配置决定是否给提交者和DBA一封邮件提醒,DBA需要知晓审核并执行过的单子
    msg_title = "[{}]工单{}#{}".format(
        WorkflowDict.workflow_type['sqlreview_display'], workflowDetail.status,
        audit_id)
    msg_content = '''发起人:{}\n组:{}\n审批流程:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
        workflowDetail.engineer_display, workflowDetail.group_name,
        audit_auth_group, workflowDetail.workflow_name, url,
        re.sub('[\r\n\f]{2,}', '\n',
               workflowDetail.sql_content[0:500].replace('\r', '')))

    if sys_config.get('mail'):
        # 邮件通知申请人,审核人,抄送DBA
        notify_users = workflowDetail.audit_auth_groups.split(',')
        notify_users.append(workflowDetail.engineer)
        listToAddr = [
            email['email'] for email in Users.objects.filter(
                username__in=notify_users).values('email')
        ]
        listCcAddr = [
            email['email'] for email in
            auth_group_users(auth_group_names=['DBA'],
                             group_id=workflowDetail.group_id).values('email')
        ]
        mailSender.send_email(msg_title,
                              msg_content,
                              listToAddr,
                              listCcAddr=listCcAddr)
    if sys_config.get('ding'):
        # 钉钉通知申请人,审核人,抄送DBA
        webhook_url = SqlGroup.objects.get(
            group_id=workflowDetail.group_id).ding_webhook
        MailSender.send_ding(webhook_url, msg_title + '\n' + msg_content)

    if sys_config.get('mail') and sys_config.get('ddl_notify_auth_group', None) \
            and workflowDetail.status == '已正常结束':
        # 判断上线语句是否存在DDL,存在则通知相关人员
        sql_content = workflowDetail.sql_content
        # 删除注释语句
        sql_content = ''.join(
            map(
                lambda x: re.compile(r'(^--\s+.*|^/\*.*\*/;\s*$)').sub(
                    '', x, count=1), sql_content.splitlines(1))).strip()
        # 去除空行
        sql_content = re.sub('[\r\n\f]{2,}', '\n', sql_content)

        # 匹配DDL语句CREATE、ALTER(排除索引变更)、DROP、TRUNCATE、RENAME
        send = 0
        for row in sql_content.strip(';').split(';'):
            # alter语法
            if re.match(
                    r"^alter\s+table\s+\S+\s+(add|alter|change|drop|rename|modify)\s+(?!.*(index|key|unique))",
                    row.strip().lower()):
                send = 1
                break
            # create语法
            elif re.match(r"^create\s+(temporary\s+)?(database|schema|table)",
                          row.strip().lower()):
                send = 1
                break
            # drop语法
            elif re.match(r"^drop", row.strip().lower()):
                send = 1
                break
            # rename语法
            elif re.match(r"^rename", row.strip().lower()):
                send = 1
                break
            # truncate语法
            elif re.match(r"^truncate", row.strip().lower()):
                send = 1
                break

        if send == 1:
            # 消息内容通知
            msg_title = '[archery]有新的DDL语句执行完成#{}'.format(audit_id)
            msg_content = '''发起人:{}\n变更组:{}\n变更实例:{}\n变更数据库:{}\n工单名称:{}\n工单地址:{}\n工单预览:{}\n'''.format(
                Users.objects.get(username=workflowDetail.engineer).display,
                workflowDetail.group_name, workflowDetail.instance_name,
                workflowDetail.db_name, workflowDetail.workflow_name, url,
                workflowDetail.sql_content[0:500])
            # 获取通知成员
            msg_to = [
                email['email']
                for email in Users.objects.filter(groups__name=sys_config.get(
                    'ddl_notify_auth_group')).values('email')
            ]

            # 发送
            mailSender.send_email(msg_title, msg_content, msg_to)
Exemplo n.º 27
0
def notify_for_execute(workflow):
    """
    工单执行结束的通知
    :param workflow:
    :return:
    """
    # 判断是否开启消息通知,未开启直接返回
    sys_config = SysConfig()
    if not sys_config.get('mail') and not sys_config.get('ding'):
        logger.info('未开启消息通知,可在系统设置中开启')
        return None
    # 获取当前审批和审批流程
    base_url = sys_config.get('archery_base_url',
                              'http://127.0.0.1:8000').rstrip('/')
    audit_auth_group, current_audit_auth_group = Audit.review_info(
        workflow.id, 2)
    audit_id = Audit.detail_by_workflow_id(workflow.id, 2).audit_id
    url = "{base_url}/workflow/{audit_id}".format(base_url=base_url,
                                                  audit_id=audit_id)
    msg_title = "[{}]工单{}#{}".format(
        WorkflowDict.workflow_type['sqlreview_display'],
        workflow.get_status_display(), audit_id)
    msg_content = '''发起人:{}\n组:{}\n审批流程:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
        workflow.engineer_display, workflow.group_name, audit_auth_group,
        workflow.workflow_name, url,
        re.sub(
            '[\r\n\f]{2,}', '\n',
            workflow.sqlworkflowcontent.sql_content[0:500].replace('\r', '')))

    # 邮件通知申请人,抄送DBA
    list_to_addr = [
        email['email'] for email in Users.objects.filter(
            username=workflow.engineer).values('email')
    ]
    list_cc_addr = [
        email['email'] for email
        in auth_group_users(auth_group_names=['DBA'],
                            group_id=workflow.group_id).values('email')
    ]
    msg_email_reciver = list_to_addr + list_cc_addr

    # 判断是发送钉钉还是发送邮件
    msg_sender = MsgSender()
    logger.info('发送消息通知,消息audit_id={}'.format(audit_id))
    logger.info('消息标题:{}\n通知对象:{}\n消息内容:{}'.format(msg_title,
                                                   msg_email_reciver,
                                                   msg_content))
    if sys_config.get('mail'):
        msg_sender.send_email(msg_title,
                              msg_content,
                              list_to_addr,
                              list_cc_addr=list_cc_addr)
    if sys_config.get('ding'):
        # 钉钉通知申请人,审核人,抄送DBA
        webhook_url = ResourceGroup.objects.get(
            group_id=workflow.group_id).ding_webhook
        MsgSender.send_ding(webhook_url, msg_title + '\n' + msg_content)

    # DDL通知
    if sys_config.get('mail') and sys_config.get(
            'ddl_notify_auth_group') and workflow.status == 'workflow_finish':
        # 判断上线语句是否存在DDL,存在则通知相关人员
        if workflow.syntax_type == 1:
            # 消息内容通知
            msg_title = '[Archery]有新的DDL语句执行完成#{}'.format(audit_id)
            msg_content = '''发起人:{}\n变更组:{}\n变更实例:{}\n变更数据库:{}\n工单名称:{}\n工单地址:{}\n工单预览:{}\n'''.format(
                Users.objects.get(username=workflow.engineer).display,
                workflow.group_name, workflow.instance.instance_name,
                workflow.db_name, workflow.workflow_name, url,
                workflow.sqlworkflowcontent.sql_content[0:500])
            # 获取通知成员ddl_notify_auth_group
            msg_to = [
                email['email']
                for email in Users.objects.filter(groups__name=sys_config.get(
                    'ddl_notify_auth_group')).values('email')
            ]

            # 发送
            logger.info('发送DDL通知,消息audit_id={}'.format(audit_id))
            logger.info('消息标题:{}\n通知对象:{}\n消息内容:{}'.format(
                msg_title, msg_to, msg_content))
            msg_sender.send_email(msg_title, msg_content, msg_to)
Exemplo n.º 28
0
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import json
import time
import traceback
import requests
from common.config import SysConfig
from sql.utils.ding_api import get_access_token, rs

logger = logging.getLogger('default')
sys_config = SysConfig()
username2ding = sys_config.get('ding_archer_username')
ding_dept_ids = sys_config.get('ding_dept_ids', '')


def get_dept_list_id_fetch_child(token, parent_dept_id):
    ids = [int(parent_dept_id)]
    url = 'https://oapi.dingtalk.com/department/list_ids?id={0}&access_token={1}'.format(
        parent_dept_id, token)
    resp = requests.get(url, timeout=3)
    ret = str(resp.content, encoding="utf8")
    s = json.loads(ret)
    if s["errcode"] == 0:
        for dept_id in s["sub_dept_id_list"]:
            ids.extend(get_dept_list_id_fetch_child(token, dept_id))
    return ids


def sync_ding_user_id():
    """
Exemplo n.º 29
0
def autoreview(request):
    workflow_id = request.POST.get('workflow_id')
    sql_content = request.POST['sql_content']
    workflow_title = request.POST['workflow_name']
    group_name = request.POST['group_name']
    group_id = ResourceGroup.objects.get(group_name=group_name).group_id
    instance_name = request.POST['instance_name']
    db_name = request.POST.get('db_name')
    is_backup = request.POST['is_backup']
    notify_users = request.POST.getlist('notify_users')

    # 服务器端参数验证
    if sql_content is None or workflow_title is None or instance_name is None or db_name is None or is_backup is None:
        context = {'errMsg': '页面提交参数可能为空'}
        return render(request, 'error.html', context)

    # 验证组权限(用户是否在该组、该组是否有指定实例)
    try:
        user_instances(request.user, 'master').get(instance_name=instance_name)
    except Exception:
        context = {'errMsg': '你所在组未关联该实例!'}
        return render(request, 'error.html', context)

    # # 删除注释语句
    # sql_content = ''.join(
    #     map(lambda x: re.compile(r'(^--.*|^/\*.*\*/;\s*$)').sub('', x, count=1),
    #         sql_content.splitlines(1))).strip()
    # # 去除空行
    # sql_content = re.sub('[\r\n\f]{2,}', '\n', sql_content)

    sql_content = sql_content.strip()

    # 交给inception进行自动审核
    try:
        inception_result = InceptionDao(instance_name=instance_name).sqlauto_review(sql_content, db_name)
    except Exception as msg:
        context = {'errMsg': msg}
        return render(request, 'error.html', context)

    if inception_result is None or len(inception_result) == 0:
        context = {'errMsg': 'inception返回的结果集为空!可能是SQL语句有语法错误'}
        return render(request, 'error.html', context)
    # 要把result转成JSON存进数据库里,方便SQL单子详细信息展示
    json_result = json.dumps(inception_result)

    # 遍历result,看是否有任何自动审核不通过的地方,并且按配置确定是标记审核不通过还是放行,放行的可以在工单内跳过inception直接执行
    sys_config = SysConfig().sys_config
    is_manual = 0
    workflow_status = Const.workflowStatus['manreviewing']
    for row in inception_result:
        # 1表示警告,不影响执行
        if row[2] == 1 and sys_config.get('auto_review_wrong', '') == '1':
            workflow_status = Const.workflowStatus['autoreviewwrong']
            break
        # 2表示严重错误,或者inception不支持的语法,标记手工执行,可以跳过inception直接执行
        elif row[2] == 2:
            is_manual = 1
            if sys_config.get('auto_review_wrong', '') in ('', '1', '2'):
                workflow_status = Const.workflowStatus['autoreviewwrong']
            break
        elif re.match(r"\w*comments\w*", row[4]):
            is_manual = 1
            if sys_config.get('auto_review_wrong', '') in ('', '1', '2'):
                workflow_status = Const.workflowStatus['autoreviewwrong']
            break

    # 判断SQL是否包含DDL语句,SQL语法 1、DDL,2、DML
    sql_syntax = 2
    for stmt in sqlparse.split(sql_content):
        statement = sqlparse.parse(stmt)[0]
        syntax_type = statement.token_first().ttype.__str__()
        if syntax_type == 'Token.Keyword.DDL':
            sql_syntax = 1
            break

    # 调用工作流生成工单
    # 使用事务保持数据一致性
    try:
        with transaction.atomic():
            # 存进数据库里
            engineer = request.user.username
            if not workflow_id:
                sql_workflow = SqlWorkflow()
                sql_workflow.create_time = timezone.now()
            else:
                sql_workflow = SqlWorkflow.objects.get(id=int(workflow_id))
            sql_workflow.workflow_name = workflow_title
            sql_workflow.group_id = group_id
            sql_workflow.group_name = group_name
            sql_workflow.engineer = engineer
            sql_workflow.engineer_display = request.user.display
            sql_workflow.audit_auth_groups = Workflow.audit_settings(group_id, WorkflowDict.workflow_type['sqlreview'])
            sql_workflow.status = workflow_status
            sql_workflow.is_backup = is_backup
            sql_workflow.review_content = json_result
            sql_workflow.instance_name = instance_name
            sql_workflow.db_name = db_name
            sql_workflow.sql_content = sql_content
            sql_workflow.execute_result = ''
            sql_workflow.is_manual = is_manual
            sql_workflow.audit_remark = ''
            sql_workflow.sql_syntax = sql_syntax
            sql_workflow.save()
            workflow_id = sql_workflow.id
            # 自动审核通过了,才调用工作流
            if workflow_status == Const.workflowStatus['manreviewing']:
                # 调用工作流插入审核信息, 查询权限申请workflow_type=2
                # 抄送通知人
                list_cc_addr = [email['email'] for email in
                                Users.objects.filter(username__in=notify_users).values('email')]
                Workflow().addworkflowaudit(request, WorkflowDict.workflow_type['sqlreview'], workflow_id,
                                            list_cc_addr=list_cc_addr)
    except Exception as msg:
        logger.error(traceback.format_exc())
        context = {'errMsg': msg}
        return render(request, 'error.html', context)

    return HttpResponseRedirect(reverse('sql:detail', args=(workflow_id,)))
Exemplo n.º 30
0
def _send(audit_info, msg_type=0, **kwargs):
    audit_id = audit_info.audit_id
    workflow_audit_remark = kwargs.get('audit_remark', '')
    workflow_url = kwargs.get('workflow_url', '')
    msg_email_cc = kwargs.get('email_cc', [])
    workflow_id = audit_info.workflow_id
    workflow_type = audit_info.workflow_type
    status = audit_info.current_status
    workflow_title = audit_info.workflow_title
    workflow_from = audit_info.create_user_display
    group_name = audit_info.group_name
    webhook_url = SqlGroup.objects.get(
        group_id=audit_info.group_id).ding_webhook

    # 获取当前审批和审批流程
    from sql.utils.workflow import Workflow
    workflow_auditors, current_workflow_auditors = Workflow.review_info(
        audit_info.workflow_id, audit_info.workflow_type)

    # 准备消息内容
    if workflow_type == WorkflowDict.workflow_type['query']:
        workflow_type_display = WorkflowDict.workflow_type['query_display']
        workflow_detail = QueryPrivilegesApply.objects.get(
            apply_id=workflow_id)
        if workflow_detail.priv_type == 1:
            workflow_content = '''数据库清单:{}\n授权截止时间:{}\n结果集:{}\n'''.format(
                workflow_detail.db_list,
                datetime.datetime.strftime(workflow_detail.valid_date,
                                           '%Y-%m-%d %H:%M:%S'),
                workflow_detail.limit_num)
        elif workflow_detail.priv_type == 2:
            workflow_content = '''数据库:{}\n表清单:{}\n授权截止时间:{}\n结果集:{}\n'''.format(
                workflow_detail.db_list, workflow_detail.table_list,
                datetime.datetime.strftime(workflow_detail.valid_date,
                                           '%Y-%m-%d %H:%M:%S'),
                workflow_detail.limit_num)
        else:
            workflow_content = ''
    elif workflow_type == WorkflowDict.workflow_type['sqlreview']:
        workflow_type_display = WorkflowDict.workflow_type['sqlreview_display']
        workflow_detail = SqlWorkflow.objects.get(pk=workflow_id)
        workflow_content = re.sub(
            '[\r\n\f]{2,}', '\n',
            workflow_detail.sql_content[0:500].replace('\r', ''))
    else:
        raise Exception('工单类型不正确')

    # 准备消息格式
    if status == WorkflowDict.workflow_status['audit_wait']:  # 申请阶段
        msg_title = "[{}]新的工单申请#{}".format(workflow_type_display, audit_id)
        # 接收人,发送给该资源组内对应权限组所有的用户
        auth_group_names = Group.objects.get(id=audit_info.current_audit).name
        msg_email_reciver = [
            user.email for user in auth_group_users([auth_group_names],
                                                    audit_info.group_id)
        ]
        # 消息内容
        msg_content = '''发起人:{}\n组:{}\n审批流程:{}\n当前审批:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
            workflow_from, group_name, workflow_auditors,
            current_workflow_auditors, workflow_title, workflow_url,
            workflow_content)
    elif status == WorkflowDict.workflow_status['audit_success']:  # 审核通过
        msg_title = "[{}]工单审核通过#{}".format(workflow_type_display, audit_id)
        # 接收人,仅发送给申请人
        msg_email_reciver = [
            Users.objects.get(username=audit_info.create_user).email
        ]
        # 消息内容
        msg_content = '''发起人:{}\n组:{}\n审批流程:{}\n工单名称:{}\n工单地址:{}\n工单详情预览:{}\n'''.format(
            workflow_from, group_name, workflow_auditors, workflow_title,
            workflow_url, workflow_content)
    elif status == WorkflowDict.workflow_status['audit_reject']:  # 审核驳回
        msg_title = "[{}]工单被驳回#{}".format(workflow_type_display, audit_id)
        # 接收人,仅发送给申请人
        msg_email_reciver = [
            Users.objects.get(username=audit_info.create_user).email
        ]
        # 消息内容
        msg_content = '''工单名称:{}\n工单地址:{}\n驳回原因:{}\n提醒:此工单被审核不通过,请按照驳回原因进行修改!'''.format(
            workflow_title, workflow_url, workflow_audit_remark)
    elif status == WorkflowDict.workflow_status['audit_abort']:  # 审核取消,通知所有审核人
        msg_title = "[{}]提交人主动终止工单#{}".format(workflow_type_display, audit_id)
        # 接收人,发送给该资源组内对应权限组所有的用户
        auth_group_names = [
            Group.objects.get(id=auth_group_id).name
            for auth_group_id in audit_info.audit_auth_groups.split(',')
        ]
        msg_email_reciver = [
            user.email
            for user in auth_group_users(auth_group_names, audit_info.group_id)
        ]
        # 消息内容
        msg_content = '''发起人:{}\n组:{}\n工单名称:{}\n工单地址:{}\n提醒:提交人主动终止流程'''.format(
            workflow_from, group_name, workflow_title, workflow_url)
    else:
        raise Exception('工单状态不正确')

    # 判断是发送钉钉还是发送邮件
    msg_sender = MailSender()
    logger.debug('消息标题:{}\n通知对象:{}\n消息内容:{}'.format(msg_title,
                                                    msg_email_reciver,
                                                    msg_content))
    if msg_type == 0:
        sys_config = SysConfig().sys_config
        if sys_config.get('mail'):
            msg_sender.send_email(msg_title,
                                  msg_content,
                                  msg_email_reciver,
                                  list_cc_addr=msg_email_cc)
        if sys_config.get('ding'):
            msg_sender.send_ding(webhook_url, msg_title + '\n' + msg_content)
    elif msg_type == 1:
        msg_sender.send_email(msg_title,
                              msg_content,
                              msg_email_reciver,
                              list_cc_addr=msg_email_cc)
    elif msg_type == 2:
        msg_sender.send_ding(webhook_url, msg_title + '\n' + msg_content)