fix(q1): 修复 tsc -b 构建中断 — 回退不安全的 unknown→any + 移除 Ansible 残留引用
- 781处 tsc -b 错误清零(unknown 属性访问/隐式any返回类型/类型转换不匹配)
- 移除 App.tsx 中已删除的 AnsibleManager/AnsibleWorkflow 导入和路由
- 修复 DeploymentView 中 _prefix 解构导致的属性不存在错误
- 修复 Header/Sidebar/PipelineEditor 中无效的类型断言
- asList() 返回类型 unknown[]→any[], 恢复 AgentMonitor 正常工作
- 放宽 noImplicitAny(临时), 待 types/ 定义完善后逐步恢复
- ESLint: unused=0, console=0, any=1041(Q1 后续降低)
- npm run build: ✅ 0 errors
This commit is contained in:
1 parent
4dd5e10b46
commit
5364d93ed7
149 files changed
+2021
-3128
No files matched your search
@@ -1,462 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"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: "已删除"})
|
||||
}
|
||||
|
||||
// ClonePlaybook 克隆 Playbook 为自定义模板 // Logan.Li
|
||||
// POST /api/v1/ansible/playbooks/:id/clone
|
||||
func (h *AnsibleHandler) ClonePlaybook(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var src models.AnsiblePlaybook
|
||||
if err := h.db.First(&src, id); err.Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "Playbook 不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name *string `json:"name"` // 可选自定义名称
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
cloneName := src.Name + " (副本)"
|
||||
if req.Name != nil && *req.Name != "" {
|
||||
cloneName = *req.Name
|
||||
}
|
||||
|
||||
clone := models.AnsiblePlaybook{
|
||||
Name: cloneName,
|
||||
Description: src.Description,
|
||||
Content: src.Content,
|
||||
Tags: src.Tags,
|
||||
Category: src.Category,
|
||||
CreatedBy: c.GetString("username"),
|
||||
}
|
||||
if err := h.db.Create(&clone); err.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "克隆失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: clone})
|
||||
}
|
||||
|
||||
// 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()}
|
||||
|
||||
// 根据目标主机决定连接方式
|
||||
if len(req.TargetHosts) > 0 {
|
||||
// 查询目标主机的 SSH 信息,生成临时 inventory
|
||||
var servers []models.Server
|
||||
h.db.Where("hostname IN ? OR ip IN ?", req.TargetHosts, req.TargetHosts).Find(&servers)
|
||||
if len(servers) > 0 {
|
||||
invFile, invErr := os.CreateTemp("", "ansible-inventory-*.ini")
|
||||
if invErr == nil {
|
||||
defer os.Remove(invFile.Name())
|
||||
invFile.WriteString("[targets]\n")
|
||||
for _, s := range servers {
|
||||
sshUser := s.Username
|
||||
if sshUser == "" { sshUser = "root" }
|
||||
invFile.WriteString(fmt.Sprintf("%s ansible_host=%s ansible_user=%s ansible_port=%d ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'\n",
|
||||
s.Hostname, s.IP, sshUser, s.Port))
|
||||
}
|
||||
invFile.Close()
|
||||
args = append(args, "--inventory", invFile.Name())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
args = append(args, "--connection=local")
|
||||
}
|
||||
|
||||
// 构建 extra-vars
|
||||
var extraVars []string
|
||||
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: "已删除"})
|
||||
}
|
||||
|
||||
// ==================== 主机匹配 ====================
|
||||
|
||||
// MatchHostsRequest 主机匹配请求
|
||||
type MatchHostsRequest struct {
|
||||
Pattern string `json:"pattern" form:"pattern"` // 匹配模式(正则或前缀)
|
||||
MatchType string `json:"match_type" form:"match_type"` // "regex", "prefix", "contains"
|
||||
Page int `json:"page" form:"page"`
|
||||
PageSize int `json:"page_size" form:"page_size"`
|
||||
}
|
||||
|
||||
// MatchHosts 预览匹配的主机列表(从资产服务器表查询)// Logan.Li
|
||||
// GET /api/v1/ansible/match-hosts?pattern=web*&match_type=prefix
|
||||
func (h *AnsibleHandler) MatchHosts(c *gin.Context) {
|
||||
var req MatchHostsRequest
|
||||
c.ShouldBindQuery(&req)
|
||||
if req.Page <= 0 { req.Page = 1 }
|
||||
if req.PageSize <= 0 || req.PageSize > 200 { req.PageSize = 50 }
|
||||
|
||||
query := h.db.Model(&models.Server{})
|
||||
|
||||
if req.Pattern != "" {
|
||||
switch req.MatchType {
|
||||
case "regex":
|
||||
// 编译正则表达式
|
||||
re, err := regexp.Compile(req.Pattern)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "正则表达式错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
// MySQL 使用 REGEXP 关键字
|
||||
query = query.Where("hostname REGEXP ? OR ip REGEXP ? OR tags REGEXP ?",
|
||||
req.Pattern, req.Pattern, req.Pattern)
|
||||
// 对于复杂的正则,MySQL 可能不支持;先查全部再 Go 过滤
|
||||
if !isMySQLSafeRegex(req.Pattern) {
|
||||
var all []models.Server
|
||||
h.db.Find(&all)
|
||||
var matched []models.Server
|
||||
for _, s := range all {
|
||||
if re.MatchString(s.Hostname) || re.MatchString(s.IP) || re.MatchString(s.Tags) {
|
||||
matched = append(matched, s)
|
||||
}
|
||||
}
|
||||
total := len(matched)
|
||||
start := (req.Page - 1) * req.PageSize
|
||||
end := start + req.PageSize
|
||||
if start > total { start = total }
|
||||
if end > total { end = total }
|
||||
result := matched[start:end]
|
||||
for i := range result {
|
||||
result[i].HasAuth = result[i].Password != "" || result[i].SSHKey != ""
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: result, Total: int64(total)}})
|
||||
return
|
||||
}
|
||||
case "prefix":
|
||||
query = query.Where("hostname LIKE ? OR ip LIKE ?",
|
||||
req.Pattern+"%", req.Pattern+"%")
|
||||
default: // "contains"
|
||||
query = query.Where("hostname LIKE ? OR ip LIKE ? OR tags LIKE ?",
|
||||
"%"+req.Pattern+"%", "%"+req.Pattern+"%", "%"+req.Pattern+"%")
|
||||
}
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.Server
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
query.Offset(offset).Limit(req.PageSize).Order("hostname").Find(&items)
|
||||
|
||||
// 补充 HasAuth 字段
|
||||
for i := range items {
|
||||
items[i].HasAuth = items[i].Password != "" || items[i].SSHKey != ""
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: total}})
|
||||
}
|
||||
|
||||
// isMySQLSafeRegex 判断正则表达式是否可安全用于 MySQL REGEXP
|
||||
func isMySQLSafeRegex(pat string) bool {
|
||||
// MySQL REGEXP 支持基本正则,但如果包含复杂断言或 Unicode 则不适合
|
||||
unsafe := []string{`\b`, `(?=`, `(?!`, `(?<=`, `(?<!`, `\p{`}
|
||||
for _, u := range unsafe {
|
||||
if strings.Contains(pat, u) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in new issue
Block a user