diff --git a/backend/internal/api/handlers/agent_install.go b/backend/internal/api/handlers/agent_install.go index e822380..5d21722 100644 --- a/backend/internal/api/handlers/agent_install.go +++ b/backend/internal/api/handlers/agent_install.go @@ -144,3 +144,113 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) { }, }) } + +// installOne 安装单个服务器的 Agent(内部方法) +func (h *AgentInstallHandler) installOne(server models.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() + + if server.Status != "running" { + return false, "", "", fmt.Errorf("服务器未运行") + } + + installCmd := fmt.Sprintf( + `curl -fsSL %s/agent/install.sh | sudo bash -s -- --server %s --token %s`, + 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 { + h.db.Model(&server).Updates(map[string]interface{}{ + "agent_status": "online", + "agent_version": "installed", + "last_check_at": time.Now(), + }) + } + + return installed, installOut, verifyOut, nil +} + +// BatchInstallAgent 批量安装 Agent +// POST /servers/install-agent +func (h *AgentInstallHandler) BatchInstallAgent(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 + } + + 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 服务未检测到运行" + } + results = append(results, InstallResult{ + ServerID: server.ID, + Hostname: server.Hostname, + IP: server.IP, + Success: true, + Installed: installed, + Message: msg, + }) + } + } + + 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, + }, + }) +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 256b8c9..6124c09 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -141,6 +141,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c // Agent 远程安装 // Logan.Li agentInstallHandler := handlers.NewAgentInstallHandler(db) servers.POST("/:id/install-agent", agentInstallHandler.InstallAgent) + servers.POST("/install-agent", agentInstallHandler.BatchInstallAgent) } // 云账号路由 diff --git a/frontend/src/components/server/ServerManager.tsx b/frontend/src/components/server/ServerManager.tsx index ca67043..9c19d38 100644 --- a/frontend/src/components/server/ServerManager.tsx +++ b/frontend/src/components/server/ServerManager.tsx @@ -73,8 +73,75 @@ export function ServerManager() { // Agent 安装向导状态 const [agentWizardOpen, setAgentWizardOpen] = useState(false); - const openInstallAgent = (server: any) => { - setAgentWizardOpen(true); + // Agent 安装状态: { [serverId]: 'installing' | 'success' | 'failed' } + const [installingIds, setInstallingIds] = useState>(new Set()); + const [installResults, setInstallResults] = useState>(new Map()); + + const openInstallAgent = async (server: any) => { + const token = useUserStore.getState().token; + if (!token) { toast.error('请先登录'); return; } + + setInstallingIds(prev => new Set(prev).add(server.id)); + try { + const res: any = await serverApi.installAgent(server.id, token); + const installed = res?.installed || false; + setInstallResults(prev => { + const next = new Map(prev); + next.set(server.id, { + success: installed, + message: installed ? '安装成功 ✓' : '命令已执行,Agent 未检测到运行' + }); + return next; + }); + if (installed) toast.success(`${server.hostname} Agent 安装成功`); + else toast.warning(`${server.hostname}: Agent 安装命令已执行`); + } catch (e: any) { + setInstallResults(prev => { + const next = new Map(prev); + next.set(server.id, { success: false, message: e?.message || '安装失败' }); + return next; + }); + toast.error(`${server.hostname} 安装失败: ${e?.message || '未知错误'}`); + } finally { + setInstallingIds(prev => { + const next = new Set(prev); + next.delete(server.id); + return next; + }); + } + }; + + const handleBatchInstallAgents = async () => { + const token = useUserStore.getState().token; + if (!token) { toast.error('请先登录'); return; } + const ids = selectedServers.map(id => Number(id)); + + setInstallingIds(prev => { + const next = new Set(prev); + ids.forEach(id => next.add(id)); + return next; + }); + + try { + const res: any = await serverApi.batchInstallAgent(ids, token); + const results = res?.results || []; + setInstallResults(prev => { + const next = new Map(prev); + results.forEach((r: any) => { + next.set(r.server_id, { success: r.installed, message: r.message }); + }); + return next; + }); + toast.success(`批量安装完成: ${res?.success || 0}/${ids.length} 成功`); + } catch (e: any) { + toast.error(`批量安装失败: ${e?.message || '未知错误'}`); + } finally { + setInstallingIds(prev => { + const next = new Set(prev); + ids.forEach(id => next.delete(id)); + return next; + }); + } }; const openTerminal = (server: any) => { @@ -360,6 +427,15 @@ export function ServerManager() { {selectedServers.length > 0 && (
已选择 {selectedServers.length} 项 +