feat: Agent采集uptime, 后端持久化kernel/uptime/diskIO, 前端磁盘BPS+IOPS展示
- 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 工具函数
This commit is contained in:
1 parent
31c2e338a3
commit
5949ec5d0c
48 files changed
+2182
-956
No files matched your search
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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: "删除成功"})
|
||||
}
|
||||
@@ -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: "凭据已关联"})
|
||||
}
|
||||
@@ -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: "服务器已彻底删除",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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: "服务器已彻底删除",
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 创建告警规则请求
|
||||
|
||||
@@ -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 告警规则创建请求
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -25,7 +25,7 @@ type AuditLog struct {
|
||||
|
||||
// TableName 表名
|
||||
func (AuditLog) TableName() string {
|
||||
return "audit_logs"
|
||||
return "sys_audit_logs"
|
||||
}
|
||||
|
||||
// AuditLogQuery 审计日志查询
|
||||
|
||||
@@ -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请求
|
||||
|
||||
@@ -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 账单统计
|
||||
|
||||
@@ -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 创建流水线请求
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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" }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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" }
|
||||
@@ -51,7 +51,7 @@ type PrometheusDataSource struct {
|
||||
|
||||
// TableName 指定表名
|
||||
func (PrometheusDataSource) TableName() string {
|
||||
return "prometheus_datasources"
|
||||
return "monitor_prometheus_datasources"
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 拨测任务创建请求
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -23,7 +23,7 @@ type TerminalSession struct {
|
||||
|
||||
// TableName 表名
|
||||
func (TerminalSession) TableName() string {
|
||||
return "terminal_sessions"
|
||||
return "sys_terminal_sessions"
|
||||
}
|
||||
|
||||
// TerminalSessionQuery 终端会话查询
|
||||
|
||||
@@ -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" }
|
||||
@@ -34,7 +34,7 @@ type ZabbixDataSource struct {
|
||||
|
||||
// TableName 指定表名
|
||||
func (ZabbixDataSource) TableName() string {
|
||||
return "zabbix_datasources"
|
||||
return "monitor_zabbix_datasources"
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user