蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】— CMDB #14:动态分组:按条件自动归类主机

蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】— CMDB #14:动态分组:按条件自动归类主机

SRE 日常运维里最常见的主机管理问题,不是 "主机数量不够",而是 "主机太多之后怎么按条件自动归类":比如把 CPU 大于 16 核、内存大于 64G 的机器自动归到 "高配资源池",或者把某个业务下所有未安装 Agent 的主机自动归到 "待安装分组"

如果全靠人工维护,分组规则很快就会和实际资源脱节。

蓝鲸 CMDB 的 动态分组(Dynamic Group)就是为解决这个痛点设计的。它把 "查询条件" 保存成可复用的分组对象,执行时再根据条件实时计算成员列表。

这篇博文我们不看抽象概念,而是直接看 CMDB 3.14.6 里动态分组的真实源码:创建链路如何通过 AutoRunTxn 保证事务与审计、条件模型如何做类型安全校验、执行路径如何分 host/set 两种对象类型下发、以及可变条件如何让运行时的参数覆盖静态条件。

  src/scene_server/host_server/service/dynamic_grouping.go         ← API 入口
  src/common/metadata/dynamic_grouping.go                          ← 条件模型/ExecuteOption
  src/scene_server/host_server/logics/dynamic_grouping.go          ← 主机动态分组执行
  src/scene_server/host_server/logics/set.go                       ← 集群动态分组执行

DynamicGroup ExecuteOption variable_condition AutoRunTxn BizCustomQuery v3.14.6

必掌握

  • 动态分组创建链路CreateDynamicGroup 如何把"参数校验 + 事务 + 审计 + IAM 注册"串成原子操作
  • 条件元数据模型DynamicGroupCondition / DynamicGroupInfoCondition / DynamicGroupInfo 三层结构
  • 执行分派逻辑ExecuteDynamicGroup 如何按 bk_obj_id 分派到 host/set 执行器
  • 可变条件机制variable_condition 如何让前端在运行时覆盖静态条件
  • 查询与删除SearchDynamicGroup 的模糊搜索与 DeleteDynamicGroup 的事务删除

了解

  • changeTimeToMatchLocalZone 为何在查询后统一修正时区
  • 动态分组与 IAM 的 BizCustomQuery 资源类型映射

一、动态分组在 CMDB 架构中的定位

What — 动态分组在 CMDB 里扮演什么角色?

DynamicGroup 是 CMDB 中 "保存查询条件为资源" 的核心机制。它不属于 host/set/model 等具体资源模型,而是一个跨模型的筛选器:把 bk_obj_id + condition 持久化后,用户可以通过 "执行动态分组"  实时获取符合条件的实例列表。对 SRE 来说,它相当于把日常运维中反复使用的筛选条件固化成可共享、可授权、可审计的 "智能查询书签"

Why — 为什么需要动态分组?解决了什么问题?

问题一:重复查询条件难以维护

SRE 经常需要查"业务 2 下 CPU>16 核且内存>64G 的主机"。如果每次都要手动拼 QueryCondition,条件一旦变化就要改所有依赖它的脚本和页面。动态分组把条件保存在服务端,前端/脚本只需要传分组 ID,条件变更时只改一处。

问题二:权限和审计缺失

如果查询条件散落在各个前端页面或脚本里,CMDB 无法知道"谁在执行什么查询"。动态分组作为一等公民资源,有自己的 IAM 权限控制、审计日志和创建者信息,所有执行行为都可追溯。

问题三:业务下和资源池下的查询语义不同

同一个"高配主机"分组,在业务下和在资源池(bk_biz_id=0)下的查询条件可能完全不同。动态分组通过 bk_biz_id 隔离命名空间,保证条件不会跨业务泄露。

没有动态分组会发生什么?

  • 没有条件复用:每次查询都要重新拼条件,脚本和页面重复代码多
  • 没有权限控制:无法对"某个分组谁能执行"做细粒度授权
  • 没有审计:无法追溯"谁在什么时候执行了哪些筛选条件"
  • 没有业务隔离:资源池和业务下的筛选条件混在一起,容易误操作
How — 从源码看动态分组的三层结构

src/common/metadata/dynamic_grouping.go 第 86-96 行,DynamicGroupCondition 是最小的条件单元:

// DynamicGroupCondition:最小条件单元,Field(字段名)+ Operator(操作符)+ Value(条件值)三元组
    // Value 用 interface{} 接收任意类型,由 Validate 按字段属性类型做严格校验
    type DynamicGroupCondition struct {
        Field    string      `json:"field" bson:"field"`    // 要查询的字段名(如 bk_host_innerip)
        Operator string      `json:"operator" bson:"operator"` // 操作符(如 $eq、$ne、$in、$gte、$regex)
        Value    interface{} `json:"value" bson:"value"`    // 条件值(类型取决于字段属性:string/number/bool/array)
    }

这里 Field 是字段名,Operator 是操作符,Value 是条件值。注意 Valueinterface{},因为同一个条件里可能是字符串、数字、数组或布尔值。

src/common/metadata/dynamic_grouping.go 第 239-251 行,DynamicGroupInfoCondition 把条件和对象类型绑定:

// DynamicGroupInfoCondition:把一组条件绑定到具体的 CMDB 对象类型(host/set/module)
    // ObjID 决定这组条件查哪个对象模型;TimeCondition 是可选的时间范围条件
    type DynamicGroupInfoCondition struct {
        ObjID         string                      `json:"bk_obj_id" bson:"bk_obj_id"`   // CMDB 对象类型(host/set/module)
        Condition     []DynamicGroupCondition   `json:"condition" bson:"condition"` // 普通字段条件数组
        TimeCondition *TimeCondition            `json:"time_condition,omitempty" bson:"time_condition,omitempty"` // 可选的时间范围条件
    }

ObjID 限制了这组条件只能用于 host 或 set;TimeCondition 是可选的时间范围条件,用于按创建时间/更新时间筛选。

src/common/metadata/dynamic_grouping.go 第 287-294 行,DynamicGroupInfo 把静态条件和可变条件分层:

// DynamicGroupInfo:动态分组的条件容器,分离静态条件和可变条件
    // Condition:静态条件,创建后固定不变,定义分组的基础筛选逻辑
    // VariableCondition:可变条件,执行时由前端传入新值覆盖同 ObjID 同字段的值,实现参数化
    type DynamicGroupInfo struct {
        Condition         []DynamicGroupInfoCondition `json:"condition" bson:"condition"`               // 静态条件(不可变)
        VariableCondition []DynamicGroupInfoCondition `json:"variable_condition" bson:"variable_condition"` // 可变条件(运行时可覆盖)
    }

Condition 是静态条件,创建后固定不变;VariableCondition 是可变条件,执行时可以由前端传入新值覆盖。这种设计让同一个分组既能保存默认筛选逻辑,又能支持运行时参数化。

src/common/metadata/dynamic_grouping.go 第 387-415 行,DynamicGroup 是顶层结构:

