Files
aiops/backend/internal/api/handlers/automation_playbook.go
T
attacker a7e2351ad2 feat: 定时任务编辑入口全栈打通 + 修复ApprovalFlow编译错误 + 完成大文件拆分重构
- CronJobManager: 编辑按钮接通 onClick → openEdit(job),弹出编辑对话框预填数据,
  复用新建/编辑共用 Dialog(editingId 区分模式),标题/按钮文案动态切换,
  保存时根据 editingId 调用 createJob 或 updateJob — 消除占位按钮
- ApprovalFlow: 修复 API 方法名不匹配导致 tsc -b 编译失败
  (getInstances→getProcesses, getMyApprovals→getProcesses({status:'pending'}),
   approveInstance→approveProcess, rejectInstance→rejectProcess)
  + 添加 ApprovalFlowManager 导出别名兼容 App.tsx 导入
- 合并已完成的大文件拆分重构: automation.go→automation_cron/script/playbook,
  ldap.go→ldap_ou/ldap_users, server.go→cloud_accounts/domain/platform/region,
  user.go→user_admin; 前端 Dashboard/K8s/Server/Approval/Monitor 子组件拆分
  // Logan.Li
2026-06-15 01:09:49 +08:00

359 lines
9.3 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"
"fmt"
"net/http"
"os"
"os/exec"
"strconv"
"time"
"ezops/internal/models"
"github.com/gin-gonic/gin"
)
// 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,
})
}