告警中心 - 新增统一告警(alert_unified)、监控告警规则(monitor_alert_rule)、指标目录(alert_metric_catalog) - 通知策略与消息分发(alert_notify_policy/dispatch/message)、静默规则 - 告警引擎按阈值/SNMP 拆分(alert_engine_monitor/snmp) 监控采集 - SNMP 常量/模板与历史指标(snmp templates/constants、snmp_metrics_history、snmp_collect) - 中间件监控 Redis 采集与种子(middleware_monitor_redis/seed) - 站点探测 pkg(siteprobe) + 站点视图(site_view) 自动化任务 - cron 调度器(cronscheduler)、脚本运行器(scriptrunner)、系统任务(systask)及运行记录(cron_job_run、system_task) 审计与凭证 - 审计设置(audit_setting) + 历史清理(auditcleaner) 前端 - 监控页面(host/middleware/site/snmp)拆分为子模块并大屏化 - 告警中心多 Tab(规则/指标目录/通知策略/静默)、任务管理组件重构、Agent CLI Markdown 渲染 - 移除旧脚本卡片/详情/日志组件,统一为 ScriptTable/ScriptCodeEditor/TerminalOutput Co-Authored-By: Claude <noreply@anthropic.com>
382 lines
9.3 KiB
Go
382 lines
9.3 KiB
Go
package controller
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"time"
|
||
|
||
"ezops/middleware"
|
||
"ezops/model"
|
||
"ezops/pkg/scriptrunner"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// GetScriptRuntime 获取本机脚本运行时能力(Shell/Python/PowerShell) // Logan.Li
|
||
func (h *AutomationHandler) GetScriptRuntime(c *gin.Context) {
|
||
rt := scriptrunner.DetectRuntime()
|
||
c.JSON(http.StatusOK, model.Response{
|
||
Code: 200,
|
||
Message: "success",
|
||
Data: rt,
|
||
})
|
||
}
|
||
|
||
// ListScripts 获取脚本列表
|
||
// @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 /scripts [get]
|
||
func (h *AutomationHandler) ListScripts(c *gin.Context) {
|
||
var query model.PageQuery
|
||
c.ShouldBindQuery(&query)
|
||
|
||
var total int64
|
||
h.db.Model(&model.Script{}).Count(&total)
|
||
|
||
var scripts []model.Script
|
||
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&scripts)
|
||
|
||
c.JSON(http.StatusOK, model.Response{
|
||
Code: 200,
|
||
Message: "success",
|
||
Data: model.ListResponse{
|
||
List: scripts,
|
||
Total: total,
|
||
},
|
||
})
|
||
}
|
||
|
||
// GetScript 获取脚本详情
|
||
// @Summary 获取脚本详情
|
||
// @Description 获取指定脚本的详细信息
|
||
// @Tags 自动化运维
|
||
// @Produce json
|
||
// @Security Bearer
|
||
// @Param id path int true "脚本ID"
|
||
// @Success 200 {object} model.Response{data=model.Script}
|
||
// @Router /scripts/{id} [get]
|
||
func (h *AutomationHandler) GetScript(c *gin.Context) {
|
||
id, _ := strconv.Atoi(c.Param("id"))
|
||
|
||
var script model.Script
|
||
if err := h.db.First(&script, 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: script,
|
||
})
|
||
}
|
||
|
||
// CreateScript 创建脚本
|
||
// @Summary 创建脚本
|
||
// @Description 创建新脚本
|
||
// @Tags 自动化运维
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security Bearer
|
||
// @Param request body model.CreateScriptRequest true "创建请求"
|
||
// @Success 200 {object} model.Response{data=model.Script}
|
||
// @Router /scripts [post]
|
||
func (h *AutomationHandler) CreateScript(c *gin.Context) {
|
||
var req model.CreateScriptRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, model.Response{
|
||
Code: 400,
|
||
Message: "请求参数错误: " + err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
script := model.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 script.Code == "" {
|
||
script.Code = randomCode("sc")
|
||
}
|
||
script.CreatedBy = middleware.GetUserID(c)
|
||
|
||
if err := h.db.Create(&script).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{
|
||
Code: 500,
|
||
Message: "创建失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.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 model.CreateScriptRequest true "更新请求"
|
||
// @Success 200 {object} model.Response{data=model.Script}
|
||
// @Router /scripts/{id} [put]
|
||
func (h *AutomationHandler) UpdateScript(c *gin.Context) {
|
||
id, _ := strconv.Atoi(c.Param("id"))
|
||
|
||
var req model.CreateScriptRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, model.Response{
|
||
Code: 400,
|
||
Message: "请求参数错误: " + err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
var script model.Script
|
||
if err := h.db.First(&script, id).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, model.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, model.Response{
|
||
Code: 500,
|
||
Message: "更新失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
h.db.First(&script, id)
|
||
|
||
c.JSON(http.StatusOK, model.Response{
|
||
Code: 200,
|
||
Message: "更新成功",
|
||
Data: script,
|
||
})
|
||
}
|
||
|
||
// DeleteScript 删除脚本
|
||
// @Summary 删除脚本
|
||
// @Description 删除指定脚本
|
||
// @Tags 自动化运维
|
||
// @Produce json
|
||
// @Security Bearer
|
||
// @Param id path int true "脚本ID"
|
||
// @Success 200 {object} model.Response
|
||
// @Router /scripts/{id} [delete]
|
||
func (h *AutomationHandler) DeleteScript(c *gin.Context) {
|
||
id, _ := strconv.Atoi(c.Param("id"))
|
||
|
||
if err := h.db.Delete(&model.Script{}, id).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{
|
||
Code: 500,
|
||
Message: "删除失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.Response{
|
||
Code: 200,
|
||
Message: "删除成功",
|
||
})
|
||
}
|
||
|
||
// BatchDeleteScripts 批量删除脚本
|
||
// @Summary 批量删除脚本
|
||
// @Description 批量删除脚本
|
||
// @Tags 自动化运维
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security Bearer
|
||
// @Param request body model.IDsRequest true "删除请求"
|
||
// @Success 200 {object} model.Response
|
||
// @Router /scripts/batch-delete [post]
|
||
func (h *AutomationHandler) BatchDeleteScripts(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.Script{}, req.IDs).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{
|
||
Code: 500,
|
||
Message: "删除失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.Response{
|
||
Code: 200,
|
||
Message: "删除成功",
|
||
})
|
||
}
|
||
|
||
// ExecuteScript 执行脚本
|
||
// @Summary 执行脚本
|
||
// @Description 在目标服务器上执行脚本
|
||
// @Tags 自动化运维
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security Bearer
|
||
// @Param id path int true "脚本ID"
|
||
// @Param request body model.ExecuteScriptRequest true "执行请求"
|
||
// @Success 200 {object} model.Response{data=model.TaskExecution}
|
||
// @Router /scripts/{id}/execute [post]
|
||
func (h *AutomationHandler) ExecuteScript(c *gin.Context) {
|
||
id, _ := strconv.Atoi(c.Param("id"))
|
||
|
||
var script model.Script
|
||
if err := h.db.First(&script, id).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, model.Response{
|
||
Code: 404,
|
||
Message: "脚本不存在",
|
||
})
|
||
return
|
||
}
|
||
|
||
// The request body is optional — when omitted we execute locally.
|
||
var req model.ExecuteScriptRequest
|
||
_ = c.ShouldBindJSON(&req)
|
||
|
||
now := time.Now()
|
||
execution := model.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, model.Response{
|
||
Code: 500,
|
||
Message: "创建执行记录失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
rt := scriptrunner.DetectRuntime()
|
||
if ok, reason := scriptrunner.CanRun(rt, script.Type); !ok {
|
||
c.JSON(http.StatusBadRequest, model.Response{
|
||
Code: 400,
|
||
Message: reason,
|
||
})
|
||
return
|
||
}
|
||
|
||
// 在 AIOps 本机执行(embed 单二进制 / Docker 容器内)
|
||
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, model.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} model.Response{data=model.ListResponse}
|
||
// @Router /task-executions [get]
|
||
func (h *AutomationHandler) ListExecutions(c *gin.Context) {
|
||
var query model.PageQuery
|
||
c.ShouldBindQuery(&query)
|
||
|
||
db := h.db.Model(&model.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 []model.TaskExecution
|
||
db.Order("created_at DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&execs)
|
||
|
||
c.JSON(http.StatusOK, model.Response{
|
||
Code: 200,
|
||
Message: "success",
|
||
Data: model.ListResponse{
|
||
List: execs,
|
||
Total: total,
|
||
},
|
||
})
|
||
}
|