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