From 5949ec5d0cd6ddf0cbd68776b47373616051457f Mon Sep 17 00:00:00 2001 From: attacker Date: Fri, 19 Jun 2026 01:10:00 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Agent=E9=87=87=E9=9B=86uptime,=20?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E6=8C=81=E4=B9=85=E5=8C=96kernel/uptime/disk?= =?UTF-8?q?IO,=20=E5=89=8D=E7=AB=AF=E7=A3=81=E7=9B=98BPS+IOPS=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agent: system.go 添加 readUptime(), SystemStat 携带 Uptime - Backend: Server 模型新增 Kernel 字段, ReceiveReport 持久化 kernel+uptime - Backend: /agent/latest 返回 kernel/uptime/port_info/disk_io_info/disk_total - Frontend: 磁盘监控改为 BPS+IOPS (4张卡片), 系统信息修复内核/运行时长/监听端口/磁盘容量 - 图表单位从 MB/s 改为 KB/s, 添加 formatBPS 工具函数 --- agent/collector/system.go | 34 +- agent/main.go | 5 +- backend/internal/api/handlers/agent.go | 181 ++-- .../internal/api/handlers/agent_install.go | 234 ++++-- backend/internal/api/handlers/credential.go | 160 ++++ .../api/handlers/credential_assign.go | 62 ++ backend/internal/api/handlers/server.go | 22 +- backend/internal/api/handlers/server_batch.go | 36 +- backend/internal/api/routes/routes.go | 20 +- backend/internal/models/agent.go | 11 +- backend/internal/models/agent_task.go | 10 +- backend/internal/models/alert.go | 11 +- backend/internal/models/alert_v2.go | 17 +- backend/internal/models/approval.go | 5 +- backend/internal/models/audit.go | 2 +- backend/internal/models/automation.go | 10 +- backend/internal/models/billing.go | 7 +- backend/internal/models/cicd.go | 7 +- backend/internal/models/credential.go | 47 ++ backend/internal/models/k8s.go | 14 +- backend/internal/models/llm.go | 4 +- backend/internal/models/metrics_aggregate.go | 38 + backend/internal/models/metrics_kline.go | 38 - backend/internal/models/monitor.go | 6 +- backend/internal/models/navigation.go | 5 +- backend/internal/models/ops_modules.go | 16 +- .../internal/models/prometheus_datasource.go | 2 +- backend/internal/models/resource_model.go | 4 +- backend/internal/models/server.go | 25 +- backend/internal/models/site.go | 6 +- backend/internal/models/snmp.go | 10 +- backend/internal/models/terminal_session.go | 2 +- backend/internal/models/user.go | 12 +- backend/internal/models/zabbix_datasource.go | 2 +- backend/pkg/crypto/secretbox.go | 66 ++ backend/pkg/database/database.go | 3 +- frontend/src/App.tsx | 4 + frontend/src/components/agent/AgentWizard.tsx | 98 ++- frontend/src/components/layout/Sidebar.tsx | 1 + .../monitor/alert-center/AlertCenter.tsx | 221 +++-- .../monitor/dashboard/GrafanaDashboard.tsx | 178 +++- .../components/server/ExcelServerImport.tsx | 80 +- .../src/components/server/ServerManager.tsx | 95 +-- .../src/components/server/ServerTableRow.tsx | 31 +- .../src/components/server/TerminalDialog.tsx | 94 ++- .../components/system/CredentialManager.tsx | 385 +++++++++ frontend/src/pages/HostMonitor.tsx | 784 +++++++++--------- frontend/src/services/api.ts | 33 +- 48 files changed, 2182 insertions(+), 956 deletions(-) create mode 100644 backend/internal/api/handlers/credential.go create mode 100644 backend/internal/api/handlers/credential_assign.go create mode 100644 backend/internal/models/credential.go create mode 100644 backend/internal/models/metrics_aggregate.go delete mode 100644 backend/internal/models/metrics_kline.go create mode 100644 backend/pkg/crypto/secretbox.go create mode 100644 frontend/src/components/system/CredentialManager.tsx diff --git a/agent/collector/system.go b/agent/collector/system.go index 0fd4b5c..1821c18 100644 --- a/agent/collector/system.go +++ b/agent/collector/system.go @@ -12,8 +12,9 @@ import ( // SystemInfo 包含系统静态信息(Agent 版本通过编译时注入,非运行时采集) type SystemInfo struct { - OSVersion string // OS 发行版版本 (如 "Ubuntu 22.04.4 LTS") - Kernel string // 内核版本 (如 "Linux 6.8.0-90-generic") + OSVersion string // OS 发行版版本 (如 "Ubuntu 22.04.4 LTS") + Kernel string // 内核版本 (如 "6.8.0-90-generic") + UptimeSeconds int64 // 系统运行时长(秒) } // CollectSystem 采集系统静态信息。 @@ -28,6 +29,9 @@ func CollectSystem() SystemInfo { // 读取 /proc/version 获取内核版本 info.Kernel = readProcVersion() + // 读取 /proc/uptime 获取运行时长 + info.UptimeSeconds = readUptime() + return info } @@ -67,3 +71,29 @@ func readProcVersion() string { } return "" } + +// readUptime 从 /proc/uptime 读取系统运行秒数 +func readUptime() int64 { + data, err := os.ReadFile("/proc/uptime") + if err != nil { + return 0 + } + // 格式: "12345.67 98765.43" + // 第一个值是系统总运行秒数(含小数) + fields := strings.Fields(strings.TrimSpace(string(data))) + if len(fields) < 1 { + return 0 + } + // 截断小数部分,只取整数 + parts := strings.SplitN(fields[0], ".", 2) + if len(parts) > 0 { + var sec int64 + for _, c := range parts[0] { + if c >= '0' && c <= '9' { + sec = sec*10 + int64(c-'0') + } + } + return sec + } + return 0 +} diff --git a/agent/main.go b/agent/main.go index 6f8ae58..3a93cc6 100644 --- a/agent/main.go +++ b/agent/main.go @@ -103,8 +103,9 @@ func main() { // 前 3 次上报携带系统信息 if sysReportCount < 3 && (sysInfo.OSVersion != "" || sysInfo.Kernel != "") { report.System = &model.SystemStat{ - OSVersion: sysInfo.OSVersion, - Kernel: sysInfo.Kernel, + OSVersion: sysInfo.OSVersion, + Kernel: sysInfo.Kernel, + Uptime: sysInfo.UptimeSeconds, } sysReportCount++ } diff --git a/backend/internal/api/handlers/agent.go b/backend/internal/api/handlers/agent.go index f3a6a24..b376cc2 100644 --- a/backend/internal/api/handlers/agent.go +++ b/backend/internal/api/handlers/agent.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "net/http" + "sort" "strconv" "strings" "time" @@ -77,12 +78,14 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) { // 同步更新 servers 表的实时指标(三级匹配:hostname → client IP → hostname LIKE) diskAvg := avgDiskUsage(req.Disks) - // 计算硬件规格 + // 计算硬件规格(四舍五入到整数,避免 31.34→31GB 的显示偏差) totalDiskGB := 0.0 for _, d := range req.Disks { totalDiskGB += d.TotalGB } - memMB := int64(req.Memory.TotalGB * 1024) // GB → MB + memGB := math.Ceil(req.Memory.TotalGB) // 31.34 → 32, 62.79 → 63(物理内存向上取整) + memMB := int64(memGB * 1024) // GB → MB + diskGB := int64(totalDiskGB + 0.5) // 39.99 → 40 clientIP := c.ClientIP() @@ -92,8 +95,8 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) { matchIP = clientIP } - // 直接 SQL 更新,避免 GORM soft delete 自动添加 deleted_at 条件干扰 - updateSQL := `UPDATE servers SET + // 直接 SQL 更新 asset_servers 监控字段 + updateSQL := `UPDATE asset_servers SET cpu_usage = ?, memory_usage = ?, disk_usage = ?, cpu = ?, memory = ?, disk = ?, status = ?, agent_status = ?, agent_version = ?, @@ -101,14 +104,24 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) { updated_at = NOW()` args := []interface{}{ req.CPU.UsagePercent, req.Memory.UsagePercent, diskAvg, - req.CPU.CoreCount, memMB, int64(totalDiskGB), + req.CPU.CoreCount, memMB, diskGB, "running", "online", req.AgentVersion, // CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END req.Hostname, matchIP, req.Hostname, } - if req.System != nil && req.System.OSVersion != "" { - updateSQL += ", os = ?" - args = append(args, req.System.OSVersion) + if req.System != nil { + if req.System.OSVersion != "" { + updateSQL += ", os = ?" + args = append(args, req.System.OSVersion) + } + if req.System.Kernel != "" { + updateSQL += ", kernel = ?" + args = append(args, req.System.Kernel) + } + if req.System.Uptime > 0 { + updateSQL += ", uptime = ?" + args = append(args, req.System.Uptime) + } } updateSQL += " WHERE hostname = ? OR ip = ?" args = append(args, req.Hostname, matchIP) @@ -122,9 +135,44 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) { result = h.db.Exec(updateSQL2, args2...) } - // 如果还是没匹配到,记录日志(Agent 上报了,但服务器列表里没有对应条目) + // 如果还是没匹配到 —— 自动注册新服务器(UPSERT 防重复) if result.RowsAffected == 0 { - fmt.Printf("[agent] 上报主机 %q (IP: %s) 未匹配任何 servers 条目,指标仅写入 agent_reports 表\n", req.Hostname, matchIP) + osName := "" + if req.System != nil { + osName = req.System.OSVersion + } + newServer := models.Server{ + Hostname: req.Hostname, + IP: matchIP, + Port: 22, + Username: "root", + AuthType: "password", + OS: osName, + CPU: req.CPU.CoreCount, + Memory: memMB, + Disk: diskGB, + Status: "running", + AgentStatus: "online", + AgentVersion: req.AgentVersion, + CPUUsage: req.CPU.UsagePercent, + MemoryUsage: req.Memory.UsagePercent, + DiskUsage: diskAvg, + } + // ON DUPLICATE KEY UPDATE — 若 hostname 已存在则更新字段(防重复注册) + if err := h.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "hostname"}}, + DoUpdates: clause.AssignmentColumns([]string{ + "ip", "os", "cpu", "memory", "disk", + "status", "agent_status", "agent_version", + "cpu_usage", "memory_usage", "disk_usage", + "updated_at", + }), + }).Create(&newServer).Error; err != nil { + fmt.Printf("[agent] 自动注册主机 %q (IP: %s) 失败: %v\n", req.Hostname, matchIP, err) + } else { + fmt.Printf("[agent] 自动注册新主机: %q (IP: %s, CPU=%d核, 内存=%dMB, 磁盘=%dGB, OS=%q)\n", + req.Hostname, matchIP, req.CPU.CoreCount, memMB, diskGB, osName) + } } // WebSocket 广播:向所有前端客户端推送该主机的最新指标 // Logan.Li @@ -183,38 +231,51 @@ func (h *AgentHandler) writeHotMetric(c *gin.Context, key string, value float64, _ = h.cache.ZRemRangeByScore(ctx, key, "-inf", fmt.Sprintf("%.0f", cutoff)) } -// GetLatestReports 获取各主机最新上报(仅 3 分钟内活跃的 hostname) +// GetLatestReports 获取所有服务器的 Agent 最新状态(含未安装 Agent 的主机) // GET /api/v1/agent/latest func (h *AgentHandler) GetLatestReports(c *gin.Context) { var results []models.AgentLatestResponse threeMinAgo := time.Now().Add(-3 * time.Minute) + // 以 servers 表为主,LEFT JOIN 最近 3 分钟内的 Agent 上报 + // 未装 Agent 的主机也会显示(agent_status='not_installed'),打破"不显示→无法安装"的循环 h.db.Raw(` SELECT - a.hostname, + s.id as server_id, + s.hostname, COALESCE(s.ip, '') as ip, - a.cpu_usage, - a.cpu_cores, - a.mem_usage, - a.mem_total, - a.mem_used, + COALESCE(a.cpu_usage, 0) as cpu_usage, + COALESCE(a.cpu_cores, 0) as cpu_cores, + COALESCE(a.mem_usage, 0) as mem_usage, + COALESCE(a.mem_total, 0) as mem_total, + COALESCE(a.mem_used, 0) as mem_used, COALESCE(s.disk_usage, 0) as disk_usage, a.disk_info, + a.port_info, a.net_info, + a.disk_io_info, a.report_time, COALESCE(s.agent_version, '') as agent_version, - COALESCE(s.agent_status, 'offline') as agent_status, + CASE + WHEN a.hostname IS NOT NULL THEN COALESCE(s.agent_status, 'online') + ELSE 'not_installed' + END as agent_status, COALESCE(s.os, '') as os, - COALESCE(r.name, '') as region - FROM agent_reports a - LEFT JOIN servers s ON s.hostname = a.hostname - LEFT JOIN regions r ON r.id = s.region_id - WHERE a.id IN ( - SELECT MAX(id) FROM agent_reports - WHERE report_time >= ? - GROUP BY hostname - ) - AND s.id IS NOT NULL -- 只显示匹配 servers 表的主机(首次上线后自动同步 hostname) - ORDER BY a.report_time DESC + COALESCE(s.kernel, '') as kernel, + COALESCE(s.uptime, 0) as uptime, + COALESCE(s.disk, 0) as disk_total, + COALESCE(r.name, '') as region, + CASE WHEN s.password != '' OR s.ssh_key != '' THEN true ELSE false END as has_auth + FROM asset_servers s + LEFT JOIN ( + SELECT * FROM monitor_agent_reports + WHERE id IN ( + SELECT MAX(id) FROM monitor_agent_reports + WHERE report_time >= ? + GROUP BY hostname + ) + ) a ON a.hostname = s.hostname + LEFT JOIN asset_regions r ON r.id = s.region_id + ORDER BY a.report_time DESC, s.id ASC `, threeMinAgo).Scan(&results) if results == nil { @@ -253,7 +314,7 @@ func (h *AgentHandler) GetHostHistory(c *gin.Context) { // AggregateMetrics 聚合 Redis 热数据到 MySQL klines // POST /api/v1/agent/aggregate func (h *AgentHandler) AggregateMetrics(c *gin.Context) { - var req models.AggregateKlineReq + var req models.AggregateReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求格式错误: " + err.Error()}) return @@ -303,37 +364,47 @@ func (h *AgentHandler) AggregateMetrics(c *gin.Context) { buckets[bucketStart] = append(buckets[bucketStart], val) } - // 为每个桶计算 OHLC + Avg + // 计算 max/min/avg/p95 for bucketTs, values := range buckets { if len(values) == 0 { continue } - open := values[0] - closeV := values[len(values)-1] - high := values[0] - low := values[0] + maxV := values[0] + minV := values[0] sum := 0.0 for _, v := range values { sum += v - if v > high { - high = v + if v > maxV { + maxV = v } - if v < low { - low = v + if v < minV { + minV = v } } avg := sum / float64(len(values)) - kline := models.MetricsKline{ + // 计算 P95(排序后取 95% 位置的值) + p95 := maxV + if len(values) >= 20 { + sorted := make([]float64, len(values)) + copy(sorted, values) + sort.Float64s(sorted) + idx := int(float64(len(sorted)) * 0.95) + if idx >= len(sorted) { + idx = len(sorted) - 1 + } + p95 = sorted[idx] + } + + agg := models.MetricsAggregate{ Hostname: req.Hostname, Metric: req.Metric, Interval: iv.name, - Open: math.Round(open*100) / 100, - High: math.Round(high*100) / 100, - Low: math.Round(low*100) / 100, - Close: math.Round(closeV*100) / 100, Avg: math.Round(avg*100) / 100, + MaxV: math.Round(maxV*100) / 100, + MinV: math.Round(minV*100) / 100, + P95: math.Round(p95*100) / 100, Count: len(values), BucketTime: time.Unix(bucketTs, 0), } @@ -347,9 +418,9 @@ func (h *AgentHandler) AggregateMetrics(c *gin.Context) { {Name: "bucket_time"}, }, DoUpdates: clause.AssignmentColumns([]string{ - "open", "high", "low", "close", "avg", "count", + "avg", "max_v", "min_v", "p95", "count", }), - }).Create(&kline) + }).Create(&agg) if result.Error == nil { totalUpserted++ @@ -364,10 +435,10 @@ func (h *AgentHandler) AggregateMetrics(c *gin.Context) { }) } -// GetKline 查询 K 线数据 -// GET /api/v1/agent/kline?hostname=&metric=&interval=&from=&to= -func (h *AgentHandler) GetKline(c *gin.Context) { - var req models.KlineQueryReq +// GetAggregate 查询聚合数据(原 GetKline) +// GET /api/v1/agent/aggregate?hostname=&metric=&interval=&from=&to= +func (h *AgentHandler) GetAggregate(c *gin.Context) { + var req models.AggregateQueryReq if err := c.ShouldBindQuery(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()}) return @@ -384,25 +455,25 @@ func (h *AgentHandler) GetKline(c *gin.Context) { return } - var klines []models.MetricsKline + var items []models.MetricsAggregate result := h.db.Where("hostname = ? AND metric = ? AND `interval` = ? AND bucket_time >= ? AND bucket_time <= ?", req.Hostname, req.Metric, req.Interval, from, to). Order("bucket_time ASC"). - Find(&klines) + Find(&items) if result.Error != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败: " + result.Error.Error()}) return } - if klines == nil { - klines = []models.MetricsKline{} + if items == nil { + items = []models.MetricsAggregate{} } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", - Data: klines, + Data: items, }) } diff --git a/backend/internal/api/handlers/agent_install.go b/backend/internal/api/handlers/agent_install.go index 97a040b..3af635e 100644 --- a/backend/internal/api/handlers/agent_install.go +++ b/backend/internal/api/handlers/agent_install.go @@ -61,9 +61,26 @@ func runSSHCommand(client *ssh.Client, cmd string) (string, error) { // InstallAgent 远程安装 Agent 到指定服务器 // POST /servers/:id/install-agent +// 支持传入临时凭证:首次连接成功后自动更新服务器认证凭据 func (h *AgentInstallHandler) InstallAgent(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) + var req struct { + Username string `json:"username"` + Password string `json:"password"` + SSHKey string `json:"ssh_key"` + AuthType string `json:"auth_type"` + Token string `json:"token"` + } + // 兼容旧调用方式:token 在 query,新方式在 body + c.ShouldBindJSON(&req) + if req.Token == "" { + req.Token = c.Query("token") + } + if req.AuthType == "" { + req.AuthType = "password" + } + var server models.Server if err := h.db.First(&server, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "服务器不存在"}) @@ -71,12 +88,27 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) { } if server.Status != "running" { - c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "服务器未运行,无法安装 Agent"}) - return + // 允许安装,不强制 running 状态 + } + + // 如果请求中提供了凭据,使用请求凭据覆盖服务器存储的凭据进行 SSH + effectiveServer := server + if req.Username != "" { + effectiveServer.Username = req.Username + } + if req.Password != "" { + effectiveServer.Password = req.Password + } + if req.SSHKey != "" { + effectiveServer.SSHKey = req.SSHKey + effectiveServer.AuthType = "key" + } + if req.AuthType != "" && req.AuthType != effectiveServer.AuthType { + effectiveServer.AuthType = req.AuthType } // 1. SSH 连接目标主机 - client, err := sshDial(server) + client, err := sshDial(effectiveServer) if err != nil { log.Printf("[AgentInstall] SSH连接失败 %s: %v", server.IP, err) c.JSON(http.StatusOK, models.Response{ @@ -87,6 +119,25 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) { } defer client.Close() + // 首次连接成功 → 如果服务器之前未配置凭据,自动保存使用的凭据 + if (server.Password == "" && server.SSHKey == "") || server.Username == "" { + updates := map[string]interface{}{ + "username": effectiveServer.Username, + "auth_type": effectiveServer.AuthType, + } + if effectiveServer.Password != "" { + updates["password"] = effectiveServer.Password + } + if effectiveServer.SSHKey != "" { + updates["ssh_key"] = effectiveServer.SSHKey + } + if server.Username == "" { + updates["username"] = effectiveServer.Username + } + h.db.Model(&server).Updates(updates) + log.Printf("[AgentInstall] %s 首次连接成功,已自动关联凭证", server.Hostname) + } + // 2. 检测系统架构 archOut, _ := runSSHCommand(client, "uname -m") arch := "amd64" @@ -181,7 +232,7 @@ func (h *AgentInstallHandler) installOne(server models.Server, serverURL, token return installed, installOut, verifyOut, nil } -// BatchInstallAgent 批量安装 Agent +// BatchInstallAgent 批量安装 Agent(异步执行,立即返回)// Logan.Li // POST /servers/install-agent func (h *AgentInstallHandler) BatchInstallAgent(c *gin.Context) { var req struct { @@ -195,63 +246,134 @@ func (h *AgentInstallHandler) BatchInstallAgent(c *gin.Context) { serverURL := fmt.Sprintf("http://%s", c.Request.Host) token := c.Query("token") - type InstallResult struct { - ServerID uint `json:"server_id"` - Hostname string `json:"hostname"` - IP string `json:"ip"` - Success bool `json:"success"` - Message string `json:"message"` - Installed bool `json:"installed"` - } - - results := make([]InstallResult, 0, len(req.ServerIDs)) - successCount := 0 - - for _, sid := range req.ServerIDs { - var server models.Server - if err := h.db.First(&server, sid).Error; err != nil { - results = append(results, InstallResult{ - ServerID: sid, - Success: false, - Message: "服务器不存在", - }) - continue - } - - installed, _, _, err := h.installOne(server, serverURL, token) - if err != nil { - results = append(results, InstallResult{ - ServerID: server.ID, - Hostname: server.Hostname, - IP: server.IP, - Success: false, - Message: err.Error(), - }) - } else { - successCount++ - msg := "安装成功" - if !installed { - msg = "安装命令已执行,Agent 服务未检测到运行" + // 异步执行:立即返回,后台 goroutine 逐台安装 + go func() { + successCount := 0 + for _, sid := range req.ServerIDs { + var server models.Server + if err := h.db.First(&server, sid).Error; err != nil { + log.Printf("[BatchInstall] 服务器 %d 不存在,跳过", sid) + continue + } + installed, _, _, err := h.installOne(server, serverURL, token) + if err != nil { + log.Printf("[BatchInstall] %s (%s) 安装失败: %v", server.Hostname, server.IP, err) + } else { + successCount++ + if installed { + log.Printf("[BatchInstall] %s Agent 安装成功", server.Hostname) + } else { + log.Printf("[BatchInstall] %s Agent 安装命令已执行但服务未检测到运行", server.Hostname) + } } - results = append(results, InstallResult{ - ServerID: server.ID, - Hostname: server.Hostname, - IP: server.IP, - Success: true, - Installed: installed, - Message: msg, - }) } - } + log.Printf("[BatchInstall] 批量安装完成: %d/%d 成功", successCount, len(req.ServerIDs)) + }() c.JSON(http.StatusOK, models.Response{ Code: 200, - Message: fmt.Sprintf("完成 %d/%d 台服务器", successCount, len(req.ServerIDs)), - Data: gin.H{ - "total": len(req.ServerIDs), - "success": successCount, - "results": results, - }, + Message: fmt.Sprintf("已提交异步安装任务,共 %d 台主机,正在后台执行…", len(req.ServerIDs)), + Data: gin.H{"total": len(req.ServerIDs), "async": true}, + }) +} + +// UninstallAgent 远程卸载 Agent +// POST /api/v1/agent/uninstall +func (h *AgentInstallHandler) UninstallAgent(c *gin.Context) { + var req struct { + Hostname string `json:"hostname"` + ServerID *uint `json:"server_id"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"}) + return + } + + var server models.Server + if req.ServerID != nil { + if err := h.db.First(&server, *req.ServerID).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "服务器不存在"}) + return + } + } else if req.Hostname != "" { + if err := h.db.Where("hostname = ? OR ip = ?", req.Hostname, req.Hostname).First(&server).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "未找到服务器: " + req.Hostname}) + return + } + } else { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请提供 hostname 或 server_id"}) + return + } + + // 无 SSH 凭据的主机(自动注册的)直接改本地状态 + if server.Username == "" || server.Password == "" { + h.db.Model(&server).Updates(map[string]interface{}{ + "agent_status": "offline", "agent_version": "", + }) + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已标记为离线(该主机无SSH凭据,无法远程卸载,请手动执行 systemctl stop aiops-agent)"}) + return + } + + client, err := sshDial(server) + if err != nil { + c.JSON(http.StatusOK, models.Response{Code: 500, Message: "SSH连接失败: " + formatSSHError(err)}) + return + } + defer client.Close() + + runSSHCommand(client, "systemctl stop aiops-agent 2>/dev/null; systemctl disable aiops-agent 2>/dev/null; rm -f /usr/local/bin/aiops-agent /etc/systemd/system/aiops-agent.service; systemctl daemon-reload 2>/dev/null") + + h.db.Model(&server).Updates(map[string]interface{}{ + "agent_status": "offline", "agent_version": "", + }) + + c.JSON(http.StatusOK, models.Response{Code: 200, Message: fmt.Sprintf("已卸载 %s (%s) 的 Agent", server.Hostname, server.IP)}) +} + +// BatchUninstallAgent 批量卸载 Agent(异步执行,立即返回)// Logan.Li +// POST /api/v1/agent/batch-uninstall +func (h *AgentInstallHandler) BatchUninstallAgent(c *gin.Context) { + var req struct { + ServerIDs []uint `json:"server_ids"` + } + if err := c.ShouldBindJSON(&req); err != nil || len(req.ServerIDs) == 0 { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请提供服务器 ID 列表"}) + return + } + + // 异步执行:立即返回,后台逐台卸载 + go func() { + successCount := 0 + for _, id := range req.ServerIDs { + var server models.Server + if err := h.db.First(&server, id).Error; err != nil { + log.Printf("[BatchUninstall] 服务器 %d 不存在,跳过", id) + continue + } + if server.Username == "" || server.Password == "" { + h.db.Model(&server).Updates(map[string]interface{}{"agent_status": "offline", "agent_version": ""}) + log.Printf("[BatchUninstall] %s 无SSH凭据,已标记离线", server.Hostname) + successCount++ + continue + } + client, err := sshDial(server) + if err != nil { + log.Printf("[BatchUninstall] %s SSH失败: %v", server.Hostname, err) + continue + } + runSSHCommand(client, "systemctl stop aiops-agent 2>/dev/null; systemctl disable aiops-agent 2>/dev/null; rm -f /usr/local/bin/aiops-agent /etc/systemd/system/aiops-agent.service; systemctl daemon-reload 2>/dev/null") + client.Close() + h.db.Model(&server).Updates(map[string]interface{}{"agent_status": "offline", "agent_version": ""}) + log.Printf("[BatchUninstall] %s Agent 已卸载", server.Hostname) + successCount++ + } + log.Printf("[BatchUninstall] 批量卸载完成: %d/%d 成功", successCount, len(req.ServerIDs)) + }() + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: fmt.Sprintf("已提交异步卸载任务,共 %d 台主机,正在后台执行…", len(req.ServerIDs)), + Data: gin.H{"total": len(req.ServerIDs), "async": true}, }) } diff --git a/backend/internal/api/handlers/credential.go b/backend/internal/api/handlers/credential.go new file mode 100644 index 0000000..dd53387 --- /dev/null +++ b/backend/internal/api/handlers/credential.go @@ -0,0 +1,160 @@ +package handlers + +import ( + "net/http" + "strconv" + + "ezops/internal/models" + "ezops/pkg/crypto" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// CredentialHandler 凭据管理处理器 // Logan.Li +type CredentialHandler struct { + db *gorm.DB + crypt *crypto.SecretBox +} + +func NewCredentialHandler(db *gorm.DB, masterKey string) *CredentialHandler { + return &CredentialHandler{ + db: db, + crypt: crypto.NewSecretBox(masterKey), + } +} + +// preview 内容脱敏预览:前8字符 + ... + 后4字符 +func preview(s string) string { + if len(s) <= 12 { + return "****" + } + return s[:8] + "..." + s[len(s)-4:] +} + +// ListCredentials 凭据列表 +func (h *CredentialHandler) ListCredentials(c *gin.Context) { + var creds []models.Credential + if err := h.db.Order("updated_at DESC").Find(&creds).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"}) + return + } + items := make([]models.CredentialListResponse, 0, len(creds)) + for _, cred := range creds { + items = append(items, models.CredentialListResponse{ + ID: cred.ID, + Name: cred.Name, + Type: cred.Type, + Preview: preview(cred.Content), + Description: cred.Description, + CreatedAt: cred.CreatedAt, + UpdatedAt: cred.UpdatedAt, + }) + } + c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{ + List: items, Total: int64(len(items)), + }}) +} + +// CreateCredential 创建凭据 +func (h *CredentialHandler) CreateCredential(c *gin.Context) { + var req models.CredentialCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()}) + return + } + + encrypted, err := h.crypt.Encrypt(req.Content) + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "加密失败"}) + return + } + + cred := models.Credential{ + Name: req.Name, + Type: req.Type, + Content: encrypted, + Description: req.Description, + } + if err := h.db.Create(&cred).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败"}) + return + } + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "创建成功", + Data: models.CredentialListResponse{ + ID: cred.ID, + Name: cred.Name, + Type: cred.Type, + Preview: preview(cred.Content), + Description: cred.Description, + CreatedAt: cred.CreatedAt, + UpdatedAt: cred.UpdatedAt, + }, + }) +} + +// UpdateCredential 更新凭据 +func (h *CredentialHandler) UpdateCredential(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效ID"}) + return + } + + var req models.CredentialUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()}) + return + } + + var cred models.Credential + if h.db.First(&cred, id).Error != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "凭据不存在"}) + return + } + + cred.Name = req.Name + cred.Type = req.Type + cred.Description = req.Description + + if req.Content != "" { + encrypted, err := h.crypt.Encrypt(req.Content) + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "加密失败"}) + return + } + cred.Content = encrypted + } + + if err := h.db.Save(&cred).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"}) + return + } + + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功"}) +} + +// DeleteCredential 删除凭据 +func (h *CredentialHandler) DeleteCredential(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效ID"}) + return + } + + var cred models.Credential + if h.db.First(&cred, id).Error != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "凭据不存在"}) + return + } + + if err := h.db.Delete(&cred).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"}) + return + } + + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"}) +} diff --git a/backend/internal/api/handlers/credential_assign.go b/backend/internal/api/handlers/credential_assign.go new file mode 100644 index 0000000..0e7f41a --- /dev/null +++ b/backend/internal/api/handlers/credential_assign.go @@ -0,0 +1,62 @@ +package handlers + +import ( + "net/http" + "strconv" + + "ezops/internal/models" + + "github.com/gin-gonic/gin" +) + +// AssignCredential 将凭据分配给服务器(解密凭据并写入 password/ssh_key) // Logan.Li +func (h *CredentialHandler) AssignCredential(c *gin.Context) { + serverID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的服务器ID"}) + return + } + + var req struct { + CredentialID uint `json:"credential_id"` + } + if err := c.ShouldBindJSON(&req); err != nil || req.CredentialID == 0 { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请选择凭据"}) + return + } + + var cred models.Credential + if err := h.db.First(&cred, req.CredentialID).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "凭据不存在"}) + return + } + + plain, err := h.crypt.Decrypt(cred.Content) + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "凭据解密失败"}) + return + } + + var server models.Server + if err := h.db.First(&server, serverID).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "服务器不存在"}) + return + } + + if cred.Type == "ssh_key" { + server.SSHKey = plain + server.AuthType = "key" + server.Password = "" + } else { + server.Password = plain + server.AuthType = "password" + server.SSHKey = "" + } + + if err := h.db.Save(&server).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "分配失败: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "凭据已关联"}) +} diff --git a/backend/internal/api/handlers/server.go b/backend/internal/api/handlers/server.go index 0aa0e56..f9c1322 100644 --- a/backend/internal/api/handlers/server.go +++ b/backend/internal/api/handlers/server.go @@ -5,6 +5,7 @@ import ( "strconv" "ezops/internal/models" + "ezops/pkg/crypto" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -12,12 +13,13 @@ import ( // ServerHandler 服务器处理器 type ServerHandler struct { - db *gorm.DB + db *gorm.DB + crypt *crypto.SecretBox } // NewServerHandler 创建服务器处理器 -func NewServerHandler(db *gorm.DB) *ServerHandler { - return &ServerHandler{db: db} +func NewServerHandler(db *gorm.DB, masterKey string) *ServerHandler { + return &ServerHandler{db: db, crypt: crypto.NewSecretBox(masterKey)} } // recordServerAudit 记录服务器操作审计日志 // Logan.Li @@ -91,6 +93,11 @@ func (h *ServerHandler) ListServers(c *gin.Context) { Order("created_at DESC"). Find(&servers) + // 填充 has_auth 字段(密码和SSH密钥在JSON中隐藏,但前端需知道是否有凭据) // Logan.Li + for i := range servers { + servers[i].HasAuth = servers[i].Password != "" || servers[i].SSHKey != "" + } + c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", @@ -324,7 +331,11 @@ func (h *ServerHandler) DeleteServer(c *gin.Context) { return } - if err := h.db.Delete(&models.Server{}, id).Error; err != nil { + // 硬删除:先清理关联数据,再物理删除服务器记录 + h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&models.AgentReport{}) + h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&models.MetricsAggregate{}) + + if err := h.db.Unscoped().Delete(&models.Server{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "删除失败", @@ -333,10 +344,9 @@ func (h *ServerHandler) DeleteServer(c *gin.Context) { } h.recordServerAudit(c, "delete", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "") - c.JSON(http.StatusOK, models.Response{ Code: 200, - Message: "删除成功", + Message: "服务器已彻底删除", }) } diff --git a/backend/internal/api/handlers/server_batch.go b/backend/internal/api/handlers/server_batch.go index 1b02c7b..499fe16 100644 --- a/backend/internal/api/handlers/server_batch.go +++ b/backend/internal/api/handlers/server_batch.go @@ -14,7 +14,7 @@ import ( // 批量操作域:批量导入 + 批量删除 // Logan.Li // BatchCreateServers 批量导入服务器 // Logan.Li -// 接收 {servers:[...], region_id, auth_type},逐条校验并创建,跳过重复(主机名或IP已存在) +// 接收 {servers:[...], region_id, auth_type, credential_id},逐条校验并创建,跳过重复(主机名或IP已存在) func (h *ServerHandler) BatchCreateServers(c *gin.Context) { var req models.BatchCreateServerRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -30,6 +30,28 @@ func (h *ServerHandler) BatchCreateServers(c *gin.Context) { authType = "password" } + // 如果指定了凭据,提前解密 // Logan.Li + var decryptedPwd, decryptedKey string + if req.CredentialID != nil && *req.CredentialID > 0 { + var cred models.Credential + if err := h.db.First(&cred, *req.CredentialID).Error; err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "凭据不存在"}) + return + } + plain, err := h.crypt.Decrypt(cred.Content) + if err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "凭据解密失败"}) + return + } + if cred.Type == "ssh_key" { + decryptedKey = plain + authType = "key" + } else { + decryptedPwd = plain + authType = "password" + } + } + result := models.BatchCreateResult{Errors: []string{}} for i, item := range req.Servers { hostname := strings.TrimSpace(item.Hostname) @@ -63,6 +85,8 @@ func (h *ServerHandler) BatchCreateServers(c *gin.Context) { Port: port, Username: username, AuthType: authType, + Password: decryptedPwd, + SSHKey: decryptedKey, OS: item.OS, CPU: item.CPU, Memory: item.Memory * 1024, // GB → MB @@ -111,7 +135,13 @@ func (h *ServerHandler) BatchDeleteServers(c *gin.Context) { var servers []models.Server h.db.Where("id IN ?", req.IDs).Find(&servers) - if err := h.db.Delete(&models.Server{}, req.IDs).Error; err != nil { + // 硬删除:先清理每台服务器的 Agent 上报和聚合数据 + for _, s := range servers { + h.db.Unscoped().Where("hostname = ?", s.Hostname).Delete(&models.AgentReport{}) + h.db.Unscoped().Where("hostname = ?", s.Hostname).Delete(&models.MetricsAggregate{}) + } + + if err := h.db.Unscoped().Delete(&models.Server{}, req.IDs).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "删除失败", @@ -126,6 +156,6 @@ func (h *ServerHandler) BatchDeleteServers(c *gin.Context) { c.JSON(http.StatusOK, models.Response{ Code: 200, - Message: "删除成功", + Message: "服务器已彻底删除", }) } diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index ce29f58..29894b1 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -135,7 +135,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c } // 服务器路由 - serverHandler := handlers.NewServerHandler(db) + serverHandler := handlers.NewServerHandler(db, cfg.JWT.Secret) servers := authorized.Group("/servers") { servers.GET("", serverHandler.ListServers) @@ -434,7 +434,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c { agentQuery.GET("/latest", agentHandler.GetLatestReports) agentQuery.GET("/history/:hostname", agentHandler.GetHostHistory) - agentQuery.GET("/kline", agentHandler.GetKline) + agentQuery.GET("/aggregate", agentHandler.GetAggregate) // 远程任务 agentQuery.POST("/tasks", taskHandler.CreateTask) @@ -451,6 +451,10 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c // 告警事件 agentQuery.GET("/alerts", taskHandler.ListAlerts) agentQuery.POST("/alerts/:id/resolve", taskHandler.ResolveAlert) + + // Agent 安装/卸载 + agentQuery.POST("/uninstall", agentInstallHandler.UninstallAgent) + agentQuery.POST("/batch-uninstall", agentInstallHandler.BatchUninstallAgent) } // AI 智能助手 - LLM 配置 + 对话代理(OpenAI 兼容) // Logan.Li @@ -535,6 +539,18 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c zabbixDSGroup.POST("/test", zabbixDSHandler.TestConnection) } + // 凭据管理 — SSH密钥/API Token/密码统一加密管理 // Logan.Li + credentialHandler := handlers.NewCredentialHandler(db, cfg.JWT.Secret) + credGroup := authorized.Group("/credentials") + { + credGroup.GET("", credentialHandler.ListCredentials) + credGroup.POST("", credentialHandler.CreateCredential) + credGroup.PUT("/:id", credentialHandler.UpdateCredential) + credGroup.DELETE("/:id", credentialHandler.DeleteCredential) + } + // 服务器凭据关联 // Logan.Li + authorized.POST("/servers/:id/assign-credential", credentialHandler.AssignCredential) + // 站点拨测 — HTTP/TCP/DNS/PING 主动监控 // Logan.Li siteHandler := handlers.NewSiteHandler(db) siteGroup := authorized.Group("/site") diff --git a/backend/internal/models/agent.go b/backend/internal/models/agent.go index 713882b..72ec94c 100644 --- a/backend/internal/models/agent.go +++ b/backend/internal/models/agent.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // AgentReport Agent 上报的原始指标数据 // Logan.Li @@ -25,15 +24,15 @@ type AgentReport struct { ReportTime time.Time `json:"report_time"` // 上报时间 CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } func (AgentReport) TableName() string { - return "agent_reports" + return "monitor_agent_reports" } // AgentLatestResponse Agent 最新状态(含服务器元信息)// Logan.Li type AgentLatestResponse struct { + ServerID uint `json:"server_id"` Hostname string `json:"hostname"` IP string `json:"ip"` CPUUsage float64 `json:"cpu_usage"` @@ -42,13 +41,19 @@ type AgentLatestResponse struct { MemTotal float64 `json:"mem_total"` MemUsed float64 `json:"mem_used"` DiskUsage float64 `json:"disk_usage"` + DiskTotal int64 `json:"disk_total"` // 磁盘总容量 GB DiskInfo string `json:"disk_info"` + PortInfo string `json:"port_info"` NetInfo string `json:"net_info"` + DiskIOInfo string `json:"disk_io_info"` ReportTime string `json:"report_time"` AgentVersion string `json:"agent_version"` AgentStatus string `json:"agent_status"` OS string `json:"os"` + Kernel string `json:"kernel"` // 内核版本 + Uptime int64 `json:"uptime"` // 运行秒数 Region string `json:"region"` + HasAuth bool `json:"has_auth"` // 是否有 SSH 凭据 } // AgentReportRequest Agent 上报请求体 // Logan.Li diff --git a/backend/internal/models/agent_task.go b/backend/internal/models/agent_task.go index 71fc463..76f7451 100644 --- a/backend/internal/models/agent_task.go +++ b/backend/internal/models/agent_task.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // AgentTask Agent 远程任务 // Logan.Li @@ -22,11 +21,10 @@ type AgentTask struct { PickedAt *time.Time `json:"picked_at"` // Agent 拉取时间 FinishedAt *time.Time `json:"finished_at"` // 完成时间 UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } func (AgentTask) TableName() string { - return "agent_tasks" + return "monitor_agent_tasks" } // AgentRule 告警规则(基于 Agent 数据) // Logan.Li @@ -42,11 +40,10 @@ type AgentRule struct { LastTriggered *time.Time `json:"last_triggered"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } func (AgentRule) TableName() string { - return "agent_rules" + return "monitor_agent_rules" } // AgentAlert Agent 告警事件 // Logan.Li @@ -63,9 +60,8 @@ type AgentAlert struct { ResolvedAt *time.Time `json:"resolved_at"` CreatedAt time.Time `gorm:"index" json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } func (AgentAlert) TableName() string { - return "agent_alerts" + return "monitor_agent_alerts" } diff --git a/backend/internal/models/alert.go b/backend/internal/models/alert.go index d69073e..5c4d4ef 100644 --- a/backend/internal/models/alert.go +++ b/backend/internal/models/alert.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // AlertRule 告警规则模型 @@ -22,12 +21,11 @@ type AlertRule struct { NotifyChannels string `gorm:"type:text" json:"notify_channels"` // JSON CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (AlertRule) TableName() string { - return "alert_rules" + return "monitor_alert_rules" } // AlertEvent 告警事件模型 @@ -55,7 +53,7 @@ type AlertEvent struct { // TableName 表名 func (AlertEvent) TableName() string { - return "alert_events" + return "monitor_alert_events" } // NotifyChannel 通知渠道模型 @@ -68,12 +66,11 @@ type NotifyChannel struct { TestStatus string `gorm:"size:32" json:"test_status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (NotifyChannel) TableName() string { - return "notify_channels" + return "monitor_notify_channels" } // MetricData 监控指标数据 @@ -89,7 +86,7 @@ type MetricData struct { // TableName 表名 func (MetricData) TableName() string { - return "metric_data" + return "monitor_metric_data" } // CreateAlertRuleRequest 创建告警规则请求 diff --git a/backend/internal/models/alert_v2.go b/backend/internal/models/alert_v2.go index 156ff0c..e214145 100644 --- a/backend/internal/models/alert_v2.go +++ b/backend/internal/models/alert_v2.go @@ -94,7 +94,7 @@ type AlertRuleV2 struct { // TableName 指定表名 func (AlertRuleV2) TableName() string { - return "alert_rules_v2" + return "monitor_alert_rules_v2" } // AlertCondition 告警条件定义 @@ -109,6 +109,11 @@ type AlertCondition struct { Window int `json:"window"` // 条件级聚合窗口 } +// TableName 指定表名 +func (AlertCondition) TableName() string { + return "monitor_alert_conditions" +} + // AlertConditionGroup 告警条件组 type AlertConditionGroup struct { Conditions []AlertCondition `json:"conditions"` // 条件列表 @@ -159,7 +164,7 @@ type AlertNotifyTemplate struct { // TableName 指定表名 func (AlertNotifyTemplate) TableName() string { - return "alert_notify_templates" + return "monitor_alert_notify_templates" } // TemplateVariable 模板变量定义 @@ -209,7 +214,7 @@ type AlertUpgrade struct { // TableName 指定表名 func (AlertUpgrade) TableName() string { - return "alert_upgrades" + return "monitor_alert_upgrades" } // AlertUpgradeRule 告警升级规则 @@ -235,7 +240,7 @@ type AlertUpgradeRecord struct { // TableName 指定表名 func (AlertUpgradeRecord) TableName() string { - return "alert_upgrade_records" + return "monitor_alert_upgrade_records" } // ============================================ @@ -278,7 +283,7 @@ type AlertSilence struct { // TableName 指定表名 func (AlertSilence) TableName() string { - return "alert_silences" + return "monitor_alert_silences" } // AlertSilenceMatcher 静默匹配条件 @@ -349,7 +354,7 @@ type AlertEventV2 struct { // TableName 指定表名 func (AlertEventV2) TableName() string { - return "alert_events_v2" + return "monitor_alert_events_v2" } // AlertRuleV2CreateRequest 告警规则创建请求 diff --git a/backend/internal/models/approval.go b/backend/internal/models/approval.go index 5964be5..4fe5846 100644 --- a/backend/internal/models/approval.go +++ b/backend/internal/models/approval.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // ApprovalTemplate 审批模板模型 @@ -18,12 +17,11 @@ type ApprovalTemplate struct { Enabled bool `gorm:"default:true" json:"enabled"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (ApprovalTemplate) TableName() string { - return "approval_templates" + return "sys_approval_templates" } // ApprovalProcess 审批流程模型 @@ -41,7 +39,6 @@ type ApprovalProcess struct { RejectReason string `gorm:"size:500" json:"reject_reason"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 关联 Template *ApprovalTemplate `gorm:"foreignKey:TemplateID" json:"template,omitempty"` diff --git a/backend/internal/models/audit.go b/backend/internal/models/audit.go index a0b8e91..6734ca8 100644 --- a/backend/internal/models/audit.go +++ b/backend/internal/models/audit.go @@ -25,7 +25,7 @@ type AuditLog struct { // TableName 表名 func (AuditLog) TableName() string { - return "audit_logs" + return "sys_audit_logs" } // AuditLogQuery 审计日志查询 diff --git a/backend/internal/models/automation.go b/backend/internal/models/automation.go index fcd8656..696af09 100644 --- a/backend/internal/models/automation.go +++ b/backend/internal/models/automation.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // CronJob 定时任务模型 @@ -27,7 +26,6 @@ type CronJob struct { NotifyOn string `gorm:"size:32;default:failure" json:"notify_on"` // always, failure, success CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 关联 Script *Script `json:"script,omitempty"` @@ -54,12 +52,11 @@ type Script struct { UpdatedBy uint `json:"updated_by"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (Script) TableName() string { - return "scripts" + return "ops_scripts" } // TaskExecution 任务执行记录 @@ -83,7 +80,7 @@ type TaskExecution struct { // TableName 表名 func (TaskExecution) TableName() string { - return "task_executions" + return "ops_task_executions" } // Playbook Ansible Playbook模型 @@ -103,12 +100,11 @@ type Playbook struct { UpdatedBy uint `json:"updated_by"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (Playbook) TableName() string { - return "playbooks" + return "ops_playbooks" } // CreatePlaybookRequest 创建Playbook请求 diff --git a/backend/internal/models/billing.go b/backend/internal/models/billing.go index 009aa39..57ae5a3 100644 --- a/backend/internal/models/billing.go +++ b/backend/internal/models/billing.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // CloudBilling 云账单模型 - 每日成本记录 @@ -21,11 +20,10 @@ type CloudBilling struct { Description string `gorm:"size:500" json:"description"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } func (CloudBilling) TableName() string { - return "cloud_billings" + return "asset_cloud_billings" } // BillingAlertRule 账单预警规则 @@ -41,11 +39,10 @@ type BillingAlertRule struct { LastTriggered *time.Time `json:"last_triggered"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } func (BillingAlertRule) TableName() string { - return "billing_alert_rules" + return "monitor_billing_alert_rules" } // BillingStats 账单统计 diff --git a/backend/internal/models/cicd.go b/backend/internal/models/cicd.go index 8fc4c1b..ef954d1 100644 --- a/backend/internal/models/cicd.go +++ b/backend/internal/models/cicd.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // Pipeline CI/CD流水线模型 @@ -26,12 +25,11 @@ type Pipeline struct { FailCount int `gorm:"default:0" json:"fail_count"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (Pipeline) TableName() string { - return "pipelines" + return "ops_pipelines" } // PipelineRun 流水线运行记录 @@ -81,12 +79,11 @@ type AppRelease struct { Description string `gorm:"size:500" json:"description"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (AppRelease) TableName() string { - return "app_releases" + return "deploy_app_releases" } // CreatePipelineRequest 创建流水线请求 diff --git a/backend/internal/models/credential.go b/backend/internal/models/credential.go new file mode 100644 index 0000000..393b851 --- /dev/null +++ b/backend/internal/models/credential.go @@ -0,0 +1,47 @@ +package models + +import ( + "time" +) + +// Credential 凭据管理模型(SSH密钥/API Token/密码等统一管理) +type Credential struct { + ID uint `gorm:"primarykey" json:"id"` + Name string `gorm:"size:128;not null" json:"name"` // 凭据名称 + Type string `gorm:"size:32;not null" json:"type"` // ssh_key, api_token, password, access_key + Content string `gorm:"type:text;not null" json:"-"` // AES加密后的内容,API不返回 + Description string `gorm:"size:500" json:"description"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (Credential) TableName() string { + return "iam_credentials" +} + +// CredentialCreateRequest 创建凭据请求 +type CredentialCreateRequest struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required,oneof=ssh_key api_token password access_key"` + Content string `json:"content" binding:"required"` // 明文,后端加密存储 + Description string `json:"description"` +} + +// CredentialUpdateRequest 更新凭据请求 +type CredentialUpdateRequest struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required,oneof=ssh_key api_token password access_key"` + Content string `json:"content"` // 空则不更新内容 + Description string `json:"description"` +} + +// CredentialListResponse 列表返回(内容脱敏) +type CredentialListResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Preview string `json:"preview"` // 内容预览(前8+后4字符) + Description string `json:"description"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/backend/internal/models/k8s.go b/backend/internal/models/k8s.go index b073d91..51e8bc4 100644 --- a/backend/internal/models/k8s.go +++ b/backend/internal/models/k8s.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // K8sCluster K8s集群模型 @@ -29,12 +28,11 @@ type K8sCluster struct { LastSyncAt *time.Time `json:"last_sync_at"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (K8sCluster) TableName() string { - return "k8s_clusters" + return "container_clusters" } // K8sNode K8s节点模型 @@ -58,7 +56,6 @@ type K8sNode struct { Labels string `gorm:"type:text" json:"labels"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 关联 Cluster *K8sCluster `json:"cluster,omitempty"` @@ -85,7 +82,6 @@ type K8sPod struct { Labels string `gorm:"type:text" json:"labels"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 关联 Cluster *K8sCluster `json:"cluster,omitempty"` @@ -110,7 +106,6 @@ type K8sDeployment struct { Labels string `gorm:"type:text" json:"labels"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 关联 Cluster *K8sCluster `json:"cluster,omitempty"` @@ -135,7 +130,6 @@ type K8sService struct { Labels string `gorm:"type:text" json:"labels"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 关联 Cluster *K8sCluster `json:"cluster,omitempty"` @@ -159,7 +153,6 @@ type K8sIngress struct { Labels string `gorm:"type:text" json:"labels"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 关联 Cluster *K8sCluster `json:"cluster,omitempty"` @@ -202,3 +195,8 @@ type TopologyLayout struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } + +// TableName 指定表名 +func (TopologyLayout) TableName() string { + return "container_topology_layouts" +} diff --git a/backend/internal/models/llm.go b/backend/internal/models/llm.go index ad9d6b6..8d998df 100644 --- a/backend/internal/models/llm.go +++ b/backend/internal/models/llm.go @@ -19,7 +19,7 @@ type LLMConfig struct { } // TableName 表名 // Logan.Li -func (LLMConfig) TableName() string { return "llm_configs" } +func (LLMConfig) TableName() string { return "sys_llm_configs" } // SaveLLMConfigRequest 保存 LLM 配置请求 // Logan.Li type SaveLLMConfigRequest struct { @@ -63,4 +63,4 @@ type ChatHistory struct { } // TableName 表名 // Logan.Li -func (ChatHistory) TableName() string { return "chat_histories" } +func (ChatHistory) TableName() string { return "ai_chat_histories" } diff --git a/backend/internal/models/metrics_aggregate.go b/backend/internal/models/metrics_aggregate.go new file mode 100644 index 0000000..b04dc1f --- /dev/null +++ b/backend/internal/models/metrics_aggregate.go @@ -0,0 +1,38 @@ +package models + +import "time" + +// MetricsAggregate 指标聚合数据(Redis 热数据 → MySQL 时序聚合)// Logan.Li +// 替代旧名 MetricsKline,OHLC 对监控无意义,改用 max/min/avg/p95 +type MetricsAggregate struct { + ID uint `gorm:"primarykey" json:"id"` + Hostname string `gorm:"size:255;index:idx_agg;not null" json:"hostname"` + Metric string `gorm:"size:32;index:idx_agg;not null" json:"metric"` // cpu / mem / load1 / disk / net_rx / net_tx + Interval string `gorm:"size:8;index:idx_agg;not null" json:"interval"` // 1m / 5m / 1h / 6h / 1d + Avg float64 `gorm:"type:decimal(10,2)" json:"avg"` + MaxV float64 `gorm:"column:max_v;type:decimal(10,2)" json:"max"` + MinV float64 `gorm:"column:min_v;type:decimal(10,2)" json:"min"` + P95 float64 `gorm:"type:decimal(10,2)" json:"p95"` + Count int `json:"count"` + BucketTime time.Time `gorm:"index:idx_agg;not null" json:"bucket_time"` + CreatedAt time.Time `json:"created_at"` +} + +func (MetricsAggregate) TableName() string { + return "monitor_metrics_aggregate" +} + +// AggregateReq 聚合触发请求 +type AggregateReq struct { + Hostname string `json:"hostname" binding:"required"` + Metric string `json:"metric" binding:"required"` // cpu / mem / load1 +} + +// AggregateQueryReq 聚合查询参数 +type AggregateQueryReq struct { + Hostname string `form:"hostname" binding:"required"` + Metric string `form:"metric" binding:"required"` + Interval string `form:"interval" binding:"required"` + From string `form:"from" binding:"required"` // RFC3339 + To string `form:"to" binding:"required"` // RFC3339 +} diff --git a/backend/internal/models/metrics_kline.go b/backend/internal/models/metrics_kline.go deleted file mode 100644 index 0fb7270..0000000 --- a/backend/internal/models/metrics_kline.go +++ /dev/null @@ -1,38 +0,0 @@ -package models - -import "time" - -// MetricsKline 指标K线聚合数据(由 Redis 热数据聚合到 MySQL)// Logan.Li -type MetricsKline struct { - ID uint `gorm:"primarykey" json:"id"` - Hostname string `gorm:"size:255;index:idx_kline;not null" json:"hostname"` - Metric string `gorm:"size:32;index:idx_kline;not null" json:"metric"` // cpu / mem / load1 - Interval string `gorm:"size:8;index:idx_kline;not null" json:"interval"` // 1m / 3m / 1h / 6h / 1d - Open float64 `gorm:"type:decimal(10,2)" json:"open"` - High float64 `gorm:"type:decimal(10,2)" json:"high"` - Low float64 `gorm:"type:decimal(10,2)" json:"low"` - Close float64 `gorm:"type:decimal(10,2)" json:"close"` - Avg float64 `gorm:"type:decimal(10,2)" json:"avg"` - Count int `json:"count"` - BucketTime time.Time `gorm:"index:idx_kline;not null" json:"bucket_time"` - CreatedAt time.Time `json:"created_at"` -} - -func (MetricsKline) TableName() string { - return "metrics_klines" -} - -// AggregateKlineReq 聚合请求体 -type AggregateKlineReq struct { - Hostname string `json:"hostname" binding:"required"` - Metric string `json:"metric" binding:"required"` // cpu / mem / load1 -} - -// KlineQueryReq K线查询参数(Query 参数) -type KlineQueryReq struct { - Hostname string `form:"hostname" binding:"required"` - Metric string `form:"metric" binding:"required"` - Interval string `form:"interval" binding:"required"` - From string `form:"from" binding:"required"` // RFC3339 - To string `form:"to" binding:"required"` // RFC3339 -} diff --git a/backend/internal/models/monitor.go b/backend/internal/models/monitor.go index 9dbd4bc..a4cf1e6 100644 --- a/backend/internal/models/monitor.go +++ b/backend/internal/models/monitor.go @@ -160,7 +160,7 @@ type MetricHistory struct { } func (MetricHistory) TableName() string { - return "metric_histories" + return "monitor_metric_histories" } // MetricValueQuery 指标值查询参数 @@ -202,7 +202,7 @@ type MetricTag struct { // TableName 指定表名 func (MetricTag) TableName() string { - return "metric_tags" + return "monitor_metric_tags" } // ============================================ @@ -351,5 +351,5 @@ type HostGroup struct { // TableName 指定表名 func (HostGroup) TableName() string { - return "host_groups" + return "asset_host_groups" } diff --git a/backend/internal/models/navigation.go b/backend/internal/models/navigation.go index f6c21b6..8fb1d15 100644 --- a/backend/internal/models/navigation.go +++ b/backend/internal/models/navigation.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // NavigationGroup 导航分组模型 @@ -18,12 +17,11 @@ type NavigationGroup struct { LinkCount int `gorm:"default:0" json:"link_count"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (NavigationGroup) TableName() string { - return "navigation_groups" + return "sys_navigation_groups" } // NavigationLink 导航链接模型 @@ -40,7 +38,6 @@ type NavigationLink struct { Enabled bool `gorm:"default:true" json:"enabled"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 关联 Group *NavigationGroup `json:"group,omitempty"` diff --git a/backend/internal/models/ops_modules.go b/backend/internal/models/ops_modules.go index a57f094..ecefa55 100644 --- a/backend/internal/models/ops_modules.go +++ b/backend/internal/models/ops_modules.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // AnsiblePlaybook Playbook 模型 // Logan.Li @@ -17,10 +16,9 @@ type AnsiblePlaybook struct { CreatedBy string `gorm:"size:255" json:"created_by"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } -func (AnsiblePlaybook) TableName() string { return "ansible_playbooks" } +func (AnsiblePlaybook) TableName() string { return "ops_ansible_playbooks" } // AnsibleJob 执行任务记录 // Logan.Li type AnsibleJob struct { @@ -37,10 +35,9 @@ type AnsibleJob struct { CompletedAt *time.Time `json:"completed_at"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } -func (AnsibleJob) TableName() string { return "ansible_jobs" } +func (AnsibleJob) TableName() string { return "ops_ansible_jobs" } // AnsibleInventory 主机清单 // Logan.Li type AnsibleInventory struct { @@ -51,10 +48,9 @@ type AnsibleInventory struct { CreatedBy string `gorm:"size:255" json:"created_by"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } -func (AnsibleInventory) TableName() string { return "ansible_inventories" } +func (AnsibleInventory) TableName() string { return "ops_ansible_inventories" } // LogSource 日志源 // Logan.Li type LogSource struct { @@ -67,10 +63,9 @@ type LogSource struct { LastSeen *time.Time `json:"last_seen"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } -func (LogSource) TableName() string { return "log_sources" } +func (LogSource) TableName() string { return "sys_log_sources" } // LogEntry 日志条目 // Logan.Li type LogEntry struct { @@ -84,7 +79,6 @@ type LogEntry struct { TraceID string `gorm:"size:100;index" json:"trace_id"` Raw string `gorm:"type:text" json:"raw"` CreatedAt time.Time `json:"created_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } -func (LogEntry) TableName() string { return "log_entries" } +func (LogEntry) TableName() string { return "sys_log_entries" } diff --git a/backend/internal/models/prometheus_datasource.go b/backend/internal/models/prometheus_datasource.go index 5f7bbc4..26b0001 100644 --- a/backend/internal/models/prometheus_datasource.go +++ b/backend/internal/models/prometheus_datasource.go @@ -51,7 +51,7 @@ type PrometheusDataSource struct { // TableName 指定表名 func (PrometheusDataSource) TableName() string { - return "prometheus_datasources" + return "monitor_prometheus_datasources" } // ============================================ diff --git a/backend/internal/models/resource_model.go b/backend/internal/models/resource_model.go index 41059bd..e40c6bc 100644 --- a/backend/internal/models/resource_model.go +++ b/backend/internal/models/resource_model.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // ResourceModel 资源模型 (CMDB 模型定义) @@ -17,9 +16,8 @@ type ResourceModel struct { Icon string `gorm:"size:64" json:"icon"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } func (ResourceModel) TableName() string { - return "resource_models" + return "asset_resource_models" } diff --git a/backend/internal/models/server.go b/backend/internal/models/server.go index 022cdf0..99db72f 100644 --- a/backend/internal/models/server.go +++ b/backend/internal/models/server.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // Server 服务器模型 @@ -18,6 +17,7 @@ type Server struct { SSHKey string `gorm:"type:text" json:"-"` AuthType string `gorm:"size:32;default:password" json:"auth_type"` // password, key OS string `gorm:"size:64" json:"os"` + Kernel string `gorm:"size:64" json:"kernel"` OSVersion string `gorm:"size:64" json:"os_version"` Arch string `gorm:"size:32" json:"arch"` CPU int `json:"cpu"` @@ -30,6 +30,7 @@ type Server struct { Uptime int64 `json:"uptime"` // seconds AgentVersion string `gorm:"size:32" json:"agent_version"` AgentStatus string `gorm:"size:32;default:offline" json:"agent_status"` // online, offline + HasAuth bool `gorm:"-" json:"has_auth"` // 是否有认证凭据(密码或SSH密钥) — 不存DB,handler 计算后返回 // Logan.Li RegionID *uint `json:"region_id"` CloudID *uint `json:"cloud_id"` Tags string `gorm:"type:text" json:"tags"` // JSON @@ -37,7 +38,6 @@ type Server struct { LastCheckAt *time.Time `json:"last_check_at"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 关联 Region *Region `json:"region,omitempty"` @@ -46,7 +46,7 @@ type Server struct { // TableName 表名 func (Server) TableName() string { - return "servers" + return "asset_servers" } // Cloud 云平台模型 @@ -60,12 +60,11 @@ type Cloud struct { Description string `gorm:"size:500" json:"description"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (Cloud) TableName() string { - return "clouds" + return "asset_clouds" } // CloudAccount 云账号模型 @@ -82,12 +81,11 @@ type CloudAccount struct { LastSyncAt *time.Time `json:"last_sync_at"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (CloudAccount) TableName() string { - return "cloud_accounts" + return "asset_cloud_accounts" } // Region 区域模型 @@ -102,12 +100,11 @@ type Region struct { VPCCount int `gorm:"default:0" json:"vpc_count"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (Region) TableName() string { - return "regions" + return "asset_regions" } // Domain 域名模型 @@ -123,12 +120,11 @@ type Domain struct { SSLExpireDate *time.Time `json:"ssl_expire_date"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (Domain) TableName() string { - return "domains" + return "asset_domains" } // CreateServerRequest 创建服务器请求 @@ -188,9 +184,10 @@ type BatchServerItem struct { // BatchCreateServerRequest 批量导入服务器请求 // Logan.Li type BatchCreateServerRequest struct { - Servers []BatchServerItem `json:"servers"` - RegionID *uint `json:"region_id"` - AuthType string `json:"auth_type"` + Servers []BatchServerItem `json:"servers"` + RegionID *uint `json:"region_id"` + AuthType string `json:"auth_type"` + CredentialID *uint `json:"credential_id"` // 凭据ID,非空则解密后写入每台服务器 } // BatchCreateResult 批量导入结果 // Logan.Li diff --git a/backend/internal/models/site.go b/backend/internal/models/site.go index 811fccb..345d355 100644 --- a/backend/internal/models/site.go +++ b/backend/internal/models/site.go @@ -82,7 +82,7 @@ type SiteTask struct { // TableName 指定表名 func (SiteTask) TableName() string { - return "site_tasks" + return "ops_site_tasks" } // ============================================ @@ -138,7 +138,7 @@ type SiteResult struct { // TableName 指定表名 func (SiteResult) TableName() string { - return "site_results" + return "ops_site_results" } // ============================================ @@ -198,7 +198,7 @@ type SiteAlert struct { // TableName 指定表名 func (SiteAlert) TableName() string { - return "site_alerts" + return "ops_site_alerts" } // SiteTaskCreateRequest 拨测任务创建请求 diff --git a/backend/internal/models/snmp.go b/backend/internal/models/snmp.go index 15c1391..4ae890d 100644 --- a/backend/internal/models/snmp.go +++ b/backend/internal/models/snmp.go @@ -92,7 +92,7 @@ type SNMPDevice struct { // TableName 指定表名 func (SNMPDevice) TableName() string { - return "snmp_devices" + return "ops_snmp_devices" } // ============================================ @@ -120,7 +120,7 @@ type SNMPTemplate struct { // TableName 指定表名 func (SNMPTemplate) TableName() string { - return "snmp_templates" + return "ops_snmp_templates" } // SNMPTemplateOID OID模板条目表 @@ -142,7 +142,7 @@ type SNMPTemplateOID struct { // TableName 指定表名 func (SNMPTemplateOID) TableName() string { - return "snmp_template_oids" + return "ops_snmp_template_oids" } // ============================================ @@ -170,7 +170,7 @@ type SNMPMetric struct { // TableName 指定表名 func (SNMPMetric) TableName() string { - return "snmp_metrics" + return "ops_snmp_metrics" } // ============================================ @@ -199,7 +199,7 @@ type SNMPInterface struct { // TableName 指定表名 func (SNMPInterface) TableName() string { - return "snmp_interfaces" + return "ops_snmp_interfaces" } // ============================================ diff --git a/backend/internal/models/terminal_session.go b/backend/internal/models/terminal_session.go index ea0b9d2..6c0ce20 100644 --- a/backend/internal/models/terminal_session.go +++ b/backend/internal/models/terminal_session.go @@ -23,7 +23,7 @@ type TerminalSession struct { // TableName 表名 func (TerminalSession) TableName() string { - return "terminal_sessions" + return "sys_terminal_sessions" } // TerminalSessionQuery 终端会话查询 diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go index c1e8e3b..420ef89 100644 --- a/backend/internal/models/user.go +++ b/backend/internal/models/user.go @@ -3,7 +3,6 @@ package models import ( "time" - "gorm.io/gorm" ) // User 用户模型 @@ -21,7 +20,6 @@ type User struct { LastLogin *time.Time `json:"last_login"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 关联 Roles []Role `gorm:"many2many:user_roles;" json:"roles,omitempty"` @@ -29,7 +27,7 @@ type User struct { // TableName 表名 func (User) TableName() string { - return "users" + return "iam_users" } // Role 角色模型 @@ -41,7 +39,6 @@ type Role struct { Status int `gorm:"default:1" json:"status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 关联 Permissions []Permission `gorm:"many2many:role_permissions;" json:"permissions,omitempty"` @@ -49,7 +46,7 @@ type Role struct { // TableName 表名 func (Role) TableName() string { - return "roles" + return "iam_roles" } // Permission 权限模型 @@ -65,12 +62,11 @@ type Permission struct { Status int `gorm:"default:1" json:"status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } // TableName 表名 func (Permission) TableName() string { - return "permissions" + return "iam_permissions" } // LoginRequest 登录请求 @@ -143,4 +139,4 @@ type LDAPConfig struct { UpdatedAt time.Time `json:"updated_at"` } -func (LDAPConfig) TableName() string { return "ldap_configs" } +func (LDAPConfig) TableName() string { return "iam_ldap_configs" } diff --git a/backend/internal/models/zabbix_datasource.go b/backend/internal/models/zabbix_datasource.go index 7929054..32b78f4 100644 --- a/backend/internal/models/zabbix_datasource.go +++ b/backend/internal/models/zabbix_datasource.go @@ -34,7 +34,7 @@ type ZabbixDataSource struct { // TableName 指定表名 func (ZabbixDataSource) TableName() string { - return "zabbix_datasources" + return "monitor_zabbix_datasources" } // ============================================ diff --git a/backend/pkg/crypto/secretbox.go b/backend/pkg/crypto/secretbox.go new file mode 100644 index 0000000..8c9aed0 --- /dev/null +++ b/backend/pkg/crypto/secretbox.go @@ -0,0 +1,66 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "errors" + "io" +) + +// SecretBox AES-256-GCM 加密器,用 sha256(key) 派生 32 字节密钥 +type SecretBox struct { + key []byte +} + +// NewSecretBox 创建加密器 +func NewSecretBox(masterKey string) *SecretBox { + h := sha256.Sum256([]byte(masterKey)) + return &SecretBox{key: h[:]} +} + +// Encrypt 加密明文,返回 base64 编码的密文(含 12 字节 nonce 前缀) +func (s *SecretBox) Encrypt(plaintext string) (string, error) { + block, err := aes.NewCipher(s.key) + if err != nil { + return "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// Decrypt 解密密文,plaintext 应为 Encrypt 的返回值 +func (s *SecretBox) Decrypt(cipherB64 string) (string, error) { + ciphertext, err := base64.StdEncoding.DecodeString(cipherB64) + if err != nil { + return "", err + } + block, err := aes.NewCipher(s.key) + if err != nil { + return "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + return "", errors.New("ciphertext too short") + } + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", err + } + return string(plaintext), nil +} diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go index b597704..0cae5db 100644 --- a/backend/pkg/database/database.go +++ b/backend/pkg/database/database.go @@ -106,7 +106,7 @@ func AutoMigrate(db *gorm.DB) error { &models.TerminalSession{}, // 终端会话录制(终端审计) // Logan.Li &models.LLMConfig{}, // 大语言模型配置(AI 智能助手) // Logan.Li &models.ChatHistory{}, // 对话历史记录(按用户隔离) // Logan.Li - &models.MetricsKline{}, // 指标K线聚合(Redis 热数据 → MySQL) // Logan.Li + &models.MetricsAggregate{}, // 指标时序聚合(Redis → MySQL)// Logan.Li &models.MonitorTarget{}, // 监控目标(主机/中间件/设备/站点) // Logan.Li &models.MonitorMetric{}, // 监控指标定义 // Logan.Li &models.MetricValue{}, // 指标当前值 // Logan.Li @@ -132,6 +132,7 @@ func AutoMigrate(db *gorm.DB) error { &models.AlertEventV2{}, // 告警事件V2 // Logan.Li &models.PrometheusDataSource{}, // Prometheus 数据源配置 // Logan.Li &models.ZabbixDataSource{}, // Zabbix 数据源配置 // Logan.Li + &models.Credential{}, // 凭据管理(SSH密钥/API Token) // Logan.Li } var lastErr error diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7469223..5235bff 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,6 +21,7 @@ import { AppReleaseManager } from '@/components/deploy/AppRelease'; import { ApprovalFlowManager } from '@/components/approval/ApprovalFlow'; import { ScriptManager } from '@/components/automation/ScriptManager'; import { SystemManager } from '@/components/system/SystemManager'; +import { CredentialManager } from '@/components/system/CredentialManager'; import { PipelineManager } from '@/components/deploy/PipelineManager'; import { ReleaseManager } from '@/components/deploy/ReleaseManager'; import { CronJobManager } from '@/components/automation/CronJobManager'; @@ -30,6 +31,7 @@ import { AIAlertAnalyzer } from '@/components/aiops/AIAlertAnalyzer'; import { AIPredictiveAnalytics } from '@/components/aiops/AIPredictiveAnalytics'; import AlertCenter from '@/components/monitor/alert-center/AlertCenter'; import TraceManager from '@/components/monitor/trace/TraceManager'; +import { HostMonitor } from '@/pages/HostMonitor'; import HostMonitorDashboard from '@/components/monitor/dashboard/HostMonitorDashboard'; import GrafanaDashboard from '@/components/monitor/dashboard/GrafanaDashboard'; import { MonitorCenter } from '@/pages/MonitorCenter'; @@ -76,6 +78,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> @@ -92,6 +95,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/components/agent/AgentWizard.tsx b/frontend/src/components/agent/AgentWizard.tsx index 4076496..191ff15 100644 --- a/frontend/src/components/agent/AgentWizard.tsx +++ b/frontend/src/components/agent/AgentWizard.tsx @@ -9,7 +9,9 @@ import { } from '@/components/ui/dialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; -import { Copy, Check, Terminal, Download, Settings, Play, Cpu, Zap, Loader2, Server } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Copy, Check, Terminal, Download, Settings, Play, Cpu, Zap, Loader2, Server, Shield } from 'lucide-react'; import { serverApi } from '@/services/api'; import { toast } from 'sonner'; @@ -17,7 +19,7 @@ interface AgentWizardProps { open: boolean; onOpenChange: (open: boolean) => void; /** 可选:从服务器列表传入的目标服务器信息 */ - server?: { id: number; hostname: string; ip: string } | null; + server?: { id: number; hostname: string; ip: string; has_auth?: boolean; username?: string; os?: string } | null; /** SSH一键安装成功回调 */ onInstalled?: (serverId: number) => void; /** SSH一键安装失败回调 */ @@ -42,6 +44,13 @@ export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed const [showManualInput, setShowManualInput] = useState(false); const [sshInstalling, setSshInstalling] = useState(false); const [activeTab, setActiveTab] = useState('auto'); + // 凭据输入(当服务器无认证凭据时显示) + const [sshUsername, setSshUsername] = useState('root'); + const [sshPassword, setSshPassword] = useState(''); + const [sshKey, setSshKey] = useState(''); + const [authType, setAuthType] = useState<'password' | 'key'>('password'); + + const needsCreds = server && !server.has_auth; // install.sh 通过 Nginx 代理下载(origin:80 → /agent/ → backend:8080) const installCommand = `curl -fsSL ${origin}/agent/install.sh | bash -s -- --server ${backend} --token ${token}`; @@ -142,11 +151,22 @@ export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed ]; // SSH 一键安装(当从服务器列表传入 server 上下文时可用) + // 如果服务器未配置凭据,将输入框中的凭据传给后端,首次连接成功后自动保存 const handleSshInstall = async () => { if (!server || !token) return; + if (needsCreds && !sshPassword && !sshKey) { + toast.error('请填写 SSH 登录凭据(密码或密钥)'); + return; + } setSshInstalling(true); try { - const res: any = await serverApi.installAgent(server.id, token); + const creds = needsCreds ? { + username: sshUsername, + password: sshPassword, + ssh_key: sshKey, + auth_type: authType, + } : undefined; + const res: any = await serverApi.installAgent(server.id, token, creds); const installed = res?.installed || false; if (installed) { toast.success(`${server.hostname} Agent 安装成功`); @@ -193,6 +213,12 @@ export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed SSH一键安装 )} + {!server && ( + + + SSH一键安装 + + )} 手动安装 @@ -240,7 +266,17 @@ export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed - {/* SSH 一键安装 — 仅当从服务器列表传入时显示 */} + {/* SSH 一键安装 — 无服务器时提示先选择 */} + {!server && ( + +
+ +

未选择目标主机

+

请先在主机列表中勾选一台主机,再点击「安装 Agent」打开此页面

+
+
+ )} + {/* SSH 一键安装 — 有服务器时显示 */} {server && (
@@ -251,6 +287,58 @@ export function AgentWizard({ open, onOpenChange, server, onInstalled, onFailed {server.ip}
+ + {/* 未配置凭据时显示输入框 */} + {needsCreds && ( +
+
+ + 该服务器未配置 SSH 凭据,请填写以下信息(首次连接成功后自动保存) +
+
+
+ + setSshUsername(e.target.value)} + placeholder="root" + className="h-8 text-sm bg-slate-900 border-slate-700 text-slate-200 mt-1" + /> +
+
+
+ + { setSshPassword(e.target.value); setAuthType('password'); }} + placeholder="******" + className="h-8 text-sm bg-slate-900 border-slate-700 text-slate-200 mt-1" + /> +
+ 或 + +
+
+ {authType === 'key' && ( +
+ +