Files
aiops/backend/internal/api/handlers/automation.go
T
attacker ae6af93360 feat: Ansible Playbook 全栈 CRUD + 真实执行 — 替换mock为后端持久化
后端:
- models/automation.go: Playbook 模型新增 Category/Tags 字段; 新增 CreatePlaybookRequest
- handlers/automation.go: 新增 runPlaybookLocal(写临时YML+ansible-playbook --connection=local执行)
  + randomCode(crypto/rand生成唯一code) + List/Get/Create/Update/Delete/Execute Playbook 完整CRUD
  Execute 创建TaskExecution(task_type=playbook)记录,采集stdout/stderr/退出码/耗时
- routes/routes.go: 注册 /playbooks 路由组(7个端点)

前端:
- services/api.ts: ansibleApi 从mock改为真实 /playbooks 后端调用
- AnsibleManager.tsx 完全重写:
  - Playbook管理: 真实数据卡片列表(id/name/category/tags/run_count/timeout) + 搜索/分类筛选
  - 新建/编辑对话框复用(编辑模式预填数据) — 新增编辑入口(原无编辑功能)
  - 查看内容: 终端风格YAML查看器 + 快捷编辑按钮
  - 执行: ConfirmProvider确认 + ansible-playbook本地执行 + toast结果反馈
  - 执行记录tab: 真实TaskExecution记录(任务名/状态/开始时间/耗时/日志查看)
  - 统计卡片: 总数/执行中/今日成功/今日失败(基于真实执行记录)
  - 移除原inventory占位tab(无后端支持,避免非功能按钮)
  - tags JSON数组与前端string[]互转映射
2026-06-14 23:14:16 +08:00

1068 lines
27 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
})
}