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:
1 parent
9af0685e90
commit
c9b6647831
4 files changed
+528
-6
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user