From 827d9a31cdca590ab60fd7e6758afd0c8f93c91c Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Sat, 25 Jul 2026 15:08:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=AD=A3=E7=A1=AE=E8=AF=86=E5=88=AB=20A?= =?UTF-8?q?gent=20=E5=AE=89=E8=A3=85=E4=B8=8E=E5=AE=A1=E8=AE=A1=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 下载脚本失败时立即终止安装并提示 WAF 拦截 - Agent 服务未运行时不再计为安装成功 - 审计列表兼容后端 status 成功失败字段 --- backend/controller/agent_install.go | 71 +++++++++++++------ backend/controller/agent_install_test.go | 24 ++++++- .../src/components/system/SystemLogsTab.tsx | 5 +- 3 files changed, 75 insertions(+), 25 deletions(-) diff --git a/backend/controller/agent_install.go b/backend/controller/agent_install.go index 1a479cb..0133293 100644 --- a/backend/controller/agent_install.go +++ b/backend/controller/agent_install.go @@ -180,6 +180,34 @@ func shellQuote(value string) string { return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'" } +// agentInstallCommand 先完整下载脚本再执行,确保 curl 失败能正确传递退出状态。 // Logan.Li +func agentInstallCommand(serverURL, token string) string { + scriptURL := shellQuote(serverURL + "/agent/install.sh") + serverArg := shellQuote(serverURL) + tokenArg := shellQuote(token) + return fmt.Sprintf( + `tmp=$(mktemp /tmp/aiops-agent-install.XXXXXX) && trap 'rm -f "$tmp"' EXIT && curl -fsSL -o "$tmp" %s && if [ "$(id -u)" -eq 0 ]; then bash "$tmp" --server %s --token %s; else sudo -n bash "$tmp" --server %s --token %s; fi`, + scriptURL, serverArg, tokenArg, serverArg, tokenArg, + ) +} + +// agentInstallErrorMessage 将常见安装失败转换为可操作的中文提示。 // Logan.Li +func agentInstallErrorMessage(serverURL, output string) string { + lowerOutput := strings.ToLower(output) + switch { + case strings.Contains(lowerOutput, "403") || strings.Contains(lowerOutput, "forbidden"): + return "域名代理或 WAF 拒绝 Agent 下载(HTTP 403),请为 /agent/* 关闭人机验证或加入机器访问白名单" + case strings.Contains(lowerOutput, "sudo: a password is required") || strings.Contains(lowerOutput, "需要密码"): + return "目标主机 SSH 用户没有免密 sudo 权限,请使用 root 账号或配置 passwordless sudo" + case strings.Contains(lowerOutput, "curl") && (strings.Contains(lowerOutput, "not found") || strings.Contains(lowerOutput, "未找到")): + return "目标主机未安装 curl" + case strings.Contains(lowerOutput, "下载失败") || strings.Contains(lowerOutput, "failed to connect") || strings.Contains(lowerOutput, "connection refused"): + return "目标主机无法下载安装脚本,请检查后端地址 " + serverURL + " 是否可达" + default: + return "Agent 安装命令执行失败" + } +} + // 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 ''") @@ -298,25 +326,13 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) { 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`, - shellQuote(serverURL), shellQuote(serverURL), shellQuote(token), - shellQuote(serverURL), shellQuote(serverURL), shellQuote(token), - ) + // 4. 先下载脚本再执行,curl/WAF 失败时不会被管道末端 bash 的退出码掩盖。 // Logan.Li + installCmd := agentInstallCommand(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 + " 是否可达" - } + msg := agentInstallErrorMessage(serverURL, installOut) h.recordAgentAudit(ac, "install", resource, resourceID, 0, fmt.Sprintf("server=%s arch=%s", serverURL, arch), installOut, msg) c.JSON(http.StatusOK, model.Response{ @@ -330,6 +346,13 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) { // 5. 验证安装是否成功 verifyOut, _ := runSSHCommand(client, "systemctl is-active aiops-agent 2>/dev/null || echo 'not_found'") installed := contains(verifyOut, "active") || contains(verifyOut, "running") + if !installed { + msg := "安装脚本已执行,但 Agent 服务未运行" + h.recordAgentAudit(ac, "install", resource, resourceID, 0, + fmt.Sprintf("server=%s arch=%s", serverURL, arch), installOut+"\n"+verifyOut, msg) + c.JSON(http.StatusOK, model.Response{Code: 500, Message: msg, Data: gin.H{"output": installOut, "verify": verifyOut}}) + return + } var agentVer string if installed { @@ -384,11 +407,7 @@ func (h *AgentInstallHandler) installOne(server model.Server, serverURL, token s // 不强制 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`, - shellQuote(serverURL), shellQuote(serverURL), shellQuote(token), - shellQuote(serverURL), shellQuote(serverURL), shellQuote(token), - ) + installCmd := agentInstallCommand(serverURL, token) installOut, err := runSSHCommand(client, installCmd) if err != nil { @@ -397,6 +416,9 @@ func (h *AgentInstallHandler) installOne(server model.Server, serverURL, token s verifyOut, _ := runSSHCommand(client, "systemctl is-active aiops-agent 2>/dev/null || echo 'not_found'") installed := contains(verifyOut, "active") || contains(verifyOut, "running") + if !installed { + return false, installOut, verifyOut, fmt.Errorf("安装脚本已执行,但 Agent 服务未运行") + } if installed { // 只更新在线状态;版本号仅在能读到真实版本时才写入,未上报时不写假版本 // Logan.Li @@ -473,8 +495,13 @@ func (h *AgentInstallHandler) BatchInstallAgent(c *gin.Context) { rid := strconv.Itoa(int(server.ID)) reqDetail := fmt.Sprintf("server=%s host=%s", serverURL, res) if err != nil { - log.Printf("[BatchInstall] %s (%s) 安装失败: %v", server.Hostname, server.IP, err) - h.recordAgentAudit(ac, "install", res, rid, 0, reqDetail, installOut, truncateAuditText(err.Error(), 500)) + errMessage := agentInstallErrorMessage(serverURL, installOut) + if errMessage == "Agent 安装命令执行失败" { + errMessage = err.Error() + } + log.Printf("[BatchInstall] %s (%s) 安装失败: %s", server.Hostname, server.IP, errMessage) + respDetail := fmt.Sprintf("=== 安装输出 ===\n%s\n=== 服务状态 ===\n%s", installOut, verifyOut) + h.recordAgentAudit(ac, "install", res, rid, 0, reqDetail, respDetail, truncateAuditText(errMessage, 500)) } else { successCount++ respDetail := fmt.Sprintf("=== 安装输出 ===\n%s\n=== 服务状态 ===\n%s", installOut, verifyOut) diff --git a/backend/controller/agent_install_test.go b/backend/controller/agent_install_test.go index c75eb3b..8fadcc7 100644 --- a/backend/controller/agent_install_test.go +++ b/backend/controller/agent_install_test.go @@ -1,6 +1,9 @@ package controller -import "testing" +import ( + "strings" + "testing" +) // TestValidateInstallServerURL 验证 Agent 公网地址仅接受安全的 HTTP(S) origin。 // Logan.Li func TestValidateInstallServerURL(t *testing.T) { @@ -36,3 +39,22 @@ func TestValidateInstallServerURL(t *testing.T) { }) } } + +// TestAgentInstallCommand 验证下载失败不会被管道末端命令掩盖。 // Logan.Li +func TestAgentInstallCommand(t *testing.T) { + command := agentInstallCommand("https://aiops.example.com", "token-value") + if strings.Contains(command, "| bash") { + t.Fatalf("安装命令不应继续使用 curl 管道: %s", command) + } + if !strings.Contains(command, "curl -fsSL -o") || !strings.Contains(command, "https://aiops.example.com/agent/install.sh") { + t.Fatalf("安装命令缺少脚本下载步骤: %s", command) + } +} + +// TestAgentInstallErrorMessage 验证 WAF 拒绝能返回明确提示。 // Logan.Li +func TestAgentInstallErrorMessage(t *testing.T) { + message := agentInstallErrorMessage("https://aiops.example.com", "curl: (22) The requested URL returned error: 403") + if !strings.Contains(message, "WAF") || !strings.Contains(message, "/agent/*") { + t.Fatalf("WAF 错误提示不完整: %s", message) + } +} diff --git a/frontend/src/components/system/SystemLogsTab.tsx b/frontend/src/components/system/SystemLogsTab.tsx index 7df2f16..5c98e2d 100644 --- a/frontend/src/components/system/SystemLogsTab.tsx +++ b/frontend/src/components/system/SystemLogsTab.tsx @@ -33,8 +33,9 @@ function normalizeLog(raw: JsonData): OperationLog { resource: raw.resource ?? '', ip: raw.ip ?? '', userAgent: raw.user_agent ?? raw.userAgent ?? '', - result: raw.result === 'failure' ? 'failure' : 'success', - message: raw.message ?? raw.detail, + // 后端审计日志使用 status=1/0 表示成功/失败,同时兼容旧 result 字段。 // Logan.Li + result: raw.result === 'failure' || raw.status === 0 || raw.status === 'failed' ? 'failure' : 'success', + message: raw.message ?? raw.detail ?? raw.error, createdAt: raw.createdAt ?? raw.created_at ?? '', }; }