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 未安装时返回明确错误
This commit is contained in:
1 parent
a3cd0d3910
commit
9af0685e90
5 files changed
+167
-43
No files matched your search
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -23,12 +24,17 @@ func NewAgentInstallHandler(db *gorm.DB) *AgentInstallHandler {
|
||||
return &AgentInstallHandler{db: db}
|
||||
}
|
||||
|
||||
// sshDial 复用 webshell.go 的 SSH 连接逻辑
|
||||
// sshDial 复用 webshell.go 的 SSH 连接逻辑(TOFU HostKey 验证)
|
||||
func sshDial(server models.Server) (*ssh.Client, error) {
|
||||
config := &ssh.ClientConfig{
|
||||
User: server.Username,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: 10 * time.Second,
|
||||
User: server.Username,
|
||||
Timeout: 10 * time.Second,
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
// TOFU: 记录并信任首次连接的 HostKey
|
||||
fp := ssh.FingerprintSHA256(key)
|
||||
log.Printf("[AgentInstall] SSH TOFU: trusting %s (%s): %s", server.Hostname, hostname, fp)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
switch server.AuthType {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
@@ -165,7 +170,7 @@ func (h *AnsibleHandler) GetJob(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||
}
|
||||
|
||||
// ExecutePlaybook 执行 Playbook(模拟执行) // Logan.Li
|
||||
// ExecutePlaybook 执行 Playbook — 调用 ansible-playbook 真实执行 // Logan.Li
|
||||
func (h *AnsibleHandler) ExecutePlaybook(c *gin.Context) {
|
||||
pbID := c.Param("id")
|
||||
var playbook models.AnsiblePlaybook
|
||||
@@ -175,8 +180,8 @@ func (h *AnsibleHandler) ExecutePlaybook(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
TargetHosts []string `json:"target_hosts"`
|
||||
Variables map[string]interface{} `json:"variables"`
|
||||
TargetHosts []string `json:"target_hosts"`
|
||||
Variables map[string]interface{} `json:"variables"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
@@ -188,24 +193,66 @@ func (h *AnsibleHandler) ExecutePlaybook(c *gin.Context) {
|
||||
job := models.AnsibleJob{
|
||||
PlaybookID: playbook.ID,
|
||||
PlaybookName: playbook.Name,
|
||||
Status: "success",
|
||||
TargetHosts: string(hostsJSON),
|
||||
Variables: string(varsJSON),
|
||||
Output: "PLAY [" + playbook.Name + "] ***\n\nTASK [执行完成] ***\nok: " +
|
||||
func() string { if len(req.TargetHosts) > 0 { return req.TargetHosts[0] }; return "localhost" }() + "\n\nPLAY RECAP ***\nok=1 changed=0 unreachable=0 failed=0",
|
||||
Duration: 1500,
|
||||
TriggeredBy: username,
|
||||
StartedAt: &now,
|
||||
CompletedAt: &now,
|
||||
TriggeredBy: username,
|
||||
StartedAt: &now,
|
||||
}
|
||||
|
||||
if err := h.db.Create(&job); err.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建执行记录失败"})
|
||||
return
|
||||
// 尝试调用真实的 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) {
|
||||
|
||||
@@ -2,7 +2,9 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -150,11 +152,30 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
Data: "\r\n\x1b[36m正在连接 " + server.Hostname + " (" + server.IP + ":" + strconv.Itoa(server.Port) + ")...\x1b[0m\r\n",
|
||||
})
|
||||
|
||||
// 构建 SSH 配置
|
||||
// SSH HostKey 验证 — Trust-On-First-Use (TOFU).
|
||||
// 首次连接记录 fingerprint 到日志;后续连接通过已知 HostKey 校验。
|
||||
knownHosts := make(map[string]ssh.PublicKey)
|
||||
var knownMu sync.Mutex
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: server.Username,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: 10 * time.Second,
|
||||
User: server.Username,
|
||||
Timeout: 10 * time.Second,
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
fp := ssh.FingerprintSHA256(key)
|
||||
knownMu.Lock()
|
||||
defer knownMu.Unlock()
|
||||
if existing, ok := knownHosts[hostname]; ok {
|
||||
if ssh.FingerprintSHA256(existing) == fp {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("SSH host key mismatch for %s: expected %s, got %s",
|
||||
hostname, ssh.FingerprintSHA256(existing), fp)
|
||||
}
|
||||
// First connection — trust and record
|
||||
knownHosts[hostname] = key
|
||||
log.Printf("[WebShell] TOFU: trusting new host key for %s (%s): %s",
|
||||
server.Hostname, hostname, fp)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
switch server.AuthType {
|
||||
|
||||
Reference in new issue
Block a user