From b3aaf335e8afc873ee6acbfe632e6c13b0b8040f Mon Sep 17 00:00:00 2001 From: iSecloud <869820505@qq.com> Date: Thu, 22 Aug 2024 15:18:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E5=8D=95=E6=8D=AE=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E8=AE=BE=E7=BD=AE=E6=94=AF=E6=8C=81=E9=9B=86=E7=BE=A4?= =?UTF-8?q?=E7=BB=B4=E5=BA=A6=20#6749?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dbm-ui/backend/iam_app/dataclass/actions.py | 14 +- .../iam_app/handlers/drf_perm/ticket.py | 36 +++- dbm-ui/backend/iam_app/handlers/permission.py | 9 +- .../iam_app/migration_json_files/initial.json | 202 +++++++++++++----- dbm-ui/backend/ticket/builders/__init__.py | 20 +- .../builders/mysql/mysql_import_sqlfile.py | 1 - .../redis/redis_toolbox_proxy_scale_down.py | 3 +- .../tendbcluster/tendb_import_sqlfile.py | 1 - dbm-ui/backend/ticket/exceptions.py | 6 + dbm-ui/backend/ticket/handler.py | 133 +++++++++++- .../migrations/0011_auto_20240821_1846.py | 27 +++ dbm-ui/backend/ticket/models/ticket.py | 34 ++- dbm-ui/backend/ticket/serializers.py | 21 +- dbm-ui/backend/ticket/views.py | 83 +++---- 14 files changed, 453 insertions(+), 137 deletions(-) create mode 100644 dbm-ui/backend/ticket/migrations/0011_auto_20240821_1846.py diff --git a/dbm-ui/backend/iam_app/dataclass/actions.py b/dbm-ui/backend/iam_app/dataclass/actions.py index a6a9562954..0dd4657fbe 100644 --- a/dbm-ui/backend/iam_app/dataclass/actions.py +++ b/dbm-ui/backend/iam_app/dataclass/actions.py @@ -152,9 +152,9 @@ class ActionEnum: common_labels=[CommonActionLabel.BIZ_MAINTAIN], ) - TICKET_CONFIG_SET = ActionMeta( + GLOBAL_TICKET_CONFIG_SET = ActionMeta( id="ticket_config_set", - name=_("单据流程设置"), + name=_("全局单据流程设置"), name_en="ticket_config_set", type="edit", related_actions=[GLOBAL_MANAGE.id], @@ -163,6 +163,16 @@ class ActionEnum: hidden=True, ) + BIZ_TICKET_CONFIG_SET = ActionMeta( + id="biz_ticket_config_set", + name=_("业务单据流程设置"), + name_en="biz_ticket_config_set", + type="edit", + related_actions=[DB_MANAGE.id], + related_resource_types=[ResourceEnum.BUSINESS, ResourceEnum.DBTYPE], + group=_("业务配置"), + ) + RESOURCE_MANAGE = ActionMeta( id="resource_manage", name=_("资源管理访问"), diff --git a/dbm-ui/backend/iam_app/handlers/drf_perm/ticket.py b/dbm-ui/backend/iam_app/handlers/drf_perm/ticket.py index 3253c67f00..c754179a94 100644 --- a/dbm-ui/backend/iam_app/handlers/drf_perm/ticket.py +++ b/dbm-ui/backend/iam_app/handlers/drf_perm/ticket.py @@ -20,6 +20,7 @@ from backend.iam_app.dataclass.actions import ActionEnum from backend.iam_app.dataclass.resources import ResourceEnum from backend.iam_app.handlers.drf_perm.base import ( + BizDBTypeResourceActionPermission, IAMPermission, MoreResourceActionPermission, RejectPermission, @@ -29,7 +30,7 @@ from backend.ticket.builders.common.base import fetch_cluster_ids from backend.ticket.constants import TicketType from backend.ticket.exceptions import ApprovalWrongOperatorException -from backend.ticket.models import Ticket +from backend.ticket.models import Ticket, TicketFlowsConfig from backend.utils.basic import get_target_items_from_details logger = logging.getLogger("root") @@ -172,3 +173,36 @@ def has_permission(self, request, view): ) return True + + +def ticket_flows_config_permission(action, request): + dbtype_cov = TicketType.get_db_type_by_ticket + permission: IAMPermission = None + + if action in ["update_ticket_flow_config", "create_ticket_flow_config"]: + if request.data.get("bk_biz_id"): + permission = BizDBTypeResourceActionPermission( + [ActionEnum.BIZ_TICKET_CONFIG_SET], + instance_biz_getter=lambda req, view: [req.data["bk_biz_id"]], + instance_dbtype_getter=lambda req, view: list(set([dbtype_cov(d) for d in req.data["ticket_types"]])), + ) + else: + permission = ResourceActionPermission( + [ActionEnum.GLOBAL_TICKET_CONFIG_SET], + ResourceEnum.DBTYPE, + instance_ids_getter=lambda req, view: [req.data["bk_biz_id"]], + ) + elif action == "delete_ticket_flow_config": + configs = list(TicketFlowsConfig.objects.filter(id__in=request.data["config_ids"])) + groups, bk_biz_ids = [c.group for c in configs], [c.bk_biz_id for c in configs] + # 只允许一个业务下的一种db类型 + if len(set(groups)) > 1 or len(set(bk_biz_ids)) > 1: + permission = RejectPermission() + else: + permission = BizDBTypeResourceActionPermission( + [ActionEnum.BIZ_TICKET_CONFIG_SET], + instance_biz_getter=lambda req, view: bk_biz_ids, + instance_dbtype_getter=lambda req, view: groups, + ) + + return [permission] diff --git a/dbm-ui/backend/iam_app/handlers/permission.py b/dbm-ui/backend/iam_app/handlers/permission.py index 5e4a8d67f5..8223cf6ca3 100644 --- a/dbm-ui/backend/iam_app/handlers/permission.py +++ b/dbm-ui/backend/iam_app/handlers/permission.py @@ -579,11 +579,10 @@ def insert_external_permission_field( ) # 填充权限字段 - if isinstance(response.data, list): - response.data = [{"permission": permission_result, **d} for d in response.data] - elif isinstance(response.data, dict): - response.data.setdefault("permission", {}) - response.data["permission"].update(permission_result) + data_list = response.data if isinstance(response.data, list) else [response.data] + for data in data_list: + data.setdefault("permission", {}) + data["permission"].update(permission_result) return response diff --git a/dbm-ui/backend/iam_app/migration_json_files/initial.json b/dbm-ui/backend/iam_app/migration_json_files/initial.json index 6499c0d1ec..6d385817ab 100644 --- a/dbm-ui/backend/iam_app/migration_json_files/initial.json +++ b/dbm-ui/backend/iam_app/migration_json_files/initial.json @@ -1015,7 +1015,7 @@ "operation": "upsert_action", "data": { "id": "ticket_config_set", - "name": "单据流程设置", + "name": "全局单据流程设置", "name_en": "ticket_config_set", "type": "edit", "related_resource_types": [ @@ -1038,10 +1038,52 @@ "hidden": true, "group": "全局设置", "subgroup": "", - "description": "单据流程设置", + "description": "全局单据流程设置", "description_en": "ticket_config_set" } }, + { + "operation": "upsert_action", + "data": { + "id": "biz_ticket_config_set", + "name": "业务单据流程设置", + "name_en": "biz_ticket_config_set", + "type": "edit", + "related_resource_types": [ + { + "system_id": "bk_cmdb", + "id": "biz", + "selection_mode": "instance", + "related_instance_selections": [ + { + "system_id": "bk_cmdb", + "id": "business" + } + ] + }, + { + "system_id": "bk_dbm", + "id": "dbtype", + "selection_mode": "instance", + "related_instance_selections": [ + { + "system_id": "bk_dbm", + "id": "dbtype_list" + } + ] + } + ], + "related_actions": [ + "db_manage" + ], + "version": 1, + "hidden": false, + "group": "业务配置", + "subgroup": "", + "description": "业务单据流程设置", + "description_en": "biz_ticket_config_set" + } + }, { "operation": "upsert_action", "data": { @@ -6827,7 +6869,7 @@ "operation": "upsert_action", "data": { "id": "tendbcluster_data_migrate", - "name": "TenDB Cluster 数据迁移", + "name": "TenDB Cluster DB克隆", "name_en": "TENDBCLUSTER_DATA_MIGRATE", "type": "execute", "related_resource_types": [ @@ -6848,7 +6890,7 @@ "hidden": false, "group": "TenDBCluster", "subgroup": "数据处理", - "description": "TenDB Cluster 数据迁移", + "description": "TenDB Cluster DB克隆", "description_en": "TENDBCLUSTER_DATA_MIGRATE" } }, @@ -7174,9 +7216,38 @@ { "operation": "upsert_action", "data": { - "id": "sqlserver_data_migrate", - "name": "SQLServer 数据迁移", - "name_en": "SQLSERVER_DATA_MIGRATE", + "id": "sqlserver_full_migrate", + "name": "SQLServer 全备迁移", + "name_en": "SQLSERVER_FULL_MIGRATE", + "type": "execute", + "related_resource_types": [ + { + "system_id": "bk_dbm", + "id": "sqlserver", + "selection_mode": "all", + "related_instance_selections": [ + { + "system_id": "bk_dbm", + "id": "sqlserver_list" + } + ] + } + ], + "related_actions": [], + "version": 1, + "hidden": false, + "group": "SQLServer", + "subgroup": "数据处理", + "description": "SQLServer 全备迁移", + "description_en": "SQLSERVER_FULL_MIGRATE" + } + }, + { + "operation": "upsert_action", + "data": { + "id": "sqlserver_incr_migrate", + "name": "SQLServer 增量迁移", + "name_en": "SQLSERVER_INCR_MIGRATE", "type": "execute", "related_resource_types": [ { @@ -7196,8 +7267,8 @@ "hidden": false, "group": "SQLServer", "subgroup": "数据处理", - "description": "SQLServer 数据迁移", - "description_en": "SQLSERVER_DATA_MIGRATE" + "description": "SQLServer 增量迁移", + "description_en": "SQLSERVER_INCR_MIGRATE" } }, { @@ -9708,57 +9779,13 @@ } ] }, - { - "name": "资源管理", - "name_en": "资源管理", - "actions": [ - { - "id": "resource_manage" - } - ], - "sub_groups": [ - { - "name": "资源池", - "name_en": "资源池", - "actions": [ - { - "id": "resource_pool_manage" - }, - { - "id": "resource_operation_view" - } - ] - }, - { - "name": "污点池", - "name_en": "污点池", - "actions": [ - { - "id": "dirty_pool_manage" - } - ] - }, - { - "name": "资源规格", - "name_en": "资源规格", - "actions": [ - { - "id": "spec_create" - }, - { - "id": "spec_update" - }, - { - "id": "spec_delete" - } - ] - } - ] - }, { "name": "业务配置", "name_en": "业务配置", "actions": [ + { + "id": "biz_ticket_config_set" + }, { "id": "dba_administrator_edit" }, @@ -9820,6 +9847,53 @@ } ] }, + { + "name": "资源管理", + "name_en": "资源管理", + "actions": [ + { + "id": "resource_manage" + } + ], + "sub_groups": [ + { + "name": "资源池", + "name_en": "资源池", + "actions": [ + { + "id": "resource_pool_manage" + }, + { + "id": "resource_operation_view" + } + ] + }, + { + "name": "污点池", + "name_en": "污点池", + "actions": [ + { + "id": "dirty_pool_manage" + } + ] + }, + { + "name": "资源规格", + "name_en": "资源规格", + "actions": [ + { + "id": "spec_create" + }, + { + "id": "spec_update" + }, + { + "id": "spec_delete" + } + ] + } + ] + }, { "name": "MySQL", "name_en": "MySQL", @@ -10824,7 +10898,10 @@ "id": "sqlserver_clear_dbs" }, { - "id": "sqlserver_data_migrate" + "id": "sqlserver_full_migrate" + }, + { + "id": "sqlserver_incr_migrate" }, { "id": "sqlserver_rollback" @@ -11527,7 +11604,10 @@ "id": "sqlserver_reset" }, { - "id": "sqlserver_data_migrate" + "id": "sqlserver_full_migrate" + }, + { + "id": "sqlserver_incr_migrate" }, { "id": "sqlserver_rollback" @@ -12830,7 +12910,11 @@ "required": true }, { - "id": "sqlserver_data_migrate", + "id": "sqlserver_full_migrate", + "required": true + }, + { + "id": "sqlserver_incr_migrate", "required": true }, { diff --git a/dbm-ui/backend/ticket/builders/__init__.py b/dbm-ui/backend/ticket/builders/__init__.py index ae7023ca60..f7ceeb8015 100644 --- a/dbm-ui/backend/ticket/builders/__init__.py +++ b/dbm-ui/backend/ticket/builders/__init__.py @@ -306,19 +306,27 @@ def enabled(cls) -> bool: """ return True + @property + def ticket_configs(self): + if not hasattr(self, "_ticket_configs"): + from backend.ticket.builders.common.base import fetch_cluster_ids + + cluster_ids = fetch_cluster_ids(self.ticket.details) + configs = TicketFlowsConfig.get_cluster_configs(self.ticket_type, self.ticket.bk_biz_id, cluster_ids) + setattr(self, "_ticket_configs", configs) + return getattr(self, "_ticket_configs") + @property def need_itsm(self): """是否需要itsm审批节点。后续默认从单据配置表获取。子类可覆写,覆写以后editable为False""" - assert self.ticket_type is not None, "Please make sure FlowBuilder set the ticket type! " - config = TicketFlowsConfig.get_config(ticket_type=self.ticket_type, bk_biz_id=self.ticket.bk_biz_id) - return config["need_itsm"] + need_itsm = any([c.configs["need_itsm"] for c in self.ticket_configs]) + return need_itsm @property def need_manual_confirm(self): """是否需要人工确认节点。后续默认从单据配置表获取。子类可覆写,覆写以后editable为False""" - assert self.ticket_type is not None, "Please make sure FlowBuilder set the ticket type! " - config = TicketFlowsConfig.get_config(ticket_type=self.ticket_type, bk_biz_id=self.ticket.bk_biz_id) - return config["need_manual_confirm"] + need_manual_confirm = any([c.configs["need_manual_confirm"] for c in self.ticket_configs]) + return need_manual_confirm @property def need_resource_pool(self): diff --git a/dbm-ui/backend/ticket/builders/mysql/mysql_import_sqlfile.py b/dbm-ui/backend/ticket/builders/mysql/mysql_import_sqlfile.py index 3a28c5bc99..67132d87c7 100644 --- a/dbm-ui/backend/ticket/builders/mysql/mysql_import_sqlfile.py +++ b/dbm-ui/backend/ticket/builders/mysql/mysql_import_sqlfile.py @@ -106,7 +106,6 @@ def format_ticket_data(self): @builders.BuilderFactory.register(TicketType.MYSQL_IMPORT_SQLFILE) class MysqlSqlImportFlowBuilder(BaseMySQLTicketFlowBuilder): serializer = MysqlSqlImportDetailSerializer - editable = False # 定义流程所用到的cls,方便继承复用 itsm_flow_builder = MysqlSqlImportItsmParamBuilder backup_flow_builder = MysqlSqlImportBackUpFlowParamBuilder diff --git a/dbm-ui/backend/ticket/builders/redis/redis_toolbox_proxy_scale_down.py b/dbm-ui/backend/ticket/builders/redis/redis_toolbox_proxy_scale_down.py index bca3a16477..04bcc82e4d 100644 --- a/dbm-ui/backend/ticket/builders/redis/redis_toolbox_proxy_scale_down.py +++ b/dbm-ui/backend/ticket/builders/redis/redis_toolbox_proxy_scale_down.py @@ -14,7 +14,6 @@ from backend.db_meta.models import Cluster from backend.flow.engine.controller.redis import RedisController -from backend.iam_app.dataclass.actions import ActionEnum from backend.ticket import builders from backend.ticket.builders.common.base import HostInfoSerializer, SkipToRepresentationMixin, fetch_cluster_ids from backend.ticket.builders.redis.base import BaseRedisTicketFlowBuilder, ClusterValidateMixin @@ -58,7 +57,7 @@ def format_ticket_data(self): super().format_ticket_data() -@builders.BuilderFactory.register(TicketType.REDIS_PROXY_SCALE_DOWN, iam=ActionEnum.REDIS_PROXY_SCALE_DOWN) +@builders.BuilderFactory.register(TicketType.REDIS_PROXY_SCALE_DOWN) class ProxyScaleDownFlowBuilder(BaseRedisTicketFlowBuilder): serializer = ProxyScaleDownDetailSerializer inner_flow_builder = ProxyScaleDownParamBuilder diff --git a/dbm-ui/backend/ticket/builders/tendbcluster/tendb_import_sqlfile.py b/dbm-ui/backend/ticket/builders/tendbcluster/tendb_import_sqlfile.py index b0c3f2a4c5..3e94888ccd 100644 --- a/dbm-ui/backend/ticket/builders/tendbcluster/tendb_import_sqlfile.py +++ b/dbm-ui/backend/ticket/builders/tendbcluster/tendb_import_sqlfile.py @@ -58,7 +58,6 @@ def format_ticket_data(self): @builders.BuilderFactory.register(TicketType.TENDBCLUSTER_IMPORT_SQLFILE) class TenDBClusterSqlImportFlowBuilder(MysqlSqlImportFlowBuilder, BaseTendbTicketFlowBuilder): serializer = TenDBClusterSqlImportDetailSerializer - editable = False # 定义流程所用到的cls,方便继承复用 itsm_flow_builder = TenDBClusterSqlImportItsmParamBuilder backup_flow_builder = TenDBClusterSqlImportBackUpFlowParamBuilder diff --git a/dbm-ui/backend/ticket/exceptions.py b/dbm-ui/backend/ticket/exceptions.py index 3e989180c5..db5482a9d5 100644 --- a/dbm-ui/backend/ticket/exceptions.py +++ b/dbm-ui/backend/ticket/exceptions.py @@ -64,3 +64,9 @@ class ApprovalWrongOperatorException(TicketBaseException): ERROR_CODE = "008" MESSAGE = _("审批处理异常") MESSAGE_TPL = _("审批处理异常{username}") + + +class TicketFlowsConfigException(TicketBaseException): + ERROR_CODE = "008" + MESSAGE = _("单据流程设置失败") + MESSAGE_TPL = _("单据流程{ticket_type}设置失败") diff --git a/dbm-ui/backend/ticket/handler.py b/dbm-ui/backend/ticket/handler.py index 81dba36c83..5e5970d462 100644 --- a/dbm-ui/backend/ticket/handler.py +++ b/dbm-ui/backend/ticket/handler.py @@ -8,17 +8,21 @@ 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 itertools import json import logging from typing import Dict, List -from django.db.models import Prefetch +from django.db import transaction +from django.db.models import Prefetch, Q +from django.forms import model_to_dict from django.utils.translation import ugettext as _ from backend import env from backend.components import ItsmApi from backend.configuration.constants import PLAT_BIZ_ID, SystemSettingsEnum from backend.configuration.models import SystemSettings +from backend.db_meta.models import Cluster from backend.db_services.ipchooser.handlers.host_handler import HostHandler from backend.ticket.builders import BuilderFactory from backend.ticket.builders.common.base import fetch_cluster_ids, fetch_instance_ids @@ -31,6 +35,7 @@ TicketFlowStatus, 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 @@ -163,7 +168,16 @@ def _get_base_info(host): @classmethod def ticket_flow_config_init(cls): """初始化单据配置""" - exist_ticket_types = list(TicketFlowsConfig.objects.all().values_list("ticket_type", flat=True)) + exist_flow_configs = TicketFlowsConfig.objects.all() + exist_ticket_types = [config.ticket_type for config in exist_flow_configs] + + # 删除不存在的单据流程 + deleted_configs = [ + config.id for config in exist_flow_configs if config.ticket_type not in BuilderFactory.registry.keys() + ] + TicketFlowsConfig.objects.filter(id__in=deleted_configs).delete() + + # 创建新单据类型流程 created_configs = [ TicketFlowsConfig( bk_biz_id=PLAT_BIZ_ID, @@ -267,3 +281,118 @@ def revoke_ticket(cls, ticket_ids, operator): # 用户终止 / 系统终止flow logger.info(_("操作人[{}]终止了单据[{}]").format(operator, ticket.id)) cls.operate_flow(ticket.id, first_running_flow.id, func="revoke", operator=operator) + + @classmethod + def create_ticket_flow_config(cls, bk_biz_id, cluster_ids, ticket_types, configs, operator): + """ + 创建单据流程 + @param bk_biz_id: 业务ID,为0表示平台业务 + @param cluster_ids: 集群ID列表,表示规则生效的集群范围 + @param ticket_types: 单据类型列表 + @param configs: 流程配置 + @param operator: 创建者 + """ + + def check_create_config(ticket_type): + if not bk_biz_id: + raise TicketFlowsConfigException(_("不允许新增平台级别的流程设置")) + + global_config = TicketFlowsConfig.objects.get(bk_biz_id=0, ticket_type=ticket_type) + biz_configs = TicketFlowsConfig.objects.filter(bk_biz_id=bk_biz_id, ticket_type=ticket_type) + + if configs["need_manual_confirm"] != global_config.configs["need_manual_confirm"]: + raise TicketFlowsConfigException(_("业务级别不允许编辑[人工确认]设置")) + + biz_cfg = biz_configs.filter(cluster_ids=[]).first() + cluster_cfg = biz_configs.exclude(cluster_ids=[]).first() + + # 不允许创建相同维度的流程 + if biz_cfg and not cluster_ids: + raise TicketFlowsConfigException(_("业务[{}]已存在{}的流程配置").format(bk_biz_id, ticket_type)) + if cluster_cfg and cluster_ids: + raise TicketFlowsConfigException(_("业务[{}]已存在{}的集群流程配置").format(bk_biz_id, ticket_type)) + # 新创建的流程,不能和生效流程的配置冲突 + effect_flows = [biz_cfg or global_config, cluster_cfg] + for ef in effect_flows: + if ef and ef.configs["need_itsm"] == configs["need_itsm"]: + raise TicketFlowsConfigException(_("业务[{}]已存在{}的相同范围配置").format(bk_biz_id, ticket_type)) + + flows_config_list = [] + for type in ticket_types: + # 校验创建单据流程配置是否合理 + check_create_config(type) + # 创建流程规则 + group = TicketType.get_db_type_by_ticket(type) + flows_config = TicketFlowsConfig( + bk_biz_id=bk_biz_id, + cluster_ids=cluster_ids, + ticket_type=type, + group=group, + configs=configs, + creator=operator, + updater=operator, + ) + flows_config_list.append(flows_config) + + TicketFlowsConfig.objects.bulk_create(flows_config_list) + + @classmethod + def update_ticket_flow_config(cls, bk_biz_id, cluster_ids, ticket_types, configs, config_ids, operator): + """ + 更新单据流程 + @param bk_biz_id: 业务ID,为0表示平台业务 + @param cluster_ids: 集群ID列表,表示规则生效的集群范围 + @param ticket_types: 单据类型列表 + @param configs: 流程配置 + @param config_ids: 更新的流程ID列表 + @param operator: 更新人 + """ + cluster_ids = cluster_ids or [] + config_ids = config_ids or [] + + config_qs = TicketFlowsConfig.objects.filter(bk_biz_id=bk_biz_id, ticket_type__in=ticket_types) + # 平台全局配置直接更新 + if not bk_biz_id: + config_qs.update(configs=configs) + return + + # 业务级别先删除,再创建,可以复用校验流程 + with transaction.atomic(): + config_qs.filter(id__in=config_ids).delete() + cls.create_ticket_flow_config(bk_biz_id, cluster_ids, ticket_types, configs, operator) + + @classmethod + def query_ticket_flows_describe(cls, bk_biz_id, db_type, ticket_types=None): + # 根据条件过滤单据配置 + config_filter = Q(bk_biz_id__in=[bk_biz_id, PLAT_BIZ_ID], group=db_type, editable=True) + if ticket_types: + config_filter &= Q(ticket_type__in=ticket_types) + ticket_flow_configs = TicketFlowsConfig.objects.filter(config_filter) + + # 获得单据flow配置映射表和集群映射表 + flow_config_map = {config.ticket_type: config.configs for config in ticket_flow_configs} + cluster_ids = list(itertools.chain(*ticket_flow_configs.values_list("cluster_ids", flat=True))) + clusters_map = {c.id: c for c in Cluster.objects.filter(id__in=cluster_ids)} + + # 获取单据流程配置信息 + flow_desc_list: List[Dict] = [] + for flow_config in ticket_flow_configs: + # 获取当前单据的执行流程描述 + flow_desc = BuilderFactory.registry[flow_config.ticket_type].describe_ticket_flows(flow_config_map) + # 获取集群的描述 + cluster_info = [ + {"cluster_id": clusters_map[c].id, "immute_domain": clusters_map[c].immute_domain} + for c in flow_config.cluster_ids + if c in clusters_map + ] + # 获取配置的基本信息 + flow_config_info = model_to_dict(flow_config) + flow_config_info.update( + ticket_type_display=flow_config.get_ticket_type_display(), + flow_desc=flow_desc, + clusters=cluster_info, + update_at=flow_config.update_at, + ) + flow_desc_list.append(flow_config_info) + + return flow_desc_list diff --git a/dbm-ui/backend/ticket/migrations/0011_auto_20240821_1846.py b/dbm-ui/backend/ticket/migrations/0011_auto_20240821_1846.py new file mode 100644 index 0000000000..09ab11ba2a --- /dev/null +++ b/dbm-ui/backend/ticket/migrations/0011_auto_20240821_1846.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.25 on 2024-08-21 10:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("ticket", "0010_flow_context"), + ] + + operations = [ + migrations.AddField( + model_name="ticketflowsconfig", + name="cluster_ids", + field=models.JSONField(default=list, verbose_name="集群ID列表"), + ), + migrations.AlterField( + model_name="ticketflowsconfig", + name="editable", + field=models.BooleanField(default=True, verbose_name="是否支持用户配置"), + ), + migrations.AddIndex( + model_name="ticketflowsconfig", + index=models.Index(fields=["bk_biz_id"], name="ticket_tick_bk_biz__593de9_idx"), + ), + ] diff --git a/dbm-ui/backend/ticket/models/ticket.py b/dbm-ui/backend/ticket/models/ticket.py index 9fbbd39c8a..1f8f3e08f1 100644 --- a/dbm-ui/backend/ticket/models/ticket.py +++ b/dbm-ui/backend/ticket/models/ticket.py @@ -233,23 +233,37 @@ class TicketFlowsConfig(AuditedModel): """ bk_biz_id = models.IntegerField(_("业务ID"), default=0) - ticket_type = models.CharField(_("单据类型"), choices=TicketType.get_choices(), max_length=128) + cluster_ids = models.JSONField(_("集群ID列表"), default=list) group = models.CharField(_("单据分组类型"), choices=DBType.get_choices(), max_length=LEN_NORMAL) - editable = models.BooleanField(_("是否支持用户配置")) + ticket_type = models.CharField(_("单据类型"), choices=TicketType.get_choices(), max_length=128) + editable = models.BooleanField(_("是否支持用户配置"), default=True) configs = models.JSONField(_("单据配置 eg: {'need_itsm': false, 'need_manual_confirm': false}"), default=dict) class Meta: verbose_name_plural = verbose_name = _("单据流程配置(TicketFlowsConfig)") - indexes = [models.Index(fields=["group"])] + indexes = [models.Index(fields=["group"]), models.Index(fields=["bk_biz_id"])] @classmethod - def get_config(cls, ticket_type, bk_biz_id=PLAT_BIZ_ID): - """获取单据类型的配置""" - # 优先获取业务的单据配置,如果业务配置没有则获取平台的 - try: - return cls.objects.get(bk_biz_id=bk_biz_id, ticket_type=ticket_type).configs - except cls.DoesNotExist: - return cls.objects.get(bk_biz_id=PLAT_BIZ_ID, ticket_type=ticket_type).configs + def get_cluster_configs(cls, ticket_type, bk_biz_id, cluster_ids): + """获取集群生效的流程配置""" + # 流程优先级:集群维度 > 业务维度 > 平台维度 + # 全局配置 + global_cfg = cls.objects.get(bk_biz_id=PLAT_BIZ_ID, ticket_type=ticket_type) + # 业务配置和集群配置 + biz_configs = cls.objects.filter(bk_biz_id=bk_biz_id, ticket_type=ticket_type) + biz_cfg = biz_configs.filter(cluster_ids=[]).first() or global_cfg + cluster_cfg = biz_configs.exclude(cluster_ids=[]).first() or biz_cfg + + # 单据不涉及集群,则返回业务/平台配置 + if not cluster_ids: + return [biz_cfg] + + # 业务或集群配置最多共存一个 + cluster_configs = [ + cluster_cfg if cluster_cfg and cluster_id in cluster_cfg.cluster_ids else biz_cfg + for cluster_id in cluster_ids + ] + return cluster_configs class ClusterOperateRecordManager(models.Manager): diff --git a/dbm-ui/backend/ticket/serializers.py b/dbm-ui/backend/ticket/serializers.py index 4d6305db69..14e6793d60 100644 --- a/dbm-ui/backend/ticket/serializers.py +++ b/dbm-ui/backend/ticket/serializers.py @@ -291,26 +291,33 @@ class QueryTicketFlowDescribeSerializer(serializers.Serializer): db_type = serializers.ChoiceField(help_text=_("单据分组类型"), choices=DBType.get_choices()) ticket_types = serializers.CharField(help_text=_("单据类型"), default="") - limit = serializers.IntegerField(help_text=_("每页限制"), required=False, default=10) - offset = serializers.IntegerField(help_text=_("起始"), required=False, default=0) - - limit = serializers.IntegerField(help_text=_("每页限制"), required=False, default=10) - offset = serializers.IntegerField(help_text=_("起始"), required=False, default=0) - def validate(self, attrs): if attrs.get("ticket_types"): attrs["ticket_types"] = attrs["ticket_types"].split(",") return attrs -class UpdateTicketFlowConfigSerializer(serializers.Serializer): +class CreateTicketFlowConfigSerializer(serializers.Serializer): bk_biz_id = serializers.IntegerField(help_text=_("业务ID"), required=False, default=PLAT_BIZ_ID) + cluster_ids = serializers.ListSerializer( + help_text=_("集群ID列表"), child=serializers.IntegerField(), required=False, default=[] + ) ticket_types = serializers.ListField( help_text=_("单据类型"), child=serializers.ChoiceField(choices=TicketType.get_choices()) ) configs = serializers.DictField(help_text=_("单据可配置项")) +class UpdateTicketFlowConfigSerializer(CreateTicketFlowConfigSerializer): + config_ids = serializers.ListField( + help_text=_("流程规则ID列表)"), child=serializers.IntegerField(), required=False, default=[] + ) + + +class DeleteTicketFlowConfigSerializer(serializers.Serializer): + config_ids = serializers.ListField(help_text=_("流程规则ID列表)"), child=serializers.IntegerField()) + + class TicketFlowDescribeDetailSerializer(serializers.Serializer): flow_desc = serializers.ListField(help_text=_("单据流程描述"), child=serializers.CharField()) db_type = serializers.ChoiceField(help_text=_("单据分组类型"), choices=DBType.get_choices()) diff --git a/dbm-ui/backend/ticket/views.py b/dbm-ui/backend/ticket/views.py index 0f53c7481e..f78c9073ff 100644 --- a/dbm-ui/backend/ticket/views.py +++ b/dbm-ui/backend/ticket/views.py @@ -10,11 +10,9 @@ """ import operator from functools import reduce -from typing import Dict, List from django.db import transaction from django.db.models import Q -from django.forms.models import model_to_dict from django.utils.translation import ugettext_lazy as _ from drf_yasg.utils import swagger_auto_schema from rest_framework import serializers, status @@ -32,7 +30,11 @@ from backend.iam_app.dataclass.actions import ActionEnum from backend.iam_app.handlers.drf_perm.base import RejectPermission, ResourceActionPermission from backend.iam_app.handlers.drf_perm.cluster import ClusterDetailPermission, InstanceDetailPermission -from backend.iam_app.handlers.drf_perm.ticket import BatchApprovalPermission, create_ticket_permission +from backend.iam_app.handlers.drf_perm.ticket import ( + BatchApprovalPermission, + create_ticket_permission, + ticket_flows_config_permission, +) from backend.iam_app.handlers.permission import Permission from backend.ticket.builders import BuilderFactory from backend.ticket.builders.common.base import InfluxdbTicketFlowBuilderPatchMixin, fetch_cluster_ids @@ -54,6 +56,8 @@ BatchTodoOperateSerializer, ClusterModifyOpSerializer, CountTicketSLZ, + CreateTicketFlowConfigSerializer, + DeleteTicketFlowConfigSerializer, FastCreateCloudComponentSerializer, GetNodesSLZ, GetTodosSLZ, @@ -114,11 +118,10 @@ def _get_custom_permissions(self): instance_getter = lambda request, view: [request.parser_context["kwargs"]["pk"]] # noqa return [ResourceActionPermission([ActionEnum.TICKET_VIEW], ResourceEnum.TICKET, instance_getter)] # 单据流程设置,关联单据流程设置动作 - elif self.action == "update_ticket_flow_config": - instance_getter = lambda request, view: list( # noqa - set([TicketType.get_db_type_by_ticket(d) for d in request.data["ticket_types"]]) - ) - return [ResourceActionPermission([ActionEnum.TICKET_CONFIG_SET], ResourceEnum.DBTYPE, instance_getter)] + elif self.action in ["update_ticket_flow_config", "create_ticket_flow_config"]: + return ticket_flows_config_permission(self.action, self.request) + elif self.action == "delete_ticket_flow_config": + return ticket_flows_config_permission(self.action, self.request) # 其他非敏感GET接口,不鉴权 elif self.action in [ "list", @@ -555,51 +558,49 @@ def get_instance_operate_records(self, request, *args, **kwargs): ) @Permission.decorator_external_permission_field( param_field=lambda d: d["db_type"], - actions=[ActionEnum.TICKET_CONFIG_SET], + actions=[ActionEnum.GLOBAL_TICKET_CONFIG_SET], resource_meta=ResourceEnum.DBTYPE, ) + @Permission.decorator_external_permission_field( + param_field=lambda d: {ResourceEnum.DBTYPE.id: d["db_type"], ResourceEnum.BUSINESS.id: d.get("bk_biz_id")}, + actions=[ActionEnum.BIZ_TICKET_CONFIG_SET], + resource_meta=[ResourceEnum.DBTYPE, ResourceEnum.BUSINESS], + ) def query_ticket_flow_describe(self, request, *args, **kwargs): - from backend.ticket.builders import BuilderFactory - data = self.params_validate(self.get_serializer_class()) - limit, offset = data["limit"], data["offset"] - - # 根据条件过滤单据配置 - config_filter = Q(bk_biz_id=data["bk_biz_id"], group=data["db_type"], editable=True) - if data.get("ticket_types"): - config_filter &= Q(ticket_type__in=data["ticket_types"]) - - ticket_flow_configs = TicketFlowsConfig.objects.filter(config_filter) - ticket_flow_config_count = ticket_flow_configs.count() - ticket_flow_configs = ticket_flow_configs[offset : offset + limit] - # 获得单据类型与单据flow配置映射表 - flow_config_map = {config.ticket_type: config.configs for config in ticket_flow_configs} - - # 获取单据流程配置信息 - flow_desc_list: List[Dict] = [] - # 获取单据流程配置信息 - for flow_config in ticket_flow_configs: - flow_config_info = model_to_dict(flow_config) - flow_config_info["ticket_type_display"] = flow_config.get_ticket_type_display() - flow_config_info["update_at"] = flow_config.update_at - # 获取当前单据的执行流程描述 - flow_desc = BuilderFactory.registry[flow_config.ticket_type].describe_ticket_flows(flow_config_map) - flow_config_info["flow_desc"] = flow_desc - flow_desc_list.append(flow_config_info) - - return Response({"count": ticket_flow_config_count, "results": flow_desc_list}) + return Response(TicketHandler.query_ticket_flows_describe(**data)) @swagger_auto_schema( - operation_summary=_("修改可编辑的单据流程"), + operation_summary=_("修改可编辑的单据流程规则"), request_body=UpdateTicketFlowConfigSerializer(), tags=[TICKET_TAG], ) @action(methods=["POST"], detail=False, serializer_class=UpdateTicketFlowConfigSerializer) def update_ticket_flow_config(self, request, *args, **kwargs): data = self.params_validate(self.get_serializer_class()) - TicketFlowsConfig.objects.filter(bk_biz_id=data["bk_biz_id"], ticket_type__in=data["ticket_types"]).update( - configs=data["configs"] - ) + TicketHandler.update_ticket_flow_config(**data, operator=request.user.username) + return Response() + + @swagger_auto_schema( + operation_summary=_("创建单据流程规则"), + request_body=CreateTicketFlowConfigSerializer(), + tags=[TICKET_TAG], + ) + @action(methods=["POST"], detail=False, serializer_class=CreateTicketFlowConfigSerializer) + def create_ticket_flow_config(self, request, *args, **kwargs): + data = self.params_validate(self.get_serializer_class()) + TicketHandler.create_ticket_flow_config(**data, operator=request.user.username) + return Response() + + @swagger_auto_schema( + operation_summary=_("删除单据流程规则"), + request_body=DeleteTicketFlowConfigSerializer(), + tags=[TICKET_TAG], + ) + @action(methods=["DELETE"], detail=False, serializer_class=DeleteTicketFlowConfigSerializer) + def delete_ticket_flow_config(self, request, *args, **kwargs): + config_ids = self.params_validate(self.get_serializer_class())["config_ids"] + TicketFlowsConfig.objects.filter(id__in=config_ids).delete() return Response() @common_swagger_auto_schema(