feat: Ansible 15个运维模板 + 主机匹配 + 远程执行

**种子模板(15个):**
- 系统信息收集、安全更新、磁盘清理、服务巡检
- 用户SSH管理、Nginx配置、SSL证书检查、配置备份
- 进程诊断、Docker清理、防火墙、时间同步
- 日志轮转、内核调优、网络诊断
- 分类覆盖:maintenance/security/configuration/backup/monitoring

**主机匹配 API:**
- GET /api/v1/ansible/match-hosts?pattern=&match_type=regex|prefix|contains
- 支持正则/前缀/模糊匹配 hostname, IP, tags
- 复杂正则回退到 Go 过滤
- 分页支持

**远程执行增强:**
- 指定 target_hosts 时自动查询 SSH 信息生成临时 inventory
- 未指定时回退到 --connection=local
- 自动禁用 StrictHostKeyChecking
This commit is contained in:
attacker committed 2026-06-19 13:31:55 +00:00
1 parent 9af0685e90
commit c9b6647831
4 files changed
+528 -6

No files matched your search

+117 -4
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"os/exec"
"regexp"
"strings"
"time"
@@ -211,12 +212,34 @@ func (h *AnsibleHandler) ExecutePlaybook(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Minute)
defer cancel()
args := []string{tmpFile.Name(), "--connection=local"}
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
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))
}
@@ -312,3 +335,93 @@ func (h *AnsibleHandler) DeleteInventory(c *gin.Context) {
}
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
}
+3
View File
@@ -451,6 +451,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
ansible.POST("/inventories", ansibleHandler.CreateInventory)
ansible.PUT("/inventories/:id", ansibleHandler.UpdateInventory)
ansible.DELETE("/inventories/:id", ansibleHandler.DeleteInventory)
// 主机匹配(资产服务器查询)// Logan.Li
ansible.GET("/match-hosts", ansibleHandler.MatchHosts)
}
// 日志监控路由 // Logan.Li