From 6bb8221be421f78dc79b3146e4502bd7bf969d3f Mon Sep 17 00:00:00 2001 From: iSecloud <869820505@qq.com> Date: Fri, 6 Sep 2024 20:18:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E5=8D=95=E6=8D=AE=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=BB=86=E5=8C=96=20#6755?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db_services/bigdata/resources/query.py | 4 +- .../db_services/mysql/dumper/handlers.py | 4 +- .../backend/tests/ticket/test_doris_flow.py | 6 +- .../backend/tests/ticket/test_mongodb_flow.py | 2 +- dbm-ui/backend/ticket/constants.py | 218 +++++++++++----- dbm-ui/backend/ticket/filters.py | 11 + dbm-ui/backend/ticket/flow_manager/base.py | 20 +- .../backend/ticket/flow_manager/delivery.py | 9 +- dbm-ui/backend/ticket/flow_manager/inner.py | 61 +++-- dbm-ui/backend/ticket/flow_manager/itsm.py | 47 +++- dbm-ui/backend/ticket/flow_manager/manager.py | 17 +- dbm-ui/backend/ticket/flow_manager/pause.py | 11 +- .../backend/ticket/flow_manager/resource.py | 16 +- dbm-ui/backend/ticket/flow_manager/timer.py | 20 +- dbm-ui/backend/ticket/handler.py | 84 +++++-- dbm-ui/backend/ticket/models/ticket.py | 23 +- dbm-ui/backend/ticket/models/todo.py | 14 +- dbm-ui/backend/ticket/serializers.py | 28 ++- dbm-ui/backend/ticket/tasks/ticket_tasks.py | 3 +- dbm-ui/backend/ticket/todos/__init__.py | 10 +- dbm-ui/backend/ticket/todos/itsm_todo.py | 68 +++++ dbm-ui/backend/ticket/views.py | 232 ++++++++---------- 22 files changed, 592 insertions(+), 316 deletions(-) create mode 100644 dbm-ui/backend/ticket/todos/itsm_todo.py diff --git a/dbm-ui/backend/db_services/bigdata/resources/query.py b/dbm-ui/backend/db_services/bigdata/resources/query.py index 552eb9a5d5..f6cbbcea13 100644 --- a/dbm-ui/backend/db_services/bigdata/resources/query.py +++ b/dbm-ui/backend/db_services/bigdata/resources/query.py @@ -21,7 +21,7 @@ from backend.db_proxy.models import ClusterExtension from backend.db_services.dbbase.resources import query from backend.db_services.ipchooser.query.resource import ResourceQueryHelper -from backend.ticket.constants import TicketFlowStatus +from backend.ticket.constants import TICKET_RUNNING_STATUS from backend.ticket.models import InstanceOperateRecord from backend.utils.time import datetime2str @@ -65,7 +65,7 @@ def _filter_instance_hook(cls, bk_biz_id, query_params, instances, **kwargs): # 获取实例的操作与实例记录 records = InstanceOperateRecord.objects.filter( - instance_id__in=instance_ids, ticket__status=TicketFlowStatus.RUNNING + instance_id__in=instance_ids, ticket__status__in=TICKET_RUNNING_STATUS ) instance_operate_records_map: Dict[int, List] = defaultdict(list) for record in records: diff --git a/dbm-ui/backend/db_services/mysql/dumper/handlers.py b/dbm-ui/backend/db_services/mysql/dumper/handlers.py index 53b1cf9b17..06db016c68 100644 --- a/dbm-ui/backend/db_services/mysql/dumper/handlers.py +++ b/dbm-ui/backend/db_services/mysql/dumper/handlers.py @@ -15,7 +15,7 @@ from backend.db_meta.enums import InstanceInnerRole from backend.db_meta.models import Cluster from backend.db_services.mysql.dumper.models import DumperSubscribeConfig -from backend.ticket.constants import FlowType, TicketFlowStatus, TicketStatus, TicketType +from backend.ticket.constants import TICKET_RUNNING_STATUS, FlowType, TicketFlowStatus, TicketStatus, TicketType from backend.ticket.models import Flow, Ticket @@ -66,7 +66,7 @@ def patch_dumper_list_info(cls, dumper_results: List[Dict], bk_biz_id: int = 0, dumper_ticket_types.remove(TicketType.TBINLOGDUMPER_INSTALL) dumper_ticket_types.extend([TicketType.MYSQL_MASTER_SLAVE_SWITCH, TicketType.MYSQL_MASTER_FAIL_OVER]) active_tickets = Ticket.objects.filter( - bk_biz_id=bk_biz_id, status=TicketStatus.RUNNING, ticket_type__in=dumper_ticket_types + bk_biz_id=bk_biz_id, status__in=TICKET_RUNNING_STATUS, ticket_type__in=dumper_ticket_types ) # 获取每个dumper单据状态与id的映射 dumper_inst_id__ticket: Dict[int, str] = {} diff --git a/dbm-ui/backend/tests/ticket/test_doris_flow.py b/dbm-ui/backend/tests/ticket/test_doris_flow.py index 8882b3967f..a076987e51 100644 --- a/dbm-ui/backend/tests/ticket/test_doris_flow.py +++ b/dbm-ui/backend/tests/ticket/test_doris_flow.py @@ -37,14 +37,14 @@ SCALEUP_POOL_TICKET_DATA, ) from backend.tests.ticket.server_base import TestFlowBase -from backend.ticket.constants import TicketFlowStatus, TicketStatus +from backend.ticket.constants import TicketFlowStatus logger = logging.getLogger("test") pytestmark = pytest.mark.django_db client = APIClient() -INITIAL_FLOW_FINISHED_STATUS = [TicketFlowStatus.SKIPPED, TicketStatus.SUCCEEDED] -CHANGED_MOCK_STATUS = [TicketFlowStatus.SKIPPED, TicketStatus.SUCCEEDED, TicketFlowStatus.RUNNING] +INITIAL_FLOW_FINISHED_STATUS = [TicketFlowStatus.SKIPPED, TicketFlowStatus.SUCCEEDED] +CHANGED_MOCK_STATUS = [TicketFlowStatus.SKIPPED, TicketFlowStatus.SUCCEEDED, TicketFlowStatus.RUNNING] @pytest.fixture(autouse=True) # autouse=True 会自动应用这个fixture到所有的测试中 diff --git a/dbm-ui/backend/tests/ticket/test_mongodb_flow.py b/dbm-ui/backend/tests/ticket/test_mongodb_flow.py index ea933a0562..6ecb142fec 100644 --- a/dbm-ui/backend/tests/ticket/test_mongodb_flow.py +++ b/dbm-ui/backend/tests/ticket/test_mongodb_flow.py @@ -53,7 +53,7 @@ pytestmark = pytest.mark.django_db client = APIClient() -INITIAL_FLOW_FINISHED_STATUS = [TicketFlowStatus.SKIPPED, TicketStatus.SUCCEEDED] +INITIAL_FLOW_FINISHED_STATUS = [TicketFlowStatus.SKIPPED, TicketFlowStatus.SUCCEEDED] CHANGED_MOCK_STATUS = [TicketFlowStatus.SKIPPED, TicketStatus.SUCCEEDED, TicketFlowStatus.RUNNING] diff --git a/dbm-ui/backend/ticket/constants.py b/dbm-ui/backend/ticket/constants.py index ab39619d07..7c29046c2b 100644 --- a/dbm-ui/backend/ticket/constants.py +++ b/dbm-ui/backend/ticket/constants.py @@ -29,7 +29,9 @@ class TodoType(str, StructuredEnum): 待办类型 """ + ITSM = EnumField("ITSM", _("主流程-单据审批")) APPROVE = EnumField("APPROVE", _("主流程-人工确认")) + INNER_FAILED = EnumField("INNER_FAILED", _("主流程-失败后待确认")) INNER_APPROVE = EnumField("INNER_APPROVE", _("自动化流程-人工确认")) RESOURCE_REPLENISH = EnumField("RESOURCE_REPLENISH", _("资源补货")) @@ -39,19 +41,24 @@ class CountType(str, StructuredEnum): 单据计数类型 """ - MY_TODO = EnumField("MY_TODO", _("我的待办")) MY_APPROVE = EnumField("MY_APPROVE", _("我的申请")) + APPROVE = EnumField("APPROVE", _("待我审批")) + TODO = EnumField("TODO", _("待我确认执行")) + RUNNING = EnumField("RUNNING", _("待我继续")) + RESOURCE_REPLENISH = EnumField("RESOURCE_REPLENISH", _("待我补货")) + FAILED = EnumField("FAILED", _("失败待处理")) + DONE = EnumField("DONE", _("我的已办")) + SELF_MANAGE = EnumField("SELF_MANAGE", _("我负责的业务")) class TodoStatus(str, StructuredEnum): """ 待办状态枚举 - TODO -> (RUNNING,可选) -> DONE_SUCCESS - | -> DONE_FAILED + TODO -> (RUNNING,可选) -> DONE_SUCCESS + | -> DONE_FAILED """ TODO = EnumField("TODO", _("待处理")) - RUNNING = EnumField("RUNNING", _("处理中")) DONE_SUCCESS = EnumField("DONE_SUCCESS", _("已处理")) DONE_FAILED = EnumField("DONE_FAILED", _("已终止")) @@ -68,18 +75,32 @@ class ResourceApplyErrCode(int, StructuredEnum): TODO_DONE_STATUS = [TodoStatus.DONE_SUCCESS, TodoStatus.DONE_FAILED] -TODO_RUNNING_STATUS = [TodoStatus.TODO, TodoStatus.RUNNING] +TODO_RUNNING_STATUS = [TodoStatus.TODO] class TicketStatus(str, StructuredEnum): """单据状态枚举""" PENDING = EnumField("PENDING", _("等待中")) + APPROVE = EnumField("APPROVE", _("待审批")) + RESOURCE_REPLENISH = EnumField("RESOURCE_REPLENISH", _("待补货")) + TODO = EnumField("TODO", _("待执行")) + TIMER = EnumField("TIMER", _("定时中")) RUNNING = EnumField("RUNNING", _("执行中")) - SUCCEEDED = EnumField("SUCCEEDED", _("成功")) + SUCCEEDED = EnumField("SUCCEEDED", _("已完成")) FAILED = EnumField("FAILED", _("失败")) - REVOKED = EnumField("REVOKED", _("撤销")) - TERMINATED = EnumField("TERMINATED", _("终止")) + REVOKED = EnumField("REVOKED", _("已撤销")) + TERMINATED = EnumField("TERMINATED", _("已终止")) + + +TICKET_RUNNING_STATUS = [ + TicketStatus.APPROVE, + TicketStatus.RESOURCE_REPLENISH, + TicketStatus.TODO, + TicketStatus.RUNNING, + TicketStatus.TIMER, +] +TICKET_FAILED_STATUS = [TicketStatus.REVOKED, TicketStatus.TERMINATED, TicketStatus.FAILED] class TicketFlowStatus(str, StructuredEnum): @@ -94,8 +115,8 @@ class TicketFlowStatus(str, StructuredEnum): SKIPPED = EnumField("SKIPPED", _("跳过")) -FLOW_FINISHED_STATUS = [TicketFlowStatus.SKIPPED, TicketStatus.SUCCEEDED] -FLOW_NOT_EXECUTE_STATUS = [TicketFlowStatus.SKIPPED, TicketStatus.PENDING] +FLOW_FINISHED_STATUS = [TicketFlowStatus.SKIPPED, TicketFlowStatus.SUCCEEDED] +FLOW_NOT_EXECUTE_STATUS = [TicketFlowStatus.SKIPPED, TicketFlowStatus.PENDING] BAMBOO_STATE__TICKET_STATE_MAP = { StateType.FINISHED.value: TicketFlowStatus.SUCCEEDED.value, @@ -178,7 +199,8 @@ def get_cluster_type_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"), _("集群维护")) @@ -190,7 +212,8 @@ def get_cluster_type_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 高可用清档"), _("数据处理")) @@ -203,7 +226,8 @@ def get_cluster_type_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) @@ -214,58 +238,97 @@ def get_cluster_type_by_ticket(cls, ticket_type): MYSQL_SLAVE_MIGRATE_UPGRADE = TicketEnumField("MYSQL_SLAVE_MIGRATE_UPGRADE", _("MySQL Slave 迁移升级"), _("版本升级")) MYSQL_RO_SLAVE_UNINSTALL = TicketEnumField("MYSQL_RO_SLAVE_UNINSTALL", _("MySQL非stanby 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_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) # 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执行"), _("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执行"), _("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 数据导出"), _("数据处理")) # 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) @@ -277,9 +340,12 @@ def get_cluster_type_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 集群重置"), _("集群维护")) @@ -287,7 +353,8 @@ def get_cluster_type_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) @@ -315,18 +382,22 @@ def get_cluster_type_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) @@ -334,8 +405,10 @@ def get_cluster_type_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集群域名重命名"), _("集群维护")) @@ -408,8 +481,10 @@ def get_cluster_type_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 全库备份"), _("备份")) @@ -417,7 +492,8 @@ def get_cluster_type_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节点数"), _("集群维护")) + MONGODB_REDUCE_SHARD_NODES = TicketEnumField("MONGODB_REDUCE_SHARD_NODES", _("MongoDB 缩容shard节点数"), + _("集群维护")) 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 实例重启"), _("集群管理")) @@ -425,7 +501,8 @@ def get_cluster_type_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 临时集群销毁"), _("集群维护")) @@ -632,6 +709,7 @@ class OperateNodeActionType(str, StructuredEnum): DISTRIBUTE = EnumField("DISTRIBUTE", _("派单")) DELIVER = EnumField("DELIVER", _("转单")) TERMINATE = EnumField("TERMINATE", _("终止节点和单据")) + WITHDRAW = EnumField("WITHDRAW", _("撤销单据")) class ItsmTicketNodeEnum(str, StructuredEnum): @@ -678,3 +756,13 @@ class TicketExpireType(str, StructuredEnum): FlowType.RESOURCE_APPLY: TicketExpireType.FLOW_TODO, FlowType.RESOURCE_BATCH_APPLY: TicketExpireType.FLOW_TODO, } + +# 根据流程类型来映射单据状态 +RUNNING_FLOW__TICKET_STATUS = { + FlowType.BK_ITSM: TicketStatus.APPROVE, + FlowType.RESOURCE_APPLY: TicketStatus.RESOURCE_REPLENISH, + FlowType.RESOURCE_BATCH_APPLY: TicketStatus.RESOURCE_REPLENISH, + FlowType.PAUSE: TicketStatus.TODO, + FlowType.INNER_FLOW: TicketStatus.RUNNING, + FlowType.TIMER: TicketStatus.TIMER, +} diff --git a/dbm-ui/backend/ticket/filters.py b/dbm-ui/backend/ticket/filters.py index 311dab5545..4565573c66 100644 --- a/dbm-ui/backend/ticket/filters.py +++ b/dbm-ui/backend/ticket/filters.py @@ -13,12 +13,14 @@ from django_filters import rest_framework as filters from backend.db_meta.models import Cluster +from backend.ticket.constants import TODO_RUNNING_STATUS from backend.ticket.models import ClusterOperateRecord, Ticket class TicketListFilter(filters.FilterSet): remark = filters.CharFilter(field_name="remark", lookup_expr="icontains", label=_("备注")) cluster = filters.CharFilter(field_name="cluster", method="filter_cluster", label=_("集群域名")) + todo = filters.CharFilter(field_name="todo", method="filter_todo", label=_("代办状态")) class Meta: model = Ticket @@ -35,3 +37,12 @@ def filter_cluster(self, queryset, name, value): clusters = Cluster.objects.filter(immute_domain__icontains=value).values_list("id", flat=True) records = ClusterOperateRecord.objects.filter(cluster_id__in=clusters).values_list("id", flat=True) return queryset.filter(clusteroperaterecord__in=records) + + def filter_todo(self, queryset, name, value): + user = self.request.user.username + if value == "running": + return queryset.filter( + todo_of_ticket__operators__contains=user, todo_of_ticket__status__in=TODO_RUNNING_STATUS + ) + else: + return queryset.filter(todo_of_ticket__done_by=user) diff --git a/dbm-ui/backend/ticket/flow_manager/base.py b/dbm-ui/backend/ticket/flow_manager/base.py index 15ce0f58ff..dd863b00a0 100644 --- a/dbm-ui/backend/ticket/flow_manager/base.py +++ b/dbm-ui/backend/ticket/flow_manager/base.py @@ -25,12 +25,13 @@ FLOW_FINISHED_STATUS, FLOW_NOT_EXECUTE_STATUS, FLOW_TYPE__EXPIRE_TYPE_CONFIG, + TICKET_EXPIRE_DEFAULT_CONFIG, FlowContext, FlowErrCode, FlowTypeConfig, TicketFlowStatus, ) -from backend.ticket.models import ClusterOperateRecord, Flow, InstanceOperateRecord, TicketFlowsConfig +from backend.ticket.models import ClusterOperateRecord, Flow, InstanceOperateRecord, TicketFlowsConfig, Todo logger = logging.getLogger("root") @@ -70,7 +71,7 @@ def status(self) -> str: if not self.flow_obj.flow_obj_id: # 任务流程未创建时未PENDING状态 - return constants.TicketStatus.PENDING + return constants.TicketFlowStatus.PENDING # 其他情况暂时认为在PENDING状态 return TicketFlowStatus.PENDING @@ -144,10 +145,11 @@ def flush_error_status_handler(self): def flush_revoke_status_handler(self, operator): """终止节点,更新相关状态和错误信息""" self.flow_obj.status = TicketFlowStatus.TERMINATED - self.flow_obj.err_code = FlowErrCode.GENERAL_ERROR if operator == DEFAULT_SYSTEM_USER: self.flow_obj.err_code = FlowErrCode.SYSTEM_TERMINATED_ERROR self.flow_obj.context = {FlowContext.EXPIRE_TIME: self.get_current_config_expire_time()} + else: + self.flow_obj.err_code = FlowErrCode.GENERAL_ERROR self.flow_obj.save(update_fields=["status", "err_code", "context", "update_at"]) # 更新操作者 self.ticket.updater = operator @@ -157,8 +159,9 @@ def get_current_config_expire_time(self): """获取当前配置的flow过期时间""" if self.flow_obj.flow_type not in FLOW_TYPE__EXPIRE_TYPE_CONFIG: return -1 - config = TicketFlowsConfig.get_config(ticket_type=self.ticket.ticket_type) - expire_time = config[FlowTypeConfig.EXPIRE_CONFIG][FLOW_TYPE__EXPIRE_TYPE_CONFIG[self.flow_obj.flow_type]] + config = TicketFlowsConfig.get_config(ticket_type=self.ticket.ticket_type).configs + expire_config = config.get(FlowTypeConfig.EXPIRE_CONFIG, TICKET_EXPIRE_DEFAULT_CONFIG) + expire_time = expire_config[FLOW_TYPE__EXPIRE_TYPE_CONFIG[self.flow_obj.flow_type]] return expire_time def create_operate_records(self, object_key, record_model, object_ids): @@ -259,4 +262,11 @@ def _retry(self) -> Any: self.run() def _revoke(self, operator) -> Any: + # 停止相关联的todo + from backend.ticket.todos import ActionType, TodoActorFactory + + todos = Todo.objects.filter(ticket=self.ticket, flow=self.flow_obj) + for todo in todos: + TodoActorFactory.actor(todo).process(operator, ActionType.TERMINATE, params={}) + # 刷新flow和单据状态 --> 终止 self.flush_revoke_status_handler(operator) diff --git a/dbm-ui/backend/ticket/flow_manager/delivery.py b/dbm-ui/backend/ticket/flow_manager/delivery.py index f5ac54089e..c07a453210 100644 --- a/dbm-ui/backend/ticket/flow_manager/delivery.py +++ b/dbm-ui/backend/ticket/flow_manager/delivery.py @@ -84,13 +84,14 @@ def _end_time(self) -> Optional[str]: return datetime2str(self.pre_flow_tree.updated_at) @property - def _summary(self) -> str: + def _summary(self) -> dict: if self.pre_flow_tree.status in FAILED_STATES: - return _("失败后继续提交") + return {"status": TicketFlowStatus.FAILED, "message": _("失败后继续提交")} elif self.pre_flow_tree.status in SUCCEED_STATES: - return _("执行成功") + return {"status": TicketFlowStatus.SUCCEEDED, "message": _("执行成功")} else: - return _("执行{}".format(StateType.get_choice_label(self.pre_flow_tree.status))) + state = StateType.get_choice_label(self.pre_flow_tree.status) + return {"status": TicketFlowStatus.RUNNING, "message": _("执行{}".format(state))} @property def _url(self) -> str: diff --git a/dbm-ui/backend/ticket/flow_manager/inner.py b/dbm-ui/backend/ticket/flow_manager/inner.py index c4d70ad206..fe0cd634cd 100644 --- a/dbm-ui/backend/ticket/flow_manager/inner.py +++ b/dbm-ui/backend/ticket/flow_manager/inner.py @@ -24,9 +24,17 @@ from backend.flow.models import FlowTree from backend.ticket import constants from backend.ticket.builders.common.base import fetch_cluster_ids -from backend.ticket.constants import BAMBOO_STATE__TICKET_STATE_MAP, FlowCallbackType, TicketType +from backend.ticket.constants import ( + BAMBOO_STATE__TICKET_STATE_MAP, + FlowCallbackType, + TicketFlowStatus, + TicketType, + TodoStatus, + TodoType, +) from backend.ticket.flow_manager.base import BaseTicketFlow -from backend.ticket.models import Flow +from backend.ticket.models import Flow, Todo +from backend.ticket.todos import BaseTodoContext from backend.utils.basic import generate_root_id from backend.utils.time import datetime2str @@ -82,22 +90,39 @@ def _end_time(self) -> Union[str, datetime]: @property def _summary(self) -> str: # TODO 可以给出具体失败的节点和原因 - return _("任务{status_display}").format(status_display=constants.TicketStatus.get_choice_label(self.status)) + return _("任务{status_display}").format(status_display=constants.TicketFlowStatus.get_choice_label(self.status)) @property def _status(self) -> str: - # 如果未找到流程树,则直接取flow_obj的status + # 查询流程树状态,如果未找到则直接取flow_obj的status if not self.flow_tree: return self.flow_obj.status + status = BAMBOO_STATE__TICKET_STATE_MAP.get(self.flow_tree.status, constants.TicketFlowStatus.RUNNING) - status = BAMBOO_STATE__TICKET_STATE_MAP.get(self.flow_tree.status, constants.TicketStatus.RUNNING) - self.flow_obj.update_status(status) - return status + # 如果任务失败,则变更todo状态 + todo_status = TodoStatus.TODO if status == TicketFlowStatus.FAILED else TodoStatus.DONE_SUCCESS + fail_todo = self.flow_obj.todo_of_flow.filter(type=TodoType.INNER_FAILED) + if fail_todo.exists() and fail_todo.first().status != todo_status: + fail_todo.update(status=todo_status) + + return self.flow_obj.update_status(status) @property def _url(self) -> str: return f"{env.BK_SAAS_HOST}/{self.ticket.bk_biz_id}/task-history/detail/{self.root_id}" + def create_inner_todo(self): + # 创建一条todo记录,在失败时变更为TODO状态 + Todo.objects.create( + name=_("【{}】单据任务执行失败,待处理").format(self.ticket.get_ticket_type_display()), + flow=self.flow_obj, + ticket=self.ticket, + type=TodoType.INNER_FAILED, + operators=[self.ticket.creator], + context=BaseTodoContext(self.flow_obj.id, self.ticket.id).to_dict(), + status=TodoStatus.DONE_SUCCESS, + ) + def check_exclusive_operations(self): """判断执行互斥""" # TODO: 目前来说,执行互斥对于同时提单或者同时重试的操作是防不住的。 @@ -122,10 +147,6 @@ def check_exclusive_operations(self): cluster_ids=cluster_ids, ticket_type=ticket_type, exclude_ticket_ids=[self.ticket.id] ) - def handle_exclusive_error(self): - """处理执行互斥后重试的逻辑""" - pass - def callback(self, callback_type: FlowCallbackType) -> None: """ inner节点独有的钩子函数,执行前置/后继流程节点动作 @@ -141,6 +162,7 @@ def run(self) -> None: # 获取or生成inner flow的root id root_id = self.flow_obj.flow_obj_id or generate_root_id() try: + self.create_inner_todo() # 由于 _run 执行后可能会触发信号,导致 current_flow 的误判,因此需提前写入 flow_obj_id self.run_status_handler(root_id) # 判断执行互斥 @@ -184,6 +206,15 @@ def _retry(self) -> Any: ) super()._retry() + def _revoke(self, operator) -> Any: + # 终止运行的pipeline + from backend.db_services.taskflow.handlers import TaskFlowHandler + + if FlowTree.objects.filter(root_id=self.flow_obj.flow_obj_id).exists(): + TaskFlowHandler(self.flow_obj.flow_obj_id).revoke_pipeline() + # 流转flow的终止状态 + super()._revoke(operator) + class QuickInnerFlow(InnerFlow): """ @@ -193,7 +224,7 @@ class QuickInnerFlow(InnerFlow): @property def _status(self) -> str: - return constants.TicketStatus.SUCCEEDED + return constants.TicketFlowStatus.SUCCEEDED @property def _summary(self) -> str: @@ -218,7 +249,7 @@ class IgnoreResultInnerFlow(InnerFlow): @property def _summary(self) -> str: return _("(执行结果可忽略)任务状态: {status_display}").format( - status_display=constants.TicketStatus.get_choice_label(self._raw_status) + status_display=constants.TicketFlowStatus.get_choice_label(self._raw_status) ) @property @@ -228,7 +259,7 @@ def _raw_status(self) -> str: @property def _status(self) -> str: status = self._raw_status - if status in [constants.TicketStatus.SUCCEEDED, constants.TicketStatus.REVOKED, constants.TicketStatus.FAILED]: - return constants.TicketStatus.SUCCEEDED + if status in [constants.TicketFlowStatus.SUCCEEDED, *constants.TICKET_FAILED_STATUS]: + return constants.TicketFlowStatus.SUCCEEDED return status diff --git a/dbm-ui/backend/ticket/flow_manager/itsm.py b/dbm-ui/backend/ticket/flow_manager/itsm.py index ff7d0d397a..a62dadc3dc 100644 --- a/dbm-ui/backend/ticket/flow_manager/itsm.py +++ b/dbm-ui/backend/ticket/flow_manager/itsm.py @@ -16,10 +16,11 @@ 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 +from backend.ticket.constants import FlowMsgStatus, FlowMsgType, TicketFlowStatus, TicketStatus, TodoStatus, TodoType from backend.ticket.flow_manager.base import BaseTicketFlow -from backend.ticket.models import Flow +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 @@ -56,24 +57,25 @@ def _end_time(self) -> Union[datetime, Any]: return self.flow_obj.update_at @property - def _summary(self) -> str: + def _summary(self) -> dict: try: logs = ItsmApi.get_ticket_logs({"sn": [self.flow_obj.flow_obj_id]}) except ApiResultError: return _("未知单据") + + # 获取单据审批状态 + current_status = self.ticket_approval_result["current_status"] + approve_result = self.ticket_approval_result["approve_result"] + summary = {"status": current_status, "approve_result": approve_result} + # 目前审批流程是固定的,取流程中第三个节点的日志作为概览即可 try: - return logs["logs"][2]["message"] + summary.update(operator=logs["logs"][2]["operator"], message=logs["logs"][2]["message"]) except (IndexError, KeyError): # 异常时根据状态取默认的概览 - status_summary_map = { - TicketStatus.RUNNING.value: _("审批中"), - TicketStatus.SUCCEEDED.value: _("已通过"), - TicketStatus.REVOKED.value: _("已撤销"), - TicketStatus.FAILED.value: _("被拒绝"), - TicketStatus.TERMINATED.value: _("已终止"), - } - return status_summary_map.get(self.status, "") + msg = TicketStatus.get_choice_label(self.status) + summary.update(operator=logs["logs"][-1]["operator"], status=self.status, message=msg) + return summary @property def _status(self) -> str: @@ -85,15 +87,19 @@ def _status(self) -> str: return self.flow_obj.update_status(TicketFlowStatus.RUNNING) # 撤单 elif current_status == ItsmTicketStatus.REVOKED: + self.flow_obj.todo_of_flow.update(status=TodoStatus.DONE_FAILED) return self.flow_obj.update_status(TicketFlowStatus.TERMINATED) # 审批通过 elif current_status == ItsmTicketStatus.FINISHED and approve_result: + self.flow_obj.todo_of_flow.update(status=TodoStatus.DONE_SUCCESS) return self.flow_obj.update_status(TicketFlowStatus.SUCCEEDED) # 审批拒绝 elif current_status == ItsmTicketStatus.FINISHED and not approve_result: + self.flow_obj.todo_of_flow.update(status=TodoStatus.DONE_FAILED) return self.flow_obj.update_status(TicketFlowStatus.TERMINATED) # 终止 elif current_status == ItsmTicketStatus.TERMINATED: + self.flow_obj.todo_of_flow.update(status=TodoStatus.DONE_FAILED) return self.flow_obj.update_status(TicketFlowStatus.TERMINATED) @property @@ -104,9 +110,20 @@ def _url(self) -> str: return "" def _run(self) -> str: + itsm_fields = {f["key"]: f["value"] for f in self.flow_obj.details["fields"]} + # 创建审批todo + operators = itsm_fields["approver"].split(",") + Todo.objects.create( + name=_("【{}】单据等待审批").format(self.ticket.get_ticket_type_display()), + flow=self.flow_obj, + ticket=self.ticket, + type=TodoType.ITSM, + operators=operators, + context=ItsmTodoContext(self.flow_obj.id, self.ticket.id).to_dict(), + ) + # 创建单据 data = ItsmApi.create_ticket(self.flow_obj.details) # 异步发送待审批消息 - itsm_fields = {f["key"]: f["value"] for f in self.flow_obj.details["fields"]} send_msg_for_flow.apply_async( kwargs={ "flow_id": self.flow_obj.id, @@ -117,3 +134,7 @@ def _run(self) -> str: } ) return data["sn"] + + def _revoke(self, operator) -> Any: + # 父类通过触发todo的终止可以终止itsm单据 + super()._revoke(operator) diff --git a/dbm-ui/backend/ticket/flow_manager/manager.py b/dbm-ui/backend/ticket/flow_manager/manager.py index c12118d2f5..8ac86ace95 100644 --- a/dbm-ui/backend/ticket/flow_manager/manager.py +++ b/dbm-ui/backend/ticket/flow_manager/manager.py @@ -89,23 +89,26 @@ def run_next_flow(self): def update_ticket_status(self): # 获取流程状态集合 - statuses = { - self.get_ticket_flow_cls(flow_type=flow.flow_type)(flow).status for flow in self.ticket.flows.all() + flow_status_map = { + self.get_ticket_flow_cls(flow_type=flow.flow_type)(flow).status: flow for flow in self.ticket.flows.all() } + statuses = set(flow_status_map.keys()) logger.info(f"update_ticket_status for ticket:{self.ticket.id}, statuses: {statuses}") + # 只要存在其中一个终止,则单据状态为已终止 if constants.TicketFlowStatus.TERMINATED in statuses: - # 只要存在其中一个终止,则单据状态为已终止 target_status = constants.TicketStatus.TERMINATED + # 只要存在其中一个失败,则单据状态为失败态 elif constants.TicketFlowStatus.FAILED in statuses: - # 只要存在其中一个失败,则单据状态为失败态 target_status = constants.TicketStatus.FAILED + # 只要存在其中一个撤销,则单据状态为撤销态 elif constants.TicketFlowStatus.REVOKED in statuses: - # 只要存在其中一个撤销,则单据状态为撤销态 target_status = constants.TicketStatus.REVOKED + # 只要有一个存在running,则需要根据flow的type决定单据的状态 elif constants.TicketFlowStatus.RUNNING in statuses: - target_status = constants.TicketStatus.RUNNING + flow = flow_status_map[constants.TicketFlowStatus.RUNNING] + target_status = constants.RUNNING_FLOW__TICKET_STATUS.get(flow.flow_type, constants.TicketStatus.RUNNING) + # 如果所有flow的状态处于完成态,则单据为成功 elif statuses.issubset(set(FLOW_FINISHED_STATUS)): - # 如果所有flow的状态处于完成态,则单据为成功 target_status = constants.TicketStatus.SUCCEEDED else: # 其他场景下状态未变更,无需更新DB diff --git a/dbm-ui/backend/ticket/flow_manager/pause.py b/dbm-ui/backend/ticket/flow_manager/pause.py index 1c1bb03a80..b643e87b9f 100644 --- a/dbm-ui/backend/ticket/flow_manager/pause.py +++ b/dbm-ui/backend/ticket/flow_manager/pause.py @@ -40,16 +40,15 @@ def _end_time(self) -> Optional[str]: @property def _summary(self) -> str: - return _("暂停状态{status_display}").format(status_display=constants.TicketStatus.get_choice_label(self.status)) + return _("暂停状态{status_display}").format( + status_display=constants.TicketFlowStatus.get_choice_label(self.status) + ) @property def _status(self) -> str: if self.ticket.todo_of_ticket.exist_unfinished(): - self.flow_obj.update_status(constants.TicketFlowStatus.RUNNING) - return constants.TicketStatus.RUNNING.value - - self.flow_obj.update_status(constants.TicketFlowStatus.SUCCEEDED) - return constants.TicketStatus.SUCCEEDED.value + return self.flow_obj.update_status(constants.TicketFlowStatus.RUNNING) + return self.flow_obj.update_status(constants.TicketFlowStatus.SUCCEEDED) @property def _url(self) -> str: diff --git a/dbm-ui/backend/ticket/flow_manager/resource.py b/dbm-ui/backend/ticket/flow_manager/resource.py index ee3b2f6012..db0d247d1e 100644 --- a/dbm-ui/backend/ticket/flow_manager/resource.py +++ b/dbm-ui/backend/ticket/flow_manager/resource.py @@ -58,7 +58,9 @@ def _end_time(self) -> Optional[str]: @property def _summary(self) -> str: - return _("资源申请状态{status_display}").format(status_display=constants.TicketStatus.get_choice_label(self.status)) + return _("资源申请状态{status_display}").format( + status_display=constants.TicketFlowStatus.get_choice_label(self.status) + ) @property def status(self) -> str: @@ -73,24 +75,24 @@ def update_flow_status(self, status): def _status(self) -> str: # 任务流程未创建时未PENDING状态 if not self.flow_obj.flow_obj_id: - return self.update_flow_status(constants.TicketStatus.PENDING.value) + return self.update_flow_status(constants.TicketFlowStatus.PENDING) # 如果资源申请成功,则直接返回success if self.resource_apply_status: - return self.update_flow_status(constants.TicketStatus.SUCCEEDED.value) + return self.update_flow_status(constants.TicketFlowStatus.SUCCEEDED) if self.flow_obj.err_msg: # 如果是其他情况引起的错误,则直接返回fail if not self.flow_obj.todo_of_flow.exists(): - return self.update_flow_status(constants.TicketStatus.FAILED.value) + return self.update_flow_status(constants.TicketFlowStatus.FAILED) # 如果是资源申请的todo状态,则判断todo是否完成 if self.ticket.todo_of_ticket.exist_unfinished(): - return self.update_flow_status(constants.TicketStatus.RUNNING.value) + return self.update_flow_status(constants.TicketFlowStatus.RUNNING) else: - return self.update_flow_status(constants.TicketStatus.SUCCEEDED.value) + return self.update_flow_status(constants.TicketFlowStatus.SUCCEEDED) # 其他情况认为还在RUNNING状态 - return self.update_flow_status(constants.TicketStatus.RUNNING.value) + return self.update_flow_status(constants.TicketFlowStatus.RUNNING) @property def _url(self) -> str: diff --git a/dbm-ui/backend/ticket/flow_manager/timer.py b/dbm-ui/backend/ticket/flow_manager/timer.py index 6031cd1f94..8dfdae4d8d 100644 --- a/dbm-ui/backend/ticket/flow_manager/timer.py +++ b/dbm-ui/backend/ticket/flow_manager/timer.py @@ -56,7 +56,7 @@ def _summary(self) -> str: return _("定时时间{},已超时{},需手动触发。暂停状态:{}").format( self.trigger_time, countdown2str(run_time - trigger_time), - constants.TicketStatus.get_choice_label(self.status), + constants.TicketFlowStatus.get_choice_label(self.status), ) now = datetime.now(timezone.utc) @@ -68,19 +68,17 @@ def _summary(self) -> str: @property def _status(self) -> str: trigger_time = str2datetime(self.trigger_time) + # 还未到定时节点,返回pending if self.expired_flag is None: - return constants.TicketStatus.PENDING.value - + return constants.TicketFlowStatus.PENDING.value + # 已过期,但是todo未处理,则返回running if self.expired_flag and self.ticket.todo_of_ticket.exist_unfinished(): - self.flow_obj.update_status(constants.TicketStatus.RUNNING.value) - return constants.TicketStatus.RUNNING.value - + return self.flow_obj.update_status(constants.TicketFlowStatus.RUNNING.value) + # 触发时间晚于当前时间,则返回running if trigger_time > datetime.now(timezone.utc): - self.flow_obj.update_status(constants.TicketStatus.RUNNING.value) - return constants.TicketStatus.RUNNING.value - - self.flow_obj.update_status(constants.TicketStatus.SUCCEEDED.value) - return constants.TicketStatus.SUCCEEDED.value + return self.flow_obj.update_status(constants.TicketFlowStatus.RUNNING.value) + # 其他情况说明已触发,返回succeed + return self.flow_obj.update_status(constants.TicketFlowStatus.SUCCEEDED.value) @property def _url(self) -> str: diff --git a/dbm-ui/backend/ticket/handler.py b/dbm-ui/backend/ticket/handler.py index 010f7f8a14..b1e6190f1a 100644 --- a/dbm-ui/backend/ticket/handler.py +++ b/dbm-ui/backend/ticket/handler.py @@ -11,6 +11,7 @@ import itertools import json import logging +import time from typing import Dict, List from django.db import transaction @@ -29,16 +30,18 @@ from backend.ticket.constants import ( FLOW_FINISHED_STATUS, ITSM_FIELD_NAME__ITSM_KEY, - FlowType, + RUNNING_FLOW__TICKET_STATUS, FlowTypeConfig, OperateNodeActionType, TicketFlowStatus, + TicketStatus, TicketType, ) from backend.ticket.exceptions import TicketFlowsConfigException from backend.ticket.flow_manager.manager import TicketFlowManager from backend.ticket.models import Flow, Ticket, TicketFlowsConfig, Todo -from backend.ticket.todos import ActionType, TodoActorFactory +from backend.ticket.serializers import TodoSerializer +from backend.ticket.todos import TodoActorFactory logger = logging.getLogger("root") @@ -208,9 +211,12 @@ def get_itsm_fields(cls, sample_sn=None): # 如果未入库,则获取任意一个ticket的信息来初始化key if not approval_key or not remark_key: - ticket_info_response = ItsmApi.get_ticket_info(params={"sn": sample_sn}) - for field in ticket_info_response["fields"]: + ticket_info = ItsmApi.get_ticket_info(params={"sn": sample_sn}) + for field in ticket_info["fields"]: + if not field["name"] in ITSM_FIELD_NAME__ITSM_KEY: + continue SystemSettings.insert_setting_value(key=ITSM_FIELD_NAME__ITSM_KEY[field["name"]], value=field["key"]) + return cls.get_itsm_fields(sample_sn) return {SystemSettingsEnum.ITSM_APPROVAL_KEY: approval_key, SystemSettingsEnum.ITSM_REMARK_KEY: remark_key} @@ -225,16 +231,23 @@ def approve_itsm_ticket(cls, ticket_id, action, operator, **kwargs): return state_id = itsm_info["current_steps"][0]["state_id"] + act_msg_tpl = _("{}对单据{}操作: {}").format(operator, ticket_id, OperateNodeActionType.get_choice_label(action)) + act_msg = kwargs.get("action_message") or act_msg_tpl + # 审批单据 + params = {"action_message": act_msg} if action == OperateNodeActionType.TRANSITION: is_approved = kwargs["is_approved"] - fields = [{"key": field, "value": json.dumps(is_approved)} for field in cls.get_itsm_fields(sn).values()] - params = {"sn": sn, "state_id": state_id, "action_type": action, "operator": operator, "fields": fields} + itsm_fields = cls.get_itsm_fields(sn) + fields = [ + {"key": itsm_fields[SystemSettingsEnum.ITSM_APPROVAL_KEY], "value": json.dumps(is_approved)}, + {"key": itsm_fields[SystemSettingsEnum.ITSM_REMARK_KEY], "value": act_msg}, + ] + params.update(sn=sn, state_id=state_id, action_type=action, operator=operator, fields=fields) ItsmApi.operate_node(params, use_admin=True) - # 终止单据 - elif action == OperateNodeActionType.TERMINATE: - action_message = _("{} 终止了此单据").format(operator) - params = {"sn": sn, "action_type": action, "operator": operator, "action_message": action_message} + # 终止/撤销单据 + elif action in [OperateNodeActionType.TERMINATE, OperateNodeActionType.WITHDRAW]: + params.update(sn=sn, action_type=action, operator=operator) ItsmApi.operate_ticket(params, use_admin=True) return sn @@ -255,8 +268,8 @@ def revoke_ticket(cls, ticket_ids, operator): - 找到第一个非成功的flow 设置为终止 - 如果有关联正在运行的todos,也设置为终止 """ - # 查询ticket,关联正在运行的flows(这里定义的"运行"指的就是非成功) - finished_status = [*FLOW_FINISHED_STATUS, Flow, TicketFlowStatus.TERMINATED] + # 查询ticket,关联正在运行的flows(这里定义的"运行"指的就是非成功/终止撤销) + finished_status = [*FLOW_FINISHED_STATUS, TicketFlowStatus.TERMINATED, TicketFlowStatus.REVOKED] running_flows = Flow.objects.filter(ticket__in=ticket_ids).exclude(status__in=finished_status) tickets = Ticket.objects.prefetch_related( Prefetch("flows", queryset=running_flows, to_attr="running_flows") @@ -265,22 +278,28 @@ def revoke_ticket(cls, ticket_ids, operator): # 对每个单据进行终止 for ticket in tickets: if not ticket.running_flows: - logger.info(_("单据[{}]没有需要终止的流程,跳过...").format(ticket.id)) continue - first_running_flow = ticket.running_flows[0] - # 如果有todo,则把所有todo终止 - todos = Todo.objects.filter(ticket=ticket, flow=first_running_flow) - for todo in todos: - TodoActorFactory.actor(todo).process(operator, ActionType.TERMINATE, params={}) + first_running_flow = ticket.running_flows[0] + cls.operate_flow(ticket.id, first_running_flow.id, func="revoke", operator=operator) + logger.info(_("操作人[{}]终止了单据[{}]").format(operator, ticket.id)) - # 如果是处于审批阶段,需要关闭itsm单据 - if first_running_flow.flow_type == FlowType.BK_ITSM: - cls.approve_itsm_ticket(ticket.id, OperateNodeActionType.TERMINATE, "admin", is_approved=False) + @classmethod + def batch_process_todo(cls, user, action, operations): + """ + 批量操作todo + @param user 用户 + @param action 动作 + @param operations: todo列表,每个item包含todo id和params + """ - # 用户终止 / 系统终止flow - logger.info(_("操作人[{}]终止了单据[{}]").format(operator, ticket.id)) - cls.operate_flow(ticket.id, first_running_flow.id, func="revoke", operator=operator) + results = [] + for operation in operations: + todo_id, params = operation["todo_id"], operation["params"] + todo = Todo.objects.get(id=todo_id) + TodoActorFactory.actor(todo).process(user, action, params) + results.append(todo) + return TodoSerializer(results, many=True).data @classmethod def create_ticket_flow_config(cls, bk_biz_id, cluster_ids, ticket_types, configs, operator): @@ -400,3 +419,20 @@ def query_ticket_flows_describe(cls, bk_biz_id, db_type, ticket_types=None): flow_desc_list.append(flow_config_info) return flow_desc_list + + @classmethod + def ticket_status_standardization(cls): + """ + 旧单据状态标准化。TODO: 迁移后此段代码可删除 + """ + # 标准化只针对running的单据,其他状态单据不影响 + running_tickets = Ticket.objects.filter(status=TicketStatus.RUNNING) + batch = 50 + + for current in range(0, running_tickets.count(), batch): + for ticket in running_tickets[current : current + batch]: + raw_status = ticket.status + ticket.status = RUNNING_FLOW__TICKET_STATUS[ticket.current_flow().flow_type] + ticket.save() + print(f"ticket[{ticket.id}] status {raw_status} ---> {ticket.status}") + time.sleep(1) diff --git a/dbm-ui/backend/ticket/models/ticket.py b/dbm-ui/backend/ticket/models/ticket.py index cea46db296..08e26537eb 100644 --- a/dbm-ui/backend/ticket/models/ticket.py +++ b/dbm-ui/backend/ticket/models/ticket.py @@ -24,6 +24,7 @@ from backend.db_monitor.exceptions import AutofixException from backend.ticket.constants import ( EXCLUSIVE_TICKET_EXCEL_PATH, + TICKET_RUNNING_STATUS, FlowRetryType, FlowType, TicketFlowStatus, @@ -116,13 +117,13 @@ class Meta: def url(self): return f"{env.BK_SAAS_HOST}/{self.bk_biz_id}/ticket-manage/index?id={self.id}" - def set_terminated(self): - self.status = TicketStatus.TERMINATED + def set_status(self, status): + self.status = status self.save() def get_cost_time(self): # 计算耗时 - if self.status in [TicketStatus.PENDING, TicketStatus.RUNNING]: + if self.status in [TicketStatus.PENDING, *TICKET_RUNNING_STATUS]: return calculate_cost_time(timezone.now(), self.create_at) return calculate_cost_time(self.update_at, self.create_at) @@ -265,11 +266,17 @@ def get_cluster_configs(cls, ticket_type, bk_biz_id, cluster_ids): ] return cluster_configs + @classmethod + def get_config(cls, ticket_type): + """获取平台配置""" + global_cfg = cls.objects.get(bk_biz_id=PLAT_BIZ_ID, ticket_type=ticket_type) + return global_cfg + class ClusterOperateRecordManager(models.Manager): def filter_actives(self, cluster_id, *args, **kwargs): """获得集群正在运行的单据记录""" - return self.filter(cluster_id=cluster_id, ticket__status=TicketFlowStatus.RUNNING, *args, **kwargs) + return self.filter(cluster_id=cluster_id, ticket__status=TicketStatus.RUNNING, *args, **kwargs) def filter_inner_actives(self, cluster_id, *args, **kwargs): """获取集群正在运行的inner flow的单据记录。此时认为集群会在互斥阶段""" @@ -349,7 +356,7 @@ def summary(self): def get_cluster_records_map(cls, cluster_ids: List[int]): """获取集群与操作记录之间的映射关系""" records = cls.objects.prefetch_related("ticket").filter( - cluster_id__in=cluster_ids, ticket__status=TicketFlowStatus.RUNNING + cluster_id__in=cluster_ids, ticket__status__in=TICKET_RUNNING_STATUS ) cluster_operate_records_map: Dict[int, List] = defaultdict(list) for record in records: @@ -371,7 +378,7 @@ class InstanceOperateRecordManager(models.Manager): def filter_actives(self, instance_id, **kwargs): return self.filter( instance_id=instance_id, - ticket__status__in=[TicketStatus.RUNNING, TicketStatus.PENDING], + ticket__status=TicketStatus.RUNNING, **kwargs, ) @@ -413,9 +420,9 @@ def summary(self): @classmethod def get_instance_records_map(cls, instance_ids: List[Union[int, str]]): - """获取实例与操作记录之间的映射关系""" + """获取实例与操作记录之间的映射关系??????""" records = InstanceOperateRecord.objects.select_related("ticket").filter( - instance_id__in=instance_ids, ticket__status=TicketStatus.RUNNING + instance_id__in=instance_ids, ticket__status__in=TICKET_RUNNING_STATUS ) instance_operator_record_map: Dict[int, List] = defaultdict(list) for record in records: diff --git a/dbm-ui/backend/ticket/models/todo.py b/dbm-ui/backend/ticket/models/todo.py index 77478bd41d..3c8df63e71 100644 --- a/dbm-ui/backend/ticket/models/todo.py +++ b/dbm-ui/backend/ticket/models/todo.py @@ -17,7 +17,15 @@ from backend import env from backend.bk_web.constants import LEN_MIDDLE, LEN_SHORT from backend.bk_web.models import AuditedModel -from backend.ticket.constants import FlowMsgStatus, FlowMsgType, TicketFlowStatus, TodoStatus, TodoType +from backend.ticket.constants import ( + TODO_RUNNING_STATUS, + FlowMsgStatus, + FlowMsgType, + TicketFlowStatus, + TicketStatus, + TodoStatus, + TodoType, +) from backend.ticket.tasks.ticket_tasks import send_msg_for_flow logger = logging.getLogger("root") @@ -25,7 +33,7 @@ class TodoManager(models.Manager): def exist_unfinished(self): - return self.filter(status__in=[TodoStatus.TODO, TodoStatus.RUNNING]).exists() + return self.filter(status__in=TODO_RUNNING_STATUS).exists() def create(self, **kwargs): todo = super().create(**kwargs) @@ -91,7 +99,7 @@ def set_success(self, username, action): def set_terminated(self, username, action): self.set_status(username, TodoStatus.DONE_FAILED) - self.ticket.set_terminated() + self.ticket.set_status(status=TicketStatus.TERMINATED) self.flow.update_status(TicketFlowStatus.TERMINATED) TodoHistory.objects.create(creator=username, todo=self, action=action) diff --git a/dbm-ui/backend/ticket/serializers.py b/dbm-ui/backend/ticket/serializers.py index 70a9686266..1c0f229ad2 100644 --- a/dbm-ui/backend/ticket/serializers.py +++ b/dbm-ui/backend/ticket/serializers.py @@ -23,7 +23,15 @@ from backend.core.encrypt.handlers import AsymmetricHandler from backend.ticket import mock_data from backend.ticket.builders import BuilderFactory -from backend.ticket.constants import CountType, FlowType, TicketStatus, TicketType, TodoStatus +from backend.ticket.constants import ( + TICKET_RUNNING_STATUS, + TODO_RUNNING_STATUS, + FlowType, + TicketFlowStatus, + TicketStatus, + TicketType, + TodoStatus, +) from backend.ticket.flow_manager.manager import TicketFlowManager from backend.ticket.models import Flow, Ticket, Todo from backend.ticket.todos import ActionType @@ -118,7 +126,7 @@ def get_status_display(self, obj): return obj.get_status_display() def get_cost_time(self, obj): - if obj.status in [TicketStatus.PENDING, TicketStatus.RUNNING]: + if obj.status in [TicketStatus.PENDING, *TICKET_RUNNING_STATUS]: return calculate_cost_time(timezone.now(), obj.create_at) return calculate_cost_time(obj.update_at, obj.create_at) @@ -164,7 +172,7 @@ def get_end_time(self, obj): def get_cost_time(self, obj): start_time = strptime(self.get_start_time(obj)) end_time = strptime(self.get_end_time(obj)) - if self.get_status(obj) in [TicketStatus.PENDING, TicketStatus.RUNNING]: + if self.get_status(obj) in [TicketFlowStatus.PENDING, TicketFlowStatus.RUNNING]: return calculate_cost_time(timezone.now(), start_time) return calculate_cost_time(end_time, start_time) @@ -215,7 +223,7 @@ class TodoSerializer(serializers.ModelSerializer): cost_time = serializers.SerializerMethodField(help_text=_("耗时")) def get_cost_time(self, obj): - if obj.status in [TodoStatus.TODO, TodoStatus.RUNNING]: + if obj.status in TODO_RUNNING_STATUS: return calculate_cost_time(timezone.now(), obj.create_at) return calculate_cost_time(obj.done_at, obj.create_at) @@ -266,10 +274,6 @@ class GetTodosSLZ(serializers.Serializer): ) -class CountTicketSLZ(serializers.Serializer): - count_type = serializers.ChoiceField(help_text=_("类型"), choices=CountType.get_choices(), default=CountType.MY_TODO) - - class ClusterModifyOpSerializer(serializers.Serializer): cluster_id = serializers.IntegerField(help_text=_("集群ID")) start_time = serializers.DateTimeField(help_text=_("查询起始时间"), required=False) @@ -383,3 +387,11 @@ def validate(self, attrs): if todo_id not in existing_todo_ids: raise serializers.ValidationError(_("待办id{}不存在".format(attrs["todo_id"]))) return attrs + + +class BatchTicketOperateSerializer(serializers.Serializer): + action = serializers.ChoiceField( + choices=[ActionType.APPROVE.value, ActionType.TERMINATE.value], help_text=_("统一动作") + ) + ticket_ids = serializers.ListField(help_text=_("单据ID列表"), child=serializers.IntegerField()) + params = serializers.JSONField(help_text=_("动作参数"), required=False, default={}) diff --git a/dbm-ui/backend/ticket/tasks/ticket_tasks.py b/dbm-ui/backend/ticket/tasks/ticket_tasks.py index da66832c42..36d130897d 100644 --- a/dbm-ui/backend/ticket/tasks/ticket_tasks.py +++ b/dbm-ui/backend/ticket/tasks/ticket_tasks.py @@ -45,7 +45,6 @@ TodoType, ) from backend.ticket.exceptions import TicketTaskTriggerException -from backend.ticket.flow_manager.inner import InnerFlow from backend.ticket.models.ticket import Flow, Ticket, TicketFlowsConfig from backend.utils.time import datetime2str @@ -69,6 +68,8 @@ def run_next_flow(self) -> None: @classmethod def retry_exclusive_inner_flow(cls) -> None: """重试互斥错误的inner flow""" + from backend.ticket.flow_manager.inner import InnerFlow + to_retry_flows = Flow.objects.filter(err_code=FlowErrCode.AUTO_EXCLUSIVE_ERROR) if not to_retry_flows: return diff --git a/dbm-ui/backend/ticket/todos/__init__.py b/dbm-ui/backend/ticket/todos/__init__.py index bafffa53a9..ab1430c15c 100644 --- a/dbm-ui/backend/ticket/todos/__init__.py +++ b/dbm-ui/backend/ticket/todos/__init__.py @@ -16,6 +16,8 @@ from django.utils.translation import ugettext_lazy as _ +from backend.constants import DEFAULT_SYSTEM_USER +from backend.ticket.exceptions import TodoWrongOperatorException from backend.ticket.models import Todo from blue_krill.data_types.enum import EnumField, StructuredEnum @@ -38,7 +40,12 @@ def name(cls): return cls.__name__ def process(self, username, action, params): - """处理操作""" + if username not in self.todo.operators and username != DEFAULT_SYSTEM_USER: + raise TodoWrongOperatorException(_("{}不在处理人: {}中,无法处理").format(username, self.todo.operators)) + self._process(username, action, params) + + def _process(self, username, action, params): + """处理操作的具体实现""" raise NotImplementedError @@ -98,7 +105,6 @@ class ActionType(str, StructuredEnum): APPROVE = EnumField("APPROVE", _("确认执行")) TERMINATE = EnumField("TERMINATE", _("终止单据")) - RESOURCE_REAPPLY = EnumField("RESOURCE_REAPPLY", _("资源重新申请")) @dataclass diff --git a/dbm-ui/backend/ticket/todos/itsm_todo.py b/dbm-ui/backend/ticket/todos/itsm_todo.py new file mode 100644 index 0000000000..1e6600022b --- /dev/null +++ b/dbm-ui/backend/ticket/todos/itsm_todo.py @@ -0,0 +1,68 @@ +# -*- 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 dataclasses import dataclass + +from backend.ticket import todos +from backend.ticket.constants import OperateNodeActionType, TodoType +from backend.ticket.todos import ActionType, BaseTodoContext + +logger = logging.getLogger("root") + + +@dataclass +class ItsmTodoContext(BaseTodoContext): + pass + + +@todos.TodoActorFactory.register(TodoType.ITSM) +class ItsmTodo(todos.TodoActor): + """来自审批中的待办""" + + def process(self, username, action, params): + # itsm的todo允许本人操作 + if username == self.todo.ticket.creator: + self._process(username, action, params) + super().process(username, action, params) + + def _process(self, username, action, params): + from backend.ticket.handler import TicketHandler + + ticket_id = self.context.get("ticket_id") + own = self.todo.ticket.creator + message = params.get("remark", "") + + # 审批人终止,认为是拒单 + if action == ActionType.TERMINATE and username != own: + TicketHandler.approve_itsm_ticket( + ticket_id, + action=OperateNodeActionType.TRANSITION, + operator=username, + is_approved=False, + action_message=message, + ) + self.todo.set_terminated(username, action) + # 自己终止,认为是撤单 + elif action == ActionType.TERMINATE and username == own: + TicketHandler.approve_itsm_ticket( + ticket_id, action=OperateNodeActionType.WITHDRAW, operator=username, action_message=message + ) + self.todo.set_terminated(username, action) + # 只允许审批人通过 + elif action == ActionType.APPROVE and username != own: + TicketHandler.approve_itsm_ticket( + ticket_id, + action=OperateNodeActionType.TRANSITION, + operator=username, + is_approved=True, + action_message=message, + ) + self.todo.set_success(username, action) diff --git a/dbm-ui/backend/ticket/views.py b/dbm-ui/backend/ticket/views.py index 9b66b6e7a4..59a457ca1a 100644 --- a/dbm-ui/backend/ticket/views.py +++ b/dbm-ui/backend/ticket/views.py @@ -12,7 +12,7 @@ from functools import reduce from django.db import transaction -from django.db.models import Q +from django.db.models import Count, Q from django.utils.translation import ugettext_lazy as _ from drf_yasg.utils import swagger_auto_schema from rest_framework import serializers, status @@ -22,7 +22,9 @@ from backend import env from backend.bk_web import viewsets +from backend.bk_web.pagination import AuditedLimitOffsetPagination from backend.bk_web.swagger import PaginatedResponseSwaggerAutoSchema, common_swagger_auto_schema +from backend.configuration.constants import DBType from backend.configuration.models import DBAdministrator from backend.db_services.ipchooser.query.resource import ResourceQueryHelper from backend.iam_app.dataclass import ResourceEnum @@ -38,29 +40,27 @@ from backend.ticket.builders import BuilderFactory from backend.ticket.builders.common.base import InfluxdbTicketFlowBuilderPatchMixin, fetch_cluster_ids from backend.ticket.constants import ( - TODO_DONE_STATUS, + TICKET_RUNNING_STATUS, + TODO_RUNNING_STATUS, CountType, - OperateNodeActionType, TicketStatus, TicketType, - TodoStatus, + TodoType, ) from backend.ticket.contexts import TicketContext from backend.ticket.exceptions import TicketDuplicationException from backend.ticket.filters import TicketListFilter from backend.ticket.flow_manager.manager import TicketFlowManager from backend.ticket.handler import TicketHandler -from backend.ticket.models import ClusterOperateRecord, InstanceOperateRecord, Ticket, TicketFlowsConfig, Todo +from backend.ticket.models import ClusterOperateRecord, InstanceOperateRecord, Ticket, TicketFlowsConfig from backend.ticket.serializers import ( - BatchApprovalSerializer, + BatchTicketOperateSerializer, BatchTodoOperateSerializer, ClusterModifyOpSerializer, - CountTicketSLZ, CreateTicketFlowConfigSerializer, DeleteTicketFlowConfigSerializer, FastCreateCloudComponentSerializer, GetNodesSLZ, - GetTodosSLZ, InstanceModifyOpSerializer, ListTicketStatusSerializer, QueryTicketFlowDescribeSerializer, @@ -77,7 +77,6 @@ UpdateTicketFlowConfigSerializer, ) from backend.ticket.todos import TodoActorFactory -from backend.utils.batch_request import request_multi_thread TICKET_TAG = "ticket" @@ -90,6 +89,7 @@ class TicketViewSet(viewsets.AuditedModelViewSet): queryset = Ticket.objects.all() serializer_class = TicketSerializer filter_class = TicketListFilter + pagination_class = AuditedLimitOffsetPagination def _get_custom_permissions(self): # 创建单据,关联单据类型的动作 @@ -114,14 +114,13 @@ def _get_custom_permissions(self): elif self.action in ["update_ticket_flow_config", "create_ticket_flow_config", "delete_ticket_flow_config"]: return ticket_flows_config_permission(self.action, self.request) # 对于处理todo的接口,可以不用鉴权,todo本身会判断是否是确认人 - elif self.action in ["process_todo", "batch_process_todo"]: + elif self.action in ["process_todo", "batch_process_todo", "batch_process_ticket"]: return [] # 其他非敏感GET接口,不鉴权 elif self.action in [ "list", "flow_types", "get_nodes", - "get_todo_tickets", "get_tickets_count", "query_ticket_flow_describe", "list_ticket_status", @@ -138,6 +137,19 @@ def _get_login_exempt_view_func(cls): # 需要豁免的接口方法与名字 return {"post": [cls.callback.__name__], "put": [], "get": [], "delete": []} + @classmethod + def _get_self_manage_tickets(cls, user): + # 超级管理员返回所有单据 + if user.username in env.ADMIN_USERS or user.is_superuser: + return Ticket.objects.all() + # 获取user管理的单据合集 + manage_filters = [ + Q(group=manage.db_type) & Q(bk_biz_id=manage.bk_biz_id) if manage.bk_biz_id else Q(group=manage.db_type) + for manage in DBAdministrator.objects.filter(users__contains=user.username) + ] + ticket_filter = Q(creator=user.username) | reduce(operator.or_, manage_filters or [Q()]) + return Ticket.objects.filter(ticket_filter) + def get_queryset(self): """ 单据queryset规则--针对list: @@ -152,16 +164,8 @@ def get_queryset(self): # 只返回自己创建的单据 if self_manage == 0: return Ticket.objects.filter(creator=username) - # 超级管理员返回所有单据 - if username in env.ADMIN_USERS or self.request.user.is_superuser: - return Ticket.objects.all() # 返回自己管理的组件单据 - manage_filters = [ - Q(group=manage.db_type) & Q(bk_biz_id=manage.bk_biz_id) if manage.bk_biz_id else Q(group=manage.db_type) - for manage in DBAdministrator.objects.filter(users__contains=username) - ] - ticket_filter = Q(creator=username) | reduce(operator.or_, manage_filters or [Q()]) - return Ticket.objects.filter(ticket_filter) + return self._get_self_manage_tickets(self.request.user) def get_serializer_context(self): context = super(TicketViewSet, self).get_serializer_context() @@ -171,28 +175,28 @@ def get_serializer_context(self): context["ticket_ctx"] = TicketContext() return context - def _verify_duplicate_ticket(self, ticket_type, details, user): - """校验是否重复提交""" + @staticmethod + def _verify_influxdb_duplicate_ticket(ticket_type, details, user, active_tickets): + current_instances = InfluxdbTicketFlowBuilderPatchMixin.get_instances(ticket_type, details) + for ticket in active_tickets: + active_instances = ticket.details["instances"] + duplicate_ids = list(set(active_instances).intersection(current_instances)) + if duplicate_ids: + raise TicketDuplicationException( + context=_("实例{}已存在相同类型的单据[{}]正在运行,请确认是否重复提交").format(duplicate_ids, ticket.id), + data={"duplicate_instance_ids": duplicate_ids, "duplicate_ticket_id": ticket.id}, + ) - active_tickets = self.get_queryset().filter(ticket_type=ticket_type, status=TicketStatus.RUNNING, creator=user) + def verify_duplicate_ticket(self, ticket_type, details, user): + """校验是否重复提交""" + active_tickets = self.get_queryset().filter( + ticket_type=ticket_type, status__in=TICKET_RUNNING_STATUS, creator=user + ) # influxdb 相关操作单独适配,这里暂时没有找到更好的写法,唯一的改进就是创建单据时,会提前提取出对比内容,比如instances - if ticket_type in [ - TicketType.INFLUXDB_ENABLE, - TicketType.INFLUXDB_DISABLE, - TicketType.INFLUXDB_REBOOT, - TicketType.INFLUXDB_DESTROY, - TicketType.INFLUXDB_REPLACE, - ]: - current_instances = InfluxdbTicketFlowBuilderPatchMixin.get_instances(ticket_type, details) - for ticket in active_tickets: - active_instances = ticket.details["instances"] - duplicate_ids = list(set(active_instances).intersection(current_instances)) - if duplicate_ids: - raise TicketDuplicationException( - context=_("实例{}已存在相同类型的单据[{}]正在运行,请确认是否重复提交").format(duplicate_ids, ticket.id), - data={"duplicate_instance_ids": duplicate_ids, "duplicate_ticket_id": ticket.id}, - ) + # TODO: 后续这段逻辑待删除,influxdb已经弃用 + if ticket_type in TicketType.get_ticket_type_by_db(DBType.InfluxDB): + self._verify_influxdb_duplicate_ticket(ticket_type, details, user, active_tickets) return cluster_ids = fetch_cluster_ids(details=details) @@ -210,7 +214,7 @@ def perform_create(self, serializer): ignore_duplication = self.request.data.get("ignore_duplication") or False # 如果不允许忽略重复提交,则进行校验 if not ignore_duplication: - self._verify_duplicate_ticket(ticket_type, self.request.data["details"], self.request.user.username) + self.verify_duplicate_ticket(ticket_type, self.request.data["details"], self.request.user.username) with transaction.atomic(): # 设置单据类别 TODO: 这里会请求两次数据库,是否考虑group参数让前端传递 @@ -381,46 +385,6 @@ def get_nodes(self, request, *args, **kwargs): return Response(hosts) - @common_swagger_auto_schema( - operation_summary=_("待办单据列表"), - query_serializer=GetTodosSLZ(), - tags=[TICKET_TAG], - ) - @Permission.decorator_permission_field( - id_field=lambda d: d["id"], - data_field=lambda d: d["results"], - actions=[ActionEnum.TICKET_VIEW], - resource_meta=ResourceEnum.TICKET, - ) - @action(methods=["GET"], detail=False, serializer_class=GetTodosSLZ) - def get_todo_tickets(self, request, *args, **kwargs): - """待办视图单据列表""" - - # 获取我的待办 - validated_data = self.params_validate(self.get_serializer_class()) - todo_status = validated_data.get("todo_status") - my_todos = Todo.objects.filter(operators__contains=request.user.username) - - # 状态筛选:已处理/未处理 - if todo_status in TODO_DONE_STATUS: - my_todos = my_todos.filter(status__in=TODO_DONE_STATUS) - elif todo_status: - my_todos = my_todos.filter(status=todo_status) - - # 复用全局过滤器 - tickets = self.filter_queryset(self.get_queryset()) - - # 关联查询单据 - my_todo_tickets = tickets.filter(id__in=my_todos.values_list("ticket_id")) - context = self.get_serializer_context() - - # 分页处理 - page = self.paginate_queryset(my_todo_tickets) - serializer = TicketSerializer(page, many=True, context=context) - resp = self.get_paginated_response(serializer.data) - resp.data["results"] = TicketHandler.add_related_object(resp.data["results"]) - return resp - @swagger_auto_schema( operation_summary=_("待办处理"), request_body=TodoOperateSerializer(), @@ -444,26 +408,43 @@ def process_todo(self, request, *args, **kwargs): @common_swagger_auto_schema( operation_summary=_("待办单据数"), - query_serializer=CountTicketSLZ(), tags=[TICKET_TAG], ) - @action(methods=["GET"], detail=False, serializer_class=CountTicketSLZ) + @action(methods=["GET"], detail=False, filter_class=None, pagination_class=None) def get_tickets_count(self, request, *args, **kwargs): - validated_data = self.params_validate(self.get_serializer_class()) - count_type = validated_data.get("count_type") - - # 待办单数量 - if count_type == CountType.MY_TODO: - my_todos = Todo.objects.filter(status=TodoStatus.TODO, operators__contains=request.user.username) - tickets = self.filter_queryset(self.get_queryset()) - my_tickets = tickets.filter(id__in=my_todos.values_list("ticket_id")) - else: - # 申请单数量 - my_tickets = Ticket.objects.filter( - creator=request.user.username, status__in=[TicketStatus.RUNNING, TicketStatus.PENDING] + """ + 获取单据的数量,目前需要获取 + - 我的申请 + - (代办)待我审批、待我确认,待我补货 + - 我的已办 + - 我负责的业务 + """ + user = request.user.username + tickets = self._get_self_manage_tickets(request.user) + count_map = {count_type: 0 for count_type in CountType.get_values()} + + # 我负责的业务 + count_map[CountType.SELF_MANAGE] = tickets.count() + # 我的申请 + count_map[CountType.MY_APPROVE] = tickets.filter(creator=user).count() + # 我的代办 + todo_status = [TicketStatus.APPROVE, TicketStatus.TODO, TicketStatus.RESOURCE_REPLENISH, TicketStatus.FAILED] + my_todo = ( + tickets.filter( + status__in=todo_status, + todo_of_ticket__operators__contains=user, + todo_of_ticket__status__in=TODO_RUNNING_STATUS, ) + .values("status") + .annotate(count=Count("status")) + .order_by("status") + ) + for todo in my_todo: + count_map[todo["status"]] = todo["count"] + # 我的已办 + count_map[CountType.DONE] = tickets.filter(todo_of_ticket__done_by=user).count() - return Response(my_tickets.count()) + return Response(count_map) @common_swagger_auto_schema( operation_summary=_("查询集群变更单据事件"), @@ -610,49 +591,42 @@ def fast_create_cloud_component(self, request, *args, **kwargs): TicketHandler.fast_create_cloud_component_method(bk_biz_id, bk_cloud_id, ips, request.user.username) return Response() - @common_swagger_auto_schema( - operation_summary=_("批量审批"), - request_body=BatchApprovalSerializer(), + @swagger_auto_schema( + operation_summary=_("批量待办处理"), + request_body=BatchTodoOperateSerializer(), + responses={status.HTTP_200_OK: TodoSerializer(many=True)}, tags=[TICKET_TAG], ) - @action(methods=["POST"], detail=False, serializer_class=BatchApprovalSerializer) - def batch_approval(self, request, *args, **kwargs): + @action(methods=["POST"], detail=False, serializer_class=BatchTodoOperateSerializer) + def batch_process_todo(self, request, *args, **kwargs): """ - sns: 单号集合 - is_approved: 是否审批通过 + 批量处理待办: 返回处理后的待办列表 """ data = self.params_validate(self.get_serializer_class()) - ticket_ids, is_approved = data["ticket_ids"], data["is_approved"] - user, itsm_action = request.user.username, OperateNodeActionType.TRANSITION - params_list = [ - {"ticket_id": ticket, "action": itsm_action, "is_approved": is_approved, "operator": user} - for ticket in ticket_ids - ] - request_multi_thread(TicketHandler.approve_itsm_ticket, params_list) - return Response() + user = request.user.username + return Response(TicketHandler.batch_process_todo(user=user, **data)) @swagger_auto_schema( - operation_summary=_("批量待办处理"), - request_body=BatchTodoOperateSerializer(), + operation_summary=_("批量单据待办处理"), + request_body=BatchTicketOperateSerializer(), responses={status.HTTP_200_OK: TodoSerializer(many=True)}, tags=[TICKET_TAG], ) - @action(methods=["POST"], detail=False, serializer_class=BatchTodoOperateSerializer) - def batch_process_todo(self, request, *args, **kwargs): + @action(methods=["POST"], detail=False, serializer_class=BatchTicketOperateSerializer) + def batch_process_ticket(self, request, *args, **kwargs): """ - 批量处理待办: 返回处理后的待办列表 + 批量处理单据的待办,处理单据的第一个todo + 根据todo的类型可以触发不同的factor函数 """ - validated_data = self.params_validate(self.get_serializer_class()) - act = validated_data["action"] - - # 批量处理待办操作 - results = [] - for operation in validated_data["operations"]: - todo_id = operation["todo_id"] - params = operation["params"] - todo = Todo.objects.get(id=todo_id) - TodoActorFactory.actor(todo).process(request.user.username, act, params) - results.append(todo) - - # 使用 TodoSerializer 序列化响应数据 - return Response(TodoSerializer(results, many=True).data) + 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))