diff --git a/dbm-ui/backend/components/bkchat/__init__.py b/dbm-ui/backend/components/bkchat/__init__.py new file mode 100644 index 0000000000..aa5085c628 --- /dev/null +++ b/dbm-ui/backend/components/bkchat/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at https://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" diff --git a/dbm-ui/backend/components/bkchat/client.py b/dbm-ui/backend/components/bkchat/client.py new file mode 100644 index 0000000000..b09a66015a --- /dev/null +++ b/dbm-ui/backend/components/bkchat/client.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at https://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + +from django.utils.translation import ugettext_lazy as _ + +from ..base import BaseApi +from ..domains import BKCHAT_APIGW_DOMAIN + + +class _BkChatApi(BaseApi): + MODULE = _("蓝鲸信息流") + BASE = BKCHAT_APIGW_DOMAIN + + def __init__(self): + self.send_msg = self.generate_data_api( + method="POST", + url="dbm_ticket_send/", + description=_("dbm消息发送"), + ) + + +BkChatApi = _BkChatApi() diff --git a/dbm-ui/backend/components/cmsi/client.py b/dbm-ui/backend/components/cmsi/client.py index a81e5077c6..056365edd2 100644 --- a/dbm-ui/backend/components/cmsi/client.py +++ b/dbm-ui/backend/components/cmsi/client.py @@ -11,8 +11,6 @@ from django.utils.translation import ugettext_lazy as _ -from blue_krill.data_types.enum import EnumField, StructuredEnum - from ..base import BaseApi from ..domains import CMSI_APIGW_DOMAIN @@ -21,16 +19,6 @@ class _CmsiApi(BaseApi): MODULE = _("消息管理") BASE = CMSI_APIGW_DOMAIN - class MsgType(str, StructuredEnum): - SMS = EnumField("sms", _("短信")) - WEIXIN = EnumField("weixin", _("微信")) - MAIL = EnumField("mail", _("邮件")) - VOICE = EnumField("voice", _("语音")) - RTX = EnumField("rtx", _("企业微信")) - WECOM_ROBOT = EnumField("wecom_robot", _("企业微信机器人")) - # 未知发送类型,配置此type一般用于跳过消息发送 - UNKNOWN = EnumField("unknown", _("未知")) - def __init__(self): self.send_msg = self.generate_data_api( method="POST", diff --git a/dbm-ui/backend/components/cmsi/handler.py b/dbm-ui/backend/components/cmsi/handler.py deleted file mode 100644 index a866c26386..0000000000 --- a/dbm-ui/backend/components/cmsi/handler.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -""" -TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. -Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. -Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. -You may obtain a copy of the License at https://opensource.org/licenses/MIT -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on -an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. -""" -import copy -import logging -from typing import Any, Dict - -from backend.components import CmsiApi -from backend.configuration.constants import SystemSettingsEnum -from backend.configuration.models import SystemSettings -from backend.exceptions import ApiError - -logger = logging.getLogger("root") - - -def get_dict_value(msg_info, key): - """ - 获取字典中某个key的值,如果不存在,则返回默认值 - """ - value = msg_info.pop(key, []) - return {key: value} if value else {} - - -class CmsiHandler: - """Cmsi发送信息常用处理函数""" - - @classmethod - def send_msg(cls, msg: Dict[str, Any]): - """发送信息,msg的定义和cmsi的send_msg所需内容一致,不过type可以多选,即一次性发送多个类型的信息""" - support_msg_types = [s["type"] for s in CmsiApi.get_msg_type()] - send_msg_types = msg.pop("msg_type", None) or SystemSettings.get_setting_value( - key=SystemSettingsEnum.SYSTEM_MSG_TYPE.value, default=["weixin", "mail"] - ) - for msg_type in send_msg_types: - msg_info = {"msg_type": msg_type, **copy.deepcopy(msg)} - # 如果是不支持的类型,则跳过 - if msg_type not in support_msg_types: - continue - # 如果是机器人,则将发送内容放在wecom_robot下 - if msg_type == CmsiApi.MsgType.WECOM_ROBOT: - receiver = ( - {"receiver": msg_info["receiver__username"].split(",")} if msg_info["receiver__username"] else {} - ) - msg_info["wecom_robot"] = { - "type": "text", - "text": { - "content": msg_info["content"], - **get_dict_value(msg_info, "mentioned_list"), - **get_dict_value(msg_info, "mentioned_mobile_list"), - }, - **receiver, - **get_dict_value(msg_info, "group_receiver"), - **get_dict_value(msg_info, "visible_to_user"), - } - # 推送消息 - try: - CmsiApi.send_msg(msg_info) - except ApiError as err: - logger.error(f"send message error, msg: {msg_info}, err:{err}") diff --git a/dbm-ui/backend/components/domains.py b/dbm-ui/backend/components/domains.py index 7e6b352b37..b3cfd70bb6 100644 --- a/dbm-ui/backend/components/domains.py +++ b/dbm-ui/backend/components/domains.py @@ -23,6 +23,7 @@ ESB_APIGW_DOMAIN = env.ESB_APIGW_DOMAIN or ESB_DOMAIN_TPL.format("esb") USER_MANAGE_APIGW_DOMAIN = env.USER_MANAGE_APIGW_DOMAIN or ESB_DOMAIN_TPL.format("usermanage") CMSI_APIGW_DOMAIN = env.CMSI_APIGW_DOMAIN or ESB_DOMAIN_TPL.format("cmsi") +BKCHAT_APIGW_DOMAIN = env.BKCHAT_APIGW_DOMAIN ITSM_APIGW_DOMAIN = env.ITSM_APIGW_DOMAIN or ESB_DOMAIN_TPL.format("itsm") BKLOG_APIGW_DOMAIN = env.BKLOG_APIGW_DOMAIN or ESB_DOMAIN_TPL.format("bk_log") BKNODEMAN_APIGW_DOMAIN = env.BKNODEMAN_APIGW_DOMAIN or ESB_DOMAIN_TPL.format("nodeman") diff --git a/dbm-ui/backend/configuration/constants.py b/dbm-ui/backend/configuration/constants.py index 42c2b71e80..220b0ef251 100644 --- a/dbm-ui/backend/configuration/constants.py +++ b/dbm-ui/backend/configuration/constants.py @@ -126,11 +126,12 @@ class BizSettingsEnum(str, StructuredEnum): OPEN_AREA_VARS = EnumField("OPEN_AREA_VARS", _("开区模板的渲染变量")) INDEPENDENT_HOSTING_DB_TYPES = EnumField("INDEPENDENT_HOSTING_DB_TYPES", _("独立托管机器的数据库类型")) - # TODO: 后续待删除 + # TODO: SKIP_GRAMMAR_CHECK 后续待删除 SKIP_GRAMMAR_CHECK = EnumField("SKIP_GRAMMAR_CHECK", _("是否跳过语义检查")) SQL_IMPORT_FORCE_ITSM = EnumField("SQL_IMPORT_FORCE_ITSM", _("是否变更SQL强制需要审批流")) BIZ_ASSISTANCE_VARS = EnumField("BIZ_ASSISTANCE_VARS", _("业务协助人员变量")) BIZ_ASSISTANCE_SWITCH = EnumField("BIZ_ASSISTANCE_SWITCH", _("业务协助开关")) + NOTIFY_CONFIG = EnumField("NOTIFY_CONFIG", _("业务通知渠道配置")) DEFAULT_DB_ADMINISTRATORS = ["admin"] diff --git a/dbm-ui/backend/configuration/views/system.py b/dbm-ui/backend/configuration/views/system.py index 308e085047..32aecc3210 100644 --- a/dbm-ui/backend/configuration/views/system.py +++ b/dbm-ui/backend/configuration/views/system.py @@ -32,7 +32,7 @@ from backend.flow.utils.cc_manage import CcManage from backend.iam_app.dataclass.actions import ActionEnum from backend.iam_app.handlers.drf_perm.base import DBManagePermission, RejectPermission, ResourceActionPermission -from backend.iam_app.handlers.drf_perm.dbconfig import BizAssistancePermission +from backend.iam_app.handlers.drf_perm.dbconfig import BizBatchSettingsPermission, BizSettingsPermission tags = [_("系统设置")] @@ -134,7 +134,8 @@ class BizSettingsViewSet(viewsets.AuditedModelViewSet): queryset = BizSettings.objects.all() action_permission_map = { - ("batch_update_settings",): [BizAssistancePermission()], + ("batch_update_settings",): [BizBatchSettingsPermission()], + ("update_settings",): [BizSettingsPermission()], } default_permission_class = [DBManagePermission()] diff --git a/dbm-ui/backend/core/notify/__init__.py b/dbm-ui/backend/core/notify/__init__.py new file mode 100644 index 0000000000..2c87a2b7a4 --- /dev/null +++ b/dbm-ui/backend/core/notify/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at https://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + +from .handlers import NotifyAdapter, send_msg diff --git a/dbm-ui/backend/core/notify/constants.py b/dbm-ui/backend/core/notify/constants.py new file mode 100644 index 0000000000..320205dcf2 --- /dev/null +++ b/dbm-ui/backend/core/notify/constants.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at https://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +from django.utils.translation import ugettext as _ + +from backend.ticket.constants import TicketStatus +from blue_krill.data_types.enum import EnumField, StructuredEnum + + +class MsgType(str, StructuredEnum): + SMS = EnumField("sms", _("短信")) + WEIXIN = EnumField("weixin", _("微信")) + MAIL = EnumField("mail", _("邮件")) + VOICE = EnumField("voice", _("语音")) + RTX = EnumField("rtx", _("企业微信")) + WECOM_ROBOT = EnumField("wecom_robot", _("企业微信机器人")) + # 未知发送类型,配置此type一般用于跳过消息发送 + UNKNOWN = EnumField("unknown", _("未知")) + + +# 默认通知:微信和邮件 +DEFAULT_BIZ_NOTIFY_CONFIG = { + status: {MsgType.RTX.value: True, MsgType.MAIL.value: True} for status in TicketStatus.get_values() +} diff --git a/dbm-ui/backend/core/notify/exceptions.py b/dbm-ui/backend/core/notify/exceptions.py new file mode 100644 index 0000000000..166eadf9a3 --- /dev/null +++ b/dbm-ui/backend/core/notify/exceptions.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at https://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + + +from django.utils.translation import ugettext_lazy as _ + +from ..exceptions import CoreBaseException + + +class NotifyBaseException(CoreBaseException): + MESSAGE = _("通知失败") diff --git a/dbm-ui/backend/core/notify/handlers.py b/dbm-ui/backend/core/notify/handlers.py new file mode 100644 index 0000000000..8729d56fd7 --- /dev/null +++ b/dbm-ui/backend/core/notify/handlers.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at https://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +import logging +import textwrap + +from celery import shared_task +from django.utils.translation import ugettext as _ +from jinja2 import Environment + +from backend import env +from backend.components import CmsiApi +from backend.components.bkchat.client import BkChatApi +from backend.configuration.constants import BizSettingsEnum +from backend.configuration.models import BizSettings +from backend.core.notify.constants import DEFAULT_BIZ_NOTIFY_CONFIG, MsgType +from backend.core.notify.exceptions import NotifyBaseException +from backend.core.notify.template import FAILED_TEMPLATE, FINISHED_TEMPLATE, TERMINATE_TEMPLATE, TODO_TEMPLATE +from backend.db_meta.models import AppCache +from backend.ticket.builders import BuilderFactory +from backend.ticket.constants import TicketStatus, TicketType +from backend.ticket.models import Flow, Ticket +from backend.ticket.todos import ActionType +from backend.utils.cache import func_cache_decorator + +logger = logging.getLogger("root") + + +class BaseNotifyHandler: + """ + 通知基类 + """ + + def __init__(self, title: str, content: str, receiver: list): + """ + @param title: 通知标题 + @param content: 通知内容 + @param receiver: 接收者列表 + """ + self.title = title + self.content = content + self.receivers = receiver + + def send_msg(self, msg_type, context): + """ + 消息发送基础函数,由子类实现 + @param msg_type: 通知类型 + @param context: 通知上下文 + """ + raise NotImplementedError + + @classmethod + def get_msg_type(cls): + """支持消息发送类型,由子类实现""" + raise NotImplementedError + + +class BkChatHandler(BaseNotifyHandler): + """ + bkchat 处理类 + 目前仅支持:企微,机器人两种模式 + """ + + @classmethod + def get_msg_type(cls): + return [MsgType.WECOM_ROBOT.value, MsgType.RTX.value] + + @staticmethod + def get_actions(msg_type, ticket): + """获取bkchat操作按钮""" + if ticket.status not in [TicketStatus.APPROVE, TicketStatus.TODO]: + return [] + # 增加回调按钮,执行和终止 + agree_action = { + "name": _("同意") if ticket.status == TicketStatus.APPROVE else _("确认执行"), + "color": "green", + "callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/batch_process_ticket/", + "callback_data": {"action": ActionType.APPROVE.value, "ticket_ids": [ticket.id]}, + } + refuse_action = { + "name": _("拒绝") if ticket.status == TicketStatus.APPROVE else _("终止单据"), + "color": "red", + "callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/batch_process_ticket/", + "callback_data": { + "action": ActionType.TERMINATE.value, + "ticket_ids": [ticket.id], + "params": {"remark": _("使用「蓝鲸审批助手」终止单据")}, + }, + } + return [agree_action, refuse_action] + + @staticmethod + def get_title_color(phase): + # 红色:已失败、已终止; 绿色:已完成;橙红色:其它 + if phase in [TicketStatus.FAILED, TicketStatus.TERMINATED]: + return "red" + elif phase in [TicketStatus.SUCCEEDED]: + return "green" + else: + return "warning" + + def render_title_content(self, msg_type, title, content, ticket, phase, operators): + """重新渲染标题和内容样式,bkchat有特定要求""" + # title 要加上样式 + title = _("「DBM」:您的{ticket_type}单据 「{ticket_id}」{status}").format( + ticket_type=TicketType.get_choice_label(ticket.ticket_type), + ticket_id=ticket.id, + status=TicketStatus.get_choice_label(phase), + color=self.get_title_color(phase), + ) + + # content要去掉点击详情,即最后一行,并且加上@处理人 + content = "\n".join(content.split("\n")[:-1]) + if msg_type == MsgType.WECOM_ROBOT: + at_list = "".join([f"<@{staff}>" for staff in operators]) + content += "\n" + at_list + + return title, content + + def send_msg(self, msg_type, context): + ticket, phase = context["ticket"], context["phase"] + operators = ticket.get_current_operators() + title, content = self.render_title_content(msg_type, self.title, self.content, ticket, phase, operators) + msg_info = { + "title": title, + # 处理人 + "approvers": operators, + # 微信消息时 receiver生效,不发群消息,群消息时,receive_group,不发送个人消息 + "receiver": self.receivers if msg_type == MsgType.RTX else [], + "receive_group": self.receivers if msg_type == MsgType.WECOM_ROBOT else [], + "summary": content, + # 操作和详情按钮 + "actions": self.get_actions(msg_type, ticket), + "click": {"click_url": ticket.url, "name": _("查看详情")}, + } + BkChatApi.send_msg(msg_info, use_admin=True) + + +class CmsiHandler(BaseNotifyHandler): + """ + cmsi 处理类,dbm通知发送的标准类 + 支持:企微,机器人,邮件,语音,微信 + """ + + @classmethod + @func_cache_decorator(cache_time=60 * 60 * 24) + def get_msg_type(cls): + return [s["type"] for s in CmsiApi.get_msg_type()] + + def _cmsi_send_msg(self, msg_type: str, **kwargs): + """ + @param msg_type: 发送类型 + @param kwargs: 额外参数 + """ + msg_info = { + "msg_type": msg_type, + "receiver__username": ",".join(self.receivers), + "title": self.title, + "content": self.content, + } + msg_info.update(kwargs) + CmsiApi.send_msg(msg_info) + + def send_mail(self, sender: str = None, cc: list = None): + """ + @param sender: 发送人,可选 + @param cc: 抄送人列表,可选 + """ + kwargs = {} + if sender: + kwargs.update(sender=sender) + if cc: + kwargs.update(cc__username=",".join(cc)) + self._cmsi_send_msg(MsgType.MAIL, **kwargs) + + def send_voice(self): + """发送语音消息""" + self._cmsi_send_msg(MsgType.VOICE.value) + + def send_weixin(self): + """发送微信消息""" + self._cmsi_send_msg(MsgType.WEIXIN.value) + + def send_rtx(self): + """发送企微消息""" + self._cmsi_send_msg(MsgType.RTX.value) + + def send_sms(self): + """发送企微消息""" + self._cmsi_send_msg(MsgType.SMS.value) + + def send_wecom_robot(self): + """企微机器人发送消息""" + wecom_robot = { + "type": "text", + "text": {"content": self.content}, + "group_receiver": self.receivers, + } + self._cmsi_send_msg(MsgType.WECOM_ROBOT.value, sender=env.WECOM_ROBOT, wecom_robot=wecom_robot) + + def send_msg(self, msg_type, context): + getattr(self, f"send_{msg_type}")() + + +class NotifyAdapter: + """DBM通知适配器""" + + register_notify_class = [CmsiHandler, BkChatHandler] + + def __init__(self, ticket_id: int, flow_id: int = None): + """ + @param ticket_id: 单据ID + @param flow_id: 流程ID + """ + # 初始化单据,流程信息 + try: + self.ticket = Ticket.objects.get(id=ticket_id) + self.flow = Flow.objects.get(id=flow_id) if flow_id else self.ticket.current_flow() + except (Ticket.DoesNotExist, Flow.DoesNotExist): + raise NotifyBaseException(_("无法初始化通知适配器,无法找到此单据{}或流程{}").format(ticket_id, flow_id)) + + # 当前阶段,对于运行中发通知的单据,实际上是【待继续】,这里做一次转换 + self.phase = TicketStatus.INNER_TODO if self.ticket.status == TicketStatus.RUNNING else self.ticket.status + + # 初始化通知人,集群额外信息 + self.bk_biz_id = self.ticket.bk_biz_id + self.receivers = self.get_receivers() + self.clusters = [cluster["immute_domain"] for cluster in self.ticket.details.pop("clusters", {}).values()] + + @classmethod + def get_support_msg_types(cls): + # 获取当前环境下支持的通知类型 + # 所有的拓展方式都需要接入CMSI,所以直接返回CMSI支持方式即可 + return CmsiApi.get_msg_type() + + @classmethod + def get_notify_class(cls, msg_type: str): + # 根据通知类型获取通知类 + if msg_type in [MsgType.WECOM_ROBOT, MsgType.RTX] and env.BKCHAT_APIGW_DOMAIN: + return BkChatHandler + else: + return CmsiHandler + + def get_receivers(self): + # 获取业务dba,业务协助人和提单人 三种角色 + biz_helpers = BizSettings.get_assistance(self.bk_biz_id) + creator = [self.ticket.creator] + # 待审批:审批人 + # 待执行、待补货、待确认、已失败、已完成、已终止: 提单人、协助人 + # 暂不通知DBA + if self.phase in [TicketStatus.PENDING]: + return creator + elif self.phase in [TicketStatus.APPROVE]: + itsm_builder = BuilderFactory.get_builder_cls(self.ticket.ticket_type).itsm_flow_builder(self.ticket) + return itsm_builder.get_approvers().split(",") + else: + return creator + biz_helpers + + def render_msg_template(self, msg_type: str): + # 获取标题,在群机器人通知则加上@人 + title = _("「DBM」:您的{ticket_type}单据「{ticket_id}」{status}").format( + ticket_type=TicketType.get_choice_label(self.ticket.ticket_type), + ticket_id=self.ticket.id, + status=TicketStatus.get_choice_label(self.phase), + ) + + # 渲染通知内容 + jinja_env = Environment() + if self.phase in [TicketStatus.SUCCEEDED]: + template = jinja_env.from_string(FINISHED_TEMPLATE) + elif self.phase in [TicketStatus.FAILED]: + template = jinja_env.from_string(FAILED_TEMPLATE) + elif self.phase == TicketStatus.TERMINATED: + template = jinja_env.from_string(TERMINATE_TEMPLATE) + else: + template = jinja_env.from_string(TODO_TEMPLATE) + + biz_name = AppCache.get_biz_name(self.bk_biz_id) + payload = { + "ticket_type": TicketType.get_choice_label(self.ticket.ticket_type), + "biz_name": f"{biz_name}(#{self.bk_biz_id}, {biz_name})", + "cluster_domains": ",".join(self.clusters), + "remark": self.ticket.remark, + "creator": self.ticket.creator, + "submit_time": self.ticket.create_at, + "update_time": self.ticket.update_at, + "status": TicketStatus.get_choice_label(self.phase), + "operators": ",".join(self.ticket.get_current_operators()), + "detail_address": self.ticket.url, + "terminate_reason": self.ticket.get_terminate_reason(), + } + content = textwrap.dedent(template.render(payload)) + return title, content + + def send_msg(self): + # 获取单据通知设置,以业务配置为准 TODO: 后续考虑单据配置吗? + biz_notify_config = BizSettings.get_setting_value( + self.bk_biz_id, key=BizSettingsEnum.NOTIFY_CONFIG, default=DEFAULT_BIZ_NOTIFY_CONFIG + ) + send_msg_config = biz_notify_config[self.phase] + send_msg_types = [msg_type for msg_type in send_msg_config if send_msg_config.get(msg_type)] + + for msg_type in send_msg_types: + notify_class = self.get_notify_class(msg_type) + + if msg_type not in notify_class.get_msg_type(): + logger.warning(_("通知类{}不支持该类型{}的消息发送").format(notify_class, msg_type)) + continue + + # 获取通知内容,发送通知 + title, content = self.render_msg_template(msg_type) + + # 如果是群机器人通知,则接受者为群ID + if msg_type == MsgType.WECOM_ROBOT: + self.receivers = send_msg_config.get(MsgType.WECOM_ROBOT.value, []) + + context = {"ticket": self.ticket, "phase": self.phase} + notify_class(title, content, self.receivers).send_msg(msg_type, context=context) + + +@shared_task +def send_msg(ticket_id: int, flow_id: int = None, raise_exception: bool = False): + # 可异步发送消息,非阻塞路径默认不抛出异常 + try: + NotifyAdapter(ticket_id, flow_id).send_msg() + except Exception as e: + err_msg = _("消息发送失败,错误信息:{}").format(e) + if not raise_exception: + logger.error(err_msg) + else: + raise NotifyBaseException(err_msg) diff --git a/dbm-ui/backend/core/notify/template.py b/dbm-ui/backend/core/notify/template.py new file mode 100644 index 0000000000..e9aee22e23 --- /dev/null +++ b/dbm-ui/backend/core/notify/template.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at https://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + +from django.utils.translation import gettext as _ + +# 待审批,待确认,待补货通知模板 +TODO_TEMPLATE = _( + """\ + 申请人: {{creator}} + 申请时间: {{submit_time}} + 所属业务: {{biz_name}} + 域名: {{cluster_domains}} + 备注: {{remark}} + 处理人: {{operators}} + 查看详情: {{detail_address}}\ + """ +) + +# 成功通知模板 +FINISHED_TEMPLATE = _( + """\ + 申请人: {{creator}} + 申请时间: {{submit_time}} + 所属业务: {{biz_name}} + 域名: {{cluster_domains}} + 完成时间: {{update_time}} + 查看详情: {{detail_address}}\ + """ +) + +# 失败通知模板 +FAILED_TEMPLATE = _( + """\ + 申请人: {{creator}} + 申请时间: {{submit_time}} + 所属业务: {{biz_name}} + 域名: {{cluster_domains}} + 失败时间: {{update_time}} + 处理人: {{operators}} + 查看详情: {{detail_address}}\ + """ +) + +# 终止通知模板 +TERMINATE_TEMPLATE = _( + """\ + 申请人: {{creator}} + 申请时间: {{submit_time}} + 所属业务: {{biz_name}} + 域名: {{cluster_domains}} + 终止时间: {{update_time}} + 终止原因: {{terminate_reason}} + 查看详情: {{detail_address}}\ + """ +) diff --git a/dbm-ui/backend/db_monitor/views/notice_group.py b/dbm-ui/backend/db_monitor/views/notice_group.py index 512f91db24..34a09b578c 100644 --- a/dbm-ui/backend/db_monitor/views/notice_group.py +++ b/dbm-ui/backend/db_monitor/views/notice_group.py @@ -19,8 +19,8 @@ from backend.bk_web import viewsets from backend.bk_web.pagination import AuditedLimitOffsetPagination from backend.bk_web.swagger import common_swagger_auto_schema -from backend.components import CmsiApi from backend.configuration.constants import PLAT_BIZ_ID +from backend.core.notify import NotifyAdapter from backend.db_monitor import serializers from backend.db_monitor.models import MonitorPolicy, NoticeGroup from backend.db_monitor.serializers import NoticeGroupSerializer @@ -132,7 +132,7 @@ def list(self, request, *args, **kwargs): @common_swagger_auto_schema(operation_summary=_("查询通知类型"), tags=[SWAGGER_TAG]) @action(methods=["GET"], detail=False) def get_msg_type(self, request, *args, **kwargs): - return Response(CmsiApi.get_msg_type()) + return Response(NotifyAdapter.get_support_msg_types()) @common_swagger_auto_schema(operation_summary=_("查询告警组名称"), tags=[SWAGGER_TAG]) @action(methods=["GET"], detail=False) diff --git a/dbm-ui/backend/db_periodic_task/local_tasks/mysql_check_partition.py b/dbm-ui/backend/db_periodic_task/local_tasks/mysql_check_partition.py index 292c0c68e0..914dd9d467 100644 --- a/dbm-ui/backend/db_periodic_task/local_tasks/mysql_check_partition.py +++ b/dbm-ui/backend/db_periodic_task/local_tasks/mysql_check_partition.py @@ -15,11 +15,10 @@ from django.utils.translation import ugettext as _ from backend import env -from backend.components import CmsiApi -from backend.components.cmsi.handler import CmsiHandler from backend.components.mysql_partition.client import DBPartitionApi from backend.configuration.constants import DBType from backend.configuration.models import DBAdministrator +from backend.core.notify.handlers import CmsiHandler from backend.db_periodic_task.local_tasks import register_periodic_task logger = logging.getLogger("root") @@ -36,7 +35,6 @@ def mysql_check_partition(): except Exception as e: # pylint: disable=broad-except logger.error(_("分区服务check_log接口异常: {}").format(e)) return - msg = {} content = "" content = _format_msg(logs["mysql_not_run"], DBType.MySQL, _("未执行"), content) content = _format_msg(logs["mysql_fail"], DBType.MySQL, _("失败"), content) @@ -51,18 +49,11 @@ def mysql_check_partition(): return chat_ids = env.MYSQL_CHATID.split(",") if content: - msg.update( - { - "receiver__username": "admin", - "group_receiver": chat_ids, - "content": _("【DBM】分区表异常情况 {} \n业务名称 bk_biz_id DB类型 失败/未执行 数量 DBA 策略ID\n{}").format( - datetime.date.today(), content - ), - "msg_type": [CmsiApi.MsgType.WECOM_ROBOT.value], - "sender": env.WECOM_ROBOT, - } + title = _("【DBM】分区表异常 ") + content = _("【DBM】分区表异常情况 {} \n业务名称 bk_biz_id DB类型 失败/未执行 数量 DBA 策略ID\n{}").format( + datetime.date.today(), content ) - CmsiHandler.send_msg(msg) + CmsiHandler(title, content, chat_ids).send_wecom_robot() return diff --git a/dbm-ui/backend/db_services/mongodb/autofix/mongodb_autofix_ticket.py b/dbm-ui/backend/db_services/mongodb/autofix/mongodb_autofix_ticket.py index e959b3a734..5361cb1943 100644 --- a/dbm-ui/backend/db_services/mongodb/autofix/mongodb_autofix_ticket.py +++ b/dbm-ui/backend/db_services/mongodb/autofix/mongodb_autofix_ticket.py @@ -17,12 +17,12 @@ from backend.configuration.constants import DBType from backend.configuration.models.dba import DBAdministrator +from backend.core import notify from backend.db_services.dbbase.constants import IpSource from backend.db_services.redis.autofix.enums import AutofixStatus from backend.db_services.redis.autofix.models import RedisAutofixCore from backend.ticket.constants import TicketType from backend.ticket.models import Ticket -from backend.ticket.tasks.ticket_tasks import send_msg_for_flow from backend.utils.time import datetime2str logger = logging.getLogger("root") @@ -91,16 +91,7 @@ def mongo_create_ticket(cluster: RedisAutofixCore, cluster_ids: list, mongos_lis ip_list = [] for host in mongos_list + mongod_list: ip_list.append(host["ip"]) - send_msg_for_flow.apply_async( - kwargs={ - "flow_id": ticket.id, - "flow_msg_type": _("通知"), - "flow_status": _("开始执行"), - "processor": ",".join(mongodb_dba), - "receiver": ",".join(mongodb_dba), - "detail_address": _("自愈ip:[{}]".format(",".join(ip_list))), - } - ) + notify.send_msg.apply_async(args=(ticket.id,)) # 回写tb_tendis_autofix_core表 cluster.ticket_id = ticket.id diff --git a/dbm-ui/backend/db_services/plugin/ticket/serializers.py b/dbm-ui/backend/db_services/plugin/ticket/serializers.py new file mode 100644 index 0000000000..92b0a4c725 --- /dev/null +++ b/dbm-ui/backend/db_services/plugin/ticket/serializers.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at https://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from backend.ticket.serializers import BatchTicketOperateSerializer + + +class OpenAPIBatchTicketOperateSerializer(BatchTicketOperateSerializer): + username = serializers.CharField(help_text=_("操作者")) diff --git a/dbm-ui/backend/db_services/plugin/ticket/views.py b/dbm-ui/backend/db_services/plugin/ticket/views.py new file mode 100644 index 0000000000..c0f9d74e1d --- /dev/null +++ b/dbm-ui/backend/db_services/plugin/ticket/views.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at https://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +import logging + +from django.utils.translation import ugettext as _ +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.response import Response + +from backend.db_services.plugin.constants import SWAGGER_TAG +from backend.db_services.plugin.ticket.serializers import OpenAPIBatchTicketOperateSerializer +from backend.db_services.plugin.view import BaseOpenAPIViewSet +from backend.ticket.handler import TicketHandler +from backend.ticket.serializers import TodoSerializer + +logger = logging.getLogger("root") + + +class TicketViewSet(BaseOpenAPIViewSet): + @swagger_auto_schema( + operation_summary=_("批量单据待办处理"), + request_body=OpenAPIBatchTicketOperateSerializer(), + responses={status.HTTP_200_OK: TodoSerializer(many=True)}, + tags=[SWAGGER_TAG], + ) + @action(methods=["POST"], detail=False, serializer_class=OpenAPIBatchTicketOperateSerializer) + def batch_process_ticket(self, request, *args, **kwargs): + params = self.params_validate(self.get_serializer_class()) + return Response(TicketHandler.batch_process_ticket(**params)) diff --git a/dbm-ui/backend/db_services/plugin/urls.py b/dbm-ui/backend/db_services/plugin/urls.py index f6ea43afc9..c412c60464 100644 --- a/dbm-ui/backend/db_services/plugin/urls.py +++ b/dbm-ui/backend/db_services/plugin/urls.py @@ -12,9 +12,11 @@ from .bf.views import BFPluginViewSet from .mysql.authorize.views import AuthorizePluginViewSet +from .ticket.views import TicketViewSet routers = DefaultRouter(trailing_slash=True) routers.register("mysql/authorize", AuthorizePluginViewSet, basename="authorize") routers.register("bf", BFPluginViewSet, basename="bfplugin") +routers.register("ticket", TicketViewSet, basename="ticket") urlpatterns = routers.urls diff --git a/dbm-ui/backend/db_services/plugin/view.py b/dbm-ui/backend/db_services/plugin/view.py new file mode 100644 index 0000000000..b89cd88924 --- /dev/null +++ b/dbm-ui/backend/db_services/plugin/view.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at https://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +import logging + +from backend.bk_web import viewsets +from backend.iam_app.handlers.drf_perm.base import RejectPermission + +logger = logging.getLogger("root") + + +class BaseOpenAPIViewSet(viewsets.SystemViewSet): + """openapi 视图基类""" + + def get_default_permission_class(self) -> list: + # 默认访问openapi的客户端都通过了网关jwt认证 + permission_class = [] if self.request.is_bk_jwt() else [RejectPermission()] + return permission_class diff --git a/dbm-ui/backend/env/__init__.py b/dbm-ui/backend/env/__init__.py index fd29c7e1ae..a041d67698 100644 --- a/dbm-ui/backend/env/__init__.py +++ b/dbm-ui/backend/env/__init__.py @@ -93,6 +93,8 @@ BK_SAAS_CALLBACK_URL = get_type_env(key="BK_SAAS_CALLBACK_URL", _type=str, default="") or BK_SAAS_HOST.replace( "https", "http" ) +# DBM网关地址 +BK_DBM_APIGATEWAY = get_type_env(key="BK_DBM_APIGATEWAY", _type=str, default="http://bk-dbm") # 其他系统访问地址 BK_DOMAIN = get_type_env(key="BK_DOMAIN", _type=str, default=".example.com") diff --git a/dbm-ui/backend/env/apigw_domains.py b/dbm-ui/backend/env/apigw_domains.py index 5fab008ced..f5a7b8cdb1 100644 --- a/dbm-ui/backend/env/apigw_domains.py +++ b/dbm-ui/backend/env/apigw_domains.py @@ -20,6 +20,7 @@ ESB_APIGW_DOMAIN = get_type_env(key="ESB_APIGW_DOMAIN", _type=str) USER_MANAGE_APIGW_DOMAIN = get_type_env(key="USER_MANAGE_APIGW_DOMAIN", _type=str) CMSI_APIGW_DOMAIN = get_type_env(key="CMSI_APIGW_DOMAIN", _type=str) +BKCHAT_APIGW_DOMAIN = get_type_env(key="BKCHAT_APIGW_DOMAIN", _type=str, default="") ITSM_APIGW_DOMAIN = get_type_env(key="ITSM_APIGW_DOMAIN", _type=str) BKLOG_APIGW_DOMAIN = get_type_env(key="BKLOG_APIGW_DOMAIN", _type=str) BKNODEMAN_APIGW_DOMAIN = get_type_env(key="BKNODEMAN_APIGW_DOMAIN", _type=str) diff --git a/dbm-ui/backend/flow/plugins/components/collections/mysql/fake_semantic_check.py b/dbm-ui/backend/flow/plugins/components/collections/mysql/fake_semantic_check.py index ef083bf1e0..4c938297c0 100644 --- a/dbm-ui/backend/flow/plugins/components/collections/mysql/fake_semantic_check.py +++ b/dbm-ui/backend/flow/plugins/components/collections/mysql/fake_semantic_check.py @@ -25,6 +25,8 @@ def _execute(self, data, parent_data, callback=None) -> bool: kwargs = data.get_one_of_inputs("kwargs") root_id = kwargs.get("root_id") + time.sleep(3600) + # 测试报错 if kwargs.get("is_error"): logging.error("test error....") diff --git a/dbm-ui/backend/flow/signal/handlers.py b/dbm-ui/backend/flow/signal/handlers.py index ddba1b66cf..d294e63cc9 100644 --- a/dbm-ui/backend/flow/signal/handlers.py +++ b/dbm-ui/backend/flow/signal/handlers.py @@ -17,11 +17,10 @@ from backend.flow.consts import StateType from backend.flow.engine.bamboo.engine import BambooEngine from backend.flow.models import FlowNode, FlowTree -from backend.ticket.constants import FlowCallbackType, FlowMsgType, FlowType, TicketFlowStatus +from backend.ticket.constants import FlowCallbackType, FlowType, TicketFlowStatus from backend.ticket.flow_manager.inner import InnerFlow from backend.ticket.flow_manager.manager import TicketFlowManager from backend.ticket.models import Ticket -from backend.ticket.tasks.ticket_tasks import send_msg_for_flow logger = logging.getLogger("flow") @@ -94,15 +93,6 @@ def callback_ticket(ticket_id, root_id): # 在认为inner flow执行结束情况下,执行inner flow的后继动作 if inner_flow_obj.status not in [TicketFlowStatus.PENDING, TicketFlowStatus.RUNNING]: - send_msg_for_flow.apply_async( - kwargs={ - "flow_id": current_flow.id, - "flow_msg_type": FlowMsgType.DONE.value, - "flow_status": TicketFlowStatus.get_choice_label(current_flow.status), - "processor": ticket.creator, - "receiver": ticket.creator, - } - ) inner_flow_obj.callback(callback_type=FlowCallbackType.POST_CALLBACK.value) # 如果flow type的类型为快速任务,则跳过callback diff --git a/dbm-ui/backend/iam_app/dataclass/actions.py b/dbm-ui/backend/iam_app/dataclass/actions.py index a392457a2f..0a5638fef2 100644 --- a/dbm-ui/backend/iam_app/dataclass/actions.py +++ b/dbm-ui/backend/iam_app/dataclass/actions.py @@ -101,6 +101,14 @@ def to_json(self): content["related_resource_types"] = related_resource_types return content + def __hash__(self): + return hash((self.id, self.name)) + + def __eq__(self, other): + if not isinstance(other, ActionMeta): + return False + return other.id == self.id + # fmt: off class ActionEnum: @@ -184,6 +192,17 @@ class ActionEnum: common_labels=[CommonActionLabel.BIZ_MAINTAIN], ) + BIZ_NOTIFY_CONFIG = ActionMeta( + id="biz_notify_config", + name=_("单据通知设置"), + name_en="biz_notify_config", + type="edit", + related_actions=[DB_MANAGE.id], + related_resource_types=[ResourceEnum.BUSINESS], + group=_("业务配置"), + common_labels=[CommonActionLabel.BIZ_MAINTAIN], + ) + RESOURCE_MANAGE = ActionMeta( id="resource_manage", name=_("资源管理访问"), diff --git a/dbm-ui/backend/iam_app/handlers/drf_perm/dbconfig.py b/dbm-ui/backend/iam_app/handlers/drf_perm/dbconfig.py index 850e2160c4..e60a602650 100644 --- a/dbm-ui/backend/iam_app/handlers/drf_perm/dbconfig.py +++ b/dbm-ui/backend/iam_app/handlers/drf_perm/dbconfig.py @@ -57,28 +57,36 @@ def instance_dbtype_getter(request, view): return BizDBConfigPermission.instance_dbtype_getter(request, view) -class BizAssistancePermission(ResourceActionPermission): +class BizSettingsPermission(ResourceActionPermission): """ - 业务单据协作相关鉴权 + 业务配置相关鉴权 """ + config_action_map = { + BizSettingsEnum.BIZ_ASSISTANCE_VARS: ActionEnum.BIZ_ASSISTANCE_VARS_CONFIG, + BizSettingsEnum.NOTIFY_CONFIG: ActionEnum.BIZ_NOTIFY_CONFIG, + BizSettingsEnum.BIZ_ASSISTANCE_SWITCH: ActionEnum.BIZ_ASSISTANCE_VARS_CONFIG, + } + def inst_ids_getter(self, request, view): - data = request.data - valid_keys = {BizSettingsEnum.BIZ_ASSISTANCE_VARS.value, BizSettingsEnum.BIZ_ASSISTANCE_SWITCH.value} - try: - # 检查 data["settings"] 中的任意一个字典的 "key" 是否在 valid_keys 中 - if any(setting["key"] in valid_keys for setting in data.get("settings", [])): - # 如果有至少一个 key 在 valid_keys 中 - self.actions = [getattr(ActionEnum, "BIZ_ASSISTANCE_VARS_CONFIG")] - else: - # 如果所有的 key 都不在 valid_keys 中 - self.actions = [] - - self.resource_meta = ResourceEnum.BUSINESS - except AttributeError: - raise NotImplementedError - - return [data["bk_biz_id"]] + action = self.config_action_map.get(request.data["key"]) + self.actions = [action] if action else [] + self.resource_meta = ResourceEnum.BUSINESS + return [request.data["bk_biz_id"]] def __init__(self): super().__init__(actions=None, resource_meta=None, instance_ids_getter=self.inst_ids_getter) + + +class BizBatchSettingsPermission(BizSettingsPermission): + """ + 业务配置批量更新鉴权 + """ + + def inst_ids_getter(self, request, view): + actions = [ + self.config_action_map[s["key"]] for s in request.data["settings"] if s["key"] in self.config_action_map + ] + self.actions = list(set(actions)) + self.resource_meta = ResourceEnum.BUSINESS + return [request.data["bk_biz_id"]] diff --git a/dbm-ui/backend/ticket/builders/mysql/mysql_fake.py b/dbm-ui/backend/ticket/builders/mysql/mysql_fake.py new file mode 100644 index 0000000000..e33e10e175 --- /dev/null +++ b/dbm-ui/backend/ticket/builders/mysql/mysql_fake.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at https://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" +# 此单据用于各种线上的后台流程测试,可以保留 + +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from backend.flow.engine.controller.mysql import MySQLController +from backend.ticket import builders +from backend.ticket.builders.mysql.base import BaseMySQLTicketFlowBuilder, MySQLBaseOperateDetailSerializer +from backend.ticket.constants import TicketType + + +class MySQLFakeDetailSerializer(MySQLBaseOperateDetailSerializer): + params = serializers.JSONField(help_text=_("测试参数")) + + +class MySQLFakeFlowParamBuilder(builders.FlowParamBuilder): + """MySQL 数据校验执行单据参数""" + + controller = MySQLController.mysql_fake_sql_semantic_check_scene + + +@builders.BuilderFactory.register(TicketType.FAKE_TICKET) +class MySQLDataMigrateFlowBuilder(BaseMySQLTicketFlowBuilder): + serializer = MySQLFakeDetailSerializer + inner_flow_builder = MySQLFakeFlowParamBuilder + + @property + def need_itsm(self): + return False + + @property + def need_manual_confirm(self): + return False diff --git a/dbm-ui/backend/ticket/constants.py b/dbm-ui/backend/ticket/constants.py index 6a4c2d6508..666b2cd451 100644 --- a/dbm-ui/backend/ticket/constants.py +++ b/dbm-ui/backend/ticket/constants.py @@ -88,7 +88,7 @@ class TicketStatus(str, StructuredEnum): TIMER = EnumField("TIMER", _("定时中")) RUNNING = EnumField("RUNNING", _("执行中")) SUCCEEDED = EnumField("SUCCEEDED", _("已完成")) - FAILED = EnumField("FAILED", _("失败")) + FAILED = EnumField("FAILED", _("已失败")) REVOKED = EnumField("REVOKED", _("已撤销")) TERMINATED = EnumField("TERMINATED", _("已终止")) # 仅展示,不参与状态流转,不落地db @@ -226,7 +226,8 @@ def get_approve_mode_by_ticket(cls, ticket_type): MYSQL_MASTER_FAIL_OVER = TicketEnumField("MYSQL_MASTER_FAIL_OVER", _("MySQL 主库故障切换"), _("集群维护")) MYSQL_HA_APPLY = TicketEnumField("MYSQL_HA_APPLY", _("MySQL 高可用部署"), register_iam=False) MYSQL_IMPORT_SQLFILE = TicketEnumField("MYSQL_IMPORT_SQLFILE", _("MySQL 变更SQL执行"), _("SQL 任务")) - MYSQL_FORCE_IMPORT_SQLFILE = TicketEnumField("MYSQL_FORCE_IMPORT_SQLFILE", _("MySQL 强制变更SQL执行"), _("SQL 任务"), register_iam=False) # noqa + MYSQL_FORCE_IMPORT_SQLFILE = TicketEnumField("MYSQL_FORCE_IMPORT_SQLFILE", _("MySQL 强制变更SQL执行"), + _("SQL 任务"), register_iam=False) # noqa MYSQL_SEMANTIC_CHECK = TicketEnumField("MYSQL_SEMANTIC_CHECK", _("MySQL 模拟执行"), register_iam=False) MYSQL_PROXY_ADD = TicketEnumField("MYSQL_PROXY_ADD", _("MySQL 添加Proxy"), _("集群维护")) MYSQL_PROXY_SWITCH = TicketEnumField("MYSQL_PROXY_SWITCH", _("MySQL 替换Proxy"), _("集群维护")) @@ -238,7 +239,8 @@ def get_approve_mode_by_ticket(cls, ticket_type): MYSQL_HA_ENABLE = TicketEnumField("MYSQL_HA_ENABLE", _("MySQL 高可用启用"), register_iam=False) MYSQL_AUTHORIZE_RULES = TicketEnumField("MYSQL_AUTHORIZE_RULES", _("MySQL 集群授权"), _("权限管理")) MYSQL_EXCEL_AUTHORIZE_RULES = TicketEnumField("MYSQL_EXCEL_AUTHORIZE_RULES", _("MySQL EXCEL授权"), _("权限管理")) - MYSQL_CLIENT_CLONE_RULES = TicketEnumField("MYSQL_CLIENT_CLONE_RULES", _("MySQL 客户端权限克隆"), register_iam=False) + MYSQL_CLIENT_CLONE_RULES = TicketEnumField("MYSQL_CLIENT_CLONE_RULES", _("MySQL 客户端权限克隆"), + register_iam=False) MYSQL_INSTANCE_CLONE_RULES = TicketEnumField("MYSQL_INSTANCE_CLONE_RULES", _("MySQL DB实例权限克隆"), _("权限管理")) MYSQL_HA_RENAME_DATABASE = TicketEnumField("MYSQL_HA_RENAME_DATABASE", _("MySQL 高可用DB重命名"), _("集群维护")) MYSQL_HA_TRUNCATE_DATA = TicketEnumField("MYSQL_HA_TRUNCATE_DATA", _("MySQL 高可用清档"), _("数据处理")) @@ -251,7 +253,8 @@ def get_approve_mode_by_ticket(cls, ticket_type): MYSQL_ROLLBACK_CLUSTER = TicketEnumField("MYSQL_ROLLBACK_CLUSTER", _("MySQL 定点构造"), _("回档")) MYSQL_HA_FULL_BACKUP = TicketEnumField("MYSQL_HA_FULL_BACKUP", _("MySQL 全库备份"), _("备份")) MYSQL_SINGLE_TRUNCATE_DATA = TicketEnumField("MYSQL_SINGLE_TRUNCATE_DATA", _("MySQL 单节点清档"), _("数据处理")) - MYSQL_SINGLE_RENAME_DATABASE = TicketEnumField("MYSQL_SINGLE_RENAME_DATABASE", _("MySQL 单节点DB重命名"), _("集群维护")) # noqa + MYSQL_SINGLE_RENAME_DATABASE = TicketEnumField("MYSQL_SINGLE_RENAME_DATABASE", _("MySQL 单节点DB重命名"), + _("集群维护")) # noqa MYSQL_HA_STANDARDIZE = TicketEnumField("MYSQL_HA_STANDARDIZE", _("TendbHA 标准化"), register_iam=False) MYSQL_HA_METADATA_IMPORT = TicketEnumField("MYSQL_HA_METADATA_IMPORT", _("TendbHA 元数据导入"), register_iam=False) MYSQL_OPEN_AREA = TicketEnumField("MYSQL_OPEN_AREA", _("MySQL 开区"), _("克隆开区"), register_iam=False) @@ -259,64 +262,105 @@ def get_approve_mode_by_ticket(cls, ticket_type): MYSQL_DUMP_DATA = TicketEnumField("MYSQL_DUMP_DATA", _("MySQL 数据导出"), _("数据处理")) MYSQL_LOCAL_UPGRADE = TicketEnumField("MYSQL_LOCAL_UPGRADE", _("MySQL 原地升级"), _("版本升级")) MYSQL_MIGRATE_UPGRADE = TicketEnumField("MYSQL_MIGRATE_UPGRADE", _("MySQL 迁移升级"), _("版本升级")) - MYSQL_SLAVE_MIGRATE_UPGRADE = TicketEnumField("MYSQL_SLAVE_MIGRATE_UPGRADE", _("MySQL Slave 迁移升级"), _("版本升级")) + MYSQL_SLAVE_MIGRATE_UPGRADE = TicketEnumField("MYSQL_SLAVE_MIGRATE_UPGRADE", _("MySQL Slave 迁移升级"), + _("版本升级")) MYSQL_RO_SLAVE_UNINSTALL = TicketEnumField("MYSQL_RO_SLAVE_UNINSTALL", _("MySQL非standby slave下架"), _("集群维护")) MYSQL_PROXY_UPGRADE = TicketEnumField("MYSQL_PROXY_UPGRADE", _("MySQL Proxy升级"), _("版本升级")) - MYSQL_HA_TRANSFER_TO_OTHER_BIZ = TicketEnumField("MYSQL_HA_TRANSFER_TO_OTHER_BIZ", _("TendbHA集群迁移至其他业务"), register_iam=False) # noqa - MYSQL_PUSH_PERIPHERAL_CONFIG = TicketEnumField("MYSQL_PUSH_PERIPHERAL_CONFIG", _("推送周边配置"), register_iam=False) - MYSQL_ACCOUNT_RULE_CHANGE = TicketEnumField("MYSQL_ACCOUNT_RULE_CHANGE", _("MySQL 授权规则变更"), register_iam=False) + MYSQL_HA_TRANSFER_TO_OTHER_BIZ = TicketEnumField("MYSQL_HA_TRANSFER_TO_OTHER_BIZ", _("TendbHA集群迁移至其他业务"), + register_iam=False) # noqa + MYSQL_PUSH_PERIPHERAL_CONFIG = TicketEnumField("MYSQL_PUSH_PERIPHERAL_CONFIG", _("推送周边配置"), + register_iam=False) + MYSQL_ACCOUNT_RULE_CHANGE = TicketEnumField("MYSQL_ACCOUNT_RULE_CHANGE", _("MySQL 授权规则变更"), + register_iam=False) # SPIDER(TenDB Cluster) - TENDBCLUSTER_OPEN_AREA = TicketEnumField("TENDBCLUSTER_OPEN_AREA", _("TenDB Cluster 开区"), _("克隆开区"), register_iam=False) # noqa + TENDBCLUSTER_OPEN_AREA = TicketEnumField("TENDBCLUSTER_OPEN_AREA", _("TenDB Cluster 开区"), _("克隆开区"), + register_iam=False) # noqa TENDBCLUSTER_CHECKSUM = TicketEnumField("TENDBCLUSTER_CHECKSUM", _("TenDB Cluster 数据校验修复"), _("数据处理")) - TENDBCLUSTER_DATA_REPAIR = TicketEnumField("TENDBCLUSTER_DATA_REPAIR", _("TenDB Cluster 数据修复"), register_iam=False) # noqa + TENDBCLUSTER_DATA_REPAIR = TicketEnumField("TENDBCLUSTER_DATA_REPAIR", _("TenDB Cluster 数据修复"), + register_iam=False) # noqa TENDBCLUSTER_PARTITION = TicketEnumField("TENDBCLUSTER_PARTITION", _("TenDB Cluster 分区管理"), _("分区管理")) - TENDBCLUSTER_PARTITION_CRON = TicketEnumField("TENDBCLUSTER_PARTITION_CRON", _("TenDB Cluster 分区定时任务"), register_iam=False) # noqa - TENDBCLUSTER_DB_TABLE_BACKUP = TicketEnumField("TENDBCLUSTER_DB_TABLE_BACKUP", _("TenDB Cluster 库表备份"), _("备份")) - TENDBCLUSTER_RENAME_DATABASE = TicketEnumField("TENDBCLUSTER_RENAME_DATABASE", _("TenDB Cluster 数据库重命名"), _("SQL 任务")) # noqa - TENDBCLUSTER_TRUNCATE_DATABASE = TicketEnumField("TENDBCLUSTER_TRUNCATE_DATABASE", _("TenDB Cluster 清档"), _("数据处理")) - TENDBCLUSTER_MASTER_FAIL_OVER = TicketEnumField("TENDBCLUSTER_MASTER_FAIL_OVER", _("TenDB Cluster 主库故障切换"), _("集群维护")) # noqa - TENDBCLUSTER_MASTER_SLAVE_SWITCH = TicketEnumField("TENDBCLUSTER_MASTER_SLAVE_SWITCH", _("TenDB Cluster 主从互切"), _("集群维护")) # noqa - TENDBCLUSTER_IMPORT_SQLFILE = TicketEnumField("TENDBCLUSTER_IMPORT_SQLFILE", _("TenDB Cluster 变更SQL执行"), _("SQL 任务")) # noqa - TENDBCLUSTER_FORCE_IMPORT_SQLFILE = TicketEnumField("TENDBCLUSTER_FORCE_IMPORT_SQLFILE", _("TenDB Cluster 强制变更SQL执行"), register_iam=False) # noqa - TENDBCLUSTER_SEMANTIC_CHECK = TicketEnumField("TENDBCLUSTER_SEMANTIC_CHECK", _("TenDB Cluster 模拟执行"), register_iam=False) # noqa - TENDBCLUSTER_SPIDER_ADD_NODES = TicketEnumField("TENDBCLUSTER_SPIDER_ADD_NODES", _("TenDB Cluster 扩容接入层"), _("集群维护")) # noqa - TENDBCLUSTER_SPIDER_REDUCE_NODES = TicketEnumField("TENDBCLUSTER_SPIDER_REDUCE_NODES", _("TenDB Cluster 缩容接入层"), _("集群维护")) # noqa - TENDBCLUSTER_SPIDER_MNT_APPLY = TicketEnumField("TENDBCLUSTER_SPIDER_MNT_APPLY", _("TenDB Cluster 添加运维节点"), _("运维 Spider 管理")) # noqa - TENDBCLUSTER_SPIDER_MNT_DESTROY = TicketEnumField("TENDBCLUSTER_SPIDER_MNT_DESTROY", _("TenDB Cluster 下架运维节点"), _("运维 Spider 管理")) # noqa - TENDBCLUSTER_SPIDER_SLAVE_APPLY = TicketEnumField("TENDBCLUSTER_SPIDER_SLAVE_APPLY", _("TenDB Cluster 部署只读接入层"), _("访问入口")) # noqa - TENDBCLUSTER_SPIDER_SLAVE_DESTROY = TicketEnumField("TENDBCLUSTER_SPIDER_SLAVE_DESTROY", _("TenDB Cluster 只读接入层下架"), _("访问入口")) # noqa - TENDBCLUSTER_RESTORE_SLAVE = TicketEnumField("TENDBCLUSTER_RESTORE_SLAVE", _("TenDB Cluster Slave重建"), _("集群维护")) # noqa - TENDBCLUSTER_RESTORE_LOCAL_SLAVE = TicketEnumField("TENDBCLUSTER_RESTORE_LOCAL_SLAVE", _("TenDB Cluster Slave原地重建"), _("集群维护")) # noqa - TENDBCLUSTER_MIGRATE_CLUSTER = TicketEnumField("TENDBCLUSTER_MIGRATE_CLUSTER", _("TenDB Cluster 主从迁移"), _("集群维护")) # noqa + TENDBCLUSTER_PARTITION_CRON = TicketEnumField("TENDBCLUSTER_PARTITION_CRON", _("TenDB Cluster 分区定时任务"), + register_iam=False) # noqa + TENDBCLUSTER_DB_TABLE_BACKUP = TicketEnumField("TENDBCLUSTER_DB_TABLE_BACKUP", _("TenDB Cluster 库表备份"), + _("备份")) + TENDBCLUSTER_RENAME_DATABASE = TicketEnumField("TENDBCLUSTER_RENAME_DATABASE", _("TenDB Cluster 数据库重命名"), + _("SQL 任务")) # noqa + TENDBCLUSTER_TRUNCATE_DATABASE = TicketEnumField("TENDBCLUSTER_TRUNCATE_DATABASE", _("TenDB Cluster 清档"), + _("数据处理")) + TENDBCLUSTER_MASTER_FAIL_OVER = TicketEnumField("TENDBCLUSTER_MASTER_FAIL_OVER", _("TenDB Cluster 主库故障切换"), + _("集群维护")) # noqa + TENDBCLUSTER_MASTER_SLAVE_SWITCH = TicketEnumField("TENDBCLUSTER_MASTER_SLAVE_SWITCH", _("TenDB Cluster 主从互切"), + _("集群维护")) # noqa + TENDBCLUSTER_IMPORT_SQLFILE = TicketEnumField("TENDBCLUSTER_IMPORT_SQLFILE", _("TenDB Cluster 变更SQL执行"), + _("SQL 任务")) # noqa + TENDBCLUSTER_FORCE_IMPORT_SQLFILE = TicketEnumField("TENDBCLUSTER_FORCE_IMPORT_SQLFILE", + _("TenDB Cluster 强制变更SQL执行"), register_iam=False) # noqa + TENDBCLUSTER_SEMANTIC_CHECK = TicketEnumField("TENDBCLUSTER_SEMANTIC_CHECK", _("TenDB Cluster 模拟执行"), + register_iam=False) # noqa + TENDBCLUSTER_SPIDER_ADD_NODES = TicketEnumField("TENDBCLUSTER_SPIDER_ADD_NODES", _("TenDB Cluster 扩容接入层"), + _("集群维护")) # noqa + TENDBCLUSTER_SPIDER_REDUCE_NODES = TicketEnumField("TENDBCLUSTER_SPIDER_REDUCE_NODES", + _("TenDB Cluster 缩容接入层"), _("集群维护")) # noqa + TENDBCLUSTER_SPIDER_MNT_APPLY = TicketEnumField("TENDBCLUSTER_SPIDER_MNT_APPLY", _("TenDB Cluster 添加运维节点"), + _("运维 Spider 管理")) # noqa + TENDBCLUSTER_SPIDER_MNT_DESTROY = TicketEnumField("TENDBCLUSTER_SPIDER_MNT_DESTROY", + _("TenDB Cluster 下架运维节点"), _("运维 Spider 管理")) # noqa + TENDBCLUSTER_SPIDER_SLAVE_APPLY = TicketEnumField("TENDBCLUSTER_SPIDER_SLAVE_APPLY", + _("TenDB Cluster 部署只读接入层"), _("访问入口")) # noqa + TENDBCLUSTER_SPIDER_SLAVE_DESTROY = TicketEnumField("TENDBCLUSTER_SPIDER_SLAVE_DESTROY", + _("TenDB Cluster 只读接入层下架"), _("访问入口")) # noqa + TENDBCLUSTER_RESTORE_SLAVE = TicketEnumField("TENDBCLUSTER_RESTORE_SLAVE", _("TenDB Cluster Slave重建"), + _("集群维护")) # noqa + TENDBCLUSTER_RESTORE_LOCAL_SLAVE = TicketEnumField("TENDBCLUSTER_RESTORE_LOCAL_SLAVE", + _("TenDB Cluster Slave原地重建"), _("集群维护")) # noqa + TENDBCLUSTER_MIGRATE_CLUSTER = TicketEnumField("TENDBCLUSTER_MIGRATE_CLUSTER", _("TenDB Cluster 主从迁移"), + _("集群维护")) # noqa TENDBCLUSTER_APPLY = TicketEnumField("TENDBCLUSTER_APPLY", _("TenDB Cluster 集群部署")) TENDBCLUSTER_ENABLE = TicketEnumField("TENDBCLUSTER_ENABLE", _("TenDB Cluster 集群启用"), register_iam=False) TENDBCLUSTER_DISABLE = TicketEnumField("TENDBCLUSTER_DISABLE", _("TenDB Cluster 集群禁用"), register_iam=False) TENDBCLUSTER_DESTROY = TicketEnumField("TENDBCLUSTER_DESTROY", _("TenDB Cluster 集群销毁"), _("集群管理")) - TENDBCLUSTER_TEMPORARY_DESTROY = TicketEnumField("TENDBCLUSTER_TEMPORARY_DESTROY", _("TenDB Cluster 临时集群销毁"), _("集群管理")) # noqa - TENDBCLUSTER_NODE_REBALANCE = TicketEnumField("TENDBCLUSTER_NODE_REBALANCE", _("TenDB Cluster 集群容量变更"), _("集群维护")) # noqa + TENDBCLUSTER_TEMPORARY_DESTROY = TicketEnumField("TENDBCLUSTER_TEMPORARY_DESTROY", _("TenDB Cluster 临时集群销毁"), + _("集群管理")) # noqa + TENDBCLUSTER_NODE_REBALANCE = TicketEnumField("TENDBCLUSTER_NODE_REBALANCE", _("TenDB Cluster 集群容量变更"), + _("集群维护")) # noqa TENDBCLUSTER_FULL_BACKUP = TicketEnumField("TENDBCLUSTER_FULL_BACKUP", _("TenDB Cluster 全库备份"), _("备份")) - TENDBCLUSTER_ROLLBACK_CLUSTER = TicketEnumField("TENDBCLUSTER_ROLLBACK_CLUSTER", _("TenDB Cluster 定点构造"), _("回档")) # noqa + TENDBCLUSTER_ROLLBACK_CLUSTER = TicketEnumField("TENDBCLUSTER_ROLLBACK_CLUSTER", _("TenDB Cluster 定点构造"), + _("回档")) # noqa TENDBCLUSTER_FLASHBACK = TicketEnumField("TENDBCLUSTER_FLASHBACK", _("TenDB Cluster 闪回"), _("回档")) - TENDBCLUSTER_CLIENT_CLONE_RULES = TicketEnumField("TENDBCLUSTER_CLIENT_CLONE_RULES", _("TenDB Cluster 客户端权限克隆"), _("权限管理")) # noqa - TENDBCLUSTER_INSTANCE_CLONE_RULES = TicketEnumField("TENDBCLUSTER_INSTANCE_CLONE_RULES", _("TenDB Cluster DB实例权限克隆"), _("权限管理")) # noqa - TENDBCLUSTER_AUTHORIZE_RULES = TicketEnumField("TENDBCLUSTER_AUTHORIZE_RULES", _("TenDB Cluster 授权"), _("权限管理")) - TENDBCLUSTER_EXCEL_AUTHORIZE_RULES = TicketEnumField("TENDBCLUSTER_EXCEL_AUTHORIZE_RULES", _("TenDB Cluster EXCEL授权"), _("权限管理")) # noqa - TENDBCLUSTER_STANDARDIZE = TicketEnumField("TENDBCLUSTER_STANDARDIZE", _("TenDB Cluster 集群标准化"), register_iam=False) - TENDBCLUSTER_METADATA_IMPORT = TicketEnumField("TENDBCLUSTER_METADATA_IMPORT", _("TenDB Cluster 元数据导入"), register_iam=False) # noqa - TENDBCLUSTER_APPEND_DEPLOY_CTL = TicketEnumField("TENDBCLUSTER_APPEND_DEPLOY_CTL", _("TenDB Cluster 追加部署中控"), register_iam=False) # noqa - TENDBSINGLE_METADATA_IMPORT = TicketEnumField("TENDBSINGLE_METADATA_IMPORT", _("TenDB Single 元数据导入"), register_iam=False) # noqa - TENDBSINGLE_STANDARDIZE = TicketEnumField("TENDBSINGLE_STANDARDIZE", _("TenDB Single 集群标准化"), register_iam=False) # noqa + TENDBCLUSTER_CLIENT_CLONE_RULES = TicketEnumField("TENDBCLUSTER_CLIENT_CLONE_RULES", + _("TenDB Cluster 客户端权限克隆"), _("权限管理")) # noqa + TENDBCLUSTER_INSTANCE_CLONE_RULES = TicketEnumField("TENDBCLUSTER_INSTANCE_CLONE_RULES", + _("TenDB Cluster DB实例权限克隆"), _("权限管理")) # noqa + TENDBCLUSTER_AUTHORIZE_RULES = TicketEnumField("TENDBCLUSTER_AUTHORIZE_RULES", _("TenDB Cluster 授权"), + _("权限管理")) + TENDBCLUSTER_EXCEL_AUTHORIZE_RULES = TicketEnumField("TENDBCLUSTER_EXCEL_AUTHORIZE_RULES", + _("TenDB Cluster EXCEL授权"), _("权限管理")) # noqa + TENDBCLUSTER_STANDARDIZE = TicketEnumField("TENDBCLUSTER_STANDARDIZE", _("TenDB Cluster 集群标准化"), + register_iam=False) + TENDBCLUSTER_METADATA_IMPORT = TicketEnumField("TENDBCLUSTER_METADATA_IMPORT", _("TenDB Cluster 元数据导入"), + register_iam=False) # noqa + TENDBCLUSTER_APPEND_DEPLOY_CTL = TicketEnumField("TENDBCLUSTER_APPEND_DEPLOY_CTL", _("TenDB Cluster 追加部署中控"), + register_iam=False) # noqa + TENDBSINGLE_METADATA_IMPORT = TicketEnumField("TENDBSINGLE_METADATA_IMPORT", _("TenDB Single 元数据导入"), + register_iam=False) # noqa + TENDBSINGLE_STANDARDIZE = TicketEnumField("TENDBSINGLE_STANDARDIZE", _("TenDB Single 集群标准化"), + register_iam=False) # noqa TENDBCLUSTER_DATA_MIGRATE = TicketEnumField("TENDBCLUSTER_DATA_MIGRATE", _("TenDB Cluster DB克隆"), _("数据处理")) TENDBCLUSTER_DUMP_DATA = TicketEnumField("TENDBCLUSTER_DUMP_DATA", _("TenDB Cluster 数据导出"), _("数据处理")) - TENDBCLUSTER_ACCOUNT_RULE_CHANGE = TicketEnumField("TENDBCLUSTER_ACCOUNT_RULE_CHANGE", _("TenDB Cluster 授权规则变更"), register_iam=False) # noqa + TENDBCLUSTER_ACCOUNT_RULE_CHANGE = TicketEnumField("TENDBCLUSTER_ACCOUNT_RULE_CHANGE", + _("TenDB Cluster 授权规则变更"), register_iam=False) # noqa # Tbinlogdumper TBINLOGDUMPER_INSTALL = TicketEnumField("TBINLOGDUMPER_INSTALL", _("TBINLOGDUMPER 上架"), register_iam=False) - TBINLOGDUMPER_REDUCE_NODES = TicketEnumField("TBINLOGDUMPER_REDUCE_NODES", _("TBINLOGDUMPER 下架"), register_iam=False) # noqa - TBINLOGDUMPER_SWITCH_NODES = TicketEnumField("TBINLOGDUMPER_SWITCH_NODES", _("TBINLOGDUMPER 切换"), register_iam=False) # noqa - TBINLOGDUMPER_DISABLE_NODES = TicketEnumField("TBINLOGDUMPER_DISABLE_NODES", _("TBINLOGDUMPER 禁用"), register_iam=False) # noqa - TBINLOGDUMPER_ENABLE_NODES = TicketEnumField("TBINLOGDUMPER_ENABLE_NODES", _("TBINLOGDUMPER 启用"), register_iam=False) # noqa + TBINLOGDUMPER_REDUCE_NODES = TicketEnumField("TBINLOGDUMPER_REDUCE_NODES", _("TBINLOGDUMPER 下架"), + register_iam=False) # noqa + TBINLOGDUMPER_SWITCH_NODES = TicketEnumField("TBINLOGDUMPER_SWITCH_NODES", _("TBINLOGDUMPER 切换"), + register_iam=False) # noqa + TBINLOGDUMPER_DISABLE_NODES = TicketEnumField("TBINLOGDUMPER_DISABLE_NODES", _("TBINLOGDUMPER 禁用"), + register_iam=False) # noqa + TBINLOGDUMPER_ENABLE_NODES = TicketEnumField("TBINLOGDUMPER_ENABLE_NODES", _("TBINLOGDUMPER 启用"), + register_iam=False) # noqa # SQLServer SQLSERVER_SINGLE_APPLY = TicketEnumField("SQLSERVER_SINGLE_APPLY", _("SQLServer 单节点部署"), register_iam=False) @@ -328,9 +372,12 @@ def get_approve_mode_by_ticket(cls, ticket_type): SQLSERVER_DISABLE = TicketEnumField("SQLSERVER_DISABLE", _("SQLServer 集群禁用"), register_iam=False) SQLSERVER_ENABLE = TicketEnumField("SQLSERVER_ENABLE", _("SQLServer 集群启用"), register_iam=False) SQLSERVER_DBRENAME = TicketEnumField("SQLSERVER_DBRENAME", _("SQLServer DB重命名"), _("集群维护")) - SQLSERVER_MASTER_SLAVE_SWITCH = TicketEnumField("SQLSERVER_MASTER_SLAVE_SWITCH", _("SQLServer 主从互切"), _("集群维护")) # noqa - SQLSERVER_MASTER_FAIL_OVER = TicketEnumField("SQLSERVER_MASTER_FAIL_OVER", _("SQLServer 主库故障切换"), _("集群维护")) - SQLSERVER_RESTORE_LOCAL_SLAVE = TicketEnumField("SQLSERVER_RESTORE_LOCAL_SLAVE", _("SQLServer 原地重建"), _("集群维护")) # noqa + SQLSERVER_MASTER_SLAVE_SWITCH = TicketEnumField("SQLSERVER_MASTER_SLAVE_SWITCH", _("SQLServer 主从互切"), + _("集群维护")) # noqa + SQLSERVER_MASTER_FAIL_OVER = TicketEnumField("SQLSERVER_MASTER_FAIL_OVER", _("SQLServer 主库故障切换"), + _("集群维护")) + SQLSERVER_RESTORE_LOCAL_SLAVE = TicketEnumField("SQLSERVER_RESTORE_LOCAL_SLAVE", _("SQLServer 原地重建"), + _("集群维护")) # noqa SQLSERVER_RESTORE_SLAVE = TicketEnumField("SQLSERVER_RESTORE_SLAVE", _("SQLServer 新机重建"), _("集群维护")) SQLSERVER_ADD_SLAVE = TicketEnumField("SQLSERVER_ADD_SLAVE", _("SQLServer 添加从库"), _("集群维护")) SQLSERVER_RESET = TicketEnumField("SQLSERVER_RESET", _("SQLServer 集群重置"), _("集群维护")) @@ -338,9 +385,11 @@ def get_approve_mode_by_ticket(cls, ticket_type): SQLSERVER_INCR_MIGRATE = TicketEnumField("SQLSERVER_INCR_MIGRATE", _("SQLServer 增量迁移"), _("数据处理")) SQLSERVER_ROLLBACK = TicketEnumField("SQLSERVER_ROLLBACK", _("SQLServer 定点构造"), _("数据处理")) SQLSERVER_AUTHORIZE_RULES = TicketEnumField("SQLSERVER_AUTHORIZE_RULES", _("SQLServer 集群授权"), _("权限管理")) - SQLSERVER_EXCEL_AUTHORIZE_RULES = TicketEnumField("SQLSERVER_EXCEL_AUTHORIZE_RULES", _("SQLServer EXCEL授权"), _("权限管理")) # noqa + SQLSERVER_EXCEL_AUTHORIZE_RULES = TicketEnumField("SQLSERVER_EXCEL_AUTHORIZE_RULES", _("SQLServer EXCEL授权"), + _("权限管理")) # noqa SQLSERVER_BUILD_DB_SYNC = TicketEnumField("SQLSERVER_BUILD_DB_SYNC", _("SQLServer DB建立同步"), register_iam=False) - SQLSERVER_MODIFY_STATUS = TicketEnumField("SQLSERVER_MODIFY_STATUS", _("SQLServer 修改故障实例状态"), register_iam=False) + SQLSERVER_MODIFY_STATUS = TicketEnumField("SQLSERVER_MODIFY_STATUS", _("SQLServer 修改故障实例状态"), + register_iam=False) # REDIS REDIS_PLUGIN_CREATE_CLB = TicketEnumField("REDIS_PLUGIN_CREATE_CLB", _("Redis 创建CLB"), _("集群管理")) @@ -366,18 +415,22 @@ def get_approve_mode_by_ticket(cls, ticket_type): REDIS_SCALE_UPDOWN = TicketEnumField("REDIS_SCALE_UPDOWN", _("Redis 集群容量变更"), _("集群维护")) REDIS_CLUSTER_CUTOFF = TicketEnumField("REDIS_CLUSTER_CUTOFF", _("Redis 整机替换"), _("集群维护")) REDIS_CLUSTER_AUTOFIX = TicketEnumField("REDIS_CLUSTER_AUTOFIX", _("Redis 故障自愈"), _("集群维护")) - REDIS_CLUSTER_INSTANCE_SHUTDOWN = TicketEnumField("REDIS_CLUSTER_INSTANCE_SHUTDOWN", _("Redis 故障自愈-实例下架"), _("集群维护")) # noqa + REDIS_CLUSTER_INSTANCE_SHUTDOWN = TicketEnumField("REDIS_CLUSTER_INSTANCE_SHUTDOWN", _("Redis 故障自愈-实例下架"), + _("集群维护")) # noqa REDIS_MASTER_SLAVE_SWITCH = TicketEnumField("REDIS_MASTER_SLAVE_SWITCH", _("Redis 主从切换"), _("集群维护")) REDIS_PROXY_SCALE_UP = TicketEnumField("REDIS_PROXY_SCALE_UP", _("Redis Proxy扩容"), _("集群维护")) REDIS_PROXY_SCALE_DOWN = TicketEnumField("REDIS_PROXY_SCALE_DOWN", _("Redis Proxy缩容"), _("集群维护")) REDIS_ADD_DTS_SERVER = TicketEnumField("REDIS_ADD_DTS_SERVER", _("Redis 新增DTS SERVER"), register_iam=False) REDIS_REMOVE_DTS_SERVER = TicketEnumField("REDIS_REMOVE_DTS_SERVER", _("Redis 删除DTS SERVER"), register_iam=False) REDIS_DATA_STRUCTURE = TicketEnumField("REDIS_DATA_STRUCTURE", _("Redis 集群数据构造"), _("数据构造")) - REDIS_DATA_STRUCTURE_TASK_DELETE = TicketEnumField("REDIS_DATA_STRUCTURE_TASK_DELETE", _("Redis 数据构造记录删除"), _("数据构造")) # noqa - REDIS_CLUSTER_SHARD_NUM_UPDATE = TicketEnumField("REDIS_CLUSTER_SHARD_NUM_UPDATE", _("Redis 集群分片数变更"), _("集群维护")) + REDIS_DATA_STRUCTURE_TASK_DELETE = TicketEnumField("REDIS_DATA_STRUCTURE_TASK_DELETE", _("Redis 数据构造记录删除"), + _("数据构造")) # noqa + REDIS_CLUSTER_SHARD_NUM_UPDATE = TicketEnumField("REDIS_CLUSTER_SHARD_NUM_UPDATE", _("Redis 集群分片数变更"), + _("集群维护")) REDIS_CLUSTER_TYPE_UPDATE = TicketEnumField("REDIS_CLUSTER_TYPE_UPDATE", _("Redis 集群类型变更"), _("集群维护")) REDIS_CLUSTER_DATA_COPY = TicketEnumField("REDIS_CLUSTER_DATA_COPY", _("Redis 集群数据复制"), _("数据传输")) - REDIS_CLUSTER_ROLLBACK_DATA_COPY = TicketEnumField("REDIS_CLUSTER_ROLLBACK_DATA_COPY", _("Redis 构造实例数据回写"), _("数据构造")) # noqa + REDIS_CLUSTER_ROLLBACK_DATA_COPY = TicketEnumField("REDIS_CLUSTER_ROLLBACK_DATA_COPY", _("Redis 构造实例数据回写"), + _("数据构造")) # noqa REDIS_DATACOPY_CHECK_REPAIR = TicketEnumField("REDIS_DATACOPY_CHECK_REPAIR", _("Redis 数据校验与修复")) REDIS_CLUSTER_ADD_SLAVE = TicketEnumField("REDIS_CLUSTER_ADD_SLAVE", _("Redis 重建从库"), _("集群维护")) REDIS_DTS_ONLINE_SWITCH = TicketEnumField("REDIS_DTS_ONLINE_SWITCH", _("Redis DTS在线切换"), register_iam=False) @@ -385,14 +438,20 @@ def get_approve_mode_by_ticket(cls, ticket_type): REDIS_SLOTS_MIGRATE = TicketEnumField("REDIS_SLOTS_MIGRATE", _("Redis slots 迁移"), register_iam=False) REDIS_VERSION_UPDATE_ONLINE = TicketEnumField("REDIS_VERSION_UPDATE_ONLINE", _("Redis 集群版本升级")) # noqa REDIS_CLUSTER_REINSTALL_DBMON = TicketEnumField("REDIS_CLUSTER_REINSTALL_DBMON", _("Redis 集群重装DBMON")) # noqa - REDIS_PREDIXY_CONFIG_SERVERS_REWRITE = TicketEnumField("REDIS_PREDIXY_CONFIG_SERVERS_REWRITE", _("predixy配置重写"), register_iam=False) # noqa - REDIS_CLUSTER_PROXYS_UPGRADE = TicketEnumField("REDIS_CLUSTER_PROXYS_UPGRADE", _("Redis 集群proxys版本升级"), register_iam=False) # noqa + REDIS_PREDIXY_CONFIG_SERVERS_REWRITE = TicketEnumField("REDIS_PREDIXY_CONFIG_SERVERS_REWRITE", _("predixy配置重写"), + register_iam=False) # noqa + REDIS_CLUSTER_PROXYS_UPGRADE = TicketEnumField("REDIS_CLUSTER_PROXYS_UPGRADE", _("Redis 集群proxys版本升级"), + register_iam=False) # noqa REDIS_DIRTY_MACHINE_CLEAR = TicketEnumField("REDIS_DIRTY_MACHINE_CLEAR", _("Redis脏机清理"), register_iam=False) - REDIS_CLUSTER_STORAGES_CLI_CONNS_KILL = TicketEnumField("REDIS_CLUSTER_STORAGES_CLI_CONNS_KILL", _("Redis 集群存储层cli连接kill"), register_iam=False) # noqa - REDIS_CLUSTER_RENAME_DOMAIN = TicketEnumField("REDIS_CLUSTER_RENAME_DOMAIN", _("Redis集群域名重命名"), _("集群维护")) + REDIS_CLUSTER_STORAGES_CLI_CONNS_KILL = TicketEnumField("REDIS_CLUSTER_STORAGES_CLI_CONNS_KILL", + _("Redis 集群存储层cli连接kill"), + register_iam=False) # noqa + REDIS_CLUSTER_RENAME_DOMAIN = TicketEnumField("REDIS_CLUSTER_RENAME_DOMAIN", _("Redis集群域名重命名"), + _("集群维护")) REDIS_CLUSTER_MAXMEMORY_SET = TicketEnumField("REDIS_CLUSTER_MAXMEMORY_SET", _("Redis 集群设置maxmemory")) # noqa REDIS_CLUSTER_LOAD_MODULES = TicketEnumField("REDIS_CLUSTER_LOAD_MODULES", _("Redis 集群安装modules")) # noqa - REDIS_TENDISPLUS_LIGHTNING_DATA = TicketEnumField("REDIS_TENDISPLUS_LIGHTNING_DATA", _("Tendisplus闪电导入数据"), _("集群维护")) # noqa + REDIS_TENDISPLUS_LIGHTNING_DATA = TicketEnumField("REDIS_TENDISPLUS_LIGHTNING_DATA", _("Tendisplus闪电导入数据"), + _("集群维护")) # noqa REDIS_CLUSTER_INS_MIGRATE = TicketEnumField("REDIS_CLUSTER_INS_MIGRATE", _("Redis 集群指定实例迁移"), _("集群管理")) REDIS_SINGLE_INS_MIGRATE = TicketEnumField("REDIS_SINGLE_INS_MIGRATE", _("Redis 主从指定实例迁移"), _("集群管理")) @@ -461,8 +520,10 @@ def get_approve_mode_by_ticket(cls, ticket_type): RIAK_CLUSTER_MIGRATE = TicketEnumField("RIAK_CLUSTER_MIGRATE", _("Riak 集群迁移"), _("集群管理")) # MONGODB - MONGODB_REPLICASET_APPLY = TicketEnumField("MONGODB_REPLICASET_APPLY", _("MongoDB 副本集集群部署"), register_iam=False) # noqa - MONGODB_SHARD_APPLY = TicketEnumField("MONGODB_SHARD_APPLY", _("MongoDB 分片集群部署"), _("集群管理"), register_iam=False) # noqa + MONGODB_REPLICASET_APPLY = TicketEnumField("MONGODB_REPLICASET_APPLY", _("MongoDB 副本集集群部署"), + register_iam=False) # noqa + MONGODB_SHARD_APPLY = TicketEnumField("MONGODB_SHARD_APPLY", _("MongoDB 分片集群部署"), _("集群管理"), + register_iam=False) # noqa MONGODB_EXEC_SCRIPT_APPLY = TicketEnumField("MONGODB_EXEC_SCRIPT_APPLY", _("MongoDB 变更脚本执行"), _("脚本任务")) MONGODB_REMOVE_NS = TicketEnumField("MONGODB_REMOVE_NS", _("MongoDB 清档"), _("数据处理")) MONGODB_FULL_BACKUP = TicketEnumField("MONGODB_FULL_BACKUP", _("MongoDB 全库备份"), _("备份")) @@ -470,7 +531,8 @@ def get_approve_mode_by_ticket(cls, ticket_type): MONGODB_ADD_MONGOS = TicketEnumField("MONGODB_ADD_MONGOS", _("MongoDB 扩容接入层"), _("集群维护")) MONGODB_REDUCE_MONGOS = TicketEnumField("MONGODB_REDUCE_MONGOS", _("MongoDB 缩容接入层"), _("集群维护")) MONGODB_ADD_SHARD_NODES = TicketEnumField("MONGODB_ADD_SHARD_NODES", _("MongoDB 扩容shard节点数"), _("集群维护")) - MONGODB_REDUCE_SHARD_NODES = TicketEnumField("MONGODB_REDUCE_SHARD_NODES", _("MongoDB 缩容shard节点数"), _("集群维护")) # noqa + MONGODB_REDUCE_SHARD_NODES = TicketEnumField("MONGODB_REDUCE_SHARD_NODES", _("MongoDB 缩容shard节点数"), + _("集群维护")) # noqa MONGODB_SCALE_UPDOWN = TicketEnumField("MONGODB_SCALE_UPDOWN", _("MongoDB 集群容量变更"), _("集群维护")) MONGODB_ENABLE = TicketEnumField("MONGODB_ENABLE", _("MongoDB 集群启用"), register_iam=False) MONGODB_INSTANCE_RELOAD = TicketEnumField("MONGODB_INSTANCE_RELOAD", _("MongoDB 实例重启"), _("集群管理")) @@ -478,7 +540,8 @@ def get_approve_mode_by_ticket(cls, ticket_type): MONGODB_DESTROY = TicketEnumField("MONGODB_DESTROY", _("MongoDB 集群删除"), _("集群管理")) MONGODB_CUTOFF = TicketEnumField("MONGODB_CUTOFF", _("MongoDB 整机替换"), _("集群维护")) MONGODB_AUTHORIZE_RULES = TicketEnumField("MONGODB_AUTHORIZE_RULES", _("MongoDB 授权"), _("权限管理")) - MONGODB_EXCEL_AUTHORIZE_RULES = TicketEnumField("MONGODB_EXCEL_AUTHORIZE_RULES", _("MongoDB Excel授权"), _("权限管理")) # noqa + MONGODB_EXCEL_AUTHORIZE_RULES = TicketEnumField("MONGODB_EXCEL_AUTHORIZE_RULES", _("MongoDB Excel授权"), + _("权限管理")) # noqa MONGODB_IMPORT = TicketEnumField("MONGODB_IMPORT", _("MongoDB 数据导入"), _("集群维护")) MONGODB_RESTORE = TicketEnumField("MONGODB_RESTORE", _("MongoDB 定点回档"), _("集群维护")) MONGODB_TEMPORARY_DESTROY = TicketEnumField("MONGODB_TEMPORARY_DESTROY", _("MongoDB 临时集群销毁"), _("集群维护")) @@ -525,6 +588,9 @@ def get_approve_mode_by_ticket(cls, ticket_type): VM_DISABLE = TicketEnumField("VM_DISABLE", _("VM 集群禁用"), register_iam=False) VM_DESTROY = TicketEnumField("VM_DESTROY", _("VM 集群删除"), _("集群管理")) + # 测试 + FAKE_TICKET = TicketEnumField("FAKE_TICKET", _("测试专用单据"), register_iam=False) + class FlowType(str, StructuredEnum): """流程类型枚举""" diff --git a/dbm-ui/backend/ticket/flow_manager/inner.py b/dbm-ui/backend/ticket/flow_manager/inner.py index 3fccd85d86..5f429e64f4 100644 --- a/dbm-ui/backend/ticket/flow_manager/inner.py +++ b/dbm-ui/backend/ticket/flow_manager/inner.py @@ -29,7 +29,6 @@ INNER_FLOW_TODO_STATUS_MAP, FlowCallbackType, FlowErrCode, - FlowMsgType, TicketFlowStatus, TicketType, TodoStatus, @@ -186,18 +185,6 @@ def run(self) -> None: ) # 处理互斥异常和非预期的异常 self.run_error_status_handler(err) - # 发送创建任务失败通知 - from backend.ticket.tasks.ticket_tasks import send_msg_for_flow - - send_msg_for_flow.apply_async( - kwargs={ - "flow_id": self.flow_obj.id, - "flow_msg_type": FlowMsgType.DONE.value, - "flow_status": TicketFlowStatus.get_choice_label(self.flow_obj.status), - "processor": self.ticket.creator, - "receiver": self.ticket.creator, - } - ) return else: # 记录inner flow的集群动作和实例动作 diff --git a/dbm-ui/backend/ticket/flow_manager/itsm.py b/dbm-ui/backend/ticket/flow_manager/itsm.py index b26b01bdb1..6a8b7474bd 100644 --- a/dbm-ui/backend/ticket/flow_manager/itsm.py +++ b/dbm-ui/backend/ticket/flow_manager/itsm.py @@ -16,10 +16,9 @@ from backend.components import ItsmApi from backend.components.itsm.constants import ItsmTicketStatus from backend.exceptions import ApiResultError -from backend.ticket.constants import FlowMsgStatus, FlowMsgType, TicketFlowStatus, TicketStatus, TodoStatus, TodoType +from backend.ticket.constants import TicketFlowStatus, TicketStatus, TodoStatus, TodoType from backend.ticket.flow_manager.base import BaseTicketFlow from backend.ticket.models import Flow, Todo -from backend.ticket.tasks.ticket_tasks import send_msg_for_flow from backend.ticket.todos.itsm_todo import ItsmTodoContext from backend.utils.time import datetime2str, standardized_time_str @@ -124,7 +123,6 @@ def _url(self) -> str: return "" def _run(self) -> str: - itsm_fields = {f["key"]: f["value"] for f in self.flow_obj.details["fields"]} Todo.objects.create( name=_("【{}】单据等待审批").format(self.ticket.get_ticket_type_display()), flow=self.flow_obj, @@ -134,16 +132,6 @@ def _run(self) -> str: ) # 创建单据 data = ItsmApi.create_ticket(self.flow_obj.details) - # 异步发送待审批消息 - send_msg_for_flow.apply_async( - kwargs={ - "flow_id": self.flow_obj.id, - "flow_msg_type": FlowMsgType.TODO.value, - "flow_status": FlowMsgStatus.PENDING.value, - "processor": itsm_fields["approver"], - "receiver": self.ticket.creator, - } - ) return data["sn"] def _revoke(self, operator) -> Any: diff --git a/dbm-ui/backend/ticket/flow_manager/manager.py b/dbm-ui/backend/ticket/flow_manager/manager.py index 8ac86ace95..896ef84f50 100644 --- a/dbm-ui/backend/ticket/flow_manager/manager.py +++ b/dbm-ui/backend/ticket/flow_manager/manager.py @@ -11,8 +11,9 @@ import logging from backend import env +from backend.core import notify from backend.ticket import constants -from backend.ticket.constants import FLOW_FINISHED_STATUS, FlowType +from backend.ticket.constants import FLOW_FINISHED_STATUS, FlowType, TicketStatus from backend.ticket.flow_manager.delivery import DeliveryFlow, DescribeTaskFlow from backend.ticket.flow_manager.inner import IgnoreResultInnerFlow, InnerFlow, QuickInnerFlow from backend.ticket.flow_manager.itsm import ItsmFlow @@ -115,5 +116,14 @@ def update_ticket_status(self): return if self.ticket.status != target_status: + origin_status = self.ticket.status self.ticket.status = target_status self.ticket.save(update_fields=["status", "update_at"]) + self.ticket_status_trigger(origin_status, target_status) + + def ticket_status_trigger(self, origin_status, target_status): + """单据状态更新后的钩子函数""" + + # 单据状态变更后,发送通知。忽略running + if target_status != TicketStatus.RUNNING: + notify.send_msg.apply_async(args=(self.ticket.id,)) diff --git a/dbm-ui/backend/ticket/handler.py b/dbm-ui/backend/ticket/handler.py index cda7560728..d34e5345a8 100644 --- a/dbm-ui/backend/ticket/handler.py +++ b/dbm-ui/backend/ticket/handler.py @@ -29,6 +29,7 @@ from backend.ticket.constants import ( FLOW_FINISHED_STATUS, RUNNING_FLOW__TICKET_STATUS, + TODO_RUNNING_STATUS, FlowType, FlowTypeConfig, OperateNodeActionType, @@ -296,6 +297,25 @@ def batch_process_todo(cls, user, action, operations): results.append(todo) return TodoSerializer(results, many=True).data + @classmethod + def batch_process_ticket(cls, username, action, ticket_ids, params): + """ + 批量操作单据的todo + @param username 用户 + @param action 动作 + @param ticket_ids 单据ID列表 + @param params 操作额外参数 + """ + + tickets = Ticket.objects.prefetch_related("todo_of_ticket").filter(id__in=ticket_ids) + # 找到单据第一个代办(排除INNER_APPROVE,这是任务流程的人工确认节点产生的,不允许在单据维度操作) + running_todos = [ + ticket.todo_of_ticket.exclude(type=TodoType.INNER_APPROVE).filter(status__in=TODO_RUNNING_STATUS).first() + for ticket in tickets + ] + operations = [{"todo_id": todo.id, "params": params} for todo in running_todos if todo] + return TicketHandler.batch_process_todo(user=username, action=action, operations=operations) + @classmethod def create_ticket_flow_config(cls, bk_biz_id, cluster_ids, ticket_types, configs, operator): """ diff --git a/dbm-ui/backend/ticket/models/ticket.py b/dbm-ui/backend/ticket/models/ticket.py index 0df2ee00a1..d1c9456aa4 100644 --- a/dbm-ui/backend/ticket/models/ticket.py +++ b/dbm-ui/backend/ticket/models/ticket.py @@ -26,11 +26,13 @@ from backend.ticket.constants import ( EXCLUSIVE_TICKET_EXCEL_PATH, TICKET_RUNNING_STATUS_SET, + FlowErrCode, FlowRetryType, FlowType, TicketFlowStatus, TicketStatus, TicketType, + TodoStatus, ) from backend.utils.excel import ExcelHandler from backend.utils.time import calculate_cost_time @@ -133,6 +135,31 @@ def get_cost_time(self): return calculate_cost_time(timezone.now(), self.create_at) return calculate_cost_time(self.update_at, self.create_at) + def get_terminate_reason(self): + # 获取单据终止原因 + if self.status != TicketStatus.TERMINATED: + return "" + + flow = self.current_flow() + # 系统终止 + if flow.err_code == FlowErrCode.SYSTEM_TERMINATED_ERROR: + return _("系统自动终止") + # 用户终止,获取所有失败的todo,拿到里面的备注 + fail_todo = flow.todo_of_flow.filter(status=TodoStatus.DONE_FAILED).first() + if not fail_todo: + return "" + # 格式化终止文案 + remark = fail_todo.context.get("remark", "") + reason = _("{}已处理(人工终止,备注: {})").format(fail_todo.done_by, remark) + return reason + + def get_current_operators(self): + # 获取当前流程处理人 + running_todo = self.todo_of_ticket.filter(status=TodoStatus.TODO).first() + if not running_todo: + return [] + return running_todo.operators + def update_details(self, **kwargs): self.details.update(kwargs) self.save(update_fields=["details", "update_at"]) diff --git a/dbm-ui/backend/ticket/models/todo.py b/dbm-ui/backend/ticket/models/todo.py index 9516896f37..f9aeeec5c0 100644 --- a/dbm-ui/backend/ticket/models/todo.py +++ b/dbm-ui/backend/ticket/models/todo.py @@ -19,15 +19,7 @@ from backend.bk_web.models import AuditedModel from backend.configuration.models import BizSettings, DBAdministrator from backend.ticket.builders import BuilderFactory -from backend.ticket.constants import ( - TODO_RUNNING_STATUS, - FlowMsgStatus, - FlowMsgType, - TicketFlowStatus, - TodoStatus, - TodoType, -) -from backend.ticket.tasks.ticket_tasks import send_msg_for_flow +from backend.ticket.constants import TODO_RUNNING_STATUS, TicketFlowStatus, TodoStatus, TodoType logger = logging.getLogger("root") @@ -65,15 +57,6 @@ def create(self, **kwargs): operators = self.get_operators(kwargs["type"], kwargs["ticket"], kwargs.get("operators", [])) kwargs["operators"] = operators todo = super().create(**kwargs) - send_msg_for_flow.apply_async( - kwargs={ - "flow_id": todo.flow.id, - "flow_msg_type": FlowMsgType.TODO.value, - "flow_status": FlowMsgStatus.UNCONFIRMED.value, - "processor": ",".join(todo.operators), - "receiver": todo.creator, - } - ) return todo diff --git a/dbm-ui/backend/ticket/serializers.py b/dbm-ui/backend/ticket/serializers.py index 285e244d82..15072e2148 100644 --- a/dbm-ui/backend/ticket/serializers.py +++ b/dbm-ui/backend/ticket/serializers.py @@ -17,10 +17,10 @@ from backend.bk_web.constants import LEN_L_LONG from backend.bk_web.serializers import AuditedSerializer, TranslationSerializerMixin -from backend.components import CmsiApi from backend.configuration.constants import PLAT_BIZ_ID, DBType from backend.core.encrypt.constants import AsymmetricCipherConfigType from backend.core.encrypt.handlers import AsymmetricHandler +from backend.core.notify.constants import MsgType from backend.ticket import mock_data from backend.ticket.builders import BuilderFactory from backend.ticket.constants import ( @@ -40,8 +40,9 @@ class TicketSendMsgSerializer(serializers.Serializer): + # TODO: 暂时废弃,用不到单据类别的通知 msg_type = serializers.ListField( - help_text=_("发送类型"), child=serializers.ChoiceField(choices=CmsiApi.MsgType.get_choices()), required=False + help_text=_("发送类型"), child=serializers.ChoiceField(choices=MsgType.get_choices()), required=False ) receiver__username = serializers.CharField(help_text=_("包含用户名,用户需在蓝鲸平台注册,多个以逗号分隔"), required=False) sender = serializers.CharField(help_text=_("发件人/企微机器人ID"), required=False) diff --git a/dbm-ui/backend/ticket/tasks/ticket_tasks.py b/dbm-ui/backend/ticket/tasks/ticket_tasks.py index b966cedaee..2d07b01839 100644 --- a/dbm-ui/backend/ticket/tasks/ticket_tasks.py +++ b/dbm-ui/backend/ticket/tasks/ticket_tasks.py @@ -11,7 +11,6 @@ import json import logging import operator -import textwrap from collections import defaultdict from datetime import datetime, timedelta from functools import reduce @@ -24,23 +23,20 @@ from django.utils.translation import gettext as _ from backend import env -from backend.components import BKLogApi, ItsmApi -from backend.components.cmsi.handler import CmsiHandler +from backend.components import BKLogApi from backend.configuration.constants import PLAT_BIZ_ID, DBType from backend.constants import DEFAULT_SYSTEM_USER from backend.db_meta.enums import ClusterType, InstanceInnerRole -from backend.db_meta.models import AppCache, Cluster, StorageInstance +from backend.db_meta.models import Cluster, StorageInstance from backend.ticket.builders.common.constants import MYSQL_CHECKSUM_TABLE, MySQLDataRepairTriggerMode from backend.ticket.constants import ( TICKET_EXPIRE_DEFAULT_CONFIG, TODO_RUNNING_STATUS, FlowErrCode, - FlowMsgType, FlowType, FlowTypeConfig, TicketExpireType, TicketFlowStatus, - TicketStatus, TicketType, TodoType, ) @@ -320,74 +316,3 @@ def apply_ticket_task( raise TicketTaskTriggerException(_("不支持的定时类型: {}").format(eta)) return res - - -@shared_task -def send_msg_for_flow( - flow_id: int, - flow_msg_type: str, - flow_status: str, - processor: str, - receiver: str = "", - detail_address: str = None, -): - """ - 异步发送消息通知 - @param flow_id: 流程ID - @param flow_msg_type: 流程类型 - @param flow_status: 流程状态展示 - @param receiver: 通知人(多个处理人用,分割) - @param processor: 处理人(多个处理人用,分割) - @param detail_address: 查看详情链接 - """ - flow = Flow.objects.get(id=flow_id) - ticket = flow.ticket - receiver = receiver or ticket.creator - ticket_type = ticket.get_ticket_type_display() - biz_name = AppCache.get_biz_name(ticket.bk_biz_id) - - # 通知模板 - content = _( - """\ - 单据类型:{ticket_type} - 所属业务:{biz_name} - 提单人:{creator} - 提单时间:{submit_time} - 处理人:{processor} - 执行情况:{flow_status} - 查看详情:{detail_address}\ - """ - ).format( - ticket_type=ticket_type, - biz_name=biz_name, - creator=ticket.creator, - submit_time=ticket.create_at.astimezone(), - processor=processor, - flow_status=flow_status, - detail_address=detail_address or ticket.url, - ) - content = textwrap.dedent(content) - - if flow.flow_type == FlowType.BK_ITSM.value: - # 调用ITSM接口查询审批状态 - data = ItsmApi.ticket_approval_result({"sn": [flow.flow_obj_id]}, use_admin=True) - try: - approval_address = data[0]["ticket_url"] - except IndexError: - approval_address = "" - content += _("\n审批链接:{approval_address}").format(approval_address=approval_address) - - if flow_msg_type == FlowMsgType.DONE.value: - flow_msg_type = TicketStatus.get_choice_label(ticket.status) - - # 通知人 = 额外通知人 + 处理人 + 提单人 - receiver__username = set(f"{receiver},{processor},{ticket.creator}".split(",")) - msg = ticket.send_msg_config or {} - msg.update( - { - "receiver__username": ",".join(receiver__username), - "title": _("【数据库管理】 {flow_msg_type}通知").format(flow_msg_type=flow_msg_type), - "content": content, - } - ) - CmsiHandler.send_msg(msg) diff --git a/dbm-ui/backend/ticket/todos/pipeline_todo.py b/dbm-ui/backend/ticket/todos/pipeline_todo.py index cd9f0b019a..b20b574442 100644 --- a/dbm-ui/backend/ticket/todos/pipeline_todo.py +++ b/dbm-ui/backend/ticket/todos/pipeline_todo.py @@ -11,12 +11,14 @@ import logging from dataclasses import dataclass +from django.db import transaction from django.utils.translation import ugettext as _ +from backend.core import notify from backend.flow.engine.bamboo.engine import BambooEngine from backend.ticket import todos from backend.ticket.constants import TodoStatus, TodoType -from backend.ticket.models import TodoHistory +from backend.ticket.models import Flow, TodoHistory from backend.ticket.todos import ActionType, BaseTodoContext logger = logging.getLogger("root") @@ -66,11 +68,18 @@ def _process(self, username, action, params): def create(cls, ticket, flow, root_id, node_id): from backend.ticket.models import Todo - # 创建一条代办 - Todo.objects.create( - name=_("【{}】流程待确认,是否继续?").format(ticket.get_ticket_type_display()), - flow=flow, - ticket=ticket, - type=TodoType.INNER_APPROVE, - context=PipelineTodoContext(flow.id, ticket.id, root_id, node_id).to_dict(), - ) + # 创建一条代办,避免同时创建代办导致重复发送通知,用事务进行提交 + with transaction.atomic(): + flow = Flow.objects.select_for_update().get(id=flow.id) + + # 当前不存在待确认的todo,则发送通知 + if not flow.todo_of_flow.filter(type=TodoType.INNER_APPROVE).count(): + notify.send_msg(ticket.id, flow.id) + + Todo.objects.create( + name=_("【{}】流程待确认,是否继续?").format(ticket.get_ticket_type_display()), + flow=flow, + ticket=ticket, + type=TodoType.INNER_APPROVE, + context=PipelineTodoContext(flow.id, ticket.id, root_id, node_id).to_dict(), + ) diff --git a/dbm-ui/backend/ticket/views.py b/dbm-ui/backend/ticket/views.py index da5564f73c..c00ea592f6 100644 --- a/dbm-ui/backend/ticket/views.py +++ b/dbm-ui/backend/ticket/views.py @@ -49,7 +49,6 @@ CountType, FlowType, TicketType, - TodoType, ) from backend.ticket.contexts import TicketContext from backend.ticket.exceptions import TicketDuplicationException @@ -606,17 +605,7 @@ def batch_process_ticket(self, request, *args, **kwargs): 根据todo的类型可以触发不同的factor函数 """ data = self.params_validate(self.get_serializer_class()) - user = request.user.username - - tickets = Ticket.objects.prefetch_related("todo_of_ticket").filter(id__in=data["ticket_ids"]) - # 找到单据第一个代办(排除INNER_APPROVE,这是任务流程的人工确认节点产生的,不允许在单据维度操作) - running_todos = [ - ticket.todo_of_ticket.exclude(type=TodoType.INNER_APPROVE).filter(status__in=TODO_RUNNING_STATUS).first() - for ticket in tickets - ] - operations = [{"todo_id": todo.id, "params": data["params"]} for todo in running_todos if todo] - - return Response(TicketHandler.batch_process_todo(user=user, action=data["action"], operations=operations)) + return Response(TicketHandler.batch_process_ticket(username=request.user.username, **data)) @swagger_auto_schema( operation_summary=_("获取单据关联任务流程信息"),