feat: 脚本/定时任务真实执行+输出捕获+执行历史 — 替换pending空壳为本地执行,采集stdout/stderr,更新统计;修复CronJobManager/Automation字段名不匹配(status→enabled,cron→cron_expr,command→content,language→type)
This commit is contained in:
1 parent
85bc2a8e05
commit
a4fd5fc178
19 files changed
+2715
-114
No files matched your search
@@ -0,0 +1,220 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AgentTaskHandler Agent 远程任务管理 // Logan.Li
|
||||
type AgentTaskHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAgentTaskHandler(db *gorm.DB) *AgentTaskHandler {
|
||||
return &AgentTaskHandler{db: db}
|
||||
}
|
||||
|
||||
// CreateTask 下发远程任务(前端调用,需 JWT)
|
||||
// POST /api/v1/agent/tasks
|
||||
func (h *AgentTaskHandler) CreateTask(c *gin.Context) {
|
||||
var req struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Type string `json:"type"` // command / script
|
||||
Command string `json:"command"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Hostname == "" || req.Command == "" {
|
||||
c.JSON(400, models.Response{Code: 400, Message: "hostname 和 command 不能为空"})
|
||||
return
|
||||
}
|
||||
if req.Timeout == 0 {
|
||||
req.Timeout = 60
|
||||
}
|
||||
if req.Type == "" {
|
||||
req.Type = "command"
|
||||
}
|
||||
|
||||
username, _ := c.Get("username")
|
||||
|
||||
task := models.AgentTask{
|
||||
TaskID: uuid.New().String(),
|
||||
Hostname: req.Hostname,
|
||||
Type: req.Type,
|
||||
Command: req.Command,
|
||||
Timeout: req.Timeout,
|
||||
Status: "pending",
|
||||
CreatedBy: fmt.Sprintf("%v", username),
|
||||
}
|
||||
|
||||
if err := h.db.Create(&task).Error; err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, models.Response{Code: 200, Message: "ok", Data: task})
|
||||
}
|
||||
|
||||
// PollTasks Agent 轮询拉取任务(Agent 调用,无需 JWT)
|
||||
// GET /api/v1/agent/tasks/poll?hostname=xxx
|
||||
func (h *AgentTaskHandler) PollTasks(c *gin.Context) {
|
||||
hostname := c.Query("hostname")
|
||||
if hostname == "" {
|
||||
c.JSON(400, models.Response{Code: 400, Message: "hostname 不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 查找该主机 pending 的任务,按时间排序,最多取 5 个
|
||||
var tasks []models.AgentTask
|
||||
h.db.Where("hostname = ? AND status = ?", hostname, "pending").
|
||||
Order("created_at ASC").
|
||||
Limit(5).
|
||||
Find(&tasks)
|
||||
|
||||
// 标记为 running(防其他 agent 重复拉取)
|
||||
now := time.Now()
|
||||
for _, t := range tasks {
|
||||
h.db.Model(&models.AgentTask{}).Where("id = ? AND status = ?", t.ID, "pending").
|
||||
Updates(map[string]interface{}{"status": "running", "picked_at": now})
|
||||
}
|
||||
|
||||
c.JSON(200, models.Response{Code: 200, Data: tasks})
|
||||
}
|
||||
|
||||
// SubmitResult Agent 回传执行结果(Agent 调用,无需 JWT)
|
||||
// POST /api/v1/agent/tasks/:task_id/result
|
||||
func (h *AgentTaskHandler) SubmitResult(c *gin.Context) {
|
||||
taskID := c.Param("task_id")
|
||||
|
||||
var req struct {
|
||||
ExitCode int `json:"exit_code"`
|
||||
Result string `json:"result"`
|
||||
Status string `json:"status"` // success / failed / timeout
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, models.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"exit_code": req.ExitCode,
|
||||
"result": req.Result,
|
||||
"status": req.Status,
|
||||
"finished_at": now,
|
||||
}
|
||||
|
||||
if err := h.db.Model(&models.AgentTask{}).Where("task_id = ?", taskID).Updates(updates).Error; err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "更新失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, models.Response{Code: 200, Message: "ok"})
|
||||
}
|
||||
|
||||
// ListTasks 查询任务列表(前端调用,需 JWT)
|
||||
// GET /api/v1/agent/tasks?hostname=xxx&status=xxx
|
||||
func (h *AgentTaskHandler) ListTasks(c *gin.Context) {
|
||||
query := h.db.Model(&models.AgentTask{})
|
||||
|
||||
if hostname := c.Query("hostname"); hostname != "" {
|
||||
query = query.Where("hostname = ?", hostname)
|
||||
}
|
||||
if status := c.Query("status"); status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
var tasks []models.AgentTask
|
||||
query.Order("created_at DESC").Limit(200).Find(&tasks)
|
||||
|
||||
c.JSON(200, models.Response{Code: 200, Data: tasks})
|
||||
}
|
||||
|
||||
// GetTask 获取单个任务详情
|
||||
// GET /api/v1/agent/tasks/:id
|
||||
func (h *AgentTaskHandler) GetTask(c *gin.Context) {
|
||||
var task models.AgentTask
|
||||
if err := h.db.First(&task, c.Param("id")).Error; err != nil {
|
||||
c.JSON(404, models.Response{Code: 404, Message: "任务不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: task})
|
||||
}
|
||||
|
||||
// ===== Agent 告警规则 =====
|
||||
|
||||
// ListRules GET /api/v1/agent/rules
|
||||
func (h *AgentTaskHandler) ListRules(c *gin.Context) {
|
||||
var rules []models.AgentRule
|
||||
h.db.Order("created_at DESC").Find(&rules)
|
||||
c.JSON(200, models.Response{Code: 200, Data: rules})
|
||||
}
|
||||
|
||||
// CreateRule POST /api/v1/agent/rules
|
||||
func (h *AgentTaskHandler) CreateRule(c *gin.Context) {
|
||||
var rule models.AgentRule
|
||||
if err := c.ShouldBindJSON(&rule); err != nil {
|
||||
c.JSON(400, models.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
rule.ID = 0
|
||||
rule.Enabled = true
|
||||
if err := h.db.Create(&rule).Error; err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: rule})
|
||||
}
|
||||
|
||||
// UpdateRule PUT /api/v1/agent/rules/:id
|
||||
func (h *AgentTaskHandler) UpdateRule(c *gin.Context) {
|
||||
var rule models.AgentRule
|
||||
if err := h.db.First(&rule, c.Param("id")).Error; err != nil {
|
||||
c.JSON(404, models.Response{Code: 404, Message: "规则不存在"})
|
||||
return
|
||||
}
|
||||
var update models.AgentRule
|
||||
if err := c.ShouldBindJSON(&update); err != nil {
|
||||
c.JSON(400, models.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
h.db.Model(&rule).Updates(update)
|
||||
c.JSON(200, models.Response{Code: 200, Data: rule})
|
||||
}
|
||||
|
||||
// DeleteRule DELETE /api/v1/agent/rules/:id
|
||||
func (h *AgentTaskHandler) DeleteRule(c *gin.Context) {
|
||||
h.db.Delete(&models.AgentRule{}, c.Param("id"))
|
||||
c.JSON(200, models.Response{Code: 200, Message: "ok"})
|
||||
}
|
||||
|
||||
// ListAlerts GET /api/v1/agent/alerts
|
||||
func (h *AgentTaskHandler) ListAlerts(c *gin.Context) {
|
||||
query := h.db.Model(&models.AgentAlert{})
|
||||
if c.Query("resolved") == "false" {
|
||||
query = query.Where("resolved = ?", false)
|
||||
}
|
||||
if hostname := c.Query("hostname"); hostname != "" {
|
||||
query = query.Where("hostname = ?", hostname)
|
||||
}
|
||||
var alerts []models.AgentAlert
|
||||
query.Order("created_at DESC").Limit(200).Find(&alerts)
|
||||
c.JSON(200, models.Response{Code: 200, Data: alerts})
|
||||
}
|
||||
|
||||
// ResolveAlert POST /api/v1/agent/alerts/:id/resolve
|
||||
func (h *AgentTaskHandler) ResolveAlert(c *gin.Context) {
|
||||
now := time.Now()
|
||||
h.db.Model(&models.AgentAlert{}).Where("id = ?", c.Param("id")).
|
||||
Updates(map[string]interface{}{"resolved": true, "resolved_at": now})
|
||||
c.JSON(200, models.Response{Code: 200, Message: "ok"})
|
||||
}
|
||||
Reference in new issue
Block a user