使用Go-zero构建基于ArangoDB的CMDB平台
要使用 Go-zero 的 API 和 RPC 结合 ArangoDB 实现一个 CMDB 平台,同时满足 CI 项、CI 属性、CI 关系的高度抽象化、动态配置调整以及 CI 属性自定义约束的需求,核心在于设计灵活的数据模型和相应的后端服务。
1. 技术栈选择
- Go-zero: 作为微服务框架,提供 API 和 RPC 功能,简化服务开发和部署。
- ArangoDB: 多模型图数据库,天然适合存储 CI 关系和灵活的属性数据。
- Go: 后端开发语言。
2. 核心数据模型设计 (ArangoDB)
ArangoDB 的 Schema-less 特性非常适合处理动态可配置的数据。我们将主要使用以下几种集合:
- ci_types (Document Collection - CI 项类型):
- _key: CI 类型唯一标识 (例如: server, database, application)
- name: CI 类型名称 (例如: "服务器", "数据库", "应用程序")
- description: CI 类型描述
- display_order: 显示顺序
- icon: 图标信息 (可选)
- ci_attributes (Document Collection - CI 属性定义):
- _key: 属性唯一标识 (例如: ip_address, os_type, cpu_cores)
- name: 属性名称 (例如: "IP 地址", "操作系统类型", "CPU 核数")
- description: 属性描述
- data_type: 数据类型 (例如: string, integer, float, boolean, enum, date)
- constraints: 自定义约束 JSON 对象 (非常重要,用于定义属性的校验规则)。 例如:
- JSON
- { "required": true, "min_length": 7, "max_length": 15, "regex": "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)#34;, "enum_values": ["Linux", "Windows", "MacOS"], // for enum type "min_value": 0, // for numeric types "max_value": 100 // for numeric types }
- display_type: 属性在前端的显示方式 (例如: text_input, select, checkbox, textarea)
- default_value: 默认值 (可选)
- ci_type_attribute_relations (Edge Collection - CI 类型与属性的关联):
- _from: ci_types/_key
- _to: ci_attributes/_key
- is_required: boolean, 是否为该 CI 类型下的必填属性
- editable: boolean, 是否可编辑
- cis (Document Collection - 具体的 CI 实例):
- _key: CI 实例唯一标识 (例如: server_001, db_002)
- ci_type_id: 关联 ci_types 的 _key
- attributes: JSON 对象,存储 CI 实例的实际属性值。 例如:
- JSON
- { "ip_address": "192.168.1.100", "os_type": "Linux", "cpu_cores": 8, "status": "Running" }
- 注意: 这里的属性键是 ci_attributes 的 _key,值是具体的属性值。
- ci_relations (Edge Collection - CI 实例间的关系):
- _from: cis/_key (源 CI 实例)
- _to: cis/_key (目标 CI 实例)
- relation_type: 关系类型 (例如: depends_on, runs_on, belongs_to, consumes)
- description: 关系描述
- properties: 关系特有的属性 (可选 JSON 对象,例如:{"port": 8080})
3. Go-zero 服务设计
我们将拆分为两个主要的服务:
3.1 CMDB API 服务 (Go-zero API)
提供前端交互的 RESTful API,处理 CMDB 的增删改查以及动态配置。
- CI 类型管理:
- POST /ci_types: 创建 CI 类型
- GET /ci_types: 获取所有 CI 类型
- GET /ci_types/:id: 获取单个 CI 类型详情
- PUT /ci_types/:id: 更新 CI 类型
- DELETE /ci_types/:id: 删除 CI 类型
- CI 属性管理:
- POST /ci_attributes: 创建 CI 属性
- GET /ci_attributes: 获取所有 CI 属性
- GET /ci_attributes/:id: 获取单个 CI 属性详情
- PUT /ci_attributes/:id: 更新 CI 属性 (包括约束 constraints)
- DELETE /ci_attributes/:id: 删除 CI 属性
- CI 类型与属性关联管理:
- POST /ci_type_attribute_relations: 关联 CI 类型和属性
- DELETE /ci_type_attribute_relations: 解除关联
- CI 实例管理:
- POST /cis: 创建 CI 实例 (需要根据 ci_type_id 和 ci_type_attribute_relations 进行属性校验)
- GET /cis: 查询 CI 实例 (支持按 CI 类型、属性值过滤、分页)
- GET /cis/:id: 获取单个 CI 实例详情
- PUT /cis/:id: 更新 CI 实例 (需要进行属性校验)
- DELETE /cis/:id: 删除 CI 实例
- CI 关系管理:
- POST /ci_relations: 创建 CI 关系
- DELETE /ci_relations/:from/:to/:relation_type: 删除 CI 关系
- GET /ci_relations/graph/:ci_id: 获取某个 CI 相关的图谱 (使用 ArangoDB AQL 的图遍历功能)
3.2 CMDB RPC 服务 (Go-zero RPC)
提供内部服务调用,例如属性校验、图数据操作等。API 服务可以调用 RPC 服务来执行核心逻辑。
- 属性校验服务:
- ValidateCIProperties(ciTypeID string, properties map[string]interface{}) error: 根据 CI 类型和定义的属性约束校验 CI 实例的属性值。这会查询 ci_type_attribute_relations 和 ci_attributes 来获取校验规则。
- 图查询服务:
- GetCIRelatedGraph(ciID string, depth int) (graphData interface{}, error): 提供更复杂的图查询功能,例如查询 N 度关联关系。
4. 关键实现细节
4.1 动态配置与抽象化
- CI 项和属性高度抽象化: 通过 ci_types 和 ci_attributes 集合实现。当需要增加新的 CI 类型或属性时,只需在 ci_types 和 ci_attributes 中创建相应文档,并通过 ci_type_attribute_relations 建立关联即可。
- CI 关系抽象化: 通过 ci_relations 边的 relation_type 字段实现。可以定义任意的关系类型。
- 动态调整: CMDB API 服务提供 CRUD 接口,使得 CI 类型、属性、关系都可以通过 API 动态配置和调整,无需修改代码。
4.2 CI 属性自定义约束
这是最核心和复杂的部分。
- ci_attributes.constraints 字段: 这个 JSON 字段是实现自定义约束的关键。在 Go 代码中,可以定义一个结构体来解析这个 JSON,并根据 data_type 和 constraints 的内容实现不同的校验逻辑。 示例 Go 校验函数签名:
- Go
- func ValidateAttributeValue(attrDef *AttributeDefinition, value interface{}) error { // attrDef 包含 name, data_type, constraints 等信息 // 根据 data_type 进行类型检查 // 根据 constraints (required, regex, min_length, max_length, enum_values, min_value, max_value) 进行具体校验 return nil // 或返回具体的校验错误 }
- 校验流程: 当创建或更新 CI 实例时,API 服务接收到 cis.attributes JSON。 根据 cis.ci_type_id 查询 ci_type_attribute_relations,获取该 CI 类型关联的所有属性定义 (ci_attributes)。 遍历 cis.attributes 中的每个键值对,找到对应的属性定义。 调用 ValidateAttributeValue 函数对每个属性值进行校验。如果属性是必填但在 cis.attributes 中缺失,也应报错。
4.3 ArangoDB AQL 使用
ArangoDB 的 AQL (ArangoDB Query Language) 在图遍历和复杂查询方面非常强大。
- 获取 CI 相关图谱:
- 代码段
- FOR v, e, p IN 1..@depth ANY @startNodeID ci_relations RETURN { vertex: v, edge: e, path: p }
- @startNodeID 为 CI 实例的 _id。
- @depth 为遍历深度。
- 查询指定 CI 类型下的 CI 实例及其属性:
- 代码段
- FOR ci IN cis FILTER ci.ci_type_id == @ciTypeID RETURN ci
- 根据属性值过滤 CI 实例:
- 代码段
- FOR ci IN cis FILTER ci.ci_type_id == "server" AND ci.attributes.os_type == "Linux" RETURN ci
5. 部署架构概览
6. 核心开发流程
- ArangoDB 数据库和集合初始化: 创建上述定义的集合。
- Go-zero Protobuf 定义: 定义 API 和 RPC 服务的 .proto 文件,包括请求和响应结构体。 API Proto: 定义 CMDB 的各种管理接口。 RPC Proto: 定义内部校验和图查询接口。
- Go-zero 代码生成: 使用 goctl 工具生成 API 和 RPC 服务的基础代码。
- 服务逻辑实现: API Service: 实现各个 API 接口,包括参数校验、ArangoDB 操作、调用 RPC 服务等。 RPC Service: 实现属性校验逻辑、ArangoDB 图查询逻辑。
- ArangoDB Driver 集成: 在 Go 服务中使用 ArangoDB Go Driver 与数据库交互。
- 错误处理和日志: 完善错误处理机制和日志记录,方便排查问题。
- 测试: 对各个接口和核心逻辑进行单元测试和集成测试。
- 部署: 将 Go-zero 服务部署到容器化平台 (如 Kubernetes) 或虚拟机上。
7. 总结
通过 Go-zero 提供的微服务能力和 ArangoDB 的多模型图数据特性,我们能够构建一个高度抽象化、可动态配置调整的 CMDB 平台。核心在于灵活的数据模型设计,特别是 CI 属性的自定义约束,以及 Go-zero API 和 RPC 服务的分层协作。这种架构不仅满足了功能需求,也为未来的扩展和维护提供了良好的基础。
提供一个完整的 Go-zero + ArangoDB CMDB 项目代码实现,这会是一个相当庞大的工程,涉及到多个服务、大量的业务逻辑、数据库交互以及详细的配置。一个完整的生产级 CMDB 平台代码量通常在数万行甚至更多,远超单个 AI 回复的范畴。
不过,我可以为你提供一个精简但功能完整的骨架代码结构和关键部分的实现示例。这个示例将涵盖:
- 项目结构:符合 Go-zero 最佳实践。
- ArangoDB 连接与操作:基础的数据库交互。
- CI 类型管理:CRUD 示例。
- CI 属性管理:包含动态约束的定义和校验逻辑。
- CI 实例管理:如何存储动态属性,并进行属性校验。
- CI 关系管理:图数据的存储示例。
- Go-zero API 和 RPC 服务:基础的定义和调用。
项目代码骨架与核心实现示例
我们将构建一个包含 api 服务和 rpc 服务的 Go-zero 项目。
1. 项目结构
cmdb-platform/
├── api/
│ └── cmd/
│ └── cmdb/
│ ├── cmdb.go
│ ├── etc/
│ │ └── cmdb.yaml
│ └── internal/
│ ├── config/
│ │ └── config.go
│ ├── handler/
│ │ ├── ciattributehandler.go
│ │ ├── ciinstancehandler.go
│ │ ├── cirelationhandler.go
│ │ └── citypehandler.go
│ ├── logic/
│ │ ├── ciattribute/
│ │ │ ├── createciattributevc.go
│ │ │ └── ...
│ │ ├── ciinstance/
│ │ │ ├── createciinstancevc.go
│ │ │ └── ...
│ │ ├── cirelation/
│ │ │ ├── createcirelationvc.go
│ │ │ └── ...
│ │ └── citype/
│ │ ├── createcitypevc.go
│ │ └── ...
│ ├── svc/
│ │ └── servicecontext.go
│ └── types/
│ └── cmdb.go # 定义 API 请求/响应结构体
├── rpc/
│ └── cmd/
│ └── cmdbcore/
│ ├── cmdbcore.go
│ ├── etc/
│ │ └── cmdbcore.yaml
│ └── internal/
│ ├── config/
│ │ └── config.go
│ ├── logic/
│ │ └── attributevalidationlogic.go
│ ├── svc/
│ │ └── servicecontext.go
│ └── pb/
│ └── cmdbcore.proto # 定义 RPC 接口
└── model/
└── arangodb/
├── client.go # ArangoDB 客户端初始化
├── citype.go # CI 类型模型
├── ciattribute.go # CI 属性模型
├── ciinstance.go # CI 实例模型
└── cirelation.go # CI 关系模型
2. Protobuf 定义
rpc/cmd/cmdbcore/pb/cmdbcore.proto (CMDB RPC 服务)
Protocol Buffers
syntax = "proto3";
package cmdbcore;
option go_package = "./pb";
service CmdbCore {
rpc ValidateCiProperties (ValidateCiPropertiesReq) returns (ValidateCiPropertiesResp);
}
message ValidateCiPropertiesReq {
string ciTypeId = 1;
map<string, string> properties = 2; // ArangoDB JSON map can be string values initially,
// actual type conversion/validation happens in logic.
}
message ValidateCiPropertiesResp {
bool success = 1;
string errorMessage = 2;
}
api/cmd/cmdb/types/cmdb.go (CMDB API 服务)
Go-zero 会根据 API 定义自动生成这个文件。 我们需要定义 API 请求和响应的 Go 结构体。例如:
Go
// ci_type.api
type CreateCiTypeReq {
Name string `json:"name"`
Description string `json:"description,optional"`
DisplayOrder int64 `json:"displayOrder,optional"`
Icon string `json:"icon,optional"`
}
type CreateCiTypeResp {
Id string `json:"id"`
}
// ci_attribute.api
type CreateCiAttributeReq {
Name string `json:"name"`
Description string `json:"description,optional"`
DataType string `json:"dataType"` // e.g., string, integer, boolean, enum
Constraints string `json:"constraints,optional"` // JSON string
DisplayType string `json:"displayType,optional"`
DefaultValue string `json:"defaultValue,optional"` // JSON string representation
}
type CreateCiAttributeResp {
Id string `json:"id"`
}
// ci_instance.api
type CreateCiInstanceReq {
CiTypeId string `json:"ciTypeId"`
Properties map[string]interface{} `json:"properties"` // Dynamically typed properties
}
type CreateCiInstanceResp {
Id string `json:"id"`
}
3. ArangoDB 连接与模型
model/arangodb/client.go
Go
package arangodb
import (
"context"
"fmt"
"log"
"time"
driver "github.com/arangodb/go-driver"
"github.com/arangodb/go-driver/http"
)
var DB driver.Database
// InitArangoDB initializes the ArangoDB connection
func InitArangoDB(url, user, password, dbName string) {
conn, err := http.NewConnection(http.ConnectionConfig{
Endpoints: []string{url},
})
if err != nil {
log.Fatalf("Failed to create ArangoDB connection: %v", err)
}
client, err := driver.NewClient(driver.ClientConfig{
Connection: conn,
Authentication: driver.BasicAuthentication(user, password),
})
if err != nil {
log.Fatalf("Failed to create ArangoDB client: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dbExists, err := client.DatabaseExists(ctx, dbName)
if err != nil {
log.Fatalf("Failed to check ArangoDB database existence: %v", err)
}
if !dbExists {
_, err = client.CreateDatabase(ctx, dbName, nil)
if err != nil {
log.Fatalf("Failed to create ArangoDB database '%s': %v", dbName, err)
}
log.Printf("ArangoDB database '%s' created successfully.", dbName)
}
db, err := client.Database(ctx, dbName)
if err != nil {
log.Fatalf("Failed to connect to ArangoDB database '%s': %v", dbName, err)
}
DB = db
log.Println("ArangoDB connection initialized successfully.")
// Ensure collections exist
ensureCollections(ctx)
}
func ensureCollections(ctx context.Context) {
collections := map[string]driver.CollectionType{
"ci_types": driver.CollectionTypeDocument,
"ci_attributes": driver.CollectionTypeDocument,
"ci_type_attribute_relations": driver.CollectionTypeEdge, // Edge collection
"cis": driver.CollectionTypeDocument,
"ci_relations": driver.CollectionTypeEdge, // Edge collection
}
for name, colType := range collections {
colExists, err := DB.CollectionExists(ctx, name)
if err != nil {
log.Fatalf("Failed to check collection '%s' existence: %v", name, err)
}
if !colExists {
_, err = DB.CreateCollection(ctx, name, &driver.CreateCollectionOptions{Type: colType})
if err != nil {
log.Fatalf("Failed to create collection '%s': %v", name, err)
}
log.Printf("Collection '%s' created successfully.", name)
}
}
}
// GetCollection returns an existing collection by name.
func GetCollection(ctx context.Context, name string) (driver.Collection, error) {
if DB == nil {
return nil, fmt.Errorf("ArangoDB is not initialized")
}
return DB.Collection(ctx, name)
}
model/arangodb/citype.go (CI 类型模型)
Go
package arangodb
import (
"context"
"fmt"
"log"
driver "github.com/arangodb/go-driver"
)
const CiTypesCollectionName = "ci_types"
// CiType represents a CI type definition
type CiType struct {
Key string `json:"_key,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
DisplayOrder int64 `json:"displayOrder,omitempty"`
Icon string `json:"icon,omitempty"`
}
// CreateCiType saves a new CI type to ArangoDB
func CreateCiType(ctx context.Context, ciType *CiType) (string, error) {
col, err := GetCollection(ctx, CiTypesCollectionName)
if err != nil {
return "", fmt.Errorf("failed to get ci_types collection: %w", err)
}
meta, err := col.CreateDocument(ctx, ciType)
if err != nil {
return "", fmt.Errorf("failed to create CI type: %w", err)
}
log.Printf("Created CI Type: %s", meta.Key)
return meta.Key, nil
}
// GetCiType retrieves a CI type by its key
func GetCiType(ctx context.Context, key string) (*CiType, error) {
col, err := GetCollection(ctx, CiTypesCollectionName)
if err != nil {
return nil, fmt.Errorf("failed to get ci_types collection: %w", err)
}
var ciType CiType
_, err = col.ReadDocument(ctx, key, &ciType)
if err != nil {
if driver.Is NotFound(err) {
return nil, fmt.Errorf("CI type with key '%s' not found", key)
}
return nil, fmt.Errorf("failed to read CI type: %w", err)
}
return &ciType, nil
}
// ListCiTypes retrieves all CI types
func ListCiTypes(ctx context.Context) ([]CiType, error) {
col, err := GetCollection(ctx, CiTypesCollectionName)
if err != nil {
return nil, fmt.Errorf("failed to get ci_types collection: %w", err)
}
query := fmt.Sprintf("FOR d IN %s RETURN d", CiTypesCollectionName)
cursor, err := DB.Query(ctx, query, nil)
if err != nil {
return nil, fmt.Errorf("failed to execute AQL query: %w", err)
}
defer cursor.Close()
var ciTypes []CiType
for {
var ciType CiType
_, err := cursor.ReadDocument(ctx, &ciType)
if driver.Is NoMoreDocuments(err) {
break
} else if err != nil {
return nil, fmt.Errorf("failed to read document from cursor: %w", err)
}
ciTypes = append(ciTypes, ciType)
}
return ciTypes, nil
}
// UpdateCiType updates an existing CI type
func UpdateCiType(ctx context.Context, key string, updates map[string]interface{}) error {
col, err := GetCollection(ctx, CiTypesCollectionName)
if err != nil {
return fmt.Errorf("failed to get ci_types collection: %w", err)
}
_, err = col.UpdateDocument(ctx, key, updates)
if err != nil {
return fmt.Errorf("failed to update CI type '%s': %w", key, err)
}
log.Printf("Updated CI Type: %s", key)
return nil
}
// DeleteCiType deletes a CI type by its key
func DeleteCiType(ctx context.Context, key string) error {
col, err := GetCollection(ctx, CiTypesCollectionName)
if err != nil {
return fmt.Errorf("failed to get ci_types collection: %w", err)
}
_, err = col.RemoveDocument(ctx, key)
if err != nil {
return fmt.Errorf("failed to delete CI type '%s': %w", key, err)
}
log.Printf("Deleted CI Type: %s", key)
return nil
}
model/arangodb/ciattribute.go (CI 属性模型)
Go
package arangodb
import (
"context"
"encoding/json"
"fmt"
"log"
driver "github.com/arangodb/go-driver"
)
const CiAttributesCollectionName = "ci_attributes"
const CiTypeAttributeRelationsCollectionName = "ci_type_attribute_relations"
// CiAttribute represents a CI attribute definition
type CiAttribute struct {
Key string `json:"_key,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
DataType string `json:"dataType"` // string, integer, float, boolean, enum, date
Constraints json.RawMessage `json:"constraints,omitempty"` // JSON string representation of constraints
DisplayType string `json:"displayType,omitempty"`
DefaultValue json.RawMessage `json:"defaultValue,omitempty"`
}
// CiTypeAttributeRelation represents the edge between CiType and CiAttribute
type CiTypeAttributeRelation struct {
From string `json:"_from"`
To string `json:"_to"`
IsRequired bool `json:"isRequired"`
Editable bool `json:"editable"`
}
// CreateCiAttribute saves a new CI attribute definition
func CreateCiAttribute(ctx context.Context, attr *CiAttribute) (string, error) {
col, err := GetCollection(ctx, CiAttributesCollectionName)
if err != nil {
return "", fmt.Errorf("failed to get ci_attributes collection: %w", err)
}
meta, err := col.CreateDocument(ctx, attr)
if err != nil {
return "", fmt.Errorf("failed to create CI attribute: %w", err)
}
log.Printf("Created CI Attribute: %s", meta.Key)
return meta.Key, nil
}
// GetCiAttribute retrieves a CI attribute by its key
func GetCiAttribute(ctx context.Context, key string) (*CiAttribute, error) {
col, err := GetCollection(ctx, CiAttributesCollectionName)
if err != nil {
return nil, fmt.Errorf("failed to get ci_attributes collection: %w", err)
}
var attr CiAttribute
_, err = col.ReadDocument(ctx, key, &attr)
if err != nil {
if driver.Is NotFound(err) {
return nil, fmt.Errorf("CI attribute with key '%s' not found", key)
}
return nil, fmt.Errorf("failed to read CI attribute: %w", err)
}
return &attr, nil
}
// ListCiAttributes retrieves all CI attributes
func ListCiAttributes(ctx context.Context) ([]CiAttribute, error) {
col, err := GetCollection(ctx, CiAttributesCollectionName)
if err != nil {
return nil, fmt.Errorf("failed to get ci_attributes collection: %w", err)
}
query := fmt.Sprintf("FOR d IN %s RETURN d", CiAttributesCollectionName)
cursor, err := DB.Query(ctx, query, nil)
if err != nil {
return nil, fmt.Errorf("failed to execute AQL query: %w", err)
}
defer cursor.Close()
var attributes []CiAttribute
for {
var attr CiAttribute
_, err := cursor.ReadDocument(ctx, &attr)
if driver.Is NoMoreDocuments(err) {
break
} else if err != nil {
return nil, fmt.Errorf("failed to read document from cursor: %w", err)
}
attributes = append(attributes, attr)
}
return attributes, nil
}
// UpdateCiAttribute updates an existing CI attribute
func UpdateCiAttribute(ctx context.Context, key string, updates map[string]interface{}) error {
col, err := GetCollection(ctx, CiAttributesCollectionName)
if err != nil {
return fmt.Errorf("failed to get ci_attributes collection: %w", err)
}
_, err = col.UpdateDocument(ctx, key, updates)
if err != nil {
return fmt.Errorf("failed to update CI attribute '%s': %w", key, err)
}
log.Printf("Updated CI Attribute: %s", key)
return nil
}
// DeleteCiAttribute deletes a CI attribute by its key
func DeleteCiAttribute(ctx context.Context, key string) error {
col, err := GetCollection(ctx, CiAttributesCollectionName)
if err != nil {
return fmt.Errorf("failed to get ci_attributes collection: %w", err)
}
_, err = col.RemoveDocument(ctx, key)
if err != nil {
return fmt.Errorf("failed to delete CI attribute '%s': %w", key, err)
}
log.Printf("Deleted CI Attribute: %s", key)
return nil
}
// CreateCiTypeAttributeRelation creates an edge between a CI type and an attribute
func CreateCiTypeAttributeRelation(ctx context.Context, ciTypeID, attributeID string, isRequired, editable bool) (string, error) {
col, err := GetCollection(ctx, CiTypeAttributeRelationsCollectionName)
if err != nil {
return "", fmt.Errorf("failed to get ci_type_attribute_relations collection: %w", err)
}
edge := CiTypeAttributeRelation{
From: fmt.Sprintf("%s/%s", CiTypesCollectionName, ciTypeID),
To: fmt.Sprintf("%s/%s", CiAttributesCollectionName, attributeID),
IsRequired: isRequired,
Editable: editable,
}
meta, err := col.CreateDocument(ctx, edge)
if err != nil {
return "", fmt.Errorf("failed to create CI type attribute relation: %w", err)
}
log.Printf("Created CI Type Attribute Relation: %s", meta.Key)
return meta.Key, nil
}
// GetAttributesForCiType retrieves all attributes associated with a specific CI type
func GetAttributesForCiType(ctx context.Context, ciTypeID string) ([]CiAttribute, error) {
query := fmt.Sprintf(`
FOR t, r IN OUTBOUND @ciTypeId %s
FILTER IS_SAME_COLLECTION("%s", r)
FOR a IN %s
FILTER a._key == PARSE_ID(r._to).key
RETURN MERGE(a, {isRequired: r.isRequired, editable: r.editable})
`, CiTypeAttributeRelationsCollectionName, CiTypeAttributeRelationsCollectionName, CiAttributesCollectionName)
bindVars := map[string]interface{}{
"ciTypeId": fmt.Sprintf("%s/%s", CiTypesCollectionName, ciTypeID),
}
cursor, err := DB.Query(ctx, query, bindVars)
if err != nil {
return nil, fmt.Errorf("failed to execute AQL query for CI type attributes: %w", err)
}
defer cursor.Close()
var attributes []CiAttribute
for {
var attr CiAttribute
_, err := cursor.ReadDocument(ctx, &attr)
if driver.Is NoMoreDocuments(err) {
break
} else if err != nil {
return nil, fmt.Errorf("failed to read document from cursor: %w", err)
}
attributes = append(attributes, attr)
}
return attributes, nil
}
model/arangodb/ciinstance.go (CI 实例模型)
Go
package arangodb
import (
"context"
"fmt"
"log"
driver "github.com/arangodb/go-driver"
)
const CiCollectionName = "cis"
// CiInstance represents a concrete CI instance
type CiInstance struct {
Key string `json:"_key,omitempty"`
CiTypeId string `json:"ciTypeId"` // References CiType._key
Attributes map[string]interface{} `json:"attributes"` // Dynamic properties
}
// CreateCiInstance saves a new CI instance
func CreateCiInstance(ctx context.Context, ci *CiInstance) (string, error) {
col, err := GetCollection(ctx, CiCollectionName)
if err != nil {
return "", fmt.Errorf("failed to get cis collection: %w", err)
}
meta, err := col.CreateDocument(ctx, ci)
if err != nil {
return "", fmt.Errorf("failed to create CI instance: %w", err)
}
log.Printf("Created CI Instance: %s", meta.Key)
return meta.Key, nil
}
// GetCiInstance retrieves a CI instance by its key
func GetCiInstance(ctx context.Context, key string) (*CiInstance, error) {
col, err := GetCollection(ctx, CiCollectionName)
if err != nil {
return nil, fmt.Errorf("failed to get cis collection: %w", err)
}
var ci CiInstance
_, err = col.ReadDocument(ctx, key, &ci)
if err != nil {
if driver.Is NotFound(err) {
return nil, fmt.Errorf("CI instance with key '%s' not found", key)
}
return nil, fmt.Errorf("failed to read CI instance: %w", err)
}
return &ci, nil
}
// UpdateCiInstance updates an existing CI instance
func UpdateCiInstance(ctx context.Context, key string, updates map[string]interface{}) error {
col, err := GetCollection(ctx, CiCollectionName)
if err != nil {
return fmt.Errorf("failed to get cis collection: %w", err)
}
_, err = col.UpdateDocument(ctx, key, updates)
if err != nil {
return fmt.Errorf("failed to update CI instance '%s': %w", key, err)
}
log.Printf("Updated CI Instance: %s", key)
return nil
}
// DeleteCiInstance deletes a CI instance by its key
func DeleteCiInstance(ctx context.Context, key string) error {
col, err := GetCollection(ctx, CiCollectionName)
if err != nil {
return fmt.Errorf("failed to get cis collection: %w", err)
}
_, err = col.RemoveDocument(ctx, key)
if err != nil {
return fmt.Errorf("failed to delete CI instance '%s': %w", key, err)
}
log.Printf("Deleted CI Instance: %s", key)
return nil
}
// ListCiInstances retrieves all CI instances (can add filters/pagination later)
func ListCiInstances(ctx context.Context) ([]CiInstance, error) {
col, err := GetCollection(ctx, CiCollectionName)
if err != nil {
return nil, fmt.Errorf("failed to get cis collection: %w", err)
}
query := fmt.Sprintf("FOR d IN %s RETURN d", CiCollectionName)
cursor, err := DB.Query(ctx, query, nil)
if err != nil {
return nil, fmt.Errorf("failed to execute AQL query: %w", err)
}
defer cursor.Close()
var instances []CiInstance
for {
var inst CiInstance
_, err := cursor.ReadDocument(ctx, &inst)
if driver.Is NoMoreDocuments(err) {
break
} else if err != nil {
return nil, fmt.Errorf("failed to read document from cursor: %w", err)
}
instances = append(instances, inst)
}
return instances, nil
}
model/arangodb/cirelation.go (CI 关系模型)
Go
package arangodb
import (
"context"
"fmt"
"log"
driver "github.com/arangodb/go-driver"
)
const CiRelationsCollectionName = "ci_relations"
// CiRelation represents an edge between two CI instances
type CiRelation struct {
From string `json:"_from"` // Format: collection/key
To string `json:"_to"` // Format: collection/key
RelationType string `json:"relationType"`
Description string `json:"description,omitempty"`
Properties map[string]interface{} `json:"properties,omitempty"` // Dynamic properties for the relation
}
// CreateCiRelation saves a new CI relation
func CreateCiRelation(ctx context.Context, relation *CiRelation) (string, error) {
col, err := GetCollection(ctx, CiRelationsCollectionName)
if err != nil {
return "", fmt.Errorf("failed to get ci_relations collection: %w", err)
}
meta, err := col.CreateDocument(ctx, relation)
if err != nil {
return "", fmt.Errorf("failed to create CI relation: %w", err)
}
log.Printf("Created CI Relation: %s", meta.Key)
return meta.Key, nil
}
// DeleteCiRelation deletes a CI relation
func DeleteCiRelation(ctx context.Context, fromCIID, toCIID, relationType string) error {
col, err := GetCollection(ctx, CiRelationsCollectionName)
if err != nil {
return fmt.Errorf("failed to get ci_relations collection: %w", err)
}
query := fmt.Sprintf(`
FOR r IN %s
FILTER r._from == @fromNodeId AND r._to == @toNodeId AND r.relationType == @relationType
REMOVE r IN %s RETURN OLD._key
`, CiRelationsCollectionName, CiRelationsCollectionName)
bindVars := map[string]interface{}{
"fromNodeId": fmt.Sprintf("%s/%s", CiCollectionName, fromCIID),
"toNodeId": fmt.Sprintf("%s/%s", CiCollectionName, toCIID),
"relationType": relationType,
}
cursor, err := DB.Query(ctx, query, bindVars)
if err != nil {
return fmt.Errorf("failed to execute AQL query to delete relation: %w", err)
}
defer cursor.Close()
if _, err := cursor.ReadDocument(ctx, nil); driver.IsNoMoreDocuments(err) {
return fmt.Errorf("relation from '%s' to '%s' with type '%s' not found", fromCIID, toCIID, relationType)
} else if err != nil {
return fmt.Errorf("failed to read result from delete query: %w", err)
}
log.Printf("Deleted CI Relation from %s to %s with type %s", fromCIID, toCIID, relationType)
return nil
}
// GetCIRelatedGraph retrieves a graph of related CIs
func GetCIRelatedGraph(ctx context.Context, ciID string, depth int) ([]interface{}, error) {
query := fmt.Sprintf(`
FOR v, e, p IN 1..@depth ANY @startNodeID %s
RETURN {
vertex: v,
edge: e,
path: p
}
`, CiRelationsCollectionName)
bindVars := map[string]interface{}{
"startNodeID": fmt.Sprintf("%s/%s", CiCollectionName, ciID),
"depth": depth,
}
cursor, err := DB.Query(ctx, query, bindVars)
if err != nil {
return nil, fmt.Errorf("failed to execute AQL query for graph: %w", err)
}
defer cursor.Close()
var results []interface{}
for {
var doc interface{}
_, err := cursor.ReadDocument(ctx, &doc)
if driver.Is NoMoreDocuments(err) {
break
} else if err != nil {
return nil, fmt.Errorf("failed to read document from cursor: %w", err)
}
results = append(results, doc)
}
return results, nil
}
4. Go-zero RPC 服务
rpc/cmd/cmdbcore/internal/logic/attributevalidationlogic.go (属性校验逻辑)
这是核心的属性校验部分。
Go
package logic
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strconv"
"cmdb-platform/model/arangodb"
"cmdb-platform/rpc/cmd/cmdbcore/internal/svc"
"cmdb-platform/rpc/cmd/cmdbcore/pb"
"github.com/zeromicro/go-zero/core/logx"
)
type AttributeValidationLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewAttributeValidationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AttributeValidationLogic {
return &AttributeValidationLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// AttributeConstraints defines the structure for attribute constraints
type AttributeConstraints struct {
Required bool `json:"required"`
MinLength int `json:"minLength,omitempty"`
MaxLength int `json:"maxLength,omitempty"`
Regex string `json:"regex,omitempty"`
EnumValues []string `json:"enumValues,omitempty"`
MinValue float64 `json:"minValue,omitempty"`
MaxValue float64 `json:"maxValue,omitempty"`
}
func (l *AttributeValidationLogic) ValidateCiProperties(in *pb.ValidateCiPropertiesReq) (*pb.ValidateCiPropertiesResp, error) {
// 1. Get all attribute definitions for the given ciTypeId
ciTypeAttrs, err := arangodb.GetAttributesForCiType(l.ctx, in.CiTypeId)
if err != nil {
l.Errorf("Failed to get attributes for CI type %s: %v", in.CiTypeId, err)
return &pb.ValidateCiPropertiesResp{Success: false, ErrorMessage: "Failed to retrieve CI type attributes"}, nil
}
// Map attribute definitions by their key for quick lookup
attrDefMap := make(map[string]arangodb.CiAttribute)
for _, attr := range ciTypeAttrs {
attrDefMap[attr.Key] = attr
}
// 2. Validate incoming properties against definitions
for attrKey, attrValueStr := range in.Properties {
attrDef, ok := attrDefMap[attrKey]
if !ok {
// Property not defined for this CI type, might be an invalid property
return &pb.ValidateCiPropertiesResp{Success: false, ErrorMessage: fmt.Sprintf("Attribute '%s' is not defined for CI type '%s'", attrKey, in.CiTypeId)}, nil
}
// Parse constraints
var constraints AttributeConstraints
if len(attrDef.Constraints) > 0 {
if err := json.Unmarshal(attrDef.Constraints, &constraints); err != nil {
l.Errorf("Failed to unmarshal constraints for attribute %s: %v", attrKey, err)
return &pb.ValidateCiPropertiesResp{Success: false, ErrorMessage: fmt.Sprintf("Invalid constraints for attribute '%s'", attrKey)}, nil
}
}
// Check if required and present
if constraints.Required && attrValueStr == "" {
return &pb.ValidateCiPropertiesResp{Success: false, ErrorMessage: fmt.Sprintf("Attribute '%s' is required but missing", attrKey)}, nil
}
// Skip further validation if value is empty and not required
if attrValueStr == "" && !constraints.Required {
continue
}
// Perform validation based on DataType and Constraints
switch attrDef.DataType {
case "string":
if err := validateString(attrValueStr, constraints); err != nil {
return &pb.ValidateCiPropertiesResp{Success: false, ErrorMessage: fmt.Sprintf("Attribute '%s' validation failed: %v", attrKey, err)}, nil
}
case "integer":
if err := validateInteger(attrValueStr, constraints); err != nil {
return &pb.ValidateCiPropertiesResp{Success: false, ErrorMessage: fmt.Sprintf("Attribute '%s' validation failed: %v", attrKey, err)}, nil
}
case "float":
if err := validateFloat(attrValueStr, constraints); err != nil {
return &pb.ValidateCiPropertiesResp{Success: false, ErrorMessage: fmt.Sprintf("Attribute '%s' validation failed: %v", attrKey, err)}, nil
}
case "boolean":
if err := validateBoolean(attrValueStr); err != nil {
return &pb.ValidateCiPropertiesResp{Success: false, ErrorMessage: fmt.Sprintf("Attribute '%s' validation failed: %v", attrKey, err)}, nil
}
case "enum":
if err := validateEnum(attrValueStr, constraints); err != nil {
return &pb.ValidateCiPropertiesResp{Success: false, ErrorMessage: fmt.Sprintf("Attribute '%s' validation failed: %v", attrKey, err)}, nil
}
// Add more data types as needed (date, complex types, etc.)
default:
l.Warnf("Unsupported data type for attribute %s: %s", attrKey, attrDef.DataType)
// Optionally, return an error for unsupported types or allow them without specific validation
}
}
// 3. Check for missing required attributes
for _, attrDef := range ciTypeAttrs {
var constraints AttributeConstraints
if len(attrDef.Constraints) > 0 {
json.Unmarshal(attrDef.Constraints, &constraints) // Ignore error, assume it was handled during definition
}
if constraints.Required {
if _, exists := in.Properties[attrDef.Key]; !exists {
return &pb.ValidateCiPropertiesResp{Success: false, ErrorMessage: fmt.Sprintf("Required attribute '%s' is missing", attrDef.Key)}, nil
}
}
}
return &pb.ValidateCiPropertiesResp{Success: true}, nil
}
func validateString(value string, c AttributeConstraints) error {
if c.MinLength > 0 && len(value) < c.MinLength {
return fmt.Errorf("minimum length is %d", c.MinLength)
}
if c.MaxLength > 0 && len(value) > c.MaxLength {
return fmt.Errorf("maximum length is %d", c.MaxLength)
}
if c.Regex != "" {
re, err := regexp.Compile(c.Regex)
if err != nil {
return fmt.Errorf("invalid regex constraint: %v", err)
}
if !re.MatchString(value) {
return fmt.Errorf("does not match regex pattern")
}
}
return nil
}
func validateInteger(value string, c AttributeConstraints) error {
val, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return fmt.Errorf("invalid integer format: %v", err)
}
if c.MinValue != 0 && float64(val) < c.MinValue {
return fmt.Errorf("minimum value is %f", c.MinValue)
}
if c.MaxValue != 0 && float64(val) > c.MaxValue {
return fmt.Errorf("maximum value is %f", c.MaxValue)
}
return nil
}
func validateFloat(value string, c AttributeConstraints) error {
val, err := strconv.ParseFloat(value, 64)
if err != nil {
return fmt.Errorf("invalid float format: %v", err)
}
if c.MinValue != 0 && val < c.MinValue {
return fmt.Errorf("minimum value is %f", c.MinValue)
}
if c.MaxValue != 0 && val > c.MaxValue {
return fmt.Errorf("maximum value is %f", c.MaxValue)
}
return nil
}
func validateBoolean(value string) error {
_, err := strconv.ParseBool(value)
if err != nil {
return fmt.Errorf("invalid boolean format, expected 'true' or 'false'")
}
return nil
}
func validateEnum(value string, c AttributeConstraints) error {
if len(c.EnumValues) == 0 {
return fmt.Errorf("enum values not defined")
}
found := false
for _, v := range c.EnumValues {
if v == value {
found = true
break
}
}
if !found {
return fmt.Errorf("value '%s' is not in allowed enum values: %v", value, c.EnumValues)
}
return nil
}
5. Go-zero API 服务
api/cmd/cmdb/internal/svc/servicecontext.go
Go
package svc
import (
"cmdb-platform/api/cmd/cmdb/internal/config"
"cmdb-platform/rpc/cmd/cmdbcore/cmdbcore" // Import RPC client
"github.com/zeromicro/go-zero/zrpc"
)
type ServiceContext struct {
Config config.Config
CmdbCoreRpc cmdbcore.CmdbCore // RPC client instance
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
CmdbCoreRpc: cmdbcore.NewCmdbCore(zrpc.NewClient(c.CmdbCoreRpc)), // Initialize RPC client
}
}
api/cmd/cmdb/internal/config/config.go
Go
package config
import (
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
rest.RestConf
ArangoDB struct {
URL string
User string
Password string
DBName string
}
CmdbCoreRpc zrpc.RpcClientConf // RPC client configuration
}
api/cmd/cmdb/internal/handler/ciinstancehandler.go (CI 实例创建 Handler 示例)
Go
package handler
import (
"net/http"
"encoding/json" // For handling dynamic properties
"cmdb-platform/api/cmd/cmdb/internal/logic/ciinstance"
"cmdb-platform/api/cmd/cmdb/internal/svc"
"cmdb-platform/api/cmd/cmdb/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func CreateCiInstanceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.CreateCiInstanceReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := ciinstance.NewCreateCiInstanceLogic(r.Context(), svcCtx)
resp, err := l.CreateCiInstance(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}
api/cmd/cmdb/internal/logic/ciinstance/createciinstancelogic.go (CI 实例创建 Logic 示例)
Go
package ciinstance
import (
"context"
"fmt"
"cmdb-platform/api/cmd/cmdb/internal/svc"
"cmdb-platform/api/cmd/cmdb/types"
"cmdb-platform/model/arangodb"
"cmdb-platform/rpc/cmd/cmdbcore/pb" // Import RPC proto
"github.com/zeromicro/go-zero/core/logx"
)
type CreateCiInstanceLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewCreateCiInstanceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateCiInstanceLogic {
return &CreateCiInstanceLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CreateCiInstanceLogic) CreateCiInstance(req *types.CreateCiInstanceReq) (resp *types.CreateCiInstanceResp, err error) {
// 1. Convert dynamic properties map[string]interface{} to map[string]string for RPC call
// This is a simplification. In a real-world scenario, you might want to serialize
// complex types (like arrays, objects) within properties to JSON strings before RPC.
// Or, modify the RPC proto to accept map<string, bytes> or a custom Any type.
propertiesForRpc := make(map[string]string)
for k, v := range req.Properties {
// Simple conversion for basic types.
// For complex types, you'd need json.Marshal(v)
propertiesForRpc[k] = fmt.Sprintf("%v", v)
}
// 2. Call RPC to validate properties
validationResp, rpcErr := l.svcCtx.CmdbCoreRpc.ValidateCiProperties(l.ctx, &pb.ValidateCiPropertiesReq{
CiTypeId: req.CiTypeId,
Properties: propertiesForRpc,
})
if rpcErr != nil {
l.Errorf("RPC call to validate CI properties failed: %v", rpcErr)
return nil, fmt.Errorf("internal server error during property validation")
}
if !validationResp.Success {
return nil, fmt.Errorf("CI property validation failed: %s", validationResp.ErrorMessage)
}
// 3. Create CI Instance in ArangoDB
ciInstance := &arangodb.CiInstance{
CiTypeId: req.CiTypeId,
Attributes: req.Properties, // Store original dynamic map
}
ciID, err := arangodb.CreateCiInstance(l.ctx, ciInstance)
if err != nil {
l.Errorf("Failed to create CI instance in DB: %v", err)
return nil, fmt.Errorf("failed to create CI instance")
}
return &types.CreateCiInstanceResp{Id: ciID}, nil
}
使用方法 (快速启动)
- 安装 Go-zero 和 ArangoDB Go Driver:
- Bash
- go install github.com/zeromicro/go-zero/tools/goctl@latest go get github.com/arangodb/go-driver
- 启动 ArangoDB: 你可以使用 Docker 启动一个本地 ArangoDB 实例:
- Bash
- docker run -p 8529:8529 -e ARANGO_ROOT_PASSWORD=testpassword -d arangodb/arangodb:latest
- (注意:testpassword 仅用于开发测试,生产环境请使用强密码)
- 创建 Go-zero 项目:
- Bash
- mkdir cmdb-platform cd cmdb-platform go mod init cmdb-platform # 创建 RPC 服务 goctl rpc init --full --tz --proto rpc/cmd/cmdbcore/pb/cmdbcore.proto # 创建 API 服务 goctl api new cmdb
- 将上述代码片段填充到对应的文件路径中。
- 更新 Go-zero 配置 (etc/*.yaml):
- rpc/cmd/cmdbcore/etc/cmdbcore.yaml
- YAML
- Name: cmdbcore.rpc ListenOn: 0.0.0.0:8081 # RPC service listen port ArangoDB: URL: http://localhost:8529 User: root Password: testpassword DBName: cmdb_db
- api/cmd/cmdb/etc/cmdb.yaml
- YAML
- Name: cmdb.api Host: 0.0.0.0 Port: 8888 # API service listen port ArangoDB: # API 服务也需要 ArangoDB 配置来直接操作 DB 模型 URL: http://localhost:8529 User: root Password: testpassword DBName: cmdb_db CmdbCoreRpc: # RPC client configuration Etcd: Hosts: - 127.0.0.1:2379 # Assuming Etcd is running locally for service discovery Key: cmdbcore.rpc
- 注意: RPC 服务发现需要 Etcd。你可以使用 Docker 启动 Etcd:
- Bash
- docker run -d -p 2379:2379 -p 2380:2380 --name etcd quay.io/coreos/etcd:latest \ /usr/local/bin/etcd --advertise-client-urls http://127.0.0.1:2379 --listen-client-urls http://0.0.0.0:2379 \ --initial-advertise-peer-urls http://127.0.0.1:2380 --listen-peer-urls http://0.0.0.0:2380 \ --initial-cluster-token etcd-cluster-1 --initial-cluster etcd-node-1=http://127.0.0.1:2380 \ --initial-cluster-state new --name etcd-node-1
- 初始化数据库连接: 修改 api/cmd/cmdb/cmdb.go 和 rpc/cmd/cmdbcore/cmdbcore.go 的 main 函数,在服务启动前调用 arangodb.InitArangoDB。
- api/cmd/cmdb/cmdb.go
- Go
- package main import ( "context" "flag" "fmt" "log" "cmdb-platform/api/cmd/cmdb/internal/config" "cmdb-platform/api/cmd/cmdb/internal/handler" "cmdb-platform/api/cmd/cmdb/internal/svc" "cmdb-platform/model/arangodb" // Import arangodb package "github.com/zeromicro/go-zero/core/conf" "github.com/zeromicro/go-zero/rest" ) var configFile = flag.String("f", "etc/cmdb.yaml", "the config file") func main() { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) // Initialize ArangoDB connection arangodb.InitArangoDB(c.ArangoDB.URL, c.ArangoDB.User, c.ArangoDB.Password, c.ArangoDB.DBName) ctx := svc.NewServiceContext(c) server := rest.MustNewServer(c.RestConf) defer server.Stop() handler.RegisterHandlers(server, ctx) // This line will be generated by goctl fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) server.Start() }
- rpc/cmd/cmdbcore/cmdbcore.go
- Go
- package main import ( "flag" "fmt" "log" "cmdb-platform/model/arangodb" // Import arangodb package "cmdb-platform/rpc/cmd/cmdbcore/internal/config" "cmdb-platform/rpc/cmd/cmdbcore/internal/server" "cmdb-platform/rpc/cmd/cmdbcore/internal/svc" "cmdb-platform/rpc/cmd/cmdbcore/pb" "github.com/zeromicro/go-zero/core/conf" "github.com/zeromicro/go-zero/core/service" "github.com/zeromicro/go-zero/zrpc" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) var configFile = flag.String("f", "etc/cmdbcore.yaml", "the config file") func main() { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) ctx := svc.NewServiceContext(c) // Initialize ArangoDB connection arangodb.InitArangoDB(c.ArangoDB.URL, c.ArangoDB.User, c.ArangoDB.Password, c.ArangoDB.DBName) s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { pb.RegisterCmdbCoreServer(grpcServer, server.NewCmdbCoreServer(ctx)) if c.Mode == service.DevMode || c.Mode == service.TestMode { reflection.Register(grpcServer) } }) defer s.Stop() fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) s.Start() }
- 生成 Go-zero 代码: 确保你已经将 Go-zero API 的定义写入 api/cmd/cmdb/cmdb.api,RPC 的定义写入 rpc/cmd/cmdbcore/pb/cmdbcore.proto。 然后运行:
- Bash
- # 在 cmdb-platform 目录下 goctl rpc protoc rpc/cmd/cmdbcore/pb/cmdbcore.proto --go_out=./rpc --go-grpc_out=./rpc --zrpc_out=./rpc goctl api go -api api/cmd/cmdb/cmdb.api -dir api/cmd/cmdb
- 运行服务:
- Bash
- # 启动 RPC 服务 go run rpc/cmd/cmdbcore/cmdbcore.go -f rpc/cmd/cmdbcore/etc/cmdbcore.yaml # 另起一个终端启动 API 服务 go run api/cmd/cmdb/cmdb.go -f api/cmd/cmdb/etc/cmdb.yaml
如何使用这个示例
- 定义 CI 类型: POST /ci_types
- JSON
- { "name": "Server", "description": "物理或虚拟机服务器", "displayOrder": 1 }
- 定义 CI 属性: POST /ci_attributes
- JSON
- { "name": "IP Address", "dataType": "string", "constraints": "{\"required\":true, \"regex\":\"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$\"}", "displayType": "text_input" }
- JSON
- { "name": "OS Type", "dataType": "enum", "constraints": "{\"required\":true, \"enumValues\":[\"Linux\", \"Windows\", \"MacOS\"]}", "displayType": "select" }
- 关联 CI 类型与属性: POST /ci_type_attribute_relations (你需要获取到 CI 类型和属性的 _key 值)
- JSON
- { "ciTypeId": "server_type_key", "attributeId": "ip_address_attr_key", "isRequired": true, "editable": true }
- 创建 CI 实例: POST /cis
- JSON
- { "ciTypeId": "server_type_key", "properties": { "ip_address_attr_key": "192.168.1.10", "os_type_attr_key": "Linux" } }
- 如果 ip_address 不符合正则表达式,或者 os_type 不在枚举值中,或者缺少必填属性,RPC 服务会返回校验错误。
这个骨架提供了一个坚实的基础,但请记住,一个真正的 CMDB 平台还需要:
- 更全面的错误处理:区分业务错误和系统错误。
- 认证与授权:保护 API 接口。
- 分页、排序和高级查询:用于列表接口。
- 事务管理:对于跨多个 ArangoDB 操作的复杂逻辑。
- 缓存机制:提高性能。
- 更完善的日志和监控。
- 前端 UI:与后端 API 交互。
- 更丰富的 CI 属性类型和约束:例如日期、文件、复杂对象等。
希望这个详细的示例能帮助你开始构建自己的 CMDB 平台!