fix: agent install.sh 404 + 三阶段构建编译agent二进制

问题: 构建上下文 ./backend 无法访问 ../agent, 导致容器内
/app/agent/deploy/install.sh 不存在, 返回 404

修复:
- Dockerfile 改为三阶段构建 (backend go1.21 + agent go1.24 交叉编译 + alpine runtime)
- docker-compose backend build context 改为项目根
- routes.go /agent/arch 安装命令改为动态获取 URL
- 新增 .dockerignore
- agent_install.go: SSH 远程安装 handler
- AgentWizard.tsx: 复制按钮 fallback + 动态 server URL
- ServerTableRow.tsx: agent 状态判断

验证: curl|bash 安装成功, agent 上报数据到 MySQL ✅
This commit is contained in:
Logan committed 2026-06-18 09:44:12 +08:00
1 parent d0344c2f52
commit d0b94d39c0
9 files changed
+281 -38

No files matched your search

@@ -0,0 +1,146 @@
package handlers
import (
"fmt"
"log"
"net/http"
"strconv"
"time"
"ezops/internal/models"
"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}
}
// sshDial 复用 webshell.go 的 SSH 连接逻辑
func sshDial(server models.Server) (*ssh.Client, error) {
config := &ssh.ClientConfig{
User: server.Username,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 10 * time.Second,
}
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 在远程主机执行命令并返回输出
func runSSHCommand(client *ssh.Client, cmd string) (string, error) {
session, err := client.NewSession()
if err != nil {
return "", err
}
defer session.Close()
output, err := session.CombinedOutput(cmd)
return string(output), err
}
// InstallAgent 远程安装 Agent 到指定服务器
// POST /servers/:id/install-agent
func (h *AgentInstallHandler) InstallAgent(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var server models.Server
if err := h.db.First(&server, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "服务器不存在"})
return
}
if server.Status != "running" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "服务器未运行,无法安装 Agent"})
return
}
// 1. SSH 连接目标主机
client, err := sshDial(server)
if err != nil {
log.Printf("[AgentInstall] SSH连接失败 %s: %v", server.IP, err)
c.JSON(http.StatusOK, models.Response{
Code: 500,
Message: "SSH 连接失败: " + formatSSHError(err),
})
return
}
defer client.Close()
// 2. 检测系统架构
archOut, _ := runSSHCommand(client, "uname -m")
arch := "amd64"
if len(archOut) > 0 && (contains(archOut, "aarch64") || contains(archOut, "arm64")) {
arch = "arm64"
}
// 3. 构造安装命令(使用 Agent API 服务地址)
// 从请求头获取后端可访问地址
serverURL := fmt.Sprintf("http://%s", c.Request.Host)
token := c.Query("token")
installCmd := fmt.Sprintf(
`curl -fsSL %s/agent/install.sh | sudo bash -s -- --server %s --token %s`,
serverURL, serverURL, token,
)
// 4. 执行安装
installOut, err := runSSHCommand(client, installCmd)
if err != nil {
c.JSON(http.StatusOK, models.Response{
Code: 500,
Message: "安装命令执行失败(可能需要 sudo 权限或 curl 未安装)",
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 状态
h.db.Model(&server).Updates(map[string]interface{}{
"agent_status": "online",
"agent_version": "installed",
"last_check_at": time.Now(),
})
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: func() string {
if installed {
return "Agent 安装成功,已自动启动并设置开机自启"
}
return "安装命令已执行,但 Agent 服务未检测到运行(请查看日志排查)"
}(),
Data: gin.H{
"installed": installed,
"arch": arch,
"output": installOut,
"verify": verifyOut,
},
})
}