diff --git a/common/static/style.css b/common/static/style.css index 13a36f1c5e..e587b26f3c 100644 --- a/common/static/style.css +++ b/common/static/style.css @@ -251,3 +251,14 @@ table.modindextable td { padding: 2px; border-collapse: collapse; } + +div.select-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.5); /* 半透明白色 */ + z-index: 10; /* 确保它位于select元素之上 */ + cursor: not-allowed; /* 显示“禁止”光标 */ +} diff --git a/common/templates/config.html b/common/templates/config.html index 93af8eeb1f..20189f03f4 100755 --- a/common/templates/config.html +++ b/common/templates/config.html @@ -183,6 +183,43 @@
SQL上线
placeholder="自动驳回的等级,1表示警告驳回,2和空表示错误才驳回,其他表示不驳回" /> +
+ +
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
diff --git a/sql/models.py b/sql/models.py index 405692459d..c964d9724a 100755 --- a/sql/models.py +++ b/sql/models.py @@ -938,9 +938,12 @@ class Meta: ("menu_document", "菜单 相关文档"), ("menu_openapi", "菜单 OpenAPI"), ("sql_submit", "提交SQL上线工单"), + ("sql_submitbatch", "提交批量SQL上线工单"), ("sql_review", "审核SQL上线工单"), + ("sql_reviewbatch", "审核批量SQL上线工单"), ("sql_execute_for_resource_group", "执行SQL上线工单(资源组粒度)"), ("sql_execute", "执行SQL上线工单(仅自己提交的)"), + ("sql_executebatch", "执行批量SQL上线工单"), ("sql_analyze", "执行SQL分析"), ("optimize_sqladvisor", "执行SQLAdvisor"), ("optimize_sqltuning", "执行SQLTuning"), diff --git a/sql/resource_group.py b/sql/resource_group.py index 6d9fa0226e..04ddd8cffb 100644 --- a/sql/resource_group.py +++ b/sql/resource_group.py @@ -148,7 +148,7 @@ def instances(request): ins = ( ins.filter(**filter_dict) .order_by(Convert("instance_name", "gbk").asc()) - .values("id", "type", "db_type", "instance_name") + .values("id", "type", "db_type", "instance_name", "db_name") ) rows = [row for row in ins] result = {"status": 0, "msg": "ok", "data": rows} diff --git a/sql/sql_workflow.py b/sql/sql_workflow.py index b812be7c51..d2c4330581 100644 --- a/sql/sql_workflow.py +++ b/sql/sql_workflow.py @@ -30,7 +30,7 @@ ) from sql.utils.tasks import add_sql_schedule, del_schedule from sql.utils.workflow_audit import Audit, get_auditor, AuditException -from .models import SqlWorkflow +from .models import SqlWorkflow, Instance, ResourceGroup logger = logging.getLogger("default") @@ -229,14 +229,16 @@ def alter_run_date(request): @permission_required("sql.sql_review", raise_exception=True) -def passed(request): +def passed(request, workflow_id=None, audit_remark=None): """ 审核通过,不执行 :param request: :return: """ - workflow_id = int(request.POST.get("workflow_id", 0)) - audit_remark = request.POST.get("audit_remark", "") + if workflow_id is None: + workflow_id = int(request.POST.get("workflow_id", 0)) + audit_remark = request.POST.get("audit_remark", "") + if workflow_id == 0: context = {"errMsg": "workflow_id参数为空."} return render(request, "error.html", context) @@ -280,19 +282,22 @@ def passed(request): return HttpResponseRedirect(reverse("sql:detail", args=(workflow_id,))) -def execute(request): +def execute(request, workflow_id=None, mode=None): """ 执行SQL :param request: :return: """ + if workflow_id is None: + workflow_id = int(request.POST.get("workflow_id", 0)) + mode = request.POST.get("mode") + # 校验多个权限 if not ( request.user.has_perm("sql.sql_execute") or request.user.has_perm("sql.sql_execute_for_resource_group") ): raise PermissionDenied - workflow_id = int(request.POST.get("workflow_id", 0)) if workflow_id == 0: context = {"errMsg": "workflow_id参数为空."} return render(request, "error.html", context) @@ -310,8 +315,6 @@ def execute(request): audit_id = Audit.detail_by_workflow_id( workflow_id=workflow_id, workflow_type=WorkflowType.SQL_REVIEW ).audit_id - # 根据执行模式进行对应修改 - mode = request.POST.get("mode") # 交由系统执行 if mode == "auto": # 修改工单状态为排队中 @@ -432,18 +435,20 @@ def timing_task(request): return HttpResponseRedirect(reverse("sql:detail", args=(workflow_id,))) -def cancel(request): +def cancel(request, workflow_id=None, audit_remark=None): """ 终止流程 :param request: :return: """ - workflow_id = int(request.POST.get("workflow_id", 0)) + if workflow_id is None: + workflow_id = int(request.POST.get("workflow_id", 0)) + audit_remark = request.POST.get("cancel_remark") + if workflow_id == 0: context = {"errMsg": "workflow_id参数为空."} return render(request, "error.html", context) sql_workflow = 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) @@ -525,3 +530,138 @@ def osc_control(request): json.dumps(result, cls=ExtendJSONEncoder, bigint_as_string=True), content_type="application/json", ) + + +@permission_required("sql.sql_reviewbatch", raise_exception=True) +def passedbatch(request): + """ + 批量审核通过,不执行 + :param request: + :return: + """ + workflow_id_list = request.POST["workflowid_array"] + workflow_id_list = json.loads(workflow_id_list) + sys_config = SysConfig() + audit_remark = sys_config.get("batch_passed_remark") + + for workflow_id in workflow_id_list: + passed(request, workflow_id, audit_remark) + + return HttpResponseRedirect("/sqlworkflow") + + +@permission_required("sql.sql_executebatch", raise_exception=True) +def executebatch(request): + """ + 执行SQL + :param request: + :return: + """ + workflow_id_list = request.POST["workflowid_array"] + workflow_id_list = json.loads(workflow_id_list) + mode = "auto" + + for workflow_id in workflow_id_list: + execute(request, workflow_id, mode) + return HttpResponseRedirect("/sqlworkflow") + + +@permission_required("sql.sql_reviewbatch", raise_exception=True) +def cancelbatch(request): + """ + 终止流程 + :param request: + :return: + """ + workflow_id_list = request.POST["workflowid_array"] + workflow_id_list = json.loads(workflow_id_list) + sys_config = SysConfig() + audit_remark = sys_config.get("batch_cancel_remark") + + for workflow_id in workflow_id_list: + cancel(request, workflow_id, audit_remark) + + return HttpResponseRedirect("/sqlworkflow") + + +@permission_required("sql.sql_submitbatch", raise_exception=True) +def checkbatch(request): + """SQL检测按钮, 此处没有产生工单""" + sql_content = request.POST.get("sql_content") + instance_names = request.POST.get("instance_name") + instance_names = json.loads(instance_names) + instances = [] + for instance_name in instance_names: + instance = Instance.objects.get(instance_name=instance_name) + instances.append(instance) + + result = {"status": 0, "msg": "ok", "data": {}} + # 服务器端参数验证 + if sql_content is None or len(instance_names) == 0: + result["status"] = 1 + result["msg"] = "页面提交参数可能为空" + return HttpResponse(json.dumps(result), content_type="application/json") + warning_count_totle = 0 + error_count_totle = 0 + # 交给engine进行检测 + check_result_arr = [] + for instance in instances: + try: + check_engine = get_engine(instance=instance) + check_result = check_engine.execute_check( + db_name=instance.db_name, sql=sql_content.strip() + ) + for i in range(len(check_result.to_dict())): + check_result.to_dict()[i]["instance"] = instance.instance_name + warning_count_totle = warning_count_totle + check_result.warning_count + error_count_totle = error_count_totle + check_result.error_count + check_result_arr = check_result_arr + check_result.to_dict() + except Exception as e: + result["status"] = 1 + result["msg"] = str(e) + return HttpResponse(json.dumps(result), content_type="application/json") + + # 处理检测结果 + result["data"]["rows"] = check_result_arr + result["data"]["CheckWarningCount"] = warning_count_totle + result["data"]["CheckErrorCount"] = error_count_totle + return HttpResponse(json.dumps(result), content_type="application/json") + + +@permission_required("sql.sql_submitbatch", raise_exception=True) +def rollbackbatch(request): + """ + 回滚流程 + :param request: + :return: + """ + workflow_id_list = request.POST["workflowid_array"] + workflow_id_list = json.loads(workflow_id_list) + workflow_list = list() + for workflow_id in workflow_id_list: + workflow = SqlWorkflow.objects.get(id=int(workflow_id)) + rollback_workflow_name = ( + f"【回滚工单】原工单Id:{workflow_id} ,{workflow.workflow_name}" + ) + query_engine = get_engine(instance=workflow.instance) + list_backup_sql = query_engine.get_rollback(workflow=workflow) + ## 获取完整的回滚SQL + sql_content = "\n".join(item[1] for item in list_backup_sql) + param = { + "workflow": { + "workflow_name": rollback_workflow_name, + "daemon_url": "", + "group_id": ResourceGroup.objects.get( + group_name=workflow.group_name + ).group_id, + "instance": workflow.instance.id, + "db_name": workflow.db_name, + "is_backup": True, + "run_date_start": None, + "run_date_end": None, + }, + "sql_content": sql_content, + } + workflow_list.append(param) + result = {"status": 0, "msg": "ok", "data": workflow_list} + return HttpResponse(json.dumps(result), content_type="application/json") diff --git a/sql/templates/sqlsubmitbatch.html b/sql/templates/sqlsubmitbatch.html new file mode 100644 index 0000000000..92bd3072c9 --- /dev/null +++ b/sql/templates/sqlsubmitbatch.html @@ -0,0 +1,665 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+ SQL上线 +
+
+
+ {% csrf_token %} + +
+