// DynamicGroup:动态分组顶层结构,代表一个完整的动态分组资源
    // AppID 对应 bk_biz_id,用于多租户隔离(资源池为 0)
    // ObjID 决定分组类型(host 或 set),决定执行时的分派路径
    // Info 承载真正的查询条件,包含静态条件和可变条件
    type DynamicGroup struct {
        AppID      int64            `json:"bk_biz_id" bson:"bk_biz_id"`     // 所属业务 ID(0 表示资源池)
        ID         string           `json:"id" bson:"id"`                   // 分组唯一 ID(UUID,由服务端生成)
        Name       string           `json:"name" bson:"name"`               // 分组名称(用户可见)
        ObjID      string           `json:"bk_obj_id" bson:"bk_obj_id"`     // 对象类型(host/set)
        Info       DynamicGroupInfo `json:"info" bson:"info"`               // 查询条件(核心字段)
        CreateUser string           `json:"create_user" bson:"create_user"` // 创建者(服务端强制赋值)
        ModifyUser string           `json:"modify_user" bson:"modify_user"` // 修改者
        CreateTime time.Time        `json:"create_time" bson:"create_time"` // 创建时间(UTC,服务端赋值)
        UpdateTime time.Time        `json:"last_time" bson:"last_time"`     // 更新时间(UTC,JSON 字段名为 last_time)
    }

AppID 对应 bk_biz_id,用于业务隔离;ObjID 决定这个分组是 host 类型还是 set 类型;Info 承载真正的查询条件。整个模型非常扁平,没有多余层级。

源码视角总结:3 层结构

  • 1 个最小单元:DynamicGroupCondition 是 Field/Operator/Value 三元组
  • 2 层条件组织:Condition(静态)+ VariableCondition(可变)
  • 1 个顶层对象:DynamicGroup 通过 bk_biz_id + bk_obj_id 实现多租户与多模型隔离

避坑提醒(源码视角):

  • 不要在 Value 里放未知类型DynamicGroupCondition.Valueinterface{},但 Validate 会按属性类型做严格校验
  • 注意 bk_biz_id 的隔离语义:资源池(bk_biz_id=0)和业务下的动态分组共用同一套接口

本节总结:动态分组是 CMDB 的"智能筛选器"

  • 不是资源存储层,而是条件管理层
  • 通过 bk_biz_id 实现多租户隔离
  • 创建/执行/查询/删除四条链路,每条都有独立的事务和审计保证

二、创建链路:CreateDynamicGroup 的原子操作

What — CreateDynamicGroup 在做什么?

CreateDynamicGroup 定义在 src/scene_server/host_server/service/dynamic_grouping.go 第 33-95 行,是动态分组的唯一创建入口。它接收前端传来的 meta.DynamicGroup JSON,经过参数校验后,通过 AutoRunTxn 把"创建分组 + 审计日志 + IAM 注册"打包成原子事务。

Why — 为什么创建链路要做成原子事务?

问题一:数据一致性

动态分组创建涉及三个写操作:向 cc_DynamicGroup 集合插入分组文档、生成审计日志、向 IAM 注册资源创建权限。如果中间某一步失败,就会出现"分组存在但审计缺失"或"IAM 已注册但分组未创建"的不一致状态。

问题二:审计合规要求

CMDB 的审计日志要求"每个创建操作都有记录"。如果创建分组成功但审计日志写入失败,从合规角度看这个操作是"半成功",运维无法追溯。

问题三:IAM 权限预注册

动态分组在 IAM 里对应 BizCustomQuery 资源类型。创建分组时同步注册"创建者授权",保证创建者立即拥有该分组的编辑权限。

How — CreateDynamicGroup 的完整执行路径

src/scene_server/host_server/service/dynamic_grouping.go 第 33-95 行的完整代码:

// CreateDynamicGroup:动态分组唯一创建入口,定义在 service/dynamic_grouping.go L34-95
    // 链路:JSON解码 → 参数校验 → 填充系统字段 → AutoRunTxn原子事务(创建+审计+IAM注册)
    func (s *Service) CreateDynamicGroup(ctx *rest.Contexts) {
        newDynamicGroup := meta.DynamicGroup{}
        if err := ctx.DecodeInto(&newDynamicGroup); err != nil { // 将HTTP请求体JSON解码为meta.DynamicGroup结构体
            blog.Errorf("create dynamic group failed, decode request body err: %v, rid: %s", err, ctx.Kit.Rid)
            ctx.RespAutoError(ctx.Kit.CCError.CCError(common.CCErrCommJSONUnmarshalFailed))
            return
        }
        if err := s.createGroupParamCheck(ctx.Kit, newDynamicGroup); err != nil { // 参数校验:校验bk_biz_id/name/obj_id及条件格式
            blog.Errorf("create request param check failed, err: %v, rid: %s", err, ctx.Kit.Rid)
            ctx.RespAutoError(err)
            return
        }
        newDynamicGroup.CreateUser = ctx.Kit.User     // 服务端强制覆盖CreateUser,防止客户端伪造
        newDynamicGroup.CreateTime = time.Now().UTC() // 服务端强制覆盖CreateTime(UTC时间),保证时间一致性
        response := &meta.IDResult{}

        autoRunTxnFunc := func() error {
            var err error
            // 第一步:向cc_DynamicGroup集合插入分组文档(服务端返回真实ID)
            response, err = s.CoreAPI.CoreService().Host().CreateDynamicGroup(ctx.Kit.Ctx, ctx.Kit.Header, &newDynamicGroup)
            if err != nil { return ctx.Kit.CCError.Error(common.CCErrCommHTTPDoRequestFailed) }
            if !response.Result { return response.CCError() }
            newDynamicGroup.ID = response.Data.ID // 从响应中提取服务端生成的UUID作为分组ID

            // 第二步:生成审计日志(记录谁在什么时候创建了什么分组)
            audit := auditlog.NewDynamicGroupAuditLog(s.CoreAPI.CoreService())
            auditParam := auditlog.NewGenerateAuditCommonParameter(ctx.Kit, meta.AuditCreate)
            auditLogs, err := audit.GenerateAuditLog(auditParam, &newDynamicGroup)
            if err != nil { return err }
            if err := audit.SaveAuditLog(ctx.Kit, auditLogs...); err != nil { return err }

            // 第三步:向IAM注册创建者权限(EnableAuthorize==true时才执行)
            if auth.EnableAuthorize() {
                if err := s.registerActionToIAM(ctx.Kit, newDynamicGroup); err != nil {
                    blog.Errorf("register created new dynamic group to iam failed, err: %v, rid: %s", err, ctx.Kit.Rid)
                    return err
                }
            }
            return nil
        }

        // 执行原子事务:创建+审计+IAM注册三者要么全部成功,要么全部回滚
        if err := s.Engine.CoreAPI.CoreService().Txn().AutoRunTxn(ctx.Kit.Ctx, ctx.Kit.Header, autoRunTxnFunc); err != nil {
            ctx.RespAutoError(err)
            return
        }
        ctx.RespEntity(response.Data) // 返回新创建的分组ID给客户端
    }

