蓝鲸 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 隔离命名空间,保证条件不会跨业务泄露。
没有动态分组会发生什么?
- 没有条件复用:每次查询都要重新拼条件,脚本和页面重复代码多
- 没有权限控制:无法对"某个分组谁能执行"做细粒度授权
- 没有审计:无法追溯"谁在什么时候执行了哪些筛选条件"
- 没有业务隔离:资源池和业务下的筛选条件混在一起,容易误操作
读 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 是条件值。注意 Value 用 interface{},因为同一个条件里可能是字符串、数字、数组或布尔值。
读 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.Value 是 interface{},但 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 资源类型。创建分组时同步注册"创建者授权",保证创建者立即拥有该分组的编辑权限。
看 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给客户端
}
这段代码的调用链非常清晰:
- 参数解码与校验(第 36-45 行):ctx.DecodeInto 把 JSON 请求体解码成 meta.DynamicGroup,然后调用 createGroupParamCheck 做格式校验(第 126-140 行)。
- 填充系统字段(第 46-47 行):CreateUser 和 CreateTime 由服务端强制赋值,前端传入的这两个字段会被忽略。
- 原子事务执行(第 90 行):AutoRunTxn 包裹整个创建逻辑。
- 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 — 条件元数据在做什么?
DynamicGroupCondition、DynamicGroupInfoCondition 和 DynamicGroupInfo 三层结构,共同定义了动态分组的条件模型。它们在 src/common/metadata/dynamic_grouping.go 中定义。
Why — 为什么条件模型要做类型安全校验?
问题一:用户输入不可信
前端传入的条件可能是字符串、数字、布尔值或数组。如果直接把 Value 塞进 MongoDB 查询,类型不匹配会导致查询失败。
问题二:操作符和字段类型必须匹配
某些操作符只支持特定类型:$eq 支持所有类型,$gt/$lt 只支持数值,$regex 只支持字符串,布尔值只能用 $eq。
问题三:动态分组支持 host 和 set 两种对象
host 动态分组可以按 bk_host_id、bk_cloud_id 等字段筛选;set 动态分组只能按 bk_set_id 筛选。
读 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 个层次的校验:
- 操作符白名单(第 100-103 行):不在 DynamicGroupOperators 里的操作符直接拒绝
- 字段支持(第 105-112 行):检查字段是否在属性映射表中,bk_default_field 例外
- 类型特化(第 125-136 行):布尔只支持 $eq,日期只支持 $gte/$lte
- 值类型匹配(第 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_id、bk_cloud_id、bk_set_id、bk_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_id、bk_cloud_id、bk_host_innerip 等主机专属字段;set 的查询条件涉及 bk_set_id、bk_set_name 等集群专属字段。
问题二:执行逻辑不同
host 动态分组的执行最终调用 ExecuteHostDynamicGroup,构建 HostCommonSearch;set 动态分组的执行调用 ExecuteSetDynamicGroup,构建 SetCommonSearch。
看 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))
}
}
这段代码的调用链非常清晰:
- 参数解析(第 450-465 行):从路径参数提取 bk_biz_id 和分组 ID,从请求体解码 meta.ExecuteOption。
- 条件构建(第 467 行):checkAndBuildParam 是执行前的核心预处理函数。
- 分派执行(第 478-522 行):按 bk_obj_id 分派。
- 兜底处理(第 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 路径:ExecuteHostDynamicGroup → HostCommonSearch
- set 路径:ExecuteSetDynamicGroup → SetCommonSearch
- 统一入口: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 在两者之间取得平衡。
问题三:安全可控
可变条件不是"任意改条件",而是只能在静态条件定义的 ObjID 和 Field 范围内覆盖。
看 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
}
这段代码的细节非常丰富:
- 三层短路(第 590-600 行):reqCondArr 为空时直接返回原条件;originCondArr 为空但有 reqCondArr 则报错;都不为空时校验合法性。
- 字段白名单校验(第 598 行):validateCond 检查前端传入的 ObjID 和 Field 是否在静态条件里存在。
- 按 ObjID 分组合并(第 602-605 行):前端传入的 variable_condition 按 ObjID 建索引,然后遍历静态条件,找到同 ObjID 的条目进行覆盖。
- 按 Field 覆盖(第 625-631 行):对于同一个 ObjID 下的多个条件,按 Field 逐个覆盖。
- 时间条件特殊处理(第 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 个合并函数:buildFinalCond 按 ObjID + 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 很难快速找到目标分组。SearchDynamicGroup 对 name 字段做 $regex 模糊搜索。
问题二:时区问题
动态分组的条件里可能包含时间范围。CMDB 部署在不同时区的机房,数据库存的是 UTC 时间,前端展示需要本地时区。changeTimeToMatchLocalZone 在查询后统一修正时间条件。
问题三:删除必须留痕
如果删除操作没有审计日志,运维无法追溯"谁在什么时候删除了哪个分组"。DeleteDynamicGroup 通过事务保证"删除分组 + 记录审计日志"原子执行。
看 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)
}
这段代码有几个值得注意的设计:
- name 模糊搜索(第 416-419 行):如果条件里有 Name 字段,自动包装成 $regex 查询。这里调用了 parser.SpecialCharChange 转义正则特殊字符。
- 业务 ID 注入(第 413 行):无论前端传什么条件,服务端强制注入 bk_app_id = bizIDInt64。
- 时区修正(第 438-441 行):查询结果返回前,对每个分组的 Condition 和 VariableCondition 调用 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 个核心设计原则
读完 src/scene_server/host_server/service/dynamic_grouping.go 和 src/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.Value 是 interface{},但 Validate 会按属性类型做严格校验
- 注意 bk_biz_id 的隔离语义:资源池(bk_biz_id=0)和业务下的动态分组共用同一套接口
- 不要忽略 IAM 注册失败的影响:如果 registerActionToIAM 失败,整个创建事务回滚,分组不会被创建
全篇总结:动态分组的 5 个核心设计
- 1 个创建入口:CreateDynamicGroup 用 AutoRunTxn 保证"创建 + 审计 + IAM 注册"原子性
- 3 层条件模型:DynamicGroupCondition → DynamicGroupInfoCondition → DynamicGroupInfo
- 2 类执行路径:ExecuteDynamicGroup 按 bk_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 行,DynamicGroupCondition 的 Field 是字段名,Operator 是操作符,Value 是条件值。
Q5. 动态分组的条件支持哪些操作符?
一句话结论:支持 8 种操作符:$eq/$ne/$in/$nin/$lte/$gte/$regex。
读 src/common/metadata/dynamic_grouping.go 第 28-65 行,DynamicGroupOperators 维护了所有支持的操作符。除上述 7 种外,还包含 filter.Contains 和 filter.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 行,validateCond 用 GetMapFromDynamicCond 构建静态条件的字段白名单。
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_id、bk_cloud_id、bk_set_id、bk_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 行,VerifyRegexValidity 用 regexp.Compile 验证正则表达式是否合法。
Q18. DynamicGroupInfo.Validate 为什么不能同时为空?
一句话结论:DynamicGroup.Validate 第 433-436 行强制要求 Condition 或 VariableCondition 至少有一个不为空。
读 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 模板与数据校验

浙公网安备 33010602011771号