package handlers import ( "bytes" "context" "crypto/rand" "encoding/hex" "fmt" "net/http" "os" "os/exec" "strconv" "time" "ezops/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // randomCode returns a short unique code for records that require a unique // string identifier (e.g. playbooks). It combines a unix timestamp with 4 // random hex bytes to guarantee uniqueness even under rapid sequential inserts. func randomCode(prefix string) string { b := make([]byte, 4) _, _ = rand.Read(b) return fmt.Sprintf("%s-%d-%s", prefix, time.Now().Unix(), hex.EncodeToString(b)) } // runLocal executes a script or shell command locally on the AIOps host and // returns the combined stdout+stderr output, the process exit code (0 = success) // and the wall-clock duration in milliseconds. A timeout prevents runaway jobs. func runLocal(scriptType, content string, timeoutSec int) (output string, exitCode int, durationMs int) { start := time.Now() timeout := time.Duration(timeoutSec) * time.Second if timeout <= 0 { timeout = 300 * time.Second } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() var cmd *exec.Cmd switch scriptType { case "python": cmd = exec.CommandContext(ctx, "python3", "-c", content) default: // shell / command / "" → run via bash cmd = exec.CommandContext(ctx, "bash", "-c", content) } var out bytes.Buffer cmd.Stdout = &out cmd.Stderr = &out err := cmd.Run() durationMs = int(time.Since(start).Milliseconds()) output = out.String() if ctx.Err() == context.DeadlineExceeded { output += fmt.Sprintf("\n[超时] 执行超过 %d 秒被终止", timeoutSec) return output, -1, durationMs } if err != nil { if exitError, ok := err.(*exec.ExitError); ok { exitCode = exitError.ExitCode() } else { exitCode = -1 output += "\n" + err.Error() } return output, exitCode, durationMs } return output, 0, durationMs } // AutomationHandler 自动化运维处理器 type AutomationHandler struct { db *gorm.DB } // NewAutomationHandler 创建自动化运维处理器 func NewAutomationHandler(db *gorm.DB) *AutomationHandler { return &AutomationHandler{db: db} } // ListCronJobs 获取定时任务列表 // @Summary 获取定时任务列表 // @Description 获取所有定时任务列表 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param page query int false "页码" // @Param page_size query int false "每页数量" // @Success 200 {object} models.Response{data=models.ListResponse} // @Router /cron-jobs [get] func (h *AutomationHandler) ListCronJobs(c *gin.Context) { var query models.PageQuery c.ShouldBindQuery(&query) var total int64 h.db.Model(&models.CronJob{}).Count(&total) var jobs []models.CronJob h.db.Preload("Script").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&jobs) c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", Data: models.ListResponse{ List: jobs, Total: total, }, }) } // GetCronJob 获取定时任务详情 // @Summary 获取定时任务详情 // @Description 获取指定定时任务的详细信息 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "任务ID" // @Success 200 {object} models.Response{data=models.CronJob} // @Router /cron-jobs/{id} [get] func (h *AutomationHandler) GetCronJob(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var job models.CronJob if err := h.db.Preload("Script").First(&job, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{ Code: 404, Message: "定时任务不存在", }) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", Data: job, }) } // CreateCronJob 创建定时任务 // @Summary 创建定时任务 // @Description 创建新定时任务 // @Tags 自动化运维 // @Accept json // @Produce json // @Security Bearer // @Param request body models.CreateCronJobRequest true "创建请求" // @Success 200 {object} models.Response{data=models.CronJob} // @Router /cron-jobs [post] func (h *AutomationHandler) CreateCronJob(c *gin.Context) { var req models.CreateCronJobRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{ Code: 400, Message: "请求参数错误: " + err.Error(), }) return } job := models.CronJob{ Name: req.Name, Description: req.Description, CronExpr: req.CronExpr, Type: req.Type, Content: req.Content, ScriptID: req.ScriptID, TargetType: req.TargetType, Timeout: req.Timeout, NotifyOn: req.NotifyOn, Enabled: true, } if job.Timeout == 0 { job.Timeout = 300 } if job.NotifyOn == "" { job.NotifyOn = "failure" } if err := h.db.Create(&job).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "创建失败", }) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "创建成功", Data: job, }) } // UpdateCronJob 更新定时任务 // @Summary 更新定时任务 // @Description 更新指定定时任务 // @Tags 自动化运维 // @Accept json // @Produce json // @Security Bearer // @Param id path int true "任务ID" // @Param request body models.CreateCronJobRequest true "更新请求" // @Success 200 {object} models.Response{data=models.CronJob} // @Router /cron-jobs/{id} [put] func (h *AutomationHandler) UpdateCronJob(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var req models.CreateCronJobRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{ Code: 400, Message: "请求参数错误: " + err.Error(), }) return } var job models.CronJob if err := h.db.First(&job, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{ Code: 404, Message: "定时任务不存在", }) return } updates := map[string]interface{}{ "name": req.Name, "description": req.Description, "cron_expr": req.CronExpr, "type": req.Type, "content": req.Content, "script_id": req.ScriptID, "target_type": req.TargetType, "timeout": req.Timeout, "notify_on": req.NotifyOn, } if err := h.db.Model(&job).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "更新失败", }) return } h.db.Preload("Script").First(&job, id) c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "更新成功", Data: job, }) } // DeleteCronJob 删除定时任务 // @Summary 删除定时任务 // @Description 删除指定定时任务 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "任务ID" // @Success 200 {object} models.Response // @Router /cron-jobs/{id} [delete] func (h *AutomationHandler) DeleteCronJob(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if err := h.db.Delete(&models.CronJob{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "删除失败", }) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "删除成功", }) } // ToggleCronJob 切换定时任务状态 // @Summary 切换定时任务状态 // @Description 启用或禁用定时任务 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "任务ID" // @Success 200 {object} models.Response{data=models.CronJob} // @Router /cron-jobs/{id}/toggle [post] func (h *AutomationHandler) ToggleCronJob(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var job models.CronJob if err := h.db.First(&job, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{ Code: 404, Message: "定时任务不存在", }) return } job.Enabled = !job.Enabled if err := h.db.Save(&job).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "操作失败", }) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "操作成功", Data: job, }) } // ExecuteCronJob 执行定时任务 // @Summary 执行定时任务 // @Description 手动执行定时任务 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "任务ID" // @Success 200 {object} models.Response{data=models.TaskExecution} // @Router /cron-jobs/{id}/execute [post] func (h *AutomationHandler) ExecuteCronJob(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var job models.CronJob if err := h.db.First(&job, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{ Code: 404, Message: "定时任务不存在", }) return } // Resolve the content to execute. For type=script, load the linked Script. content := job.Content scriptType := "shell" if job.Type == "script" && job.ScriptID != nil { var script models.Script if err := h.db.First(&script, *job.ScriptID).Error; err == nil { content = script.Content scriptType = script.Type h.db.Model(&script).UpdateColumn("run_count", script.RunCount+1) } } now := time.Now() execution := models.TaskExecution{ TaskType: "cron", TaskID: uint(id), TaskName: job.Name, Status: "running", TargetType: job.TargetType, Targets: job.Targets, StartedAt: &now, } if err := h.db.Create(&execution).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "创建执行记录失败", }) return } // Execute locally on the AIOps host. output, exitCode, duration := runLocal(scriptType, content, job.Timeout) finishedAt := time.Now() status := "success" if exitCode != 0 { status = "failed" } h.db.Model(&execution).Updates(map[string]interface{}{ "status": status, "output": output, "duration": duration, "finished_at": finishedAt, }) // Update cron job statistics. statsUpdates := map[string]interface{}{ "last_run_at": now, "last_status": status, "run_count": job.RunCount + 1, } if status == "success" { statsUpdates["success_count"] = job.SuccessCount + 1 } else { statsUpdates["fail_count"] = job.FailCount + 1 } h.db.Model(&job).Updates(statsUpdates) execution.Output = output execution.Status = status execution.Duration = duration execution.FinishedAt = &finishedAt msg := "执行成功" if status == "failed" { msg = "执行失败" } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: msg, Data: execution, }) } // ListScripts 获取脚本列表 // @Summary 获取脚本列表 // @Description 获取所有脚本列表 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param page query int false "页码" // @Param page_size query int false "每页数量" // @Success 200 {object} models.Response{data=models.ListResponse} // @Router /scripts [get] func (h *AutomationHandler) ListScripts(c *gin.Context) { var query models.PageQuery c.ShouldBindQuery(&query) var total int64 h.db.Model(&models.Script{}).Count(&total) var scripts []models.Script h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&scripts) c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", Data: models.ListResponse{ List: scripts, Total: total, }, }) } // GetScript 获取脚本详情 // @Summary 获取脚本详情 // @Description 获取指定脚本的详细信息 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "脚本ID" // @Success 200 {object} models.Response{data=models.Script} // @Router /scripts/{id} [get] func (h *AutomationHandler) GetScript(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var script models.Script if err := h.db.First(&script, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{ Code: 404, Message: "脚本不存在", }) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", Data: script, }) } // CreateScript 创建脚本 // @Summary 创建脚本 // @Description 创建新脚本 // @Tags 自动化运维 // @Accept json // @Produce json // @Security Bearer // @Param request body models.CreateScriptRequest true "创建请求" // @Success 200 {object} models.Response{data=models.Script} // @Router /scripts [post] func (h *AutomationHandler) CreateScript(c *gin.Context) { var req models.CreateScriptRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{ Code: 400, Message: "请求参数错误: " + err.Error(), }) return } script := models.Script{ Name: req.Name, Code: req.Code, Description: req.Description, Type: req.Type, Content: req.Content, Params: req.Params, Timeout: req.Timeout, } if script.Type == "" { script.Type = "shell" } if script.Timeout == 0 { script.Timeout = 300 } if err := h.db.Create(&script).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "创建失败", }) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "创建成功", Data: script, }) } // UpdateScript 更新脚本 // @Summary 更新脚本 // @Description 更新指定脚本 // @Tags 自动化运维 // @Accept json // @Produce json // @Security Bearer // @Param id path int true "脚本ID" // @Param request body models.CreateScriptRequest true "更新请求" // @Success 200 {object} models.Response{data=models.Script} // @Router /scripts/{id} [put] func (h *AutomationHandler) UpdateScript(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var req models.CreateScriptRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{ Code: 400, Message: "请求参数错误: " + err.Error(), }) return } var script models.Script if err := h.db.First(&script, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{ Code: 404, Message: "脚本不存在", }) return } updates := map[string]interface{}{ "name": req.Name, "description": req.Description, "type": req.Type, "content": req.Content, "params": req.Params, "timeout": req.Timeout, "version": script.Version + 1, } if err := h.db.Model(&script).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "更新失败", }) return } h.db.First(&script, id) c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "更新成功", Data: script, }) } // DeleteScript 删除脚本 // @Summary 删除脚本 // @Description 删除指定脚本 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "脚本ID" // @Success 200 {object} models.Response // @Router /scripts/{id} [delete] func (h *AutomationHandler) DeleteScript(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if err := h.db.Delete(&models.Script{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "删除失败", }) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "删除成功", }) } // ExecuteScript 执行脚本 // @Summary 执行脚本 // @Description 在目标服务器上执行脚本 // @Tags 自动化运维 // @Accept json // @Produce json // @Security Bearer // @Param id path int true "脚本ID" // @Param request body models.ExecuteScriptRequest true "执行请求" // @Success 200 {object} models.Response{data=models.TaskExecution} // @Router /scripts/{id}/execute [post] func (h *AutomationHandler) ExecuteScript(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var script models.Script if err := h.db.First(&script, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{ Code: 404, Message: "脚本不存在", }) return } // The request body is optional — when omitted we execute locally. var req models.ExecuteScriptRequest _ = c.ShouldBindJSON(&req) now := time.Now() execution := models.TaskExecution{ TaskType: "script", TaskID: uint(id), TaskName: script.Name, Status: "running", TargetType: req.TargetType, StartedAt: &now, } if err := h.db.Create(&execution).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "创建执行记录失败", }) return } // Execute locally on the AIOps host. output, exitCode, duration := runLocal(script.Type, script.Content, script.Timeout) finishedAt := time.Now() status := "success" if exitCode != 0 { status = "failed" } h.db.Model(&execution).Updates(map[string]interface{}{ "status": status, "output": output, "duration": duration, "finished_at": finishedAt, }) h.db.Model(&script).UpdateColumn("run_count", script.RunCount+1) execution.Output = output execution.Status = status execution.Duration = duration execution.FinishedAt = &finishedAt msg := "执行成功" if status == "failed" { msg = "执行失败" } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: msg, Data: execution, }) } // ListExecutions 获取任务执行记录列表 // @Summary 获取任务执行记录 // @Description 获取脚本/定时任务的执行历史记录(支持 task_type / task_id 筛选) // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param page query int false "页码" // @Param page_size query int false "每页数量" // @Param task_type query string false "任务类型 (cron/script)" // @Param task_id query string false "任务ID" // @Success 200 {object} models.Response{data=models.ListResponse} // @Router /task-executions [get] func (h *AutomationHandler) ListExecutions(c *gin.Context) { var query models.PageQuery c.ShouldBindQuery(&query) db := h.db.Model(&models.TaskExecution{}) if taskType := c.Query("task_type"); taskType != "" { db = db.Where("task_type = ?", taskType) } if taskID := c.Query("task_id"); taskID != "" { db = db.Where("task_id = ?", taskID) } var total int64 db.Count(&total) var execs []models.TaskExecution db.Order("created_at DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&execs) c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", Data: models.ListResponse{ List: execs, Total: total, }, }) } // runPlaybookLocal executes an Ansible playbook YAML locally on the AIOps host. // It writes the content to a temporary file and invokes `ansible-playbook` // against localhost with a local connection. If the ansible-playbook binary is // missing the exec error is captured in the output. Returns combined // stdout+stderr, the exit code (-1 on failure) and duration in milliseconds. func runPlaybookLocal(content, inventory string, timeoutSec int) (output string, exitCode int, durationMs int) { start := time.Now() timeout := time.Duration(timeoutSec) * time.Second if timeout <= 0 { timeout = 600 * time.Second } // Write the playbook YAML to a temporary file. tmpFile, err := os.CreateTemp("", "ezops-playbook-*.yml") if err != nil { return fmt.Sprintf("创建临时文件失败: %v", err), -1, 0 } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(content); err != nil { tmpFile.Close() return fmt.Sprintf("写入临时文件失败: %v", err), -1, 0 } tmpFile.Close() // Write an optional inventory file. inventoryArg := "localhost," if inventory != "" { invFile, err := os.CreateTemp("", "ezops-inventory-*.ini") if err == nil { if _, werr := invFile.WriteString(inventory); werr == nil { inventoryArg = invFile.Name() defer os.Remove(invFile.Name()) } invFile.Close() } } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() args := []string{"-i", inventoryArg} if inventoryArg == "localhost," { args = append(args, "--connection=local") } args = append(args, tmpFile.Name()) cmd := exec.CommandContext(ctx, "ansible-playbook", args...) var out bytes.Buffer cmd.Stdout = &out cmd.Stderr = &out runErr := cmd.Run() durationMs = int(time.Since(start).Milliseconds()) output = out.String() if ctx.Err() == context.DeadlineExceeded { output += fmt.Sprintf("\n[超时] 执行超过 %d 秒被终止", timeoutSec) return output, -1, durationMs } if runErr != nil { if exitError, ok := runErr.(*exec.ExitError); ok { exitCode = exitError.ExitCode() } else { // Binary not found or other exec error (e.g. ansible-playbook not // installed). Provide a clear hint instead of an empty output. exitCode = -1 if output == "" { output = fmt.Sprintf("无法执行 ansible-playbook:%v\n(请确认 AIOps 主机已安装 ansible:yum install -y ansible / pip install ansible)", runErr) } } return output, exitCode, durationMs } return output, 0, durationMs } // ListPlaybooks 获取Playbook列表 // @Summary 获取Playbook列表 // @Description 获取所有Ansible Playbook列表 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param page query int false "页码" // @Param page_size query int false "每页数量" // @Success 200 {object} models.Response{data=models.ListResponse} // @Router /playbooks [get] func (h *AutomationHandler) ListPlaybooks(c *gin.Context) { var query models.PageQuery c.ShouldBindQuery(&query) var total int64 h.db.Model(&models.Playbook{}).Count(&total) var playbooks []models.Playbook h.db.Order("created_at DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&playbooks) c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", Data: models.ListResponse{ List: playbooks, Total: total, }, }) } // GetPlaybook 获取Playbook详情 // @Summary 获取Playbook详情 // @Description 获取指定Playbook的详细信息 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "Playbook ID" // @Success 200 {object} models.Response{data=models.Playbook} // @Router /playbooks/{id} [get] func (h *AutomationHandler) GetPlaybook(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var playbook models.Playbook if err := h.db.First(&playbook, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{ Code: 404, Message: "Playbook不存在", }) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "success", Data: playbook, }) } // CreatePlaybook 创建Playbook // @Summary 创建Playbook // @Description 创建新的Ansible Playbook // @Tags 自动化运维 // @Accept json // @Produce json // @Security Bearer // @Param request body models.CreatePlaybookRequest true "创建请求" // @Success 200 {object} models.Response{data=models.Playbook} // @Router /playbooks [post] func (h *AutomationHandler) CreatePlaybook(c *gin.Context) { var req models.CreatePlaybookRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{ Code: 400, Message: "请求参数错误: " + err.Error(), }) return } playbook := models.Playbook{ Name: req.Name, Code: randomCode("pb"), Description: req.Description, Content: req.Content, Category: req.Category, Tags: req.Tags, Variables: req.Variables, Inventory: req.Inventory, Timeout: req.Timeout, } if playbook.Category == "" { playbook.Category = "deployment" } if playbook.Timeout == 0 { playbook.Timeout = 600 } if err := h.db.Create(&playbook).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "创建失败: " + err.Error(), }) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "创建成功", Data: playbook, }) } // UpdatePlaybook 更新Playbook // @Summary 更新Playbook // @Description 更新指定Playbook // @Tags 自动化运维 // @Accept json // @Produce json // @Security Bearer // @Param id path int true "Playbook ID" // @Param request body models.CreatePlaybookRequest true "更新请求" // @Success 200 {object} models.Response{data=models.Playbook} // @Router /playbooks/{id} [put] func (h *AutomationHandler) UpdatePlaybook(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var req models.CreatePlaybookRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{ Code: 400, Message: "请求参数错误: " + err.Error(), }) return } var playbook models.Playbook if err := h.db.First(&playbook, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{ Code: 404, Message: "Playbook不存在", }) return } updates := map[string]interface{}{ "name": req.Name, "description": req.Description, "content": req.Content, "category": req.Category, "tags": req.Tags, "variables": req.Variables, "inventory": req.Inventory, "timeout": req.Timeout, } if err := h.db.Model(&playbook).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "更新失败", }) return } h.db.First(&playbook, id) c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "更新成功", Data: playbook, }) } // DeletePlaybook 删除Playbook // @Summary 删除Playbook // @Description 删除指定Playbook // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "Playbook ID" // @Success 200 {object} models.Response // @Router /playbooks/{id} [delete] func (h *AutomationHandler) DeletePlaybook(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if err := h.db.Delete(&models.Playbook{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "删除失败", }) return } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: "删除成功", }) } // ExecutePlaybook 执行Playbook // @Summary 执行Playbook // @Description 在AIOps主机上本地执行Ansible Playbook // @Tags 自动化运维 // @Accept json // @Produce json // @Security Bearer // @Param id path int true "Playbook ID" // @Success 200 {object} models.Response{data=models.TaskExecution} // @Router /playbooks/{id}/execute [post] func (h *AutomationHandler) ExecutePlaybook(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var playbook models.Playbook if err := h.db.First(&playbook, id).Error; err != nil { c.JSON(http.StatusNotFound, models.Response{ Code: 404, Message: "Playbook不存在", }) return } now := time.Now() execution := models.TaskExecution{ TaskType: "playbook", TaskID: uint(id), TaskName: playbook.Name, Status: "running", StartedAt: &now, } if err := h.db.Create(&execution).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, Message: "创建执行记录失败", }) return } // Execute the playbook locally on the AIOps host. output, exitCode, duration := runPlaybookLocal(playbook.Content, playbook.Inventory, playbook.Timeout) finishedAt := time.Now() status := "success" if exitCode != 0 { status = "failed" } h.db.Model(&execution).Updates(map[string]interface{}{ "status": status, "output": output, "duration": duration, "finished_at": finishedAt, }) h.db.Model(&playbook).UpdateColumn("run_count", playbook.RunCount+1) execution.Output = output execution.Status = status execution.Duration = duration execution.FinishedAt = &finishedAt msg := "执行成功" if status == "failed" { msg = "执行失败" } c.JSON(http.StatusOK, models.Response{ Code: 200, Message: msg, Data: execution, }) }