这段代码的调用链非常清晰:

  1. 参数解码与校验(第 36-45 行):ctx.DecodeInto 把 JSON 请求体解码成 meta.DynamicGroup,然后调用 createGroupParamCheck 做格式校验(第 126-140 行)。
  2. 填充系统字段(第 46-47 行):CreateUserCreateTime 由服务端强制赋值,前端传入的这两个字段会被忽略。
  3. 原子事务执行(第 90 行):AutoRunTxn 包裹整个创建逻辑。
  4. IAM 注册(第 80-85 行):auth.EnableAuthorize() 判断总开关,开启时才调用 registerActionToIAM

一个关键细节:registerActionToIAM

src/scene_server/host_server/service/dynamic_grouping.go 第 97-123 行,registerActionToIAM 会先重新查询刚创建的分组,再调用 AuthManager.Authorizer.RegisterResourceCreatorAction 向 IAM 注册创建者权限:

// registerActionToIAM:向IAM注册动态分组的创建者权限,定义在 service/dynamic_grouping.go L98-123
    // 关键细节:必须先重新查询(GetDynamicGroup)拿到服务端生成的真实ID,再注册到IAM
    // 这样做的原因:CreateDynamicGroup返回ID后直接注册,若MongoDB写入成功但事务未提交,IAM注册会拿到不存在的ID
    func (s *Service) registerActionToIAM(kit *rest.Kit,
        dynamicGroup meta.DynamicGroup) error {
        bizID := strconv.FormatInt(dynamicGroup.AppID, 10) // AppID从int64转string(IAM接口要求string类型的业务ID)
        // 重新查询刚创建的分组,获取服务端生成的真实ID和Name(不在事务外注册的原因见上方)
        resp, err := s.CoreAPI.CoreService().Host().GetDynamicGroup(
            kit.Ctx, bizID, dynamicGroup.ID, kit.Header)
        if err != nil { return err }

        // 构造IAM实例:Type=BizCustomQuery(IAM中的动态分组资源类型)、ID/Name/Creator三者必填
        iamInstance := meta.IamInstanceWithCreator{
            Type:    string(iam.BizCustomQuery), // 对应IAM系统中的 biz_custom_query 资源类型
            ID:      resp.Data.ID,                // 服务端生成的UUID(真实ID)
            Name:    resp.Data.Name,              // 分组名称(用于IAM控制台展示)
            Creator: kit.User,                    // 创建者(IAM自动给创建者授权)
        }
        // RegisterResourceCreatorAction:注册"资源创建者自动拥有该资源编辑权限"的IAM规则
        if _, err = s.AuthManager.Authorizer.RegisterResourceCreatorAction(
            kit.Ctx, kit.Header, iamInstance); err != nil {
            return err
        }
        return nil
    }

重新查询是为了拿到服务端生成的 ID,保证传递给 IAM 的 ID 是准确的。

本节总结:创建链路的 3 个关键设计

  • 1 个事务:AutoRunTxn 保证创建 + 审计 + IAM 注册原子性
  • 2 个强制字段:CreateUser / CreateTime 由服务端赋值
  • 1 个条件开关:EnableAuthorize() 控制 IAM 注册是否执行

三、条件元数据:DynamicGroupCondition 的类型安全设计

What — 条件元数据在做什么?

DynamicGroupConditionDynamicGroupInfoConditionDynamicGroupInfo 三层结构,共同定义了动态分组的条件模型。它们在 src/common/metadata/dynamic_grouping.go 中定义。

Why — 为什么条件模型要做类型安全校验?

问题一:用户输入不可信

前端传入的条件可能是字符串、数字、布尔值或数组。如果直接把 Value 塞进 MongoDB 查询,类型不匹配会导致查询失败。

问题二:操作符和字段类型必须匹配

某些操作符只支持特定类型:$eq 支持所有类型,$gt/$lt 只支持数值,$regex 只支持字符串,布尔值只能用 $eq

问题三:动态分组支持 host 和 set 两种对象

host 动态分组可以按 bk_host_idbk_cloud_id 等字段筛选;set 动态分组只能按 bk_set_id 筛选。

How — 三层条件模型的源码实现

src/common/metadata/dynamic_grouping.go 第 86-96 行,DynamicGroupCondition 是最小的条件单元:

// DynamicGroupCondition:最小条件单元,Field(字段名)+ Operator(操作符)+ Value(条件值)三元组
    // Value 用 interface{} 接收任意类型,由 Validate 按字段属性类型做严格校验
    type DynamicGroupCondition struct {
        Field    string      `json:"field" bson:"field"`    // 要查询的字段名(如 bk_host_innerip)
        Operator string      `json:"operator" bson:"operator"` // 操作符(如 $eq、$ne、$in、$gte、$regex)
        Value    interface{} `json:"value" bson:"value"`    // 条件值(类型取决于字段属性:string/number/bool/array)
    }

src/common/metadata/dynamic_grouping.go 第 28-65 行,操作符枚举:

// 动态分组支持的全部操作符,共7种核心操作符,外加2种 filter 包内路径
    const (
        DynamicGroupOperatorEQ   = "$eq"   // 等于:支持所有类型
        DynamicGroupOperatorNE   = "$ne"   // 不等于
        DynamicGroupOperatorIN   = "$in"   // 包含(在数组中)
        DynamicGroupOperatorNIN  = "$nin"  // 不包含(不在数组中)
        DynamicGroupOperatorLTE  = "$lte"  // 小于等于(仅数值/日期)
        DynamicGroupOperatorGTE  = "$gte"  // 大于等于(仅数值/日期)
        DynamicGroupOperatorLIKE = "$regex" // 正则匹配(仅字符串,相当于模糊搜索)
    )

    // DynamicGroupOperators 将操作符注册到白名单,Validate 时只接受白名单内的操作符
    var DynamicGroupOperators = map[string]string{
        DynamicGroupOperatorEQ:           DynamicGroupOperatorEQ,
        DynamicGroupOperatorNE:           DynamicGroupOperatorNE,
        DynamicGroupOperatorIN:           DynamicGroupOperatorIN,
        DynamicGroupOperatorNIN:          DynamicGroupOperatorNIN,
        DynamicGroupOperatorLTE:          DynamicGroupOperatorLTE,
        DynamicGroupOperatorGTE:          DynamicGroupOperatorGTE,
        DynamicGroupOperatorLIKE:         DynamicGroupOperatorLIKE,
        string(filter.Contains):           string(filter.Contains),       // 数组包含(filter包)
        string(filter.ContainsSensitive):  string(filter.ContainsSensitive), // 区分大小写的数组包含
    }

src/common/metadata/dynamic_grouping.go 第 98-168 行,Validate 函数做了 4 个层次的校验:

  1. 操作符白名单(第 100-103 行):不在 DynamicGroupOperators 里的操作符直接拒绝
  2. 字段支持(第 105-112 行):检查字段是否在属性映射表中,bk_default_field 例外
  3. 类型特化(第 125-136 行):布尔只支持 $eq,日期只支持 $gte/$lte
  4. 值类型匹配(第 138-165 行):根据属性类型验证 Value 的实际类型

src/common/metadata/dynamic_grouping.go 第 253-285 行,DynamicGroupInfoCondition.Validate 的特殊处理:

