Files
aiops/backend/pkg/database/database.go
T
2026-07-09 14:16:56 +08:00

293 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package database
import (
"fmt"
"log"
"strings"
"time"
"ezops/config"
"ezops/model"
"ezops/pkg/appdir"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// New 创建数据库连接
func New(cfg config.DatabaseConfig) (*gorm.DB, error) {
var dialector gorm.Dialector
switch cfg.Driver {
case "mysql":
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName, cfg.Charset)
dialector = mysql.Open(dsn)
case "postgres":
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName)
dialector = postgres.Open(dsn)
case "sqlite":
dsn := appdir.Resolve(cfg.DBName)
if err := appdir.EnsureParentDir(dsn); err != nil {
return nil, fmt.Errorf("创建数据库目录失败: %w", err)
}
if !strings.Contains(dsn, "?") {
// WAL 提升并发读,busy_timeout 降低锁冲突 // Logan.Li
dsn += "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)"
}
dialector = sqlite.Open(dsn)
if cfg.MaxConn > 4 {
cfg.MaxConn = 4
}
if cfg.MaxIdle > 2 {
cfg.MaxIdle = 2
}
default:
return nil, fmt.Errorf("unsupported database driver: %s", cfg.Driver)
}
db, err := gorm.Open(dialector, &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
return nil, err
}
// 设置连接池
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(cfg.MaxConn)
sqlDB.SetMaxIdleConns(cfg.MaxIdle)
sqlDB.SetConnMaxLifetime(time.Hour)
return db, nil
}
// AutoMigrate 自动迁移数据库表
// 逐表迁移:单张表的迁移错误(如索引名冲突)不会阻塞其他表,
// 避免 GORM + MySQL 8.0 的 "Can't DROP" 索引问题导致整个启动失败
func AutoMigrate(db *gorm.DB) error {
allModels := []interface{}{
&model.User{},
&model.Role{},
&model.Permission{},
&model.Server{},
&model.Cloud{},
&model.CloudAccount{},
&model.Region{},
&model.Domain{},
&model.KubernetesCluster{},
&model.KubernetesNode{},
&model.KubernetesPod{},
&model.KubernetesDeployment{},
&model.KubernetesService{},
&model.KubernetesIngress{},
&model.AlertRule{},
&model.AlertEvent{},
&model.AlertContact{}, // 报警联系人 // Logan.Li
&model.AlertContactGroup{}, // 报警联系组 // Logan.Li
&model.AlertNotifyPolicy{}, // 告警集合通知策略 // Logan.Li
&model.AlertNotifySettings{}, // 告警消息模版 // Logan.Li
&model.NotifyChannel{},
&model.Pipeline{},
&model.PipelineRun{},
&model.BuildNode{},
&model.AppRelease{},
&model.CronJob{},
&model.Script{},
&model.TaskExecution{},
&model.ApprovalTemplate{},
&model.ApprovalProcess{},
&model.ApprovalStep{},
&model.NavigationGroup{},
&model.NavigationLink{},
&model.AuditLog{},
&model.AuditSettings{}, // 审计保留策略 // Logan.Li
&model.LoginLog{}, // 登录日志 // Logan.Li
&model.SystemTaskStatus{}, // 内置系统任务状态 // Logan.Li
&model.LDAPConfig{},
&model.ResourceModel{},
&model.CloudBilling{},
&model.BillingAlertRule{},
&model.AgentReport{},
&model.AgentTask{},
&model.AgentRule{},
&model.AgentAlert{},
&model.AgentPortPolicy{}, // Agent 端口策略 // Logan.Li
&model.AnsiblePlaybook{},
&model.AnsibleJob{},
&model.AnsibleInventory{},
&model.TopologyLayout{}, // 拓扑布局保存 // Logan.Li
&model.TerminalSession{}, // 终端会话录制(终端审计) // Logan.Li
&model.RDPSession{}, // RDP 图形会话审计 // Logan.Li
&model.SessionCommand{}, // 终端会话命令审计 // Logan.Li
&model.LLMConfig{}, // 大语言模型配置(AI 智能助手) // Logan.Li
&model.ChatHistory{}, // 对话历史记录(按用户隔离) // Logan.Li
&model.AIAnalysisTrace{}, // AI 分析上下文与结果审计回放 // Logan.Li
&model.MetricsAggregate{}, // 指标时序聚合(Redis → MySQL)// Logan.Li
&model.MonitorTarget{}, // 监控目标(主机/中间件/设备/站点) // Logan.Li
&model.MonitorMetric{}, // 监控指标定义 // Logan.Li
&model.MetricHistory{}, // 指标历史时序 // Logan.Li
&model.HostGroup{}, // 主机组 // Logan.Li
&model.SNMPDevice{}, // SNMP设备 // Logan.Li
&model.SNMPTemplate{}, // SNMP模板 // Logan.Li
&model.SNMPTemplateOID{}, // SNMP模板OID // Logan.Li
&model.SNMPMetric{}, // SNMP采集指标 // Logan.Li
&model.SNMPInterface{}, // SNMP接口信息 // Logan.Li
&model.SiteTask{}, // 站点拨测任务 // Logan.Li
&model.SiteResult{}, // 拨测结果 // Logan.Li
&model.SiteAlert{}, // 拨测告警 // Logan.Li
&model.MonitorAlertRule{}, // 中间件/站点拨测告警规则 // Logan.Li
&model.PrometheusDataSource{}, // Prometheus 数据源配置 // Logan.Li
&model.ZabbixDataSource{}, // Zabbix 数据源配置 // Logan.Li
&model.LogDataSource{}, // Loki / ES 日志数据源 // Logan.Li
&model.Credential{}, // 凭据管理(SSH密钥/API Token) // Logan.Li
&model.DeployEnvironment{}, // 发布环境分组 // Logan.Li
&model.DeployPipelineMember{}, // 流水线成员 // Logan.Li
&model.DeployUserGroup{}, // 用户组 // Logan.Li
&model.DeployUserGroupMember{}, // 用户组成员 // Logan.Li
&model.ItemRole{}, // Jenkins-style Item Role (regex pipeline matching) // Logan.Li
&model.UserItemRole{}, // User ↔ ItemRole association // Logan.Li
&model.LogScopeRole{}, // 日志查询范围权限(正则匹配索引/Label)// Logan.Li
&model.UserLogScopeRole{}, // 用户 ↔ 日志范围权限 // Logan.Li
&model.BuildMasterConfig{}, // Master 构建节点并发配置 // Logan.Li
&model.DeployTool{}, // 工具配置(懒加载/预装/禁用) // Logan.Li
&model.AlertCondition{}, // 告警条件 // Logan.Li
&model.AlertNotifyTemplate{}, // 告警通知模板 // Logan.Li
&model.AlertUpgrade{}, // 告警升级策略 // Logan.Li
&model.AlertUpgradeRecord{}, // 告警升级记录 // Logan.Li
&model.AlertSilence{}, // 告警静默 // Logan.Li
&model.HealthProbe{}, // 健康探针配置 // Logan.Li
&model.ApprovalWorkflow{}, // 审批流程定义 // Logan.Li
&model.ApprovalWorkflowStep{}, // 审批流程步骤 // Logan.Li
&model.AIRun{}, // AI 任务统一运行记录 // Logan.Li
&model.AIRunMessage{}, // AI 任务回放消息 // Logan.Li
&model.AIRunSpan{}, // AI 工具/LLM/Guardrail Trace Span // Logan.Li
&model.AICapability{}, // AI 内置/MCP/技能能力注册 // Logan.Li
&model.AICapabilityPolicy{}, // AI 工具能力策略 // Logan.Li
&model.OpsSession{}, // 人机统一执行会话 // Logan.Li
&model.OpsCommand{}, // 人机统一命令审计 // Logan.Li
&model.TraceDataSource{}, // 链路追踪数据源 Jaeger/Tempo // Logan.Li
}
var lastErr error
for _, m := range allModels {
if err := db.AutoMigrate(m); err != nil {
// 记录但不阻塞 — 表和大多数索引已正常创建,
// 个别索引 DROP 冲突不影响核心功能
lastErr = err
}
}
// 全部跑完后返回 nil:单张表的索引错误不应阻断服务启动
_ = lastErr
applySchemaPatches(db)
return nil
}
// applySchemaPatches 补全 AutoMigrate 可能遗漏的列(MySQL 索引冲突时 User 表不会自动加列)// Logan.Li
func applySchemaPatches(db *gorm.DB) {
m := db.Migrator()
if !m.HasTable(&model.User{}) {
return
}
patchUserColumn := func(field string, col string) {
if !m.HasColumn(&model.User{}, col) {
if err := m.AddColumn(&model.User{}, field); err != nil {
log.Printf("[DB] 补列 %s 失败: %v", col, err)
}
}
}
patchUserColumn("ApiAccessKeyExpiresAt", "api_access_key_expires_at")
patchUserColumn("ApiAccessKeyID", "api_access_key_id")
patchUserColumn("ApiAccessKeyDescription", "api_access_key_description")
patchUserColumn("ApiAccessKeyDurationMonths", "api_access_key_duration_months")
patchUserColumn("ApiAccessKeyLastUsedAt", "api_access_key_last_used_at")
// 补全日志数据源表可能缺失的列(旧表仅有基础字段)// Logan.Li
patchLogDSColumn := func(field string, col string) {
if !m.HasColumn(&model.LogDataSource{}, col) {
if err := m.AddColumn(&model.LogDataSource{}, field); err != nil {
log.Printf("[DB] 补列 monitor_log_datasources.%s 失败: %v", col, err)
}
}
}
patchLogDSColumn("AuthType", "auth_type")
patchLogDSColumn("Token", "token")
patchLogDSColumn("Headers", "headers")
// 日志权限关联表:AutoMigrate 在 MySQL 索引冲突时可能只创建策略表未创建用户关联表 // Logan.Li
ensureTable := func(tbl interface{}, name string) {
if !m.HasTable(tbl) {
if err := db.AutoMigrate(tbl); err != nil {
log.Printf("[DB] 补表 %s 失败: %v", name, err)
}
}
}
ensureTable(&model.LogScopeRole{}, "iam_log_scope_roles")
if !m.HasTable("iam_user_log_scope_roles") {
createUserLogScopeRolesTable(db)
}
// LDAP 目录类型列(openldap | ad) // Logan.Li
if m.HasTable(&model.LDAPConfig{}) && !m.HasColumn(&model.LDAPConfig{}, "server_type") {
if err := m.AddColumn(&model.LDAPConfig{}, "ServerType"); err != nil {
log.Printf("[DB] 补列 iam_ldap_configs.server_type 失败: %v", err)
}
}
// 服务器 RDP/协议字段(旧库 AutoMigrate 可能未加列)// Logan.Li
patchServerColumn := func(field string, col string) {
if !m.HasColumn(&model.Server{}, col) {
if err := m.AddColumn(&model.Server{}, field); err != nil {
log.Printf("[DB] 补列 asset_servers.%s 失败: %v", col, err)
}
}
}
patchServerColumn("OSType", "os_type")
patchServerColumn("Protocol", "protocol")
patchServerColumn("RDPPort", "rdp_port")
patchServerColumn("Alias", "alias")
patchServerColumn("CloudInstanceName", "cloud_instance_name")
patchServerColumn("CloudInstanceID", "cloud_instance_id")
// 将历史仅填 os=windows 的记录补齐协议字段 // Logan.Li
if m.HasColumn(&model.Server{}, "os_type") && m.HasColumn(&model.Server{}, "protocol") {
_ = db.Exec(`
UPDATE asset_servers
SET os_type = 'windows', protocol = 'rdp', rdp_port = IF(rdp_port IS NULL OR rdp_port = 0, 3389, rdp_port)
WHERE (os_type IS NULL OR os_type = '' OR protocol IS NULL OR protocol = '')
AND LOWER(os) LIKE '%windows%'
`).Error
}
}
func createUserLogScopeRolesTable(db *gorm.DB) {
sql := `CREATE TABLE IF NOT EXISTS iam_user_log_scope_roles (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
log_scope_role_id BIGINT UNSIGNED NOT NULL,
created_at DATETIME(3) NULL,
UNIQUE KEY idx_iam_user_log_scope_role (user_id, log_scope_role_id),
KEY idx_iam_user_log_scope_role_role (log_scope_role_id)
)`
if db.Dialector.Name() == "sqlite" {
sql = `CREATE TABLE IF NOT EXISTS iam_user_log_scope_roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
log_scope_role_id INTEGER NOT NULL,
created_at DATETIME,
UNIQUE(user_id, log_scope_role_id)
)`
}
if err := db.Exec(sql).Error; err != nil {
log.Printf("[DB] 创建 iam_user_log_scope_roles 失败: %v", err)
}
}