package controller import ( "fmt" "net/http" "strconv" "time" "ezops/model" "ezops/pkg/remoteexec" "github.com/gin-gonic/gin" ) // ListCronJobs 获取定时任务列表 // @Summary 获取定时任务列表 // @Description 获取所有定时任务列表 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param page query int false "页码" // @Param page_size query int false "每页数量" // @Success 200 {object} model.Response{data=model.ListResponse} // @Router /cron-jobs [get] func (h *AutomationHandler) ListCronJobs(c *gin.Context) { var query model.PageQuery c.ShouldBindQuery(&query) var total int64 h.db.Model(&model.CronJob{}).Count(&total) var jobs []model.CronJob h.db.Preload("Script").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&jobs) c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", Data: model.ListResponse{ List: jobs, Total: total, }, }) } // GetCronJob 获取定时任务详情 // @Summary 获取定时任务详情 // @Description 获取指定定时任务的详细信息 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "任务ID" // @Success 200 {object} model.Response{data=model.CronJob} // @Router /cron-jobs/{id} [get] func (h *AutomationHandler) GetCronJob(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var job model.CronJob if err := h.db.Preload("Script").First(&job, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{ Code: 404, Message: "定时任务不存在", }) return } c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", Data: job, }) } // CreateCronJob 创建定时任务 // @Summary 创建定时任务 // @Description 创建新定时任务 // @Tags 自动化运维 // @Accept json // @Produce json // @Security Bearer // @Param request body model.CreateCronJobRequest true "创建请求" // @Success 200 {object} model.Response{data=model.CronJob} // @Router /cron-jobs [post] func (h *AutomationHandler) CreateCronJob(c *gin.Context) { var req model.CreateCronJobRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{ Code: 400, Message: "请求参数错误: " + err.Error(), }) return } job := model.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, model.Response{ Code: 500, Message: "创建失败", }) return } c.JSON(http.StatusOK, model.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 model.CreateCronJobRequest true "更新请求" // @Success 200 {object} model.Response{data=model.CronJob} // @Router /cron-jobs/{id} [put] func (h *AutomationHandler) UpdateCronJob(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var req model.CreateCronJobRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{ Code: 400, Message: "请求参数错误: " + err.Error(), }) return } var job model.CronJob if err := h.db.First(&job, id).Error; err != nil { c.JSON(http.StatusNotFound, model.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, model.Response{ Code: 500, Message: "更新失败", }) return } h.db.Preload("Script").First(&job, id) c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "更新成功", Data: job, }) } // DeleteCronJob 删除定时任务 // @Summary 删除定时任务 // @Description 删除指定定时任务 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "任务ID" // @Success 200 {object} model.Response // @Router /cron-jobs/{id} [delete] func (h *AutomationHandler) DeleteCronJob(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if err := h.db.Delete(&model.CronJob{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{ Code: 500, Message: "删除失败", }) return } c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "删除成功", }) } // BatchDeleteCronJobs 批量删除定时任务 // @Summary 批量删除定时任务 // @Description 批量删除定时任务 // @Tags 自动化运维 // @Accept json // @Produce json // @Security Bearer // @Param request body model.IDsRequest true "删除请求" // @Success 200 {object} model.Response // @Router /cron-jobs/batch-delete [delete] func (h *AutomationHandler) BatchDeleteCronJobs(c *gin.Context) { var req model.IDsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{ Code: 400, Message: "请求参数错误", }) return } if err := h.db.Delete(&model.CronJob{}, req.IDs).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{ Code: 500, Message: "删除失败", }) return } c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "删除成功", }) } // ToggleCronJob 切换定时任务状态 // @Summary 切换定时任务状态 // @Description 启用或禁用定时任务 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "任务ID" // @Success 200 {object} model.Response{data=model.CronJob} // @Router /cron-jobs/{id}/toggle [post] func (h *AutomationHandler) ToggleCronJob(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var job model.CronJob if err := h.db.First(&job, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{ Code: 404, Message: "定时任务不存在", }) return } job.Enabled = !job.Enabled if err := h.db.Save(&job).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{ Code: 500, Message: "操作失败", }) return } c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "操作成功", Data: job, }) } // ExecuteCronJob 执行定时任务 // @Summary 执行定时任务 // @Description 手动执行定时任务 // @Tags 自动化运维 // @Produce json // @Security Bearer // @Param id path int true "任务ID" // @Success 200 {object} model.Response{data=model.TaskExecution} // @Router /cron-jobs/{id}/execute [post] func (h *AutomationHandler) ExecuteCronJob(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var job model.CronJob if err := h.db.First(&job, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{ Code: 404, Message: "定时任务不存在", }) return } // Resolve the content to execute. For type=script, load the linked Script. content := job.Content scriptType := job.Type // shell | python | command | script if scriptType == "command" { scriptType = "shell" // one-liner commands run via bash } if job.Type == "script" && job.ScriptID != nil { var script model.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 := model.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, model.Response{ Code: 500, Message: "创建执行记录失败", }) return } var output string var exitCode int var duration int status := "success" // 根据 TargetType 决定执行方式 // Logan.Li if job.TargetType != "" && job.TargetType != "local" && job.Targets != "" { // 远程目标执行 — 通过 SSH 并发执行 remoteResults, remoteErr := ExecuteCronOnTargets(h.db, job, scriptType, content, job.Timeout) duration = int(time.Since(now).Milliseconds()) if remoteErr != nil { output = fmt.Sprintf("远程执行调度失败: %v", remoteErr) exitCode = -1 status = "failed" } else { // 格式化远程执行结果 output = remoteexec.FormatExecResults(remoteResults) // 任意一台失败则整体标记为 failed for _, r := range remoteResults { if !r.Success { status = "failed" exitCode = -1 break } } } } else { // 本地执行 output, exitCode, duration = runLocal(scriptType, content, job.Timeout) if exitCode != 0 { status = "failed" } } finishedAt := time.Now() 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, model.Response{ Code: 200, Message: msg, Data: execution, }) }