// DynamicGroupInfoCondition.Validate:校验一组条件的格式和类型安全性
    // validatefunc 是回调函数,根据 ObjID 查询该对象的所有属性定义(属性ID → 属性类型)
    // attributeMap 维护字段名到类型的映射,用于后续类型校验
    func (c *DynamicGroupInfoCondition) Validate(validatefunc Validatefunc) error {
        // 第一步:调用回调获取该对象的所有属性定义(从 MongoDB 查 model_object_att)
        attributes, err := validatefunc(c.ObjID)
        if err != nil { return fmt.Errorf("validate dynamic group failed, %+v", err) }

        attributeMap := make(map[string]string)
        for _, attribute := range attributes {
            attributeMap[attribute.PropertyID] = attribute.PropertyType // 构建字段名→类型映射
        }

        // 第二步:按对象类型自动注入内建字段(前端未传这些字段时也要能通过校验)
        switch c.ObjID {
        case common.BKInnerObjIDSet: // set 对象:注入 set_id 为整型字段
            attributeMap[common.BKSetIDField] = common.FieldTypeInt
        case common.BKInnerObjIDModule: // module 对象:注入 module_id 为整型字段
            attributeMap[common.BKModuleIDField] = common.FieldTypeInt
        case common.BKInnerObjIDHost: // host 对象:注入 host_id 和 cloud_id 为整型字段
            attributeMap[common.BKHostIDField] = common.FieldTypeInt
            attributeMap[common.BKCloudIDField] = common.FieldTypeInt
        }
        // 第三步:用 attributeMap 对每个 Condition 做类型安全校验(见上方 Validate 函数)
        for _, cond := range c.Condition {
            if err := cond.Validate(attributeMap); err != nil { return err }
        }
        return nil
    }

switch 分支对 host/set/module 三种对象自动注入必需字段(bk_host_idbk_cloud_idbk_set_idbk_module_id),保证即使前端没传这些字段,校验也不会报错。

本节总结:条件模型的 3 层结构

  • DynamicGroupCondition:Field + Operator + Value 三元组
  • DynamicGroupInfoCondition:ObjID + Condition + TimeCondition
  • DynamicGroupInfo:Condition(静态)+ VariableCondition(可变)双层结构

四、执行分派:ExecuteDynamicGroup 的 host/set 双路径

What — ExecuteDynamicGroup 在做什么?

ExecuteDynamicGroup 定义在 src/scene_server/host_server/service/dynamic_grouping.go 第 445-523 行,是动态分组执行入口。它接收分组 ID 和可选的 variable_condition,先校验参数、查询分组详情、合并条件,然后按 bk_obj_id 分派到 host 或 set 执行逻辑。

Why — 为什么执行要分 host 和 set 两条路径?

问题一:对象模型不同

host 和 set 是 CMDB 中完全不同的资源模型。host 的查询条件涉及 bk_host_idbk_cloud_idbk_host_innerip 等主机专属字段;set 的查询条件涉及 bk_set_idbk_set_name 等集群专属字段。

问题二:执行逻辑不同

host 动态分组的执行最终调用 ExecuteHostDynamicGroup,构建 HostCommonSearch;set 动态分组的执行调用 ExecuteSetDynamicGroup,构建 SetCommonSearch

How — ExecuteDynamicGroup 的分派逻辑

src/scene_server/host_server/service/dynamic_grouping.go 第 445-523 行的完整代码:

// ExecuteDynamicGroup:动态分组执行入口,定义在 service/dynamic_grouping.go L446-523
    // 链路:解析参数 → checkAndBuildParam(校验+查询+合并条件)→ 按ObjID分派到host/set执行器
    func (s *Service) ExecuteDynamicGroup(ctx *rest.Contexts) {
        req := ctx.Request
        targetID := req.PathParameter(common.BKFieldID)        // 从URL路径提取分组ID
        bizID := req.PathParameter(common.BKAppIDField)       // 从URL路径提取业务ID
        bizIDInt64, err := strconv.ParseInt(bizID, 10, 64)  // string→int64业务ID
        if err != nil {
            ctx.RespAutoError(ctx.Kit.CCError.Errorf(common.CCErrCommParamsIsInvalid, common.BKAppIDField))
            return
        }

        input := new(meta.ExecuteOption)
        if err := ctx.DecodeInto(input); err != nil { // 解码请求体:包含variable_condition/fields/page/disable_counter
            ctx.RespAutoError(ctx.Kit.CCError.CCError(common.CCErrCommJSONUnmarshalFailed))
            return
        }

        // 核心预处理:校验参数+查询分组详情+合并静态条件和可变条件→返回最终searchConditions
        result, searchConditions, err := s.checkAndBuildParam(ctx.Kit, input, bizIDInt64, targetID)
        if err != nil {
            blog.Errorf("check and build request param failed, err: %v, rid: %s", err, ctx.Kit.Rid)
            ctx.RespAutoError(err)
            return
        }

        targetDynamicGroup := result.Data
        searchPage := input.Page
        // 按ObjID分派:host走ExecuteHostDynamicGroup,set走ExecuteSetDynamicGroup
        switch targetDynamicGroup.ObjID {
        case common.BKInnerObjIDHost: // host对象类型:构建HostCommonSearch,调用主机执行器
            searchHostCondition := meta.HostCommonSearch{AppID: bizIDInt64, Condition: searchConditions, Page: searchPage}
            data, err := logics.NewLogics(s.Engine, s.CacheDB, s.AuthManager).
                ExecuteHostDynamicGroup(ctx.Kit, &searchHostCondition, input.Fields, input.DisableCounter)
            if err != nil { ctx.RespAutoError(...) ; return }
            ctx.RespEntity(meta.InstDataInfo{Count: data.Count, Info: data.Info}) // 返回主机列表
            return

        case common.BKInnerObjIDSet: // set对象类型:构建SetCommonSearch,调用集群执行器
            searchSetCondition := meta.SetCommonSearch{AppID: bizIDInt64, Condition: searchConditions, Page: searchPage}
            data, err := logics.NewLogics(s.Engine, s.CacheDB, s.AuthManager).
                ExecuteSetDynamicGroup(ctx.Kit, &searchSetCondition, input.Fields, input.DisableCounter)
            if err != nil { ctx.RespAutoError(...) ; return }
            ctx.RespEntity(meta.InstDataInfo{Count: data.Count, Info: data.Info}) // 返回集群列表
            return

        default: // 兜底:ObjID既不是host也不是set,说明分组类型非法
            blog.Errorf("execute dynamic group failed, err: unknown group object type: [%s]",
                targetDynamicGroup.ObjID)
            ctx.RespAutoError(ctx.Kit.CCError.Errorf(common.CCSystemUnknownError))
        }
    }

这段代码的调用链非常清晰:

  1. 参数解析(第 450-465 行):从路径参数提取 bk_biz_id 和分组 ID,从请求体解码 meta.ExecuteOption
  2. 条件构建(第 467 行):checkAndBuildParam 是执行前的核心预处理函数。
  3. 分派执行(第 478-522 行):按 bk_obj_id 分派。
  4. 兜底处理(第 517-522 行):如果 bk_obj_id 既不是 host 也不是 set,返回 CCSystemUnknownError