+                        
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ {% if enable_backup_switch %} +
+ +
+ {% endif %} + +
+ + +
+ + +
+
+ + + + +
+ + + +
+
+
+
+
+
+
+
+
+ 检测结果 +
+ +
+
+ +
+
+
+
+
+ + +{% endblock content %} +{% block js %} + {% load static %} + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock %} diff --git a/sql/templates/sqlworkflow.html b/sql/templates/sqlworkflow.html index 09b8e8a758..381621d93d 100644 --- a/sql/templates/sqlworkflow.html +++ b/sql/templates/sqlworkflow.html @@ -37,12 +37,33 @@
-
+ {% if enable_batch_workflow %} +
+ + +
+ {% endif %}
@@ -185,6 +206,10 @@ } }, columns: [{ + title: '', + field: 'checkbox', + checkbox: true + },{ title: '工单名称', field: 'workflow_name', formatter: function (value, row, index) { @@ -268,6 +293,196 @@ } + // 选择批量处理操作 + function get_batch_oper() { + var batch_oper_value = $("#batchOper").val() + switch (batch_oper_value) { + case "batchSubmit": + apply_batch_submit(); + break; + case "batchRollback": + apply_batch_rollback(); + break; + case "batchPass": + apply_batch_pass(); + break; + case "batchExecute": + apply_batch_execute(); + break; + case "batchCancel": + apply_batch_cancel(); + break; + } + }; + // 提交批量工单 + function apply_batch_submit() { + var selectElement = document.getElementById('batchOper'); + selectElement.selectedIndex = 0; // 设定为第一个选项 + window.location.href = '/submitsqlbatch/'; + }; + // 提交批量回滚 + function apply_batch_rollback() { + //获取form对象,判断输入,通过则提交 + var AllSelections = $("#sqlaudit-list").bootstrapTable('getSelections'); + if(AllSelections.length == 0){ + alert("请勾选需要提交回滚的工单!!!"); + var selectElement = document.getElementById('batchOper'); + selectElement.selectedIndex = 0; // 设定为第一个选项 + } else { + var AllSelectionIds = []; + for (var i = 0; i < AllSelections.length; i++) { + AllSelectionIds.push(AllSelections[i]['id']) + } + $("#select-overlay").show(); + alert("请求已发送后台,请稍等..."); + $.ajax({ + url:"/rollbackbatch/", + type:"POST", + data:{ + 'workflowid_array': JSON.stringify(AllSelectionIds) + }, + success:function(data) { + if(data.status == 1){ + alert("勾选了包含非【已正常结束】状态的工单,请核查重新提交"); + location.reload(); + }else { + // console.log(data.data); + sqlSubmit(data.data) + } + } + }) + } + }; + // 提交批量审核 + function apply_batch_pass() { + //获取form对象,判断输入,通过则提交 + // $("#audit_remark").val($("#remark").val()); + var AllSelections = $("#sqlaudit-list").bootstrapTable('getSelections'); + if(AllSelections.length == 0){ + alert("请勾选需要批量审核的工单!!!"); + var selectElement = document.getElementById('batchOper'); + selectElement.selectedIndex = 0; // 设定为第一个选项 + } else { + var AllSelectionIds = []; + for (var i = 0; i < AllSelections.length; i++) { + AllSelectionIds.push(AllSelections[i]['id']) + } + $("#select-overlay").show(); + alert("请求已发送后台,请稍等"); + $.ajax({ + url:"/passedbatch/", + type:"POST", + data:{ + 'workflowid_array': JSON.stringify(AllSelectionIds) + }, + success:function(data) { + if(data.status == 1){ + alert("勾选了包含非【等待审核人审核】状态的工单,请核查重新提交"); + location.reload(); + }else { + location.reload(); + } + } + }) + } + }; + // 提交批量执行 + function apply_batch_execute () { + var AllSelections = $("#sqlaudit-list").bootstrapTable('getSelections'); + if(AllSelections.length == 0){ + alert("请勾选需要批量执行的工单!!!"); + var selectElement = document.getElementById('batchOper'); + selectElement.selectedIndex = 0; // 设定为第一个选项 + } else { + var AllSelectionIds = []; + for (var i = 0; i < AllSelections.length; i++) { + AllSelectionIds.push(AllSelections[i]['id']) + } + var isContinue = confirm("请确认是否立即执行?"); + $("#select-overlay").show(); + alert("请求已发送后台,请稍等..."); + if (isContinue) { + $.ajax({ + url:"/executebatch/", + type:"POST", + data:{ + 'workflowid_array': JSON.stringify(AllSelectionIds) + }, + success:function(data) { + if(data.status == 1){ + alert("勾选了包含非【审核通过】状态的工单,请核查重新提交"); + location.reload(); + }else { + location.reload(); + } + } + }) + } else { + location.reload(); + } + } + }; + // 提交批量终止 + function apply_batch_cancel() { + var AllSelections = $("#sqlaudit-list").bootstrapTable('getSelections'); + //获取form对象,判断输入,通过则提交 + if(AllSelections.length == 0){ + alert("请勾选需要终止的工单!!!"); + var selectElement = document.getElementById('batchOper'); + selectElement.selectedIndex = 0; // 设定为第一个选项 + } else { + var AllSelectionIds = []; + for (var i = 0; i < AllSelections.length; i++) { + AllSelectionIds.push(AllSelections[i]['id']) + } + $("#select-overlay").show(); + alert("请求已发送后台,请稍等..."); + $.ajax({ + url:"/cancelbatch/", + type:"POST", + data:{ + 'workflowid_array': JSON.stringify(AllSelectionIds) + }, + success:function(data) { + if(data.status == 1){ + alert("勾选了包含非【审核通过】或【等待审核人审核】状态的工单,请核查重新提交"); + location.reload(); + }else { + location.reload(); + } + } + }) + } + }; + + function sqlSubmit(workflow_array) { + workflow_array.forEach(function(value) { + console.log(typeof(value)) + console.log(value) + $.ajax({ + type: "post", + url: "/api/v1/workflow/", + dataType: "json", + contentType: 'application/json;', + data: JSON.stringify(value), + beforeSend: function (xhr) { + }, + complete: function () { + }, + success: function (data) { + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + if (XMLHttpRequest.responseJSON) { + alert(XMLHttpRequest.responseText) + } else { + alert(errorThrown); + } + } + }); + }) + location.reload(); + } + // 获取操作日志 function getLog(obj) { var workflow_id = $(obj).attr("workflow_id"); @@ -332,6 +547,10 @@ }) } + $("#batchOper").change(function () { + get_batch_oper(); + }); + //筛选变动自动刷新 $("#navStatus").change(function () { get_workflow_list(); diff --git a/sql/urls.py b/sql/urls.py index e2511c79e9..f75fb125d5 100644 --- a/sql/urls.py +++ b/sql/urls.py @@ -162,4 +162,10 @@ path("audit/input/", audit_log.audit_input), path("user/list/", user.lists), path("user/qrcode//", totp.generate_qrcode), + path("submitsqlbatch/", views.submit_sql_batch), + path("checkbatch/", sql_workflow.checkbatch), + path("passedbatch/", sql_workflow.passedbatch), + path("executebatch/", sql_workflow.executebatch), + path("cancelbatch/", sql_workflow.cancelbatch), + path("rollbackbatch/", sql_workflow.rollbackbatch), ] diff --git a/sql/views.py b/sql/views.py index 79011defc2..229e470aa4 100644 --- a/sql/views.py +++ b/sql/views.py @@ -147,15 +147,16 @@ def sqlworkflow(request): ) resource_group = ResourceGroup.objects.filter(group_id__in=resource_group_id) - return render( - request, - "sqlworkflow.html", - { - "status_list": SQL_WORKFLOW_CHOICES, - "instance": instance, - "resource_group": resource_group, - }, - ) + # 获取系统配置 + archer_config = SysConfig() + + context = { + "status_list": SQL_WORKFLOW_CHOICES, + "instance": instance, + "resource_group": resource_group, + "enable_batch_workflow": archer_config.get("enable_batch_workflow"), + } + return render(request, "sqlworkflow.html", context) @permission_required("sql.sql_submit", raise_exception=True) @@ -181,6 +182,32 @@ def submit_sql(request): return render(request, "sqlsubmit.html", context) +@permission_required("sql.sql_submitbatch", raise_exception=True) +def submit_sql_batch(request): + """提交SQL的页面""" + user = request.user + # 获取组信息 + group_list = user_groups(user) + + # 获取所有有效用户,通知对象 + active_user = Users.objects.filter(is_active=1) + + # 获取系统配置 + archer_config = SysConfig() + + # 主动创建标签 + InstanceTag.objects.get_or_create( + tag_code="can_write", defaults={"tag_name": "支持上线", "active": True} + ) + + context = { + "group_list": group_list, + "enable_backup_switch": archer_config.get("enable_backup_switch"), + "engines": engine_map, + } + return render(request, "sqlsubmitbatch.html", context) + + def detail(request, workflow_id): """展示SQL工单详细页面""" workflow_detail = get_object_or_404(SqlWorkflow, pk=workflow_id)