From 0c8f211d6ef292c685df47689ccce5d48249a787 Mon Sep 17 00:00:00 2001
From: gqp <446105468@qq.com>
Date: Mon, 9 Dec 2024 11:51:00 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=A2=E9=98=85=E4=B8=8B=E5=8F=91?=
 =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=8A=A8=E6=80=81=E5=88=86=E7=BB=84=20(close?=
 =?UTF-8?q?d=20#2507)=20#=20Reviewed,=20transaction=20id:=2027904?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/backend/subscription/tools.py            | 206 ++++++++++++++++--
 apps/backend/subscription/views.py            |   4 +-
 apps/node_man/constants.py                    |  11 +
 .../0085_alter_subscription_node_type.py      |  18 ++
 apps/node_man/models.py                       |   2 +
 common/api/modules/cc.py                      |   9 +
 6 files changed, 233 insertions(+), 17 deletions(-)
 create mode 100644 apps/node_man/migrations/0085_alter_subscription_node_type.py

diff --git a/apps/backend/subscription/tools.py b/apps/backend/subscription/tools.py
index f40e68782..2969f0a3f 100644
--- a/apps/backend/subscription/tools.py
+++ b/apps/backend/subscription/tools.py
@@ -468,6 +468,14 @@ def get_host_detail_by_template(bk_obj_id, template_info_list: list, bk_biz_id:
         host_info_result = batch_request(
             call_func, dict(bk_service_template_ids=template_ids, bk_biz_id=bk_biz_id, fields=fields)
         )
+    elif bk_obj_id == models.Subscription.NodeType.DYNAMIC_GROUP:
+        # 集群模板
+        call_func = client_v2.cc.find_host_by_set_template
+        template_ids = [info["bk_inst_id"] for info in template_info_list]
+        bk_set_ids = [info["bk_set_id"] for info in template_info_list]
+        host_info_result = batch_request(
+            call_func, dict(bk_set_template_ids=template_ids, bk_set_ids=bk_set_ids, bk_biz_id=bk_biz_id, fields=fields)
+        )
     else:
         # 集群模板
         call_func = client_v2.cc.find_host_by_set_template
@@ -489,6 +497,26 @@ def get_host_detail_by_template(bk_obj_id, template_info_list: list, bk_biz_id:
     return host_info_result
 
 
+def get_host_module_info_by_host_ids(bk_host_id_chunks, bk_biz_id):
+    """
+    根据主机id列表查询主机业务关系信息
+    :param bk_host_id_chunks: 主机id列表
+    :param bk_biz_id: 业务ID
+    """
+    # 补充实例所属模块ID
+    host_biz_relations = []
+
+    with ThreadPoolExecutor(max_workers=settings.CONCURRENT_NUMBER) as ex:
+        tasks = [
+            ex.submit(client_v2.cc.find_host_biz_relations, dict(bk_host_id=chunk, bk_biz_id=bk_biz_id))
+            for chunk in bk_host_id_chunks
+        ]
+        for future in as_completed(tasks):
+            host_biz_relations.extend(future.result())
+
+    return host_biz_relations
+
+
 def get_service_instances_by_template(bk_obj_id, template_info_list: list, bk_biz_id: int = None):
     """
     根据集群模板ID/服务模板ID获得服务实例列表
@@ -718,6 +746,14 @@ def set_template_scope_nodes(scope):
             # 兼容 service_template_id 不存在的场景
             if "service_template_id" in node and node["service_template_id"] in template_ids
         ]
+    elif scope["node_type"] == models.Subscription.NodeType.DYNAMIC_GROUP:
+        # 转化服务模板为node
+        scope["nodes"] = [
+            {"bk_inst_id": node["bk_module_id"], "bk_obj_id": "module"}
+            for node in modules_info
+            # 兼容 bk_set_id 不存在的场景
+            if "bk_set_id" in node and node["bk_set_id"] in template_ids
+        ]
     else:
         # 转化集群模板为node
         scope["nodes"] = [
@@ -823,6 +859,35 @@ def get_scope_labels_func(
     }
 
 
+def execute_dynamic_groups(nodes: List[dict], bk_biz_id: int, bk_obj_id: str, fields: List[str]):
+    """
+    执行动态分组
+    :param nodes: 节点列表
+    :param bk_biz_id: 业务id
+    :param bk_obj_id: 分组目标(目前只支持 host 和 set )
+    :param fields: 属性列表
+    """
+    params = [
+        {
+            "func": CCApi.execute_dynamic_group,
+            "params": {
+                "fields": fields,
+                "bk_biz_id": bk_biz_id,
+                "id": node["bk_inst_id"],
+                "no_request": True,
+            },
+            "sort": "id",
+            "limit": constants.LIST_SERVICE_INSTANCE_DETAIL_LIMIT,
+        }
+        for node in nodes
+        if node["bk_obj_id"] == bk_obj_id
+    ]
+
+    return batch_call(
+        batch_request, params, extend_result=True, interval=constants.LIST_SERVICE_INSTANCE_DETAIL_INTERVAL
+    )
+
+
 def get_instances_by_scope_with_checker(
     scope: Dict[str, Union[Dict, int, Any]], steps: List[models.SubscriptionStep], *args, **kwargs
 ) -> Dict[str, Dict[str, Union[Dict, Any]]]:
@@ -888,7 +953,7 @@ def get_instances_by_scope(scope: Dict[str, Union[Dict, int, Any]]) -> Dict[str,
     else:
         module_to_topo = {}
 
-    nodes = scope["nodes"]
+    nodes: List[dict] = scope["nodes"]
     if not nodes:
         # 兼容节点为空的情况
         return {}
@@ -935,27 +1000,21 @@ def get_instances_by_scope(scope: Dict[str, Union[Dict, int, Any]]) -> Dict[str,
         # 校验是否都选择了同一种模板
         bk_obj_id_set = check_instances_object_type(nodes)
         if scope["object_type"] == models.Subscription.ObjectType.HOST:
-            # 补充实例所属模块ID
-            host_biz_relations = []
             instances.extend(
                 [
                     {"host": inst}
                     for inst in get_host_detail_by_template(list(bk_obj_id_set)[0], nodes, bk_biz_id=bk_biz_id)
                 ]
             )
-            bk_host_id_chunks = chunk_lists([instance["host"]["bk_host_id"] for instance in instances], 500)
-            with ThreadPoolExecutor(max_workers=settings.CONCURRENT_NUMBER) as ex:
-                tasks = [
-                    ex.submit(client_v2.cc.find_host_biz_relations, dict(bk_host_id=chunk, bk_biz_id=bk_biz_id))
-                    for chunk in bk_host_id_chunks
-                ]
-                for future in as_completed(tasks):
-                    host_biz_relations.extend(future.result())
+
+            host_biz_relations = get_host_module_info_by_host_ids(
+                bk_host_id_chunks=chunk_lists([instance["host"]["bk_host_id"] for instance in instances], 500),
+                bk_biz_id=bk_biz_id,
+            )
 
             # 转化模板为节点
             nodes = set_template_scope_nodes(scope)
             instances = add_host_module_info(host_biz_relations, instances)
-
         else:
             # 补充服务实例中的信息
             # 转化模板为节点,**注意不可在get_service_instance_by_inst之后才转换**
@@ -964,6 +1023,125 @@ def get_instances_by_scope(scope: Dict[str, Union[Dict, int, Any]]) -> Dict[str,
                 [{"service": inst} for inst in get_service_instance_by_inst(bk_biz_id, nodes, module_to_topo)]
             )
 
+    # 按照动态分组查询
+    elif scope["node_type"] == models.Subscription.NodeType.DYNAMIC_GROUP:
+        # 获取动态分组主机信息
+        host_infos = execute_dynamic_groups(
+            nodes=nodes,
+            bk_biz_id=bk_biz_id,
+            bk_obj_id=constants.CmdbGroupObjId.HOST.value,
+            fields=["bk_host_id"],
+        )
+
+        # 获取动态分组集群信息
+        set_infos = execute_dynamic_groups(
+            nodes=nodes,
+            bk_biz_id=bk_biz_id,
+            bk_obj_id=constants.CmdbGroupObjId.SET.value,
+            fields=["bk_set_id", "set_template_id"],
+        )
+
+        if scope["object_type"] == models.Subscription.ObjectType.HOST:
+            # 根据主机信息填充主机实例
+            if host_infos:
+                instances.extend(
+                    [
+                        {"host": inst, "source": "host_infos"}
+                        for inst in get_host_detail(
+                            host_info_list=[
+                                {
+                                    "bk_biz_id": bk_biz_id,
+                                    "bk_host_id": host_info["bk_host_id"],
+                                }
+                                for host_info in host_infos
+                            ],
+                            bk_biz_id=bk_biz_id,
+                            source="get_instances_by_scope",
+                        )
+                    ]
+                )
+
+            # 根据集群信息填充主机实例
+            if set_infos:
+                instances.extend(
+                    [
+                        {"host": inst, "source": "set_infos"}
+                        for inst in get_host_detail_by_template(
+                            bk_obj_id=models.Subscription.NodeType.DYNAMIC_GROUP,
+                            template_info_list=[
+                                {
+                                    "bk_set_id": set_info["bk_set_id"],
+                                    "bk_inst_id": set_info["set_template_id"],
+                                }
+                                for set_info in set_infos
+                            ],
+                            bk_biz_id=bk_biz_id,
+                        )
+                    ]
+                )
+
+                host_biz_relations = get_host_module_info_by_host_ids(
+                    bk_host_id_chunks=chunk_lists([instance["host"]["bk_host_id"] for instance in instances], 500),
+                    bk_biz_id=bk_biz_id,
+                )
+
+                # 转化模板为节点
+                nodes = set_template_scope_nodes(
+                    scope={
+                        "bk_biz_id": bk_biz_id,
+                        "node_type": models.Subscription.NodeType.DYNAMIC_GROUP,
+                        "nodes": [
+                            {
+                                "bk_inst_id": set_info["bk_set_id"],
+                            }
+                            for set_info in set_infos
+                        ],
+                    }
+                )
+                instances = add_host_module_info(host_biz_relations, instances)
+
+            # 去重主机id去重
+            instances = list({instance["host"]["bk_host_id"]: instance for instance in instances}.values())
+
+        else:
+            # 根据主机信息填充服务实例
+            if host_infos:
+                instances.extend(
+                    [
+                        {"service": inst, "source": "host_infos"}
+                        for inst in get_service_instances(
+                            bk_biz_id=bk_biz_id,
+                            filter_id_list=[host_info["bk_host_id"] for host_info in host_infos],
+                            filter_field_name=FilterFieldName.BK_HOST_LIST,
+                            ignore_exception=False,
+                        )
+                    ]
+                )
+
+            # 根据集群信息填充服务实例
+            if set_infos:
+                nodes = set_template_scope_nodes(
+                    scope={
+                        "bk_biz_id": bk_biz_id,
+                        "node_type": models.Subscription.NodeType.DYNAMIC_GROUP,
+                        "nodes": [
+                            {
+                                "bk_inst_id": set_info["bk_set_id"],
+                            }
+                            for set_info in set_infos
+                        ],
+                    }
+                )
+                instances.extend(
+                    [
+                        {"service": inst, "source": "set_infos"}
+                        for inst in get_service_instance_by_inst(bk_biz_id, nodes, module_to_topo)
+                    ]
+                )
+
+            # 根据服务实例id去重
+            instances = list({instance["service"]["id"]: instance for instance in instances}.values())
+
     if not need_register:
         # 补充必要的主机或实例相关信息
 
@@ -1010,7 +1188,7 @@ def get_instances_by_scope(scope: Dict[str, Union[Dict, int, Any]]) -> Dict[str,
     return instances_dict
 
 
-def add_host_info_to_instances(bk_biz_id: int, scope: Dict, instances: Dict):
+def add_host_info_to_instances(bk_biz_id: int, scope: Dict, instances: List):
     """
     补全实例的主机信息
     :param bk_biz_id: 业务ID
@@ -1087,7 +1265,7 @@ def add_scope_info_to_instances(nodes: List, scope: Dict, instances: List[Dict],
     :return:
     """
     for instance in instances:
-        if scope["node_type"] == models.Subscription.NodeType.INSTANCE:
+        if scope["node_type"] == models.Subscription.NodeType.INSTANCE or instance.pop("source", "") == "host_infos":
             _add_scope_info_to_inst_instances(scope, instance)
         else:
             _add_scope_info_to_topo_instances(scope, instance, nodes, module_to_topo)
diff --git a/apps/backend/subscription/views.py b/apps/backend/subscription/views.py
index a2e0ff811..c513a95ea 100644
--- a/apps/backend/subscription/views.py
+++ b/apps/backend/subscription/views.py
@@ -123,9 +123,7 @@ def create_subscription(self, request):
             subscription_task = models.SubscriptionTask.objects.create(
                 subscription_id=subscription.id, scope=subscription.scope, actions={}
             )
-            tasks.run_subscription_task_and_create_instance.delay(
-                subscription, subscription_task, language=get_language()
-            )
+            tasks.run_subscription_task_and_create_instance(subscription, subscription_task, language=get_language())
             result["task_id"] = subscription_task.id
 
         return Response(result)
diff --git a/apps/node_man/constants.py b/apps/node_man/constants.py
index cde490d4a..a7bf6c4cc 100644
--- a/apps/node_man/constants.py
+++ b/apps/node_man/constants.py
@@ -1114,6 +1114,17 @@ def _get_member__alias_map(cls) -> Dict[Enum, str]:
         return {cls.V4: _("IPv4"), cls.V6: _("IPv6")}
 
 
+class CmdbGroupObjId(EnhanceEnum):
+    """IP 版本"""
+
+    HOST = "host"
+    SET = "set"
+
+    @classmethod
+    def _get_member__alias_map(cls) -> Dict[Enum, str]:
+        return {cls.HOST: _("主机"), cls.SET: _("集群")}
+
+
 class PolicyRollBackType:
     SUPPRESSED = "SUPPRESSED"
     LOSE_CONTROL = "LOSE_CONTROL"
diff --git a/apps/node_man/migrations/0085_alter_subscription_node_type.py b/apps/node_man/migrations/0085_alter_subscription_node_type.py
new file mode 100644
index 000000000..a0dbba563
--- /dev/null
+++ b/apps/node_man/migrations/0085_alter_subscription_node_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2024-12-26 08:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('node_man', '0084_update_isp_and_accesspoint_regionid_cityid'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='subscription',
+            name='node_type',
+            field=models.CharField(choices=[('TOPO', '动态实例(拓扑)'), ('INSTANCE', '静态实例'), ('SERVICE_TEMPLATE', '服务模板'), ('SET_TEMPLATE', '集群模板'), ('DYNAMIC_GROUP', '动态分组')], db_index=True, max_length=20, verbose_name='节点类型'),
+        ),
+    ]
diff --git a/apps/node_man/models.py b/apps/node_man/models.py
index 173e1bf8c..952b8794c 100644
--- a/apps/node_man/models.py
+++ b/apps/node_man/models.py
@@ -1831,12 +1831,14 @@ class NodeType(object):
         INSTANCE = "INSTANCE"
         SERVICE_TEMPLATE = "SERVICE_TEMPLATE"
         SET_TEMPLATE = "SET_TEMPLATE"
+        DYNAMIC_GROUP = "DYNAMIC_GROUP"
 
     NODE_TYPE_CHOICES = (
         (NodeType.TOPO, _("动态实例(拓扑)")),
         (NodeType.INSTANCE, _("静态实例")),
         (NodeType.SERVICE_TEMPLATE, _("服务模板")),
         (NodeType.SET_TEMPLATE, _("集群模板")),
+        (NodeType.DYNAMIC_GROUP, _("动态分组")),
     )
 
     class CategoryType(object):
diff --git a/common/api/modules/cc.py b/common/api/modules/cc.py
index 4837be105..3e3fb9a6e 100644
--- a/common/api/modules/cc.py
+++ b/common/api/modules/cc.py
@@ -256,3 +256,12 @@ def __init__(self):
             before_request=add_esb_info_before_request,
             api_name="list_service_instance_detail",
         )
+        self.execute_dynamic_group = DataAPI(
+            method="POST",
+            url=CC_APIGATEWAY_ROOT_V2 + "execute_dynamic_group/",
+            module=self.MODULE,
+            simple_module=self.SIMPLE_MODULE,
+            description="执行动态分组",
+            before_request=add_esb_info_before_request,
+            api_name="search_set_v2",
+        )