一个关键细节:checkAndBuildParam

src/scene_server/host_server/service/dynamic_grouping.go 第 525-585 行,checkAndBuildParam 会先查询分组详情,再校验 VariableCondition 是否合法。它做四件事:校验 Fields 非空、校验分页参数合法、查询分组详情、合并 Condition + VariableCondition

本节总结:执行分派的 2 条路径

  • host 路径:ExecuteHostDynamicGroupHostCommonSearch
  • set 路径:ExecuteSetDynamicGroupSetCommonSearch
  • 统一入口:checkAndBuildParam 做参数校验 + 条件合并

五、可变条件:variable_condition 的运行时覆盖机制

What — 可变条件在做什么?

variable_condition 是动态分组运行时参数覆盖的机制。它在 src/common/metadata/dynamic_grouping.go 第 287-294 行定义为 DynamicGroupInfo.VariableCondition,在执行时由前端传入,覆盖静态 Condition 中同 ObjID 同字段的值。

Why — 为什么需要可变条件?

问题一:同一分组,不同场景需要不同参数

假设 SRE 创建了一个"按业务时间筛选主机"的动态分组,静态条件里 bk_create_time $gte 2024-01-01。但每次执行时,SRE 可能想查"最近 7 天"、"最近 30 天"或"本月"。如果每次都要修改分组定义,太麻烦;如果前端直接改查询条件,又失去了分组的共享意义。variable_condition 让前端在执行时传入新的时间范围。

问题二:条件复用 + 参数化的平衡

完全静态的条件不够灵活,完全动态的参数又失去分组的意义。variable_condition 在两者之间取得平衡。

问题三:安全可控

可变条件不是"任意改条件",而是只能在静态条件定义的 ObjIDField 范围内覆盖。

How — 可变条件的合并与校验

src/scene_server/host_server/service/dynamic_grouping.go 第 587-649 行,buildFinalCond 是可变条件合并的核心函数:

// buildFinalCond:合并可变条件(请求中的 variable_condition)到静态可变条件中
    // reqCondArr:前端传入的可变条件,originCondArr:分组定义中保存的静态可变条件
    // 三层短路逻辑:1.无req则返回origin 2.有req但无origin则报错(无字段可覆盖) 3.都非空则校验后合并
    func buildFinalCond(kit *rest.Kit, reqCondArr, originCondArr []meta.DynamicGroupInfoCondition) (
        []meta.DynamicGroupInfoCondition, error) {

        // 第一层短路:前端没传variable_condition,直接返回原静态可变条件
        if len(reqCondArr) == 0 {
            return originCondArr, nil
        }

        // 第二层短路:前端传了variable_condition但分组没定义任何可变条件(无字段可覆盖,报错)
        if len(reqCondArr) != 0 && len(originCondArr) == 0 {
            return nil, errors.New("variable_condition param is invalid")
        }

        // 第三层:校验前端传入的ObjID和Field是否在静态条件白名单中(防越权覆盖未定义字段)
        if err := validateCond(kit, reqCondArr, originCondArr); err != nil {
            return nil, err
        }

        // 按ObjID构建前端可变条件索引(同一ObjID只取最后一个条件)
        reqCondMap := make(map[string]meta.DynamicGroupInfoCondition)
        for _, cond := range reqCondArr {
            reqCondMap[cond.ObjID] = cond
        }

        // 遍历静态可变条件:按ObjID匹配→按Field覆盖
        for idx, cond := range originCondArr {
            reqCond, ok := reqCondMap[cond.ObjID]
            if !ok { continue } // 静态条件中的ObjID前端没传,跳过

            // 把前端传入的同ObjID下的多个Field条件构建成map,方便按Field快速查找
            updateCondMap := make(map[string]meta.DynamicGroupCondition)
            for _, fieldCond := range reqCond.Condition {
                updateCondMap[fieldCond.Field] = fieldCond
            }

            // 把前端传入的时间条件Rules也构建成map
            updateTimeRuleMap := make(map[string]meta.TimeConditionItem)
            if reqCond.TimeCondition != nil {
                for _, rule := range reqCond.TimeCondition.Rules {
                    updateTimeRuleMap[rule.Field] = rule
                }
            }

            // 按Field覆盖:遍历静态条件中的每个subCond,若前端传了同Field则覆盖
            for subIdx, subCond := range cond.Condition {
                updateCond, exist := updateCondMap[subCond.Field]
                if !exist { continue } // 前端没传这个Field,保持原值
                originCondArr[idx].Condition[subIdx] = updateCond // 覆盖(同位置替换)
            }

            // 时间条件特殊处理:TimeCondition.Rules也支持按Field覆盖
            if cond.TimeCondition == nil { continue }
            for subIdx, rule := range cond.TimeCondition.Rules {
                updateTimeRule, exist := updateTimeRuleMap[rule.Field]
                if !exist { continue }
                originCondArr[idx].TimeCondition.Rules[subIdx] = updateTimeRule
            }
        }
        return originCondArr, nil
    }

这段代码的细节非常丰富:

  1. 三层短路(第 590-600 行):reqCondArr 为空时直接返回原条件;originCondArr 为空但有 reqCondArr 则报错;都不为空时校验合法性。
  2. 字段白名单校验(第 598 行):validateCond 检查前端传入的 ObjIDField 是否在静态条件里存在。
  3. 按 ObjID 分组合并(第 602-605 行):前端传入的 variable_conditionObjID 建索引,然后遍历静态条件,找到同 ObjID 的条目进行覆盖。
  4. 按 Field 覆盖(第 625-631 行):对于同一个 ObjID 下的多个条件,按 Field 逐个覆盖。
  5. 时间条件特殊处理(第 634-645 行):TimeCondition.Rules 也支持覆盖。

src/scene_server/host_server/service/dynamic_grouping.go 第 651-680 行,validateCond 的白名单校验:

// validateCond:校验前端传入的可变条件字段是否在分组的静态可变条件白名单中
    // 防止前端通过 variable_condition 覆盖未定义的字段(越权风险)
    func validateCond(kit *rest.Kit, reqCondArr, originCondArr []meta.DynamicGroupInfoCondition) error {
        // 把分组定义的静态可变条件构建成 map[ObjID]→set{Field, Field, ...},作为白名单
        originFieldMap := meta.GetMapFromDynamicCond(originCondArr)

        // 遍历前端传入的每个可变条件,检查ObjID和Field是否在白名单中
        for _, cond := range reqCondArr {
            // 第一层校验:ObjID必须在静态条件中定义(否则说明前端尝试改一个分组不支持的对象类型)
            if _, ok := originFieldMap[cond.ObjID]; !ok {
                return errors.New("variable_condition param is invalid")
            }
            // 第二层校验:该ObjID下的每个Field都必须在白名单中(防止覆盖未声明的字段)
            for _, subCond := range cond.Condition {
                if _, ok := originFieldMap[cond.ObjID][subCond.Field]; !ok {
                    blog.Errorf("variable_condition param field:%s is invalid, rid: %s", subCond.Field, kit.Rid)
                    return fmt.Errorf("variable_condition param field:%s is invalid", subCond.Field)
                }
            }
            // 第三层校验:时间条件Rules的Field同样必须在白名单中
            if cond.TimeCondition == nil { continue }
            for _, rule := range cond.TimeCondition.Rules {
                if _, ok := originFieldMap[cond.ObjID][rule.Field]; !ok {
                    blog.Errorf("variable_condition param field:%s is invalid, rid: %s", rule.Field, kit.Rid)
                    return fmt.Errorf("variable_condition param field:%s is invalid", rule.Field)
                }
            }
        }
        return nil
    }

