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
@@ -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},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user