feat(remote-exec): 新增远程执行页(执行/上传) + 移除ansible命名 + 主机监控bug修复
远程执行页 RemoteExecPage:关联主机多选 + 批量执行命令 + 批量上传文件(Go-native SSH),高危操作 useConfirm 确认;后端新增 multipart /remote-exec/upload-file 接口(接收文件→SCP 转发到多台主机)。 移除 ansible 命名:菜单 /ansible→/remote、组 id automation→task、子项 id ansible→remote-exec、种子显示名 Ansible→远程执行;删除 dead code ansibleApi。 主机监控 bug:installOne 不再强制 server.Status==running(默认 stopped 导致批量安装必失败);UninstallAgent 凭据判断修正(SSH key 主机 Password 为空不应误判为无凭据而跳过远程卸载)。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
0b2e43ea9e
commit
f820f0271f
8 files changed
+356
-61
No files matched your search
@@ -211,9 +211,7 @@ func (h *AgentInstallHandler) installOne(server model.Server, serverURL, token s
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if server.Status != "running" {
|
||||
return false, "", "", fmt.Errorf("服务器未运行")
|
||||
}
|
||||
// 不强制 server.Status==running:只要 SSH 可达即可安装(DB 状态可能滞后于实际开机状态) // Logan.Li
|
||||
|
||||
installCmd := fmt.Sprintf(
|
||||
`curl -fsSL %s/agent/install.sh | sudo bash -s -- --server %s --token %s`,
|
||||
@@ -312,8 +310,8 @@ func (h *AgentInstallHandler) UninstallAgent(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 无 SSH 凭据的主机(自动注册的)直接改本地状态
|
||||
if server.Username == "" || server.Password == "" {
|
||||
// 无 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": "",
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -133,6 +134,61 @@ func (h *RemoteExecHandler) BatchUpload(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: results})
|
||||
}
|
||||
|
||||
// UploadFile 前端上传文件到多台远程主机(multipart:file + server_ids + remote_path) // Logan.Li
|
||||
// 后端接收文件到临时路径,经 SSH SCP 转发到目标主机,完成后清理临时文件。
|
||||
// POST /api/v1/remote-exec/upload-file
|
||||
func (h *RemoteExecHandler) UploadFile(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请选择要上传的文件"})
|
||||
return
|
||||
}
|
||||
remotePath := strings.TrimSpace(c.PostForm("remote_path"))
|
||||
if remotePath == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请填写远程目标路径(含文件名)"})
|
||||
return
|
||||
}
|
||||
var serverIDs []uint
|
||||
if err := json.Unmarshal([]byte(c.PostForm("server_ids")), &serverIDs); err != nil || len(serverIDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请选择目标主机"})
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 保存上传文件到临时路径
|
||||
tmp, err := os.CreateTemp("", "aiops-upload-*")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建临时文件失败"})
|
||||
return
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
tmp.Close()
|
||||
defer os.Remove(tmpPath)
|
||||
if err := c.SaveUploadedFile(file, tmpPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "保存上传文件失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 查服务器 + 构建 SSH 配置
|
||||
var servers []model.Server
|
||||
if err := h.db.Where("id IN ?", serverIDs).Find(&servers).Error; err != nil || len(servers) == 0 {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "未找到目标服务器"})
|
||||
return
|
||||
}
|
||||
configs := make([]remoteexec.SSHConfig, 0, len(servers))
|
||||
for _, s := range servers {
|
||||
cfg, err := buildSSHConfig(h.db, s)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: fmt.Sprintf("服务器 %s 认证配置错误: %v", s.Hostname, err)})
|
||||
return
|
||||
}
|
||||
configs = append(configs, cfg)
|
||||
}
|
||||
|
||||
// 3. 并发 SCP 上传到所有目标主机
|
||||
results := remoteexec.BatchUpload(configs, tmpPath, remotePath, 5)
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: results})
|
||||
}
|
||||
|
||||
// ==================== 主机匹配 ====================
|
||||
|
||||
// MatchHosts 预览匹配的主机列表(从资产服务器表查询)// Logan.Li
|
||||
|
||||
@@ -701,6 +701,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
{
|
||||
remoteExec.POST("/execute", remoteExecHandler.BatchExec)
|
||||
remoteExec.POST("/upload", remoteExecHandler.BatchUpload)
|
||||
remoteExec.POST("/upload-file", remoteExecHandler.UploadFile)
|
||||
remoteExec.GET("/hosts", remoteExecHandler.MatchHosts)
|
||||
remoteExec.POST("/server/:id/exec", remoteExecHandler.ExecOnServer)
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func SeedData(db *gorm.DB) {
|
||||
// 自动化运维
|
||||
{Name: "定时任务", Code: "auto:cron", Type: "menu", Sort: 50, Status: 1},
|
||||
{Name: "脚本管理", Code: "auto:script", Type: "menu", Sort: 51, Status: 1},
|
||||
{Name: "Ansible", Code: "auto:ansible", Type: "menu", Sort: 52, Status: 1},
|
||||
{Name: "远程执行", Code: "auto:ansible", Type: "menu", Sort: 52, Status: 1},
|
||||
// AIOps
|
||||
{Name: "智能助手", Code: "aiops:assistant", Type: "menu", Sort: 60, Status: 1},
|
||||
{Name: "告警分析", Code: "aiops:analysis", Type: "menu", Sort: 61, Status: 1},
|
||||
|
||||
Reference in new issue
Block a user