本节总结:可变条件的 3 个关键设计

  • 1 个合并函数:buildFinalCondObjID + Field 覆盖静态条件
  • 1 个校验函数:validateCond 保证可变条件不能引入非法字段
  • 1 个展平动作:checkAndBuildParam 把两层条件合并成一层,执行器无感知

六、查询与删除:SearchDynamicGroup / DeleteDynamicGroup

What — 查询和删除在做什么?

SearchDynamicGroup 定义在 src/scene_server/host_server/service/dynamic_grouping.go 第 372-443 行,是动态分组的列表查询入口,支持分页和 name 模糊搜索。DeleteDynamicGroup 定义在第 280-341 行,是删除入口,支持事务删除 + 审计日志。

Why — 为什么查询要做 name 模糊搜索?为什么删除要做事务?

问题一:动态分组数量可能很多

一个中等规模的 CMDB 实例可能有上百个动态分组。如果只能精确匹配名称,SRE 很难快速找到目标分组。SearchDynamicGroupname 字段做 $regex 模糊搜索。

问题二:时区问题

动态分组的条件里可能包含时间范围。CMDB 部署在不同时区的机房,数据库存的是 UTC 时间,前端展示需要本地时区。changeTimeToMatchLocalZone 在查询后统一修正时间条件。

问题三:删除必须留痕

如果删除操作没有审计日志,运维无法追溯"谁在什么时候删除了哪个分组"。DeleteDynamicGroup 通过事务保证"删除分组 + 记录审计日志"原子执行。

How — 查询与删除的源码细节

src/scene_server/host_server/service/dynamic_grouping.go 第 372-443 行,SearchDynamicGroup 的核心逻辑:

// SearchDynamicGroup:动态分组列表查询,支持分页和name模糊搜索
    // 链路:解析分页参数 → 强制注入业务ID → name字段包装为模糊搜索 → 调用coreservice → 时区修正
    func (s *Service) SearchDynamicGroup(ctx *rest.Contexts) {
        req := ctx.Request
        bizID := req.PathParameter("bk_biz_id")        // 从路径参数获取业务ID
        bizIDInt64, err := strconv.ParseInt(bizID, 10, 64) // string→int64
        if err != nil { ctx.RespAutoError(...); return }  // 业务ID格式错误直接返回

        input := new(meta.QueryCondition)               // 请求体解码为QueryCondition
        if err := ctx.DecodeInto(input); err != nil { ctx.RespAutoError(...); return }
        if input.Page.Start < 0 { ctx.RespAutoError(...); return } // 起始页不能为负
        if input.Page.IsIllegal() { ctx.RespAutoError(...); return } // limit必须合法

        // 业务ID强制注入:无论前端传什么条件,服务端强制覆盖bk_biz_id(多租户隔离)
        var condition map[string]interface{}
        if input.Condition != nil {
            condition = input.Condition
        } else {
            condition = make(map[string]interface{}) // 条件为空时初始化为空map
        }
        condition[common.BKAppIDField] = bizIDInt64  // 强制注入业务ID过滤条件

        // name字段模糊搜索:若有name则包装为 $like(实际底层是regex)模糊匹配
        // SpecialCharChange 转义正则特殊字符(如 . * + ? 等),防止正则注入
        name, ok := condition["name"].(string)
        if ok && len(name) != 0 {
            condition["name"] = common.KvMap{common.BKDBLIKE: parser.SpecialCharChange(name)}
        }

        input.Condition = condition  // 替换为最终查询条件
        // 调用 coreservice 层的 SearchDynamicGroup
        result, err := s.CoreAPI.CoreService().Host().SearchDynamicGroup(ctx.Kit.Ctx, ctx.Kit.Header, input)
        if err != nil { ctx.RespAutoError(...); return }
        if !result.Result { ctx.RespAutoError(...); return }

        // 时区修正:数据库存的是UTC时间,统一转换为用户本地时区后再返回前端
        for _, dynamicGroup := range result.Data.Info {
            changeTimeToMatchLocalZone(dynamicGroup.Info.Condition)         // 静态条件时区修正
            changeTimeToMatchLocalZone(dynamicGroup.Info.VariableCondition) // 可变条件时区修正
        }
        ctx.RespEntity(result.Data)
    }

这段代码有几个值得注意的设计:

  1. name 模糊搜索(第 416-419 行):如果条件里有 Name 字段,自动包装成 $regex 查询。这里调用了 parser.SpecialCharChange 转义正则特殊字符。
  2. 业务 ID 注入(第 413 行):无论前端传什么条件,服务端强制注入 bk_app_id = bizIDInt64
  3. 时区修正(第 438-441 行):查询结果返回前,对每个分组的 ConditionVariableCondition 调用 changeTimeToMatchLocalZone

src/scene_server/host_server/service/dynamic_grouping.go 第 716-732 行,changeTimeToMatchLocalZone 的实现:

// changeTimeToMatchLocalZone:将时间条件从UTC转换为用户本地时区
    // 数据库存储的time.Time是UTC格式,前端展示需要本地时区(time.Local())
    // 仅修改时间值(rule.Start.Time / rule.End.Time),不影响时间格式
    func changeTimeToMatchLocalZone(conditions []meta.DynamicGroupInfoCondition) {
        for _, condition := range conditions {
            // 跳过没有时间条件的项(无TimeCondition或Rules为nil)
            if condition.TimeCondition == nil || condition.TimeCondition.Rules == nil {
                continue
            }
            for _, rule := range condition.TimeCondition.Rules {
                if rule.Start != nil { // 开始时间存在则转换
                    rule.Start.Time = rule.Start.Local()
                }
                if rule.End != nil {   // 结束时间存在则转换
                    rule.End.Time = rule.End.Local()
                }
            }
        }
    }

src/scene_server/host_server/service/dynamic_grouping.go 第 280-341 行,DeleteDynamicGroup 的核心逻辑:

