Files
aiops/backend/internal/api/handlers/ansible.go
T
attacker 9af0685e90 fix: 修复架构分析报告指出的P0问题
**AI 告警分析(P0):**
- 替换 setTimeout 伪AI为真实 LLM 调用 (aiApi.chat)
- LLM 不可用时回退到规则模板(generateAIAnalysis)
- 移除 Math.random() 随机置信度/相似案例
- 分析结果从 LLM JSON 响应解析

**SSH HostKey 验证(P0):**
- webshell.go: InsecureIgnoreHostKey → TOFU 模式
- agent_install.go: 同步修复
- 首次连接记录 SHA256 fingerprint 到日志
- 后续连接校验 HostKey 一致性

**Ansible 执行引擎(P0):**
- ExecutePlaybook 从模拟改为真实 ansible-playbook 调用
- 支持 extra-vars 参数传递
- 5分钟超时保护
- ansible-playbook 未安装时返回明确错误
2026-06-19 11:51:03 +00:00

315 lines
9.7 KiB
Go

package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"strings"
"time"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// AnsibleHandler Ansible 自动化处理器 // Logan.Li
type AnsibleHandler struct {
db *gorm.DB
}
func NewAnsibleHandler(db *gorm.DB) *AnsibleHandler {
return &AnsibleHandler{db: db}
}
// ==================== Playbook ====================
func (h *AnsibleHandler) ListPlaybooks(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
db := h.db.Model(&models.AnsiblePlaybook{})
if name := c.Query("name"); name != "" {
db = db.Where("name LIKE ?", "%"+name+"%")
}
if cat := c.Query("category"); cat != "" {
db = db.Where("category = ?", cat)
}
var total int64
db.Count(&total)
var items []models.AnsiblePlaybook
db.Order("updated_at DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&items)
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: total}})
}
func (h *AnsibleHandler) GetPlaybook(c *gin.Context) {
var item models.AnsiblePlaybook
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "Playbook 不存在"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
}
func (h *AnsibleHandler) CreatePlaybook(c *gin.Context) {
var req struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Content string `json:"content" binding:"required"`
Tags string `json:"tags"`
Category string `json:"category"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
return
}
username := c.GetString("username")
item := models.AnsiblePlaybook{
Name: req.Name, Description: req.Description, Content: req.Content,
Tags: req.Tags, Category: req.Category, CreatedBy: username,
}
if err := h.db.Create(&item); err.Error != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
}
func (h *AnsibleHandler) UpdatePlaybook(c *gin.Context) {
id := c.Param("id")
var item models.AnsiblePlaybook
if err := h.db.First(&item, id); err.Error != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
return
}
var req struct {
Name *string `json:"name"`
Description *string `json:"description"`
Content *string `json:"content"`
Tags *string `json:"tags"`
Category *string `json:"category"`
}
c.ShouldBindJSON(&req)
updates := map[string]interface{}{}
if req.Name != nil { updates["name"] = *req.Name }
if req.Description != nil { updates["description"] = *req.Description }
if req.Content != nil { updates["content"] = *req.Content }
if req.Tags != nil { updates["tags"] = *req.Tags }
if req.Category != nil { updates["category"] = *req.Category }
h.db.Model(&item).Updates(updates)
h.db.First(&item, id)
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
}
func (h *AnsibleHandler) DeletePlaybook(c *gin.Context) {
id := c.Param("id")
if err := h.db.Delete(&models.AnsiblePlaybook{}, id); err.Error != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
}
// BatchDeletePlaybooks 批量删除 Playbook // Logan.Li
func (h *AnsibleHandler) BatchDeletePlaybooks(c *gin.Context) {
var req models.IDsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误"})
return
}
if len(req.IDs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "ids 不能为空"})
return
}
if err := h.db.Delete(&models.AnsiblePlaybook{}, req.IDs).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
}
// ==================== Job ====================
func (h *AnsibleHandler) ListJobs(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
db := h.db.Model(&models.AnsibleJob{})
if status := c.Query("status"); status != "" {
db = db.Where("status = ?", status)
}
if pbID := c.Query("playbook_id"); pbID != "" {
db = db.Where("playbook_id = ?", pbID)
}
var total int64
db.Count(&total)
var items []models.AnsibleJob
db.Order("created_at DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&items)
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: total}})
}
func (h *AnsibleHandler) GetJob(c *gin.Context) {
var item models.AnsibleJob
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
}
// ExecutePlaybook 执行 Playbook — 调用 ansible-playbook 真实执行 // Logan.Li
func (h *AnsibleHandler) ExecutePlaybook(c *gin.Context) {
pbID := c.Param("id")
var playbook models.AnsiblePlaybook
if err := h.db.First(&playbook, pbID); err.Error != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "Playbook 不存在"})
return
}
var req struct {
TargetHosts []string `json:"target_hosts"`
Variables map[string]interface{} `json:"variables"`
}
c.ShouldBindJSON(&req)
username := c.GetString("username")
hostsJSON, _ := json.Marshal(req.TargetHosts)
varsJSON, _ := json.Marshal(req.Variables)
now := time.Now()
job := models.AnsibleJob{
PlaybookID: playbook.ID,
PlaybookName: playbook.Name,
TargetHosts: string(hostsJSON),
Variables: string(varsJSON),
TriggeredBy: username,
StartedAt: &now,
}
// 尝试调用真实的 ansible-playbook
content := playbook.Content
if content != "" {
tmpFile, err := os.CreateTemp("", "ansible-playbook-*.yml")
if err == nil {
defer os.Remove(tmpFile.Name())
tmpFile.WriteString(content)
tmpFile.Close()
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Minute)
defer cancel()
args := []string{tmpFile.Name(), "--connection=local"}
// 构建 extra-vars
var extraVars []string
if len(req.TargetHosts) > 0 {
extraVars = append(extraVars, "target_hosts="+strings.Join(req.TargetHosts, ","))
}
for k, v := range req.Variables {
extraVars = append(extraVars, fmt.Sprintf("%s=%v", k, v))
}
if len(extraVars) > 0 {
args = append(args, "--extra-vars", strings.Join(extraVars, " "))
}
cmd := exec.CommandContext(ctx, "ansible-playbook", args...)
output, runErr := cmd.CombinedOutput()
job.Duration = int64(time.Since(now).Milliseconds())
if runErr != nil {
job.Status = "failed"
job.Output = fmt.Sprintf("ansible-playbook 执行失败: %v\n\n%s", runErr, string(output))
} else {
job.Status = "success"
job.Output = string(output)
}
job.CompletedAt = timePtr(time.Now())
h.db.Create(&job)
c.JSON(http.StatusOK, models.Response{Code: 200, Data: job})
return
}
}
// Fallback: ansible-playbook 不可用或无内容 → 返回错误
job.Status = "failed"
job.Output = "无法执行:ansible-playbook 未安装或 Playbook 内容为空。\n请在 AIOps 主机执行:apt install ansible / yum install ansible"
job.Duration = 0
job.CompletedAt = timePtr(time.Now())
h.db.Create(&job)
c.JSON(http.StatusOK, models.Response{Code: 200, Data: job})
}
func timePtr(t time.Time) *time.Time { return &t }
// ==================== Inventory ====================
func (h *AnsibleHandler) ListInventories(c *gin.Context) {
var items []models.AnsibleInventory
h.db.Order("updated_at DESC").Find(&items)
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: int64(len(items))}})
}
func (h *AnsibleHandler) CreateInventory(c *gin.Context) {
var req struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Hosts interface{} `json:"hosts"` // 数组
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
return
}
hostsJSON, _ := json.Marshal(req.Hosts)
item := models.AnsibleInventory{
Name: req.Name, Description: req.Description,
Hosts: string(hostsJSON), CreatedBy: c.GetString("username"),
}
h.db.Create(&item)
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
}
func (h *AnsibleHandler) UpdateInventory(c *gin.Context) {
id := c.Param("id")
var item models.AnsibleInventory
if err := h.db.First(&item, id); err.Error != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
return
}
var req struct {
Name *string `json:"name"`
Description *string `json:"description"`
Hosts interface{} `json:"hosts"`
}
c.ShouldBindJSON(&req)
updates := map[string]interface{}{}
if req.Name != nil { updates["name"] = *req.Name }
if req.Description != nil { updates["description"] = *req.Description }
if req.Hosts != nil {
hostsJSON, _ := json.Marshal(req.Hosts)
updates["hosts"] = string(hostsJSON)
}
h.db.Model(&item).Updates(updates)
h.db.First(&item, id)
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
}
func (h *AnsibleHandler) DeleteInventory(c *gin.Context) {
if err := h.db.Delete(&models.AnsibleInventory{}, c.Param("id")); err.Error != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
}