package services import ( "bufio" "bytes" "context" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "regexp" "runtime" "strconv" "strings" "sync" "time" "github.com/ansible-deploy/internal/models" "gopkg.in/yaml.v3" ) // AnsibleService Ansible服务 type AnsibleService struct { config *Config hosts map[string]*models.Host groups map[string]*models.HostGroup inventoryPath string tasks map[string]*models.TaskExecution taskLock sync.RWMutex sshKeys map[string]*models.SSHKey sshKeyDir string startTime time.Time } // NewAnsibleService 创建Ansible服务 func NewAnsibleService(cfg *Config) *AnsibleService { sshKeyDir := filepath.Join(cfg.InventoryDir, "ssh_keys") os.MkdirAll(sshKeyDir, 0700) svc := &AnsibleService{ config: cfg, hosts: make(map[string]*models.Host), groups: make(map[string]*models.HostGroup), inventoryPath: filepath.Join(cfg.InventoryDir, "hosts"), tasks: make(map[string]*models.TaskExecution), sshKeys: make(map[string]*models.SSHKey), sshKeyDir: sshKeyDir, startTime: time.Now(), } svc.groups["all"] = &models.HostGroup{Name: "all", Description: "所有主机"} svc.groups["ungrouped"] = &models.HostGroup{Name: "ungrouped", Description: "未分组主机"} svc.loadHosts() svc.loadGroups() svc.loadSSHKeys() return svc } // ===== SSH密钥管理 ===== func (s *AnsibleService) loadSSHKeys() { files, _ := os.ReadDir(s.sshKeyDir) for _, f := range files { if f.IsDir() || !strings.HasSuffix(f.Name(), ".pem") { continue } name := strings.TrimSuffix(f.Name(), ".pem") path := filepath.Join(s.sshKeyDir, f.Name()) fingerprint := s.getKeyFingerprint(path) info, _ := f.Info() s.sshKeys[name] = &models.SSHKey{ Name: name, Path: path, Fingerprint: fingerprint, CreatedAt: info.ModTime(), } } } func (s *AnsibleService) getKeyFingerprint(path string) string { cmd := exec.Command("ssh-keygen", "-lf", path) out, err := cmd.Output() if err != nil { return "" } fields := strings.Fields(string(out)) if len(fields) >= 2 { return fields[1] } return "" } func (s *AnsibleService) ListSSHKeys() []models.SSHKey { var keys []models.SSHKey for _, k := range s.sshKeys { keys = append(keys, *k) } return keys } func (s *AnsibleService) AddSSHKey(name string, privateKey string) error { if name == "" { return fmt.Errorf("密钥名称不能为空") } if strings.Contains(name, "/") || strings.Contains(name, "..") { return fmt.Errorf("密钥名称包含非法字符") } keyPath := filepath.Join(s.sshKeyDir, name+".pem") if _, err := os.Stat(keyPath); err == nil { return fmt.Errorf("密钥已存在: %s", name) } if err := os.WriteFile(keyPath, []byte(privateKey), 0600); err != nil { return fmt.Errorf("写入密钥失败: %v", err) } fingerprint := s.getKeyFingerprint(keyPath) s.sshKeys[name] = &models.SSHKey{ Name: name, Path: keyPath, Fingerprint: fingerprint, CreatedAt: time.Now(), } return nil } func (s *AnsibleService) DeleteSSHKey(name string) error { keyPath := filepath.Join(s.sshKeyDir, name+".pem") if _, err := os.Stat(keyPath); os.IsNotExist(err) { return fmt.Errorf("密钥不存在: %s", name) } os.Remove(keyPath) delete(s.sshKeys, name) return nil } // resolveSSHKeyPath 解析SSH密钥路径(名称或完整路径) func (s *AnsibleService) resolveSSHKeyPath(keyRef string) string { if keyRef == "" { return "" } // 如果是完整路径,直接返回 if strings.HasPrefix(keyRef, "/") { return keyRef } // 尝试从密钥库查找 if k, ok := s.sshKeys[keyRef]; ok { return k.Path } // 尝试加.pem后缀 keyPath := filepath.Join(s.sshKeyDir, keyRef+".pem") if _, err := os.Stat(keyPath); err == nil { return keyPath } return keyRef } // ===== 文件分发 ===== func (s *AnsibleService) DistributeFile(req models.FileDistributeRequest) (*models.TaskExecution, error) { task := &models.TaskExecution{ ID: s.generateID(), Name: "文件分发 → " + req.DestPath, Hosts: req.Hosts, Status: "running", StartTime: time.Now(), TotalHosts: len(req.Hosts), } s.taskLock.Lock() s.tasks[task.ID] = task s.taskLock.Unlock() go s.runFileDistribute(task, req) return task, nil } func (s *AnsibleService) runFileDistribute(task *models.TaskExecution, req models.FileDistributeRequest) { var sw syncWriter sw.buf = bytes.NewBuffer(nil) for i, hostName := range req.Hosts { host := s.findHostByName(hostName) if host == nil { sw.WriteString(fmt.Sprintf("[%s] ✗ 主机不存在\n", hostName)) s.taskLock.Lock() task.FailedHosts++ task.Progress = i + 1 task.Output = sw.String() s.taskLock.Unlock() continue } start := time.Now() var args []string if req.Content != "" { // 使用copy模块分发内容 args = []string{ host.Name, "-i", s.inventoryPath, "-m", "copy", "-a", fmt.Sprintf("content='%s' dest=%s", strings.ReplaceAll(req.Content, "'", "\\'"), req.DestPath), "-u", host.Username, } } else { // 使用copy模块分发文件 copyArgs := fmt.Sprintf("src=%s dest=%s", req.SourceFile, req.DestPath) if req.Owner != "" { copyArgs += " owner=" + req.Owner } if req.Group != "" { copyArgs += " group=" + req.Group } if req.Mode != "" { copyArgs += " mode=" + req.Mode } args = []string{ host.Name, "-i", s.inventoryPath, "-m", "copy", "-a", copyArgs, "-u", host.Username, } } // 认证 if host.AuthType == "sshkey" && host.SSHKey != "" { keyPath := s.resolveSSHKeyPath(host.SSHKey) args = append(args, "--private-key", keyPath) } else if host.Password != "" { args = append(args, "--extra-vars", fmt.Sprintf("ansible_password=%s", host.Password)) } if host.Port != 0 && host.Port != 22 { args = append(args, "--extra-vars", fmt.Sprintf("ansible_port=%d", host.Port)) } cmd := exec.Command(s.config.AnsiblePath, args...) cmd.Env = append(os.Environ(), "ANSIBLE_HOST_KEY_CHECKING=False") output, err := cmd.CombinedOutput() duration := time.Since(start).Milliseconds() s.taskLock.Lock() if err != nil { sw.WriteString(fmt.Sprintf("[%s] ✗ 失败 (%dms): %s\n", hostName, duration, string(output))) task.FailedHosts++ } else { sw.WriteString(fmt.Sprintf("[%s] ✓ 成功 (%dms)\n", hostName, duration)) task.SuccessHosts++ } task.Progress = i + 1 task.Output = sw.String() s.taskLock.Unlock() } s.taskLock.Lock() task.EndTime = time.Now() if task.FailedHosts > 0 { task.Status = "failed" } else { task.Status = "success" } s.taskLock.Unlock() } // ===== 系统信息 ===== func (s *AnsibleService) GetSystemInfo() models.SystemInfo { info := models.SystemInfo{ Version: "2.0.0", AnsiblePath: s.config.AnsiblePath, Hostname: getHostname(), OS: runtime.GOOS, Arch: runtime.GOARCH, HostCount: len(s.hosts), TaskCount: len(s.tasks), Uptime: time.Since(s.startTime).Round(time.Second).String(), } // 获取组数量(排除系统组) groupCount := 0 for name := range s.groups { if name != "all" && name != "ungrouped" { groupCount++ } } info.GroupCount = groupCount // Playbook数量 files, _ := os.ReadDir(s.config.PlaybookDir) for _, f := range files { if !f.IsDir() && strings.HasSuffix(f.Name(), ".yml") { info.PlaybookCount++ } } // Ansible版本 cmd := exec.Command("ansible", "--version") out, err := cmd.Output() if err == nil { lines := strings.Split(string(out), "\n") if len(lines) > 0 { info.AnsibleVer = strings.TrimSpace(lines[0]) } } return info } func getHostname() string { h, _ := os.Hostname() return h } // ===== 原有功能 ===== func (s *AnsibleService) loadGroups() { groupsFile := filepath.Join(s.config.InventoryDir, "groups.json") data, err := os.ReadFile(groupsFile) if err != nil { return } var groups map[string]models.HostGroup if err := json.Unmarshal(data, &groups); err == nil { for name, g := range groups { if name != "all" && name != "ungrouped" { gcopy := g s.groups[name] = &gcopy } } } } func (s *AnsibleService) generateID() string { hash := md5.New() hash.Write([]byte(time.Now().String() + strconv.Itoa(os.Getpid()))) return hex.EncodeToString(hash.Sum(nil))[:8] } func (s *AnsibleService) loadInventory() { invFile := filepath.Join(s.config.InventoryDir, "hosts") data, err := os.ReadFile(invFile) if err != nil { return } scanner := bufio.NewScanner(bytes.NewReader(data)) var currentGroup string groupVars := make(map[string]map[string]string) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { currentGroup = strings.Trim(line, "[]") continue } if strings.Contains(line, "=") { parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { if groupVars[currentGroup] == nil { groupVars[currentGroup] = make(map[string]string) } groupVars[currentGroup][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) } } if strings.Contains(line, "ansible_host") { re := regexp.MustCompile(`(\S+)\s+ansible_host=(\S+)`) if matches := re.FindStringSubmatch(line); len(matches) == 3 { host := &models.Host{ ID: s.generateID(), Name: matches[1], IP: matches[2], Status: "unknown", } s.hosts[host.ID] = host } } } } func (s *AnsibleService) loadHosts() { hostsFile := filepath.Join(s.config.InventoryDir, "hosts.json") data, err := os.ReadFile(hostsFile) if err != nil { return } var hosts []models.Host if err := json.Unmarshal(data, &hosts); err == nil { for _, h := range hosts { host := h if host.ID == "" { host.ID = s.generateID() } if host.Port == 0 { host.Port = 22 } if host.Username == "" { host.Username = "root" } if host.Status == "" { host.Status = "pending" } s.hosts[host.ID] = &host } s.saveHosts() } } func (s *AnsibleService) saveHosts() error { hostsFile := filepath.Join(s.config.InventoryDir, "hosts.json") var hosts []models.Host for _, h := range s.hosts { hcopy := *h if hcopy.ID == "" { hcopy.ID = s.generateID() h.ID = hcopy.ID } hosts = append(hosts, hcopy) } data, _ := json.MarshalIndent(hosts, "", " ") if err := os.WriteFile(hostsFile, data, 0644); err != nil { return err } s.updateInventoryFile() return nil } func (s *AnsibleService) updateInventoryFile() { var lines []string lines = append(lines, "# Ansible Inventory File") lines = append(lines, "# Generated by ansible-deploy") lines = append(lines, "") groupedHosts := make(map[string][]models.Host) for _, h := range s.hosts { if len(h.Groups) == 0 { groupedHosts["ungrouped"] = append(groupedHosts["ungrouped"], *h) } else { for _, g := range h.Groups { groupedHosts[g] = append(groupedHosts[g], *h) } } } for group, hosts := range groupedHosts { lines = append(lines, fmt.Sprintf("[%s]", group)) for _, h := range hosts { line := fmt.Sprintf(" %s ansible_host=%s", h.Name, h.IP) if h.Port != 0 && h.Port != 22 { line += fmt.Sprintf(" ansible_port=%d", h.Port) } if h.Username != "" { line += fmt.Sprintf(" ansible_user=%s", h.Username) } if h.AuthType == "sshkey" && h.SSHKey != "" { keyPath := s.resolveSSHKeyPath(h.SSHKey) line += fmt.Sprintf(" ansible_ssh_private_key_file=%s", keyPath) } // 写入主机自定义变量(如 bmc_ip, bmc_user 等) for k, v := range h.Vars { line += fmt.Sprintf(" %s=%s", k, v) } lines = append(lines, line) } lines = append(lines, "") } invFile := filepath.Join(s.config.InventoryDir, "hosts") os.WriteFile(invFile, []byte(strings.Join(lines, "\n")), 0644) } func (s *AnsibleService) ListHosts() []models.Host { var hosts []models.Host for _, h := range s.hosts { hosts = append(hosts, *h) } return hosts } func (s *AnsibleService) AddHost(host models.Host) error { host.ID = s.generateID() host.CreatedAt = time.Now() host.UpdatedAt = time.Now() host.Status = "pending" s.hosts[host.ID] = &host return s.saveHosts() } func (s *AnsibleService) DeleteHost(id string) error { if _, ok := s.hosts[id]; !ok { return fmt.Errorf("主机不存在") } delete(s.hosts, id) return s.saveHosts() } func (s *AnsibleService) UpdateHost(id string, host models.Host) error { if _, ok := s.hosts[id]; !ok { return fmt.Errorf("主机不存在") } host.ID = id host.UpdatedAt = time.Now() s.hosts[id] = &host return s.saveHosts() } func (s *AnsibleService) findHostByName(name string) *models.Host { for _, h := range s.hosts { if h.Name == name { return h } } return nil } func (s *AnsibleService) ListGroups() []models.HostGroup { var groups []models.HostGroup for _, g := range s.groups { gcopy := *g var hostList []models.Host for _, h := range s.hosts { if gcopy.Name == "all" { hcopy := *h hostList = append(hostList, hcopy) continue } for _, hGroup := range h.Groups { if hGroup == gcopy.Name { hcopy := *h hostList = append(hostList, hcopy) break } } if len(h.Groups) == 0 && gcopy.Name == "ungrouped" { hcopy := *h hostList = append(hostList, hcopy) } } gcopy.HostList = hostList groups = append(groups, gcopy) } return groups } func (s *AnsibleService) CreateGroup(group models.HostGroup) error { if _, ok := s.groups[group.Name]; ok { return fmt.Errorf("组已存在") } s.groups[group.Name] = &group return s.saveGroups() } func (s *AnsibleService) DeleteGroup(name string) error { if name == "all" || name == "ungrouped" { return fmt.Errorf("不能删除系统组") } delete(s.groups, name) return s.saveGroups() } func (s *AnsibleService) UpdateGroup(name string, group models.HostGroup) error { if _, ok := s.groups[name]; !ok { return fmt.Errorf("组不存在") } s.groups[name] = &group return s.saveGroups() } func (s *AnsibleService) saveGroups() error { groupsFile := filepath.Join(s.config.InventoryDir, "groups.json") data, _ := json.MarshalIndent(s.groups, "", " ") return os.WriteFile(groupsFile, data, 0644) } func (s *AnsibleService) TestConnection(hostID string) (*models.CommandResult, error) { host, ok := s.hosts[hostID] if !ok { return nil, fmt.Errorf("主机不存在") } start := time.Now() result := &models.CommandResult{Host: host.Name, Success: false} args := []string{ host.Name, "-i", s.inventoryPath, "-m", "ping", "-u", host.Username, } if host.AuthType == "sshkey" && host.SSHKey != "" { keyPath := s.resolveSSHKeyPath(host.SSHKey) args = append(args, "--private-key", keyPath) } else if host.Password != "" { args = append(args, "--extra-vars", fmt.Sprintf("ansible_password=%s", host.Password)) } if host.Port != 0 && host.Port != 22 { args = append(args, "--extra-vars", fmt.Sprintf("ansible_port=%d", host.Port)) } cmd := exec.Command(s.config.AnsiblePath, args...) cmd.Env = append(os.Environ(), "ANSIBLE_HOST_KEY_CHECKING=False") output, err := cmd.CombinedOutput() result.Duration = time.Since(start).Milliseconds() result.Output = string(output) if err != nil { result.Error = err.Error() host.Status = "offline" } else { result.Success = true if strings.Contains(string(output), "SUCCESS") || strings.Contains(string(output), "pong") { host.Status = "online" } else { host.Status = "offline" } } host.LastCheck = time.Now() s.saveHosts() return result, nil } func (s *AnsibleService) ExecuteCommand(req models.CommandRequest) ([]models.CommandResult, error) { var results []models.CommandResult for _, hostName := range req.Hosts { result := s.runCommand(hostName, req.Command, req.Timeout) results = append(results, result) } return results, nil } func (s *AnsibleService) runCommand(hostName string, command string, timeout int) models.CommandResult { start := time.Now() result := models.CommandResult{Host: hostName, Success: false} host := s.findHostByName(hostName) if host == nil { result.Error = "主机不存在" return result } if timeout == 0 { timeout = s.config.SSHTimeout } ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) defer cancel() args := []string{ host.Name, "-i", s.inventoryPath, "-m", "shell", "-a", command, "-u", host.Username, } if host.AuthType == "sshkey" && host.SSHKey != "" { keyPath := s.resolveSSHKeyPath(host.SSHKey) args = append(args, "--private-key", keyPath) } else if host.Password != "" { args = append(args, "--extra-vars", fmt.Sprintf("ansible_password=%s", host.Password)) } if host.Port != 0 && host.Port != 22 { args = append(args, "--extra-vars", fmt.Sprintf("ansible_port=%d", host.Port)) } cmd := exec.CommandContext(ctx, s.config.AnsiblePath, args...) cmd.Env = append(os.Environ(), "ANSIBLE_HOST_KEY_CHECKING=False") output, err := cmd.CombinedOutput() result.Duration = time.Since(start).Milliseconds() result.Output = string(output) if err != nil { result.Error = err.Error() if exitErr, ok := err.(*exec.ExitError); ok { result.ExitCode = exitErr.ExitCode() } } else { result.Success = true result.ExitCode = 0 } return result } func (s *AnsibleService) BatchExecute(req models.CommandRequest) *models.BatchCommandResult { result := &models.BatchCommandResult{ TaskID: s.generateID(), Total: len(req.Hosts), Results: make([]models.CommandResult, 0), } task := &models.TaskExecution{ ID: result.TaskID, Name: "批量命令执行", Hosts: req.Hosts, Status: "running", StartTime: time.Now(), TotalHosts: len(req.Hosts), } s.taskLock.Lock() s.tasks[result.TaskID] = task s.taskLock.Unlock() if req.Parallel { var wg sync.WaitGroup results := make(chan models.CommandResult, len(req.Hosts)) parallelism := s.config.MaxParallelism if parallelism <= 0 { parallelism = 10 } semaphore := make(chan struct{}, parallelism) for _, host := range req.Hosts { wg.Add(1) go func(h string) { defer wg.Done() semaphore <- struct{}{} defer func() { <-semaphore }() r := s.runCommand(h, req.Command, req.Timeout) results <- r }(host) } go func() { wg.Wait() close(results) }() for r := range results { result.Results = append(result.Results, r) if r.Success { result.Success++ } else { result.Failed++ } s.updateTaskProgress(result.TaskID, 1) } } else { for _, host := range req.Hosts { r := s.runCommand(host, req.Command, req.Timeout) result.Results = append(result.Results, r) if r.Success { result.Success++ } else { result.Failed++ } s.updateTaskProgress(result.TaskID, 1) } } task.Status = "completed" task.EndTime = time.Now() return result } func (s *AnsibleService) updateTaskProgress(taskID string, increment int) { s.taskLock.Lock() defer s.taskLock.Unlock() if task, ok := s.tasks[taskID]; ok { task.Progress += increment task.SuccessHosts = task.Progress if task.Progress >= task.TotalHosts { task.Status = "completed" task.EndTime = time.Now() } } } func (s *AnsibleService) ListTasks() []*models.TaskExecution { s.taskLock.RLock() defer s.taskLock.RUnlock() var tasks []*models.TaskExecution for _, t := range s.tasks { tasks = append(tasks, t) } return tasks } func (s *AnsibleService) GetTask(id string) *models.TaskExecution { s.taskLock.RLock() defer s.taskLock.RUnlock() return s.tasks[id] } func (s *AnsibleService) CancelTask(id string) error { s.taskLock.Lock() defer s.taskLock.Unlock() if task, ok := s.tasks[id]; ok { if task.Status == "running" { task.Status = "cancelled" task.EndTime = time.Now() return nil } return fmt.Errorf("任务无法取消") } return fmt.Errorf("任务不存在") } func (s *AnsibleService) ExecutePlaybook(req models.PlaybookExecutionRequest) (*models.TaskExecution, error) { playbookPath := filepath.Join(s.config.PlaybookDir, req.Name+".yml") if _, err := os.Stat(playbookPath); os.IsNotExist(err) { return nil, fmt.Errorf("Playbook不存在: %s", req.Name) } task := &models.TaskExecution{ ID: s.generateID(), Name: req.Name, Playbook: playbookPath, Hosts: req.Hosts, Status: "running", StartTime: time.Now(), TotalHosts: len(req.Hosts), SuccessHosts: 0, FailedHosts: 0, } s.taskLock.Lock() s.tasks[task.ID] = task s.taskLock.Unlock() go s.runPlaybook(task, playbookPath, req) return task, nil } func (s *AnsibleService) runPlaybook(task *models.TaskExecution, playbookPath string, req models.PlaybookExecutionRequest) { var args []string args = append(args, "-i", s.inventoryPath) if len(req.Hosts) > 0 { args = append(args, "-l", strings.Join(req.Hosts, ",")) } if len(req.ExtraVars) > 0 { varsJSON, _ := json.Marshal(req.ExtraVars) args = append(args, "-e", string(varsJSON)) } if len(req.Tags) > 0 { args = append(args, "-t", strings.Join(req.Tags, ",")) } if len(req.SkipTags) > 0 { args = append(args, "--skip-tags", strings.Join(req.SkipTags, ",")) } if req.Verbose != "" { args = append(args, "-"+req.Verbose) } if req.Diff { args = append(args, "-D") } if req.Check { args = append(args, "-C") } if req.Become != nil { if *req.Become { args = append(args, "-b") } else { args = append(args, "--no-become") } } if req.Forks > 0 { args = append(args, "-f", strconv.Itoa(req.Forks)) } if req.ExtraArgs != "" { extraParts := strings.Fields(req.ExtraArgs) args = append(args, extraParts...) } args = append(args, playbookPath) cmd := exec.Command("ansible-playbook", args...) if req.Timeout > 0 { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Timeout)*time.Second) defer cancel() cmd = exec.CommandContext(ctx, "ansible-playbook", args...) } sw := &syncWriter{buf: bytes.NewBuffer(nil)} cmd.Stdout = sw cmd.Stderr = sw done := make(chan struct{}) go func() { ticker := time.NewTicker(200 * time.Millisecond) defer ticker.Stop() var lastLen int for { select { case <-ticker.C: s.taskLock.Lock() sw.mu.Lock() currentLen := sw.buf.Len() if currentLen > lastLen { task.Output = sw.buf.String() lastLen = currentLen } sw.mu.Unlock() s.taskLock.Unlock() case <-done: return } } }() err := cmd.Run() close(done) sw.mu.Lock() finalOutput := sw.buf.String() sw.mu.Unlock() s.taskLock.Lock() task.Output = finalOutput task.EndTime = time.Now() if err != nil { task.Status = "failed" task.Error = err.Error() } else { task.Status = "success" } s.taskLock.Unlock() } type syncWriter struct { buf *bytes.Buffer mu sync.Mutex } func (w *syncWriter) Write(p []byte) (n int, err error) { w.mu.Lock() defer w.mu.Unlock() return w.buf.Write(p) } func (w *syncWriter) String() string { w.mu.Lock() defer w.mu.Unlock() return w.buf.String() } func (w *syncWriter) WriteString(s string) (n int, err error) { w.mu.Lock() defer w.mu.Unlock() return w.buf.WriteString(s) } func (s *AnsibleService) ListPlaybooks() []models.Playbook { var playbooks []models.Playbook files, _ := os.ReadDir(s.config.PlaybookDir) for _, f := range files { if !f.IsDir() && strings.HasSuffix(f.Name(), ".yml") { name := strings.TrimSuffix(f.Name(), ".yml") playbookPath := filepath.Join(s.config.PlaybookDir, f.Name()) playbook := models.Playbook{Name: name, Path: playbookPath} data, err := os.ReadFile(playbookPath) if err == nil { var playEntries []map[string]interface{} if yaml.Unmarshal(data, &playEntries) == nil && len(playEntries) > 0 { first := playEntries[0] if nameVal, ok := first["name"]; ok { playbook.Description = fmt.Sprintf("%v", nameVal) } if varsVal, ok := first["vars"]; ok { if varsMap, ok := varsVal.(map[string]interface{}); ok { playbook.Variables = varsMap } } } } playbooks = append(playbooks, playbook) } } return playbooks } func (s *AnsibleService) GetPlaybook(name string) (*models.Playbook, error) { playbookPath := filepath.Join(s.config.PlaybookDir, name+".yml") data, err := os.ReadFile(playbookPath) if err != nil { return nil, fmt.Errorf("Playbook不存在") } var playbook models.Playbook playbook.Name = name playbook.Path = playbookPath if err := yaml.Unmarshal(data, &playbook); err != nil { return nil, fmt.Errorf("Playbook解析失败") } return &playbook, nil } func (s *AnsibleService) CreatePlaybook(name string, content string) error { if name == "" { return fmt.Errorf("Playbook名称不能为空") } if strings.Contains(name, "/") || strings.Contains(name, "..") { return fmt.Errorf("Playbook名称包含非法字符") } playbookPath := filepath.Join(s.config.PlaybookDir, name+".yml") if _, err := os.Stat(playbookPath); err == nil { return fmt.Errorf("Playbook已存在: %s", name) } var dummy interface{} if err := yaml.Unmarshal([]byte(content), &dummy); err != nil { return fmt.Errorf("YAML格式错误: %v", err) } return os.WriteFile(playbookPath, []byte(content), 0644) } func (s *AnsibleService) DeletePlaybook(name string) error { if strings.Contains(name, "/") || strings.Contains(name, "..") { return fmt.Errorf("Playbook名称包含非法字符") } playbookPath := filepath.Join(s.config.PlaybookDir, name+".yml") if _, err := os.Stat(playbookPath); os.IsNotExist(err) { return fmt.Errorf("Playbook不存在: %s", name) } return os.Remove(playbookPath) } func (s *AnsibleService) UpdatePlaybook(name string, content string) error { if strings.Contains(name, "/") || strings.Contains(name, "..") { return fmt.Errorf("Playbook名称包含非法字符") } playbookPath := filepath.Join(s.config.PlaybookDir, name+".yml") if _, err := os.Stat(playbookPath); os.IsNotExist(err) { return fmt.Errorf("Playbook不存在: %s", name) } var dummy interface{} if err := yaml.Unmarshal([]byte(content), &dummy); err != nil { return fmt.Errorf("YAML格式错误: %v", err) } return os.WriteFile(playbookPath, []byte(content), 0644) } func (s *AnsibleService) GetPlaybookContent(name string) (string, error) { if strings.Contains(name, "/") || strings.Contains(name, "..") { return "", fmt.Errorf("Playbook名称包含非法字符") } playbookPath := filepath.Join(s.config.PlaybookDir, name+".yml") data, err := os.ReadFile(playbookPath) if err != nil { return "", fmt.Errorf("Playbook不存在") } return string(data), nil } func (s *AnsibleService) CheckAnsibleInstalled() bool { cmd := exec.Command("ansible", "--version") err := cmd.Run() return err == nil } func (s *AnsibleService) GetInventoryPath() string { return s.inventoryPath }