package controller import ( "context" "fmt" "log" "net" "net/http" "strconv" "strings" "time" "ezops/model" "github.com/gin-gonic/gin" "golang.org/x/crypto/ssh" "gorm.io/gorm" ) // AgentInstallHandler Agent 远程安装处理器 // Logan.Li type AgentInstallHandler struct { db *gorm.DB } func NewAgentInstallHandler(db *gorm.DB) *AgentInstallHandler { return &AgentInstallHandler{db: db} } // auditCtx 快照请求上下文(用户身份 + 来源 IP),用于在异步 goroutine 中安全记录审计 // Logan.Li // gin.Context 在请求返回后会被 gin 池复用,goroutine 内禁止直接访问 c,必须提前快照 type auditCtx struct { userID uint username string ip string userAgent string } // newAuditCtx 从 gin.Context 提取审计所需上下文快照 // Logan.Li func newAuditCtx(c *gin.Context) auditCtx { uid, _ := c.Get("userID") uname, _ := c.Get("username") id, _ := uid.(uint) name, _ := uname.(string) return auditCtx{ userID: id, username: name, ip: c.ClientIP(), userAgent: c.Request.UserAgent(), } } // truncateAuditText 截断过长输出,避免审计 Error 字段膨胀 // Logan.Li func truncateAuditText(s string, n int) string { s = strings.TrimSpace(s) if len(s) > n { return s[:n] + "..." } return s } // recordAgentAudit 记录 Agent 安装/卸载/升级操作审计 // Logan.Li // status: 1=成功, 0=失败;errMsg 记录失败原因(SSH 错误、脚本输出等),用于追溯"安装过程遇到什么问题" func (h *AgentInstallHandler) recordAgentAudit(ac auditCtx, action, resource, resourceID string, status int, errMsg string) { uid := ac.userID h.db.Create(&model.AuditLog{ UserID: &uid, Username: ac.username, IP: ac.ip, UserAgent: ac.userAgent, Module: "agent", Action: action, Resource: resource, ResourceID: resourceID, Status: status, Error: errMsg, }) } // sshDial 复用 webshell.go 的 SSH 连接逻辑(TOFU HostKey 验证) func sshDial(server model.Server) (*ssh.Client, error) { config := &ssh.ClientConfig{ User: server.Username, Timeout: 10 * time.Second, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { // TOFU: 记录并信任首次连接的 HostKey fp := ssh.FingerprintSHA256(key) log.Printf("[AgentInstall] SSH TOFU: trusting %s (%s): %s", server.Hostname, hostname, fp) return nil }, } switch server.AuthType { case "key": signer, err := ssh.ParsePrivateKey([]byte(server.SSHKey)) if err != nil { config.Auth = []ssh.AuthMethod{ssh.Password(server.Password)} } else { config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} } default: config.Auth = []ssh.AuthMethod{ssh.Password(server.Password)} } addr := server.IP + ":" + strconv.Itoa(server.Port) return ssh.Dial("tcp", addr, config) } // runSSHCommand 在远程主机执行命令并返回输出(默认 60s 超时,防止 sudo 密码等待导致挂死)// Logan.Li func runSSHCommand(client *ssh.Client, cmd string) (string, error) { return runSSHCommandWithTimeout(client, cmd, 60*time.Second) } // runSSHCommandWithTimeout 支持自定义超时 func runSSHCommandWithTimeout(client *ssh.Client, cmd string, timeout time.Duration) (string, error) { session, err := client.NewSession() if err != nil { return "", err } defer session.Close() ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() type result struct { output []byte err error } done := make(chan result, 1) go func() { out, err := session.CombinedOutput(cmd) done <- result{output: out, err: err} }() select { case <-ctx.Done(): session.Signal(ssh.SIGTERM) return "", fmt.Errorf("命令执行超时(%s): %s", timeout, cmd) case r := <-done: return string(r.output), r.err } } // installServerURL 根据请求构造后端可访问地址,兼容 Nginx 等反向代理 // Logan.Li func installServerURL(c *gin.Context) string { scheme := "http" if c.Request.TLS != nil { scheme = "https" } if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" { scheme = proto } // 优先使用 X-Forwarded-Host(Nginx 暴露给客户端的真实域名),其次用 Host // Logan.Li host := c.Request.Host if fwd := c.GetHeader("X-Forwarded-Host"); fwd != "" { host = fwd } // 去掉 scheme 的默认端口,避免生成 http://example.com:80 这类无效地址 // Logan.Li if scheme == "http" && strings.HasSuffix(host, ":80") { host = strings.TrimSuffix(host, ":80") } if scheme == "https" && strings.HasSuffix(host, ":443") { host = strings.TrimSuffix(host, ":443") } return fmt.Sprintf("%s://%s", scheme, host) } // detectAgentVersion SSH 到目标主机读取已安装 Agent 的真实版本号 // Logan.Li func detectAgentVersion(client *ssh.Client) string { verOut, _ := runSSHCommand(client, "/usr/local/bin/aiops-agent version 2>/dev/null || /usr/local/bin/aiops-agent -v 2>/dev/null || echo ''") verOut = strings.TrimSpace(verOut) // 输出形如: AIOps Agent v2.1.0 (built ...) go=go1.24... if strings.HasPrefix(verOut, "AIOps Agent ") { fields := strings.Fields(verOut) if len(fields) >= 3 { return fields[2] } } return "" } // 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 model.Server if err := h.db.First(&server, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "服务器不存在"}) return } if server.Status != "running" { // 允许安装,不强制 running 状态 } // 审计上下文 + 资源描述(同步流程,提前快照)// Logan.Li ac := newAuditCtx(c) resource := server.Hostname + " (" + server.IP + ")" resourceID := strconv.Itoa(int(server.ID)) // 如果请求中提供了凭据,使用请求凭据覆盖服务器存储的凭据进行 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(effectiveServer) if err != nil { log.Printf("[AgentInstall] SSH连接失败 %s: %v", server.IP, err) h.recordAgentAudit(ac, "install", resource, resourceID, 0, "SSH连接失败: "+formatSSHError(err)) c.JSON(http.StatusOK, model.Response{ Code: 500, Message: "SSH 连接失败: " + formatSSHError(err), }) return } 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" if len(archOut) > 0 && (contains(archOut, "aarch64") || contains(archOut, "arm64")) { arch = "arm64" } // 3. 构造安装命令(使用 Agent API 服务地址) serverURL := installServerURL(c) token := req.Token if token == "" { token = c.Query("token") } // 4. 构造安装命令:root 用户直接 bash,非 root 用 sudo -n(无密码才执行,避免挂死)// Logan.Li installCmd := fmt.Sprintf( `if [ "$(id -u)" -eq 0 ]; then curl -fsSL %s/agent/install.sh | bash -s -- --server %s --token %s; else curl -fsSL %s/agent/install.sh | sudo -n bash -s -- --server %s --token %s; fi`, serverURL, serverURL, token, serverURL, serverURL, token, ) // 5. 执行安装 installOut, err := runSSHCommand(client, installCmd) if err != nil { msg := "安装命令执行失败" lowerOut := strings.ToLower(installOut) if strings.Contains(lowerOut, "sudo: a password is required") || strings.Contains(lowerOut, "需要密码") { msg = "目标主机 SSH 用户没有免密 sudo 权限,请使用 root 账号或配置 passwordless sudo" } else if strings.Contains(lowerOut, "curl") && (strings.Contains(lowerOut, "not found") || strings.Contains(lowerOut, "未找到")) { msg = "目标主机未安装 curl" } else if strings.Contains(lowerOut, "下载失败") || strings.Contains(lowerOut, "failed to connect") || strings.Contains(lowerOut, "connection refused") { msg = "目标主机无法下载安装脚本,请检查后端地址 " + serverURL + " 是否可达" } h.recordAgentAudit(ac, "install", resource, resourceID, 0, msg+": "+truncateAuditText(installOut, 300)) c.JSON(http.StatusOK, model.Response{ Code: 500, Message: msg, Data: installOut, }) return } // 5. 验证安装是否成功 verifyOut, _ := runSSHCommand(client, "systemctl is-active aiops-agent 2>/dev/null || echo 'not_found'") installed := contains(verifyOut, "active") || contains(verifyOut, "running") if installed { // 只更新在线状态;版本号仅在能读到真实版本时才写入,未上报时不写假版本(等 Agent 首次上报真实数据)// Logan.Li updates := map[string]interface{}{ "agent_status": "online", "last_check_at": time.Now(), } if agentVersion := detectAgentVersion(client); agentVersion != "" { updates["agent_version"] = agentVersion } h.db.Model(&server).Updates(updates) } // 审计:成功或"命令已执行但服务未运行"均记录,便于追溯 // Logan.Li if installed { h.recordAgentAudit(ac, "install", resource, resourceID, 1, "") } else { h.recordAgentAudit(ac, "install", resource, resourceID, 0, "安装命令已执行但服务未检测到运行: "+truncateAuditText(verifyOut, 300)) } c.JSON(http.StatusOK, model.Response{ Code: 200, Message: func() string { if installed { return "Agent 安装成功,已自动启动并设置开机自启" } return "安装命令已执行,但 Agent 服务未检测到运行(请查看日志排查)" }(), Data: gin.H{ "installed": installed, "arch": arch, "output": installOut, "verify": verifyOut, }, }) } // installOne 安装单个服务器的 Agent(内部方法) func (h *AgentInstallHandler) installOne(server model.Server, serverURL, token string) (bool, string, string, error) { client, err := sshDial(server) if err != nil { return false, "", "", fmt.Errorf("SSH连接失败: %w", err) } defer client.Close() // 不强制 server.Status==running:只要 SSH 可达即可安装(DB 状态可能滞后于实际开机状态) // Logan.Li installCmd := fmt.Sprintf( `if [ "$(id -u)" -eq 0 ]; then curl -fsSL %s/agent/install.sh | bash -s -- --server %s --token %s; else curl -fsSL %s/agent/install.sh | sudo -n bash -s -- --server %s --token %s; fi`, serverURL, serverURL, token, serverURL, serverURL, token, ) installOut, err := runSSHCommand(client, installCmd) if err != nil { return false, installOut, "", err } verifyOut, _ := runSSHCommand(client, "systemctl is-active aiops-agent 2>/dev/null || echo 'not_found'") installed := contains(verifyOut, "active") || contains(verifyOut, "running") if installed { // 只更新在线状态;版本号仅在能读到真实版本时才写入,未上报时不写假版本 // Logan.Li updates := map[string]interface{}{ "agent_status": "online", "last_check_at": time.Now(), } if agentVersion := detectAgentVersion(client); agentVersion != "" { updates["agent_version"] = agentVersion } h.db.Model(&server).Updates(updates) } return installed, installOut, verifyOut, nil } // BatchInstallAgent 批量安装 Agent(异步执行,立即返回)// Logan.Li // POST /servers/install-agent // 支持传入临时凭据:key 为服务器 ID,首次连接成功后自动保存到服务器记录 func (h *AgentInstallHandler) BatchInstallAgent(c *gin.Context) { var req struct { ServerIDs []uint `json:"server_ids"` Credentials map[string]struct { Username string `json:"username"` Password string `json:"password"` SSHKey string `json:"ssh_key"` AuthType string `json:"auth_type"` } `json:"credentials"` } if err := c.ShouldBindJSON(&req); err != nil || len(req.ServerIDs) == 0 { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请提供要安装的服务器 ID 列表"}) return } serverURL := installServerURL(c) token := c.Query("token") ac := newAuditCtx(c) // 提前快照上下文,goroutine 内不可再用 c // Logan.Li // 异步执行:立即返回,后台 goroutine 逐台安装 go func() { successCount := 0 for _, sid := range req.ServerIDs { var server model.Server if err := h.db.First(&server, sid).Error; err != nil { log.Printf("[BatchInstall] 服务器 %d 不存在,跳过", sid) continue } // 如果请求提供了该服务器的临时凭据,覆盖使用(不立即写入 DB,由 installOne 首次连接成功后保存) // Logan.Li effectiveServer := server if cred, ok := req.Credentials[fmt.Sprintf("%d", sid)]; ok { if cred.Username != "" { effectiveServer.Username = cred.Username } if cred.Password != "" { effectiveServer.Password = cred.Password } if cred.SSHKey != "" { effectiveServer.SSHKey = cred.SSHKey effectiveServer.AuthType = "key" } if cred.AuthType != "" { effectiveServer.AuthType = cred.AuthType } } installed, _, verifyOut, err := h.installOne(effectiveServer, serverURL, token) res := server.Hostname + " (" + server.IP + ")" rid := strconv.Itoa(int(server.ID)) if err != nil { log.Printf("[BatchInstall] %s (%s) 安装失败: %v", server.Hostname, server.IP, err) h.recordAgentAudit(ac, "install", res, rid, 0, truncateAuditText(err.Error(), 500)) } else { successCount++ if installed { log.Printf("[BatchInstall] %s Agent 安装成功", server.Hostname) h.recordAgentAudit(ac, "install", res, rid, 1, "") } else { log.Printf("[BatchInstall] %s Agent 安装命令已执行但服务未检测到运行", server.Hostname) h.recordAgentAudit(ac, "install", res, rid, 0, "服务未检测到运行: "+truncateAuditText(verifyOut, 300)) } } } log.Printf("[BatchInstall] 批量安装完成: %d/%d 成功", successCount, len(req.ServerIDs)) }() c.JSON(http.StatusOK, model.Response{ Code: 200, 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, model.Response{Code: 400, Message: "参数错误"}) return } var server model.Server if req.ServerID != nil { if err := h.db.First(&server, *req.ServerID).Error; err != nil { c.JSON(http.StatusNotFound, model.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, model.Response{Code: 404, Message: "未找到服务器: " + req.Hostname}) return } } else { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请提供 hostname 或 server_id"}) return } ac := newAuditCtx(c) // Logan.Li resource := server.Hostname + " (" + server.IP + ")" resourceID := strconv.Itoa(int(server.ID)) // 无 SSH 凭据的主机(自动注册的)直接改本地状态;注意 SSH key 认证的主机 Password 为空但 SSHKey 非空,仍可远程卸载 // Logan.Li if server.Username == "" || (server.Password == "" && server.SSHKey == "") { h.db.Model(&server).Updates(map[string]interface{}{ "agent_status": "offline", "agent_version": "", }) h.recordAgentAudit(ac, "uninstall", resource, resourceID, 1, "无SSH凭据,已标记离线") c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已标记为离线(该主机无SSH凭据,无法远程卸载,请手动执行 systemctl stop aiops-agent)"}) return } client, err := sshDial(server) if err != nil { h.recordAgentAudit(ac, "uninstall", resource, resourceID, 0, "SSH连接失败: "+formatSSHError(err)) c.JSON(http.StatusOK, model.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": "", }) h.recordAgentAudit(ac, "uninstall", resource, resourceID, 1, "") c.JSON(http.StatusOK, model.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, model.Response{Code: 400, Message: "请提供服务器 ID 列表"}) return } ac := newAuditCtx(c) // 提前快照上下文,goroutine 内不可再用 c // Logan.Li // 异步执行:立即返回,后台逐台卸载 go func() { successCount := 0 for _, id := range req.ServerIDs { var server model.Server if err := h.db.First(&server, id).Error; err != nil { log.Printf("[BatchUninstall] 服务器 %d 不存在,跳过", id) continue } res := server.Hostname + " (" + server.IP + ")" rid := strconv.Itoa(int(server.ID)) if server.Username == "" || (server.Password == "" && server.SSHKey == "") { h.db.Model(&server).Updates(map[string]interface{}{"agent_status": "offline", "agent_version": ""}) log.Printf("[BatchUninstall] %s 无SSH凭据,已标记离线", server.Hostname) h.recordAgentAudit(ac, "uninstall", res, rid, 1, "无SSH凭据,已标记离线") successCount++ continue } client, err := sshDial(server) if err != nil { log.Printf("[BatchUninstall] %s SSH失败: %v", server.Hostname, err) h.recordAgentAudit(ac, "uninstall", res, rid, 0, "SSH连接失败: "+formatSSHError(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) h.recordAgentAudit(ac, "uninstall", res, rid, 1, "") successCount++ } log.Printf("[BatchUninstall] 批量卸载完成: %d/%d 成功", successCount, len(req.ServerIDs)) }() c.JSON(http.StatusOK, model.Response{ Code: 200, Message: fmt.Sprintf("已提交异步卸载任务,共 %d 台主机,正在后台执行…", len(req.ServerIDs)), Data: gin.H{"total": len(req.ServerIDs), "async": true}, }) } // BatchUpgradeAgent 批量升级 Agent 到最新版本(SSH下载新二进制 + 重启服务) // POST /api/v1/agent/batch-upgrade func (h *AgentInstallHandler) BatchUpgradeAgent(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, model.Response{Code: 400, Message: "请提供要升级的服务器 ID 列表"}) return } serverURL := installServerURL(c) ac := newAuditCtx(c) // 提前快照上下文,goroutine 内不可再用 c // Logan.Li // 异步执行:立即返回,后台逐台升级 go func() { successCount := 0 for _, sid := range req.ServerIDs { var server model.Server if err := h.db.First(&server, sid).Error; err != nil { log.Printf("[BatchUpgrade] 服务器 %d 不存在,跳过", sid) continue } if server.Username == "" || (server.Password == "" && server.SSHKey == "") { log.Printf("[BatchUpgrade] %s 无SSH凭据,跳过", server.Hostname) continue } res := server.Hostname + " (" + server.IP + ")" rid := strconv.Itoa(int(server.ID)) // 检测架构 client, err := sshDial(server) if err != nil { log.Printf("[BatchUpgrade] %s SSH连接失败: %v", server.Hostname, err) h.recordAgentAudit(ac, "upgrade", res, rid, 0, "SSH连接失败: "+formatSSHError(err)) continue } archOut, _ := runSSHCommand(client, "uname -m") binaryFile := "aiops-agent-linux-amd64" if len(archOut) > 0 && (contains(archOut, "aarch64") || contains(archOut, "arm64")) { binaryFile = "aiops-agent-linux-arm64" } // 下载新二进制 + 获取版本号 + 重启服务 upgradeScript := fmt.Sprintf( `curl -fsSL -o /tmp/aiops-agent %s/api/agent/bin/%s && chmod +x /tmp/aiops-agent && systemctl stop aiops-agent 2>/dev/null; mv /tmp/aiops-agent /usr/local/bin/aiops-agent && systemctl start aiops-agent; ver=$(/usr/local/bin/aiops-agent version 2>/dev/null | awk '/AIOps Agent/{print $3}'); echo "UPGRADE_OK ver=$ver"`, serverURL, binaryFile, ) out, err := runSSHCommand(client, upgradeScript) client.Close() if err != nil || !contains(out, "UPGRADE_OK") { log.Printf("[BatchUpgrade] %s 升级失败: output=%s err=%v", server.Hostname, out, err) h.recordAgentAudit(ac, "upgrade", res, rid, 0, "升级失败: "+truncateAuditText(out, 300)) continue } // 从输出提取版本号 agentVersion := "unknown" parts := strings.Split(out, "ver=") if len(parts) > 1 { agentVersion = strings.TrimSpace(parts[1]) } h.db.Model(&server).Updates(map[string]interface{}{ "agent_version": agentVersion, "last_check_at": time.Now(), }) log.Printf("[BatchUpgrade] %s Agent 升级成功", server.Hostname) h.recordAgentAudit(ac, "upgrade", res, rid, 1, "") successCount++ } log.Printf("[BatchUpgrade] 批量升级完成: %d/%d 成功", successCount, len(req.ServerIDs)) }() c.JSON(http.StatusOK, model.Response{ Code: 200, Message: fmt.Sprintf("已提交异步升级任务,共 %d 台主机,正在后台执行…", len(req.ServerIDs)), Data: gin.H{"total": len(req.ServerIDs), "async": true}, }) } // RemoteExec 直连 SSH 执行命令(即时返回,无轮询) // POST /api/v1/agent/exec func (h *AgentInstallHandler) RemoteExec(c *gin.Context) { var req struct { Hostname string `json:"hostname"` Command string `json:"command"` Timeout int `json:"timeout"` } if err := c.ShouldBindJSON(&req); err != nil || req.Hostname == "" || req.Command == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请提供 hostname 和 command"}) return } if req.Timeout <= 0 { req.Timeout = 30 } var server model.Server if err := h.db.Where("hostname = ? OR ip = ?", req.Hostname, req.Hostname).First(&server).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "未找到服务器: " + req.Hostname}) return } client, err := sshDial(server) if err != nil { c.JSON(http.StatusOK, model.Response{Code: 500, Message: "SSH连接失败: " + err.Error()}) return } defer client.Close() type result struct { out string err error } done := make(chan result, 1) go func() { out, err := runSSHCommand(client, req.Command) done <- result{out, err} }() select { case r := <-done: if r.err != nil { c.JSON(http.StatusOK, model.Response{Code: 200, Message: "执行完成", Data: gin.H{"output": r.out, "error": r.err.Error()}}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "执行成功", Data: gin.H{"output": r.out}}) case <-time.After(time.Duration(req.Timeout) * time.Second): c.JSON(http.StatusOK, model.Response{Code: 408, Message: fmt.Sprintf("命令执行超时(%ds)", req.Timeout)}) } }