// DeleteDynamicGroup:动态分组删除,定义在 service/dynamic_grouping.go L281-341
    // 关键设计:先查后删(先Get再Delete),通过AutoRunTxn保证"删除+审计"原子性
    func (s *Service) DeleteDynamicGroup(ctx *rest.Contexts) {
        req := ctx.Request
        bizID := req.PathParameter("bk_biz_id")     // 路径参数:业务ID
        targetID := req.PathParameter("id")          // 路径参数:分组ID

        // 预查询分组详情:必须先Get拿到完整dynamicGroup对象,审计日志需要记录被删除对象的信息
        // 删除后数据就查不到了,所以必须在删除前把对象快照保存到 local 变量
        result, err := s.CoreAPI.CoreService().Host().GetDynamicGroup(
            ctx.Kit.Ctx, bizID, targetID, ctx.Kit.Header)
        if err != nil { ctx.RespAutoError(...); return }
        // 业务校验:查询到完整对象后才能进入事务
        if !result.Result { ctx.RespAutoError(result.CCError()); return }
        dynamicGroup := result.Data  // 完整分组对象快照,用于审计日志

        // 事务:删除MongoDB文档 + 生成审计日志,要么都成功要么都回滚
        autoRunTxnFunc := func() error {
            // 第一步:调用coreservice删除cc_DynamicGroup集合中的文档
            result, err := s.CoreAPI.CoreService().Host().DeleteDynamicGroup(
                ctx.Kit.Ctx, bizID, targetID, ctx.Kit.Header)
            if err != nil { return ctx.Kit.CCError.Error(common.CCErrCommHTTPDoRequestFailed) }
            if !result.Result { return result.CCError() }

            // 第二步:生成审计日志(记录被删除分组的完整快照,含创建者、条件等)
            audit := auditlog.NewDynamicGroupAuditLog(s.CoreAPI.CoreService())
            auditParam := auditlog.NewGenerateAuditCommonParameter(ctx.Kit, meta.AuditDelete) // 操作类型=Delete
            auditLogs, err := audit.GenerateAuditLog(auditParam, &dynamicGroup)              // 用事务外的对象快照
            if err != nil { return err }
            if err := audit.SaveAuditLog(ctx.Kit, auditLogs...); err != nil { return err }  // 持久化审计日志
            return nil
        }

        // 执行原子事务
        if err := s.Engine.CoreAPI.CoreService().Txn().AutoRunTxn(
            ctx.Kit.Ctx, ctx.Kit.Header, autoRunTxnFunc); err != nil {
            ctx.RespAutoError(err)
            return
        }
        ctx.RespEntity(nil) // 删除成功返回空对象
    }

删除链路的设计和创建链路对称:先查询分组详情(为了审计日志需要完整对象),再通过事务执行"删除 + 审计"。

本节总结:查询与删除的设计

  • 查询:Name 模糊搜索 + 业务 ID 强制注入 + 时区修正
  • 删除:先查后删 + 事务保证 + 审计日志
  • 安全:parser.SpecialCharChange 防止正则注入

七、动态分组的 5 个核心设计原则

源码视角总结:从动态分组看 CMDB 的条件管理哲学

读完 src/scene_server/host_server/service/dynamic_grouping.gosrc/common/metadata/dynamic_grouping.go,可以把动态分组的设计哲学总结为 5 个原则:

1. 条件即资源

动态分组把"查询条件"提升为一级资源,拥有独立的 ID、名称、创建者、审计日志和 IAM 权限。

2. 静态 + 可变双层结构

Condition + VariableCondition 的设计,让动态分组既能保存固定的筛选逻辑,又能支持运行时参数覆盖。

3. 类型安全前置

DynamicGroupCondition.Validate 在创建时就校验操作符和字段类型的匹配关系,把非法查询拦截在入口处。

4. 事务保证一致性

创建和删除都使用 AutoRunTxn,保证"数据操作 + 审计日志 + IAM 注册"要么全部成功,要么全部回滚。

5. 多租户隔离

bk_biz_id 作为命名空间,保证业务 A 的动态分组不会泄露给业务 B。查询时服务端强制注入 bk_app_id

避坑提醒(源码视角):

  • 不要在 Value 里放未知类型DynamicGroupCondition.Valueinterface{},但 Validate 会按属性类型做严格校验
  • 注意 bk_biz_id 的隔离语义:资源池(bk_biz_id=0)和业务下的动态分组共用同一套接口
  • 不要忽略 IAM 注册失败的影响:如果 registerActionToIAM 失败,整个创建事务回滚,分组不会被创建

全篇总结:动态分组的 5 个核心设计

  • 1 个创建入口:CreateDynamicGroupAutoRunTxn 保证"创建 + 审计 + IAM 注册"原子性
  • 3 层条件模型:DynamicGroupConditionDynamicGroupInfoConditionDynamicGroupInfo
  • 2 类执行路径:ExecuteDynamicGroupbk_obj_id 分派到 ExecuteHostDynamicGroup / ExecuteSetDynamicGroup
  • 1 套可变条件:variable_condition + buildFinalCond 实现运行时条件覆盖
  • 1 个查询入口:SearchDynamicGroup 支持 name 模糊搜索 + 时区修正

FAQ(常见 20 问)

Q1. CMDB 3.14.6 的动态分组创建入口在哪里?

一句话结论:CreateDynamicGroup 是唯一创建入口,定义在 src/scene_server/host_server/service/dynamic_grouping.go 第 33 行。

src/scene_server/host_server/service/dynamic_grouping.go 第 33-95 行,CreateDynamicGroup 接收 meta.DynamicGroup JSON,经过参数校验后,通过 AutoRunTxn 把"创建分组 + 审计日志 + IAM 注册"打包成原子事务。

Q2. 动态分组创建时为什么必须走 AutoRunTxn 事务?

一句话结论:因为创建涉及 MongoDB 写入、审计日志、IAM 注册三个步骤,必须保证原子性。

src/scene_server/host_server/service/dynamic_grouping.go 第 90 行,AutoRunTxn 包裹整个创建逻辑。如果 IAM 注册失败,已经写入 MongoDB 的分组文档会被回滚。

Q3. 动态分组的数据模型是什么样的?

一句话结论:DynamicGroup 包含 AppID、ID、Name、ObjID、Info、CreateUser 等字段。

src/common/metadata/dynamic_grouping.go 第 387-415 行,DynamicGroup 是顶层结构体。AppID 对应 bk_biz_id,用于业务隔离;ObjID 决定分组类型(host/set);Info 承载查询条件。

Q4. DynamicGroupCondition 的三元组是什么?

一句话结论:Field + Operator + Value,是动态分组最小的条件单元。

src/common/metadata/dynamic_grouping.go 第 86-96 行,DynamicGroupConditionField 是字段名,Operator 是操作符,Value 是条件值。

Q5. 动态分组的条件支持哪些操作符?

一句话结论:支持 8 种操作符:$eq/$ne/$in/$nin/$lte/$gte/$regex

src/common/metadata/dynamic_grouping.go 第 28-65 行,DynamicGroupOperators 维护了所有支持的操作符。除上述 7 种外,还包含 filter.Containsfilter.ContainsSensitive 两种包内路径。

Q6. 动态分组支持哪些对象类型?

一句话结论:当前版本支持 host 和 set 两种对象类型。

src/common/metadata/dynamic_grouping.go 第 67-80 行,DynamicGroupConditionTypes 静态表定义了 host 动态分组支持 host/set/module 三种 ObjID,set 动态分组只支持 set 一种 ObjID。执行路径里第 478-522 行的 switch 也只处理这两种。

