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..20b8d38a51 --- /dev/null +++ b/dbm-ui/backend/core/notify/handlers.py @@ -0,0 +1,352 @@ +# -*- 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.exceptions import ApiResultError +from backend.ticket.builders import BuilderFactory +from backend.ticket.constants import TicketStatus, TicketType, TodoStatus +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 [] + + todo = ticket.todo_of_ticket.filter(status=TodoStatus.TODO).first() + if not todo: + return [] + + # 增加回调按钮,执行和终止 + agree_action = { + "name": _("同意") if ticket.status == TicketStatus.APPROVE else _("确认执行"), + "color": "green", + "callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/bkchat_process_todo/", + "callback_data": {"action": ActionType.APPROVE.value, "todo_id": todo.id, "params": {}}, + } + refuse_action = { + "name": _("拒绝") if ticket.status == TicketStatus.APPROVE else _("终止单据"), + "color": "red", + "callback_url": f"{env.BK_DBM_APIGATEWAY}/tickets/bkchat_process_todo/", + "callback_data": { + "action": ActionType.TERMINATE.value, + "todo_id": todo.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, receivers): + """重新渲染标题和内容样式,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 receivers]) + content += "\n" + at_list + + return title, content + + def send_msg(self, msg_type, context): + ticket, phase, receivers = context["ticket"], context["phase"], context["receivers"] + title, content = self.render_title_content(msg_type, self.title, self.content, ticket, phase, receivers) + msg_info = { + "title": title, + # 处理人 + "approvers": ticket.get_current_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)) + # 邮件的换行要用
的html + self.content = self.content.replace("\n", "
") + 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.content = f"{self.title}\n{self.content}" + 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支持方式即可 + # 暂不暴露微信的通知方式 + msg_types = CmsiApi.get_msg_type() + msg_type_map = {msg["type"]: msg for msg in msg_types} + msg_type_map[MsgType.WEIXIN.value]["is_active"] = False + return list(msg_type_map.values()) + + def get_notify_class(self, msg_type: str): + # 根据通知类型获取通知类,以及通知所需的上下文 + if msg_type in [MsgType.WECOM_ROBOT, MsgType.RTX] and env.BKCHAT_APIGW_DOMAIN: + context = {"ticket": self.ticket, "phase": self.phase, "receivers": self.get_receivers()} + return BkChatHandler, context + 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]: + receivers = creator + elif self.phase in [TicketStatus.APPROVE]: + itsm_builder = BuilderFactory.get_builder_cls(self.ticket.ticket_type).itsm_flow_builder(self.ticket) + receivers = itsm_builder.get_approvers().split(",") + else: + receivers = creator + biz_helpers + # 去重后返回 + return list(dict.fromkeys(receivers)) + + 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.astimezone().strftime("%Y-%m-%d %H:%M:%S%z"), + "update_time": self.ticket.update_at.astimezone().strftime("%Y-%m-%d %H:%M:%S%z"), + "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): + # 获取单据通知设置,优先: 单据配置 > 业务配置 > 默认业务配置 + if self.phase in self.ticket.send_msg_config: + send_msg_config = self.ticket.send_msg_config[self.phase] + else: + 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, context = 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, []) + + try: + notify_class(title, content, self.receivers).send_msg(msg_type, context=context) + except (ApiResultError, Exception) as e: + logger.error(_("[{}]消息发送失败,错误信息: {}").format(MsgType.get_choice_label(msg_type), e)) + + +@shared_task +def send_msg(ticket_id: int, flow_id: int = None): + # 可异步发送消息,非阻塞路径默认不抛出异常 + NotifyAdapter(ticket_id, flow_id).send_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..d1b60f9453 --- /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/mysql/permission/authorize/handlers.py b/dbm-ui/backend/db_services/mysql/permission/authorize/handlers.py index b2fda084e7..75215fde86 100644 --- a/dbm-ui/backend/db_services/mysql/permission/authorize/handlers.py +++ b/dbm-ui/backend/db_services/mysql/permission/authorize/handlers.py @@ -16,7 +16,6 @@ from django.utils.translation import ugettext_lazy as _ from backend import env -from backend.components import CmsiApi from backend.components.gcs.client import GcsApi, GcsDirectApi from backend.components.mysql_priv_manager.client import DBPrivManagerApi from backend.components.scr.client import ScrApi @@ -211,8 +210,8 @@ def parse_domain(raw_domain): creator=operator or self.operator, remark=_("第三方请求授权"), details=authorize_info_slz.validated_data, - # 自动授权单,不发送通知 - send_msg_config={"msg_type": [CmsiApi.MsgType.UNKNOWN.value]}, + # 自动授权单,成功/失败不发送通知 + send_msg_config={TicketStatus.SUCCEEDED.value: {}, TicketStatus.FAILED.value: {}}, ) task_id = str(ticket.id) return {"task_id": task_id, "platform": "dbm", "job_id": task_id} diff --git a/dbm-ui/backend/db_services/plugin/constants.py b/dbm-ui/backend/db_services/plugin/constants.py index bc29cd9419..f3868d829c 100644 --- a/dbm-ui/backend/db_services/plugin/constants.py +++ b/dbm-ui/backend/db_services/plugin/constants.py @@ -9,4 +9,4 @@ specific language governing permissions and limitations under the License. """ -SWAGGER_TAG = "plugin" +SWAGGER_TAG = "OpenAPI" 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..ba1272af61 --- /dev/null +++ b/dbm-ui/backend/db_services/plugin/ticket/serializers.py @@ -0,0 +1,28 @@ +# -*- 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, TodoOperateSerializer + + +class OpenAPIBatchTicketOperateSerializer(BatchTicketOperateSerializer): + username = serializers.CharField(help_text=_("操作者")) + + +class OpenAPIBkChatProcessTodoSerializer(TodoOperateSerializer): + username = serializers.CharField(help_text=_("操作者")) + + +class OpenAPIBkChatProcessTodoResponseSerializer(serializers.Serializer): + response_msg = serializers.CharField(help_text=_("返回信息")) + response_color = 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..85f1d55ba8 --- /dev/null +++ b/dbm-ui/backend/db_services/plugin/ticket/views.py @@ -0,0 +1,74 @@ +# -*- 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, + OpenAPIBkChatProcessTodoResponseSerializer, + OpenAPIBkChatProcessTodoSerializer, +) +from backend.db_services.plugin.view import BaseOpenAPIViewSet +from backend.ticket.constants import TodoStatus, TodoType +from backend.ticket.exceptions import TodoDuplicateProcessException +from backend.ticket.handler import TicketHandler +from backend.ticket.models import Todo +from backend.ticket.todos import TodoActorFactory + +logger = logging.getLogger("root") + + +class TicketViewSet(BaseOpenAPIViewSet): + @swagger_auto_schema( + operation_summary=_("批量单据待办处理"), + request_body=OpenAPIBatchTicketOperateSerializer(), + 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)) + + @swagger_auto_schema( + operation_summary=_("待办处理(bkchat专属)"), + request_body=OpenAPIBkChatProcessTodoSerializer(), + responses={status.HTTP_200_OK: OpenAPIBkChatProcessTodoResponseSerializer()}, + tags=[SWAGGER_TAG], + ) + @action(methods=["POST"], detail=False, serializer_class=OpenAPIBkChatProcessTodoSerializer) + def bkchat_process_todo(self, request, *args, **kwargs): + """ + bkchat专属的待办处理,区别主要是返回结构不同 + """ + params = self.params_validate(self.get_serializer_class()) + + todo = Todo.objects.get(id=params["todo_id"]) + if todo.type not in [TodoType.ITSM, TodoType.APPROVE]: + return Response({"response_msg": _("暂不支持该类型{}todo的处理").fromat(todo.type), "response_color": "red"}) + + # 确认todo,忽略重复操作 + try: + TodoActorFactory.actor(todo).process(params["username"], params["action"], params["params"]) + except TodoDuplicateProcessException: + pass + + # 根据操作类型获取文案和按钮颜色 + todo.refresh_from_db() + if todo.status == TodoStatus.DONE_FAILED: + return Response({"response_msg": _("{} 已终止").format(todo.done_by), "response_color": "red"}) + elif todo.status == TodoStatus.DONE_SUCCESS: + return Response({"response_msg": _("{} 已确认").format(todo.done_by), "response_color": "green"}) 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..180e568f74 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(60 * 5) + # 测试报错 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/tests/ticket/mongo/test_mongodb_flow.py b/dbm-ui/backend/tests/ticket/mongo/test_mongodb_flow.py index 6ecb142fec..627476096d 100644 --- a/dbm-ui/backend/tests/ticket/mongo/test_mongodb_flow.py +++ b/dbm-ui/backend/tests/ticket/mongo/test_mongodb_flow.py @@ -98,7 +98,7 @@ class TestMangodbFlow: @patch.object(TicketViewSet, "get_permissions", lambda x: []) @patch("backend.ticket.flow_manager.itsm.ItsmApi", ItsmApiMock()) @patch("backend.db_services.cmdb.biz.CCApi", CCApiMock()) - @patch("backend.components.cmsi.handler.CmsiHandler.send_msg", lambda msg: "有一条MANGOS 扩容接入层待办需要您处理") + @patch("backend.core.notify.send_msg.apply_async", lambda *args, **kwargs: "有一条MANGOS 扩容接入层待办需要您处理") @patch( "backend.ticket.flow_manager.resource.ResourceApplyFlow.apply_resource", lambda resource_request_id, node_infos: (1, MANGOS_ADD_SOURCE_DATA), @@ -138,7 +138,7 @@ def test_mango_add_mangos_flow( @patch.object(TicketViewSet, "get_permissions", lambda x: []) @patch("backend.ticket.flow_manager.itsm.ItsmApi", ItsmApiMock()) @patch("backend.db_services.cmdb.biz.CCApi", CCApiMock()) - @patch("backend.components.cmsi.handler.CmsiHandler.send_msg", lambda msg: "有一条MANGOS 缩容接入层待办需要您处理") + @patch("backend.core.notify.send_msg.apply_async", lambda *args, **kwargs: "有一条MANGOS 缩容接入层待办需要您处理") @patch("backend.db_services.cmdb.biz.Permission", PermissionMock) def test_mango_reduce_mangos_flow( self, mock_pause_status, mocked_status, mocked__run, mocked_permission_classes, query_fixture, db @@ -173,7 +173,7 @@ def test_mango_reduce_mangos_flow( @patch.object(TicketViewSet, "get_permissions", lambda x: []) @patch("backend.ticket.flow_manager.itsm.ItsmApi", ItsmApiMock()) @patch("backend.db_services.cmdb.biz.CCApi", CCApiMock()) - @patch("backend.components.cmsi.handler.CmsiHandler.send_msg", lambda msg: "有一条MANGODB 集群下架待办需要您处理") + @patch("backend.core.notify.send_msg.apply_async", lambda *args, **kwargs: "有一条MANGODB 集群下架待办需要您处理") @patch("backend.db_services.cmdb.biz.Permission", PermissionMock) def test_mangodb_destroy_flow( self, mock_pause_status, mocked_status, mocked__run, mocked_permission_classes, query_fixture, db @@ -208,7 +208,7 @@ def test_mangodb_destroy_flow( @patch.object(TicketViewSet, "get_permissions", lambda x: []) @patch("backend.ticket.flow_manager.itsm.ItsmApi", ItsmApiMock()) @patch("backend.db_services.cmdb.biz.CCApi", CCApiMock()) - @patch("backend.components.cmsi.handler.CmsiHandler.send_msg", lambda msg: "有一条MANGO 整机替换待办需要您处理") + @patch("backend.core.notify.send_msg.apply_async", lambda *args, **kwargs: "有一条MANGO 整机替换待办需要您处理") @patch( "backend.ticket.flow_manager.resource.ResourceApplyFlow.apply_resource", lambda resource_request_id, node_infos: (1, MANGODB_SOURCE_APPLICATION_DATA), @@ -248,7 +248,7 @@ def test_mango_outoff_flow( @patch.object(TicketViewSet, "get_permissions", lambda x: []) @patch("backend.ticket.flow_manager.itsm.ItsmApi", ItsmApiMock()) @patch("backend.db_services.cmdb.biz.CCApi", CCApiMock()) - @patch("backend.components.cmsi.handler.CmsiHandler.send_msg", lambda msg: "有一条MONGODB 集群清档待办需要您处理") + @patch("backend.core.notify.send_msg.apply_async", lambda *args, **kwargs: "有一条MONGODB 集群清档待办需要您处理") @patch("backend.db_services.cmdb.biz.Permission", PermissionMock) def test_mongo_remove_ns( self, diff --git a/dbm-ui/backend/tests/ticket/mysql/test_mysql_flow.py b/dbm-ui/backend/tests/ticket/mysql/test_mysql_flow.py index 04cb827666..edece51082 100644 --- a/dbm-ui/backend/tests/ticket/mysql/test_mysql_flow.py +++ b/dbm-ui/backend/tests/ticket/mysql/test_mysql_flow.py @@ -118,7 +118,7 @@ def test_authorize_ticket_flow( @patch.object(TicketViewSet, "get_permissions", lambda x: []) @patch("backend.ticket.flow_manager.itsm.ItsmApi", ItsmApiMock()) @patch("backend.db_services.cmdb.biz.CCApi", CCApiMock()) - @patch("backend.components.cmsi.handler.CmsiHandler.send_msg", lambda msg: "有一条MySQL 高可用部署待办需要您处理") + @patch("backend.core.notify.send_msg.apply_async", lambda *args, **kwargs: "有一条MySQL 高可用部署待办需要您处理") @patch("backend.db_services.cmdb.biz.Permission", PermissionMock) @patch("backend.ticket.builders.mysql.mysql_single_apply.DBConfigApi", DBConfigApiMock) def test_mysql_single_apply_flow( @@ -174,7 +174,7 @@ def test_sql_import_flow(self, mocked_status, mocked__run, mocked_permission_cla @patch.object(TicketViewSet, "get_permissions", lambda x: []) @patch("backend.ticket.flow_manager.itsm.ItsmApi", ItsmApiMock()) @patch("backend.db_services.cmdb.biz.CCApi", CCApiMock()) - @patch("backend.components.cmsi.handler.CmsiHandler.send_msg", lambda msg: "有一条MySQL 高可用部署待办需要您处理") + @patch("backend.core.notify.send_msg.apply_async", lambda *args, **kwargs: "有一条MySQL 高可用部署待办需要您处理") @patch( "backend.ticket.flow_manager.resource.ResourceApplyFlow.apply_resource", lambda resource_request_id, node_infos: (1, APPLY_RESOURCE_RETURN_DATA), diff --git a/dbm-ui/backend/tests/ticket/server_base.py b/dbm-ui/backend/tests/ticket/server_base.py index 3162867b9b..f287361263 100644 --- a/dbm-ui/backend/tests/ticket/server_base.py +++ b/dbm-ui/backend/tests/ticket/server_base.py @@ -27,7 +27,7 @@ class TestFlowBase: patch.object(TicketViewSet, "get_permissions"), patch("backend.ticket.flow_manager.itsm.ItsmApi", ItsmApiMock()), patch("backend.db_services.cmdb.biz.CCApi", CCApiMock()), - patch("backend.components.cmsi.handler.CmsiHandler.send_msg", lambda msg: "有一条待办事项需要您处理"), + patch("backend.core.notify.send_msg.apply_async", lambda *args, **kwargs: "有一条待办事项需要您处理"), patch("backend.db_services.cmdb.biz.Permission", PermissionMock), ] 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/exceptions.py b/dbm-ui/backend/ticket/exceptions.py index db5482a9d5..387118d25a 100644 --- a/dbm-ui/backend/ticket/exceptions.py +++ b/dbm-ui/backend/ticket/exceptions.py @@ -60,6 +60,12 @@ class TodoWrongOperatorException(TicketBaseException): MESSAGE_TPL = _("错误的todo处理人{username}") +class TodoDuplicateProcessException(TicketBaseException): + ERROR_CODE = "010" + MESSAGE = _("重复操作") + MESSAGE_TPL = _("重复操作") + + class ApprovalWrongOperatorException(TicketBaseException): ERROR_CODE = "008" MESSAGE = _("审批处理异常") @@ -67,6 +73,6 @@ class ApprovalWrongOperatorException(TicketBaseException): class TicketFlowsConfigException(TicketBaseException): - ERROR_CODE = "008" + ERROR_CODE = "009" MESSAGE = _("单据流程设置失败") MESSAGE_TPL = _("单据流程{ticket_type}设置失败") 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..8b99309774 100644 --- a/dbm-ui/backend/ticket/flow_manager/manager.py +++ b/dbm-ui/backend/ticket/flow_manager/manager.py @@ -10,9 +10,12 @@ """ import logging +from django.db import transaction + 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 @@ -114,6 +117,20 @@ def update_ticket_status(self): # 其他场景下状态未变更,无需更新DB return - if self.ticket.status != target_status: - self.ticket.status = target_status - self.ticket.save(update_fields=["status", "update_at"]) + # 原子更新单据状态 + with transaction.atomic(): + ticket = Ticket.objects.select_for_update().get(id=self.ticket.id) + if ticket.status == target_status: + return + origin_status, ticket.status = ticket.status, target_status + ticket.save(update_fields=["status", "update_at"]) + self.ticket_status_trigger(origin_status, target_status) + + def ticket_status_trigger(self, origin_status, target_status): + """单据状态更新后的钩子函数""" + + # 单据状态变更后,发送通知。 + # 忽略运行中:流转到内置任务无需通知,待继续在todo创建时才触发通知 + # 忽略待补货:到资源申请节点,单据状态总会流转为待补货,但是只有待补货todo创建才触发通知 + if target_status not in [TicketStatus.RUNNING, TicketStatus.RESOURCE_REPLENISH]: + notify.send_msg.apply_async(args=(self.ticket.id,)) diff --git a/dbm-ui/backend/ticket/flow_manager/resource.py b/dbm-ui/backend/ticket/flow_manager/resource.py index 4fa5166810..e1f056e805 100644 --- a/dbm-ui/backend/ticket/flow_manager/resource.py +++ b/dbm-ui/backend/ticket/flow_manager/resource.py @@ -21,6 +21,7 @@ from backend.components.dbresource.client import DBResourceApi from backend.configuration.constants import AffinityEnum from backend.configuration.models import DBAdministrator +from backend.core import notify from backend.db_meta.models import Spec from backend.db_services.dbresource.exceptions import ResourceApplyException, ResourceApplyInsufficientException from backend.db_services.ipchooser.constants import CommonEnum @@ -214,6 +215,7 @@ def create_replenish_todo(self): flow_id=self.flow_obj.id, ticket_id=self.ticket.id, user=self.ticket.creator, administrators=dba ).to_dict(), ) + notify.send_msg.apply_async(args=(self.ticket.id,)) def fetch_apply_params(self, ticket_data): """ diff --git a/dbm-ui/backend/ticket/handler.py b/dbm-ui/backend/ticket/handler.py index cda7560728..c389086d1c 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, @@ -230,7 +231,13 @@ def approve_itsm_ticket(cls, ticket_id, action, operator, **kwargs): act_msg = kwargs.get("action_message") or act_msg_tpl # 审批单据 - params = {"action_message": act_msg} + params = { + "sn": sn, + "action_message": act_msg, + "action_type": action, + "operator": operator, + "bk_username": operator, + } if action == OperateNodeActionType.TRANSITION: is_approved = kwargs["is_approved"] itsm_fields = cls.get_itsm_fields(flow.ticket.ticket_type) @@ -238,11 +245,10 @@ def approve_itsm_ticket(cls, ticket_id, action, operator, **kwargs): {"key": itsm_fields[0], "value": json.dumps(is_approved)}, {"key": itsm_fields[1], "value": act_msg}, ] - params.update(sn=sn, state_id=state_id, action_type=action, operator=operator, fields=fields) + params.update(state_id=state_id, fields=fields) ItsmApi.operate_node(params) # 终止/撤销单据 elif action in [OperateNodeActionType.TERMINATE, OperateNodeActionType.WITHDRAW]: - params.update(sn=sn, action_type=action, operator=operator) ItsmApi.operate_ticket(params) return sn @@ -296,6 +302,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/__init__.py b/dbm-ui/backend/ticket/todos/__init__.py index ad71f9d014..3cf9742f88 100644 --- a/dbm-ui/backend/ticket/todos/__init__.py +++ b/dbm-ui/backend/ticket/todos/__init__.py @@ -19,7 +19,7 @@ from backend.constants import DEFAULT_SYSTEM_USER from backend.ticket.constants import TODO_RUNNING_STATUS -from backend.ticket.exceptions import TodoWrongOperatorException +from backend.ticket.exceptions import TodoDuplicateProcessException, TodoWrongOperatorException from backend.ticket.models import Todo from blue_krill.data_types.enum import EnumField, StructuredEnum @@ -55,7 +55,7 @@ def allow_superuser_process(self): def process(self, username, action, params): # 当状态已经被确认,则不允许重复操作 if self.todo.status not in TODO_RUNNING_STATUS: - raise TodoWrongOperatorException(_("当前代办操作已经处理,不能重复处理!")) + raise TodoDuplicateProcessException(_("当前代办操作已经处理,不能重复处理!")) # 允许系统内置用户确认 if username == DEFAULT_SYSTEM_USER: diff --git a/dbm-ui/backend/ticket/todos/pipeline_todo.py b/dbm-ui/backend/ticket/todos/pipeline_todo.py index cd9f0b019a..ace2ad5d0b 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.apply_async(args=(ticket.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=_("获取单据关联任务流程信息"), diff --git a/dbm-ui/backend/utils/time.py b/dbm-ui/backend/utils/time.py index 90f809bb1d..b5510d6e8a 100644 --- a/dbm-ui/backend/utils/time.py +++ b/dbm-ui/backend/utils/time.py @@ -43,7 +43,7 @@ def timezone2timestamp(date: Union[str, datetime.datetime]) -> int: return int(time_parse(date).timestamp()) -def datetime2str(o_datetime: datetime.datetime, fmt: str = DATETIME_PATTERN, aware_check: bool = True) -> str: +def datetime2str(o_datetime: datetime.datetime, aware_check: bool = True) -> str: """ 将时间对象转换为时间字符串,可选时区强校验 """ diff --git a/dbm-ui/scripts/ci/install.sh b/dbm-ui/scripts/ci/install.sh index a25c698417..bb44b26cfc 100755 --- a/dbm-ui/scripts/ci/install.sh +++ b/dbm-ui/scripts/ci/install.sh @@ -18,6 +18,7 @@ pip install poetry >> /tmp/pip_install.log # 进入dbm-ui进行操作 cd $DBM_DIR +poetry self add poetry-plugin-export poetry export --without-hashes -f requirements.txt --output requirements.txt pip install -r requirements.txt >> /tmp/pip_install.log