Q7. ExecuteDynamicGroup 的执行分派逻辑是什么?

一句话结论:按 bk_obj_id 分派,host 走 ExecuteHostDynamicGroup,set 走 ExecuteSetDynamicGroup

src/scene_server/host_server/service/dynamic_grouping.go 第 478-522 行,host 路径构建 HostCommonSearch,set 路径构建 SetCommonSearch

Q8. 什么是 variable_condition?有什么用?

一句话结论:variable_condition 是运行时可变条件,允许前端在执行时覆盖静态条件。

src/common/metadata/dynamic_grouping.go 第 287-294 行,DynamicGroupInfo 包含 Condition(静态)和 VariableCondition(可变)。执行时 buildFinalCond 把可变条件合并到静态条件上。

Q9. 可变条件可以随意新增字段吗?

一句话结论:不可以。validateCond 会检查可变条件的 ObjID 和 Field 是否在静态条件中存在。

src/scene_server/host_server/service/dynamic_grouping.go 第 651-680 行,validateCondGetMapFromDynamicCond 构建静态条件的字段白名单。

Q10. SearchDynamicGroup 支持模糊搜索吗?

一句话结论:支持。如果条件里包含 name 字段,会自动包装成 $regex 模糊搜索。

src/scene_server/host_server/service/dynamic_grouping.go 第 416-419 行,condition["name"] 会被包装成 common.KvMap{common.BKDBLIKE: parser.SpecialCharChange(name)}

Q11. SearchDynamicGroup 返回结果前为什么要修正时区?

一句话结论:因为动态分组的条件里可能包含时间范围,数据库存的是 UTC,需要转换到用户所在时区。

src/scene_server/host_server/service/dynamic_grouping.go 第 438-441 行和第 716-732 行的 changeTimeToMatchLocalZone

Q12. 删除动态分组时为什么要先查询再删除?

一句话结论:因为审计日志需要记录被删除分组的完整信息,删除后就查不到了。

src/scene_server/host_server/service/dynamic_grouping.go 第 290-303 行,DeleteDynamicGroup 先调用 GetDynamicGroup 查询分组详情,保存到 dynamicGroup 变量后,才执行删除。

Q13. 动态分组和 IAM 有什么关系?

一句话结论:创建动态分组时,会向 IAM 注册 BizCustomQuery 资源创建权限,让创建者自动拥有该分组的操作权限。

src/scene_server/host_server/service/dynamic_grouping.go 第 97-123 行,registerActionToIAM 构造 meta.IamInstanceWithCreator{Type: string(iam.BizCustomQuery), ID, Name, Creator},调用 AuthManager.Authorizer.RegisterResourceCreatorAction

Q14. DynamicGroupInfoCondition.Validate 会自动注入哪些字段?

一句话结论:根据 ObjID 自动注入 bk_host_idbk_cloud_idbk_set_idbk_module_id 等内建字段。

src/common/metadata/dynamic_grouping.go 第 265-275 行,switch 分支针对 Set、Module、Host 三种对象类型分别注入对应整型字段到 attributeMap

Q15. DynamicGroupCondition 的 Validate 函数做了哪些校验?

一句话结论:操作符白名单 + 字段支持 + 类型特化(bool/date)+ 值类型匹配 4 个层次。

src/common/metadata/dynamic_grouping.go 第 98-168 行,第 100-103 行检查操作符,第 125-136 行处理 bool/date 特化,第 138-165 行根据操作符和属性类型验证值。

Q16. 动态分组与 host/set 的资源类型映射是什么?

一句话结论:CMDB 的 meta.DynamicGrouping 在 IAM 中映射到 BizCustomQuery(值 "biz_custom_query")。

src/ac/iam/adaptor.go 第 75 行 ccIamResTypeMap 映射,以及第 252-259 行 resourceActionMap 中动态分组的动作映射。

Q17. 动态分组支持 $regex 模糊匹配吗?

一句话结论:支持。DynamicGroupOperatorLIKE = "$regex" 通过 VerifyRegexValidity 验证正则合法性。

src/common/metadata/dynamic_grouping.go 第 170-191 行,VerifyRegexValidityregexp.Compile 验证正则表达式是否合法。

Q18. DynamicGroupInfo.Validate 为什么不能同时为空?

一句话结论:DynamicGroup.Validate 第 433-436 行强制要求 ConditionVariableCondition 至少有一个不为空。

src/common/metadata/dynamic_grouping.go 第 417-438 行,Validate 在两个条件都为空时返回 "info.condition and info.variable_condition can not be empty at the same time"

Q19. DynamicGroup 的 UpdateTime 在哪里更新?

一句话结论:由 coreservice 在 UpdateDynamicGroup 时设置,对应 BSON 字段 "last_time"

src/common/metadata/dynamic_grouping.go 第 412-414 行,UpdateTime time.Time `json:"last_time" bson:"last_time",注意 JSON 字段是 last_time 而不是驼峰式。

Q20. 如果 IAM 服务宕机,动态分组会怎么样?

一句话结论:如果 EnableAuthorize() == true,创建时会因为 IAM 注册失败而回滚;如果为 false,则不受影响。

src/scene_server/host_server/service/dynamic_grouping.go 第 80-85 行,auth.EnableAuthorize() 控制 IAM 注册是否执行。如果 IAM 宕机且开关开启,registerActionToIAM 会返回网络错误,整个创建事务回滚。

FAQ 全篇总纲

本 FAQ 覆盖了 CMDB 3.14.6 动态分组的 5 大维度:

  • 创建与事务:Q1(创建入口)、Q2(AutoRunTxn)、Q13(IAM 集成)、Q20(IAM 宕机影响)
  • 数据模型:Q3(DynamicGroup 模型)、Q4(Condition 三元组)、Q14(自动注入字段)、Q18(空条件校验)、Q19(UpdateTime 字段)
  • 条件校验与操作符:Q5(操作符)、Q15(Validate 4 层校验)、Q17($regex 校验)
  • 执行分派与可变条件:Q6(对象类型)、Q7(分派逻辑)、Q8(variable_condition)、Q9(字段白名单)、Q16(IAM 类型映射)
  • 查询与删除:Q10(模糊搜索)、Q11(时区修正)、Q12(先查后删)

Roadmap 后续预告

下一篇 #15:云区域管理 — 多云/混合云账号与区域管理

将带你深入 src/scene_server/host_server/service/cloudarea.go,看云账号、云区域、云主机三层的关联关系如何建立,以及同步外部云平台资源时如何做字段映射和冲突处理。

后续专题预告:

  • #16:拓扑缓存加速 — 业务拓扑秒级查询的 Redis 缓存策略
  • #17:k8s 资源纳管 — Pod/Deployment/Service 统一管理
  • #18:容器拓扑关联 — Pod ↔ Host ↔ 机柜全链路映射
  • #19:主机锁定 — 并发控制与分布式锁
  • #20:批量导入导出 — Excel 模板与数据校验
posted @ 2026-07-06 21:23  左扬  阅读(17)  评论(0)    收藏  举报