Files

983 rindas
23 KiB
Go

Šis fails satur neviennozīmīgus unikoda simbolus
Šis fails satur unikoda simbolus, kas var tikt sajauktas ar citām rakstzīmēm. Ja šķiet, ka tas ir ar nolūku, šo brīdinājumu var droši neņemt vērā. Jāizmanto atsoļa taustiņš (Esc), lai atklātu tās.
package services
import (
"bufio"
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"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
}
// NewAnsibleService 创建Ansible服务
func NewAnsibleService(cfg *Config) *AnsibleService {
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),
}
// 初始化默认组
svc.groups["all"] = &models.HostGroup{Name: "all", Description: "所有主机"}
svc.groups["ungrouped"] = &models.HostGroup{Name: "ungrouped", Description: "未分组主机"}
// 加载现有数据
svc.loadHosts()
svc.loadGroups()
return svc
}
// loadGroups 加载主机组列表
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
}
}
}
}
// generateID 生成唯一ID
func (s *AnsibleService) generateID() string {
hash := md5.New()
hash.Write([]byte(time.Now().String()))
return hex.EncodeToString(hash.Sum(nil))[:8]
}
// loadInventory 加载资产清单
func (s *AnsibleService) loadInventory() {
invFile := filepath.Join(s.config.InventoryDir, "hosts")
data, err := os.ReadFile(invFile)
if err != nil {
return
}
// 解析INI格式的inventory
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
}
}
}
}
// loadHosts 加载主机列表
func (s *AnsibleService) loadHosts() {
// 从hosts.json加载详细配置(唯一数据源)
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()
}
}
// 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
// 确保每个主机都有ID,并更新map中的指针
if hcopy.ID == "" {
hcopy.ID = s.generateID()
h.ID = hcopy.ID // 更新map中的指针
}
hosts = append(hosts, hcopy)
}
data, _ := json.MarshalIndent(hosts, "", " ")
if err := os.WriteFile(hostsFile, data, 0644); err != nil {
return err
}
// 更新inventory文件
s.updateInventoryFile()
return nil
}
// updateInventoryFile 更新inventory文件
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 != "" {
line += fmt.Sprintf(" ansible_ssh_private_key_file=%s", h.SSHKey)
}
lines = append(lines, line)
}
lines = append(lines, "")
}
invFile := filepath.Join(s.config.InventoryDir, "hosts")
os.WriteFile(invFile, []byte(strings.Join(lines, "\n")), 0644)
}
// ListHosts 获取主机列表
func (s *AnsibleService) ListHosts() []models.Host {
var hosts []models.Host
for _, h := range s.hosts {
hosts = append(hosts, *h)
}
return hosts
}
// AddHost 添加主机
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()
}
// DeleteHost 删除主机
func (s *AnsibleService) DeleteHost(id string) error {
if _, ok := s.hosts[id]; !ok {
return fmt.Errorf("主机不存在")
}
delete(s.hosts, id)
return s.saveHosts()
}
// UpdateHost 更新主机
func (s *AnsibleService) UpdateHost(id string, host models.Host) error {
if _, ok := s.hosts[id]; !ok {
return fmt.Errorf("主机不存在")
}
host.UpdatedAt = time.Now()
s.hosts[id] = &host
return s.saveHosts()
}
// ListGroups 获取主机组列表
func (s *AnsibleService) ListGroups() []models.HostGroup {
var groups []models.HostGroup
for _, g := range s.groups {
gcopy := *g
// 动态展开组内主机(通过 host.Groups 字段关联,而非 group.Hosts
var hostList []models.Host
for _, h := range s.hosts {
if gcopy.Name == "all" {
// all 组包含所有主机
hcopy := *h
hostList = append(hostList, hcopy)
continue
}
for _, hGroup := range h.Groups {
if hGroup == gcopy.Name {
hcopy := *h
hostList = append(hostList, hcopy)
break
}
}
// 也检查主机的默认组(ungrouped)
if len(h.Groups) == 0 && gcopy.Name == "ungrouped" {
hcopy := *h
hostList = append(hostList, hcopy)
}
}
gcopy.HostList = hostList
groups = append(groups, gcopy)
}
return groups
}
// CreateGroup 创建主机组
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()
}
// DeleteGroup 删除主机组
func (s *AnsibleService) DeleteGroup(name string) error {
if name == "all" || name == "ungrouped" {
return fmt.Errorf("不能删除系统组")
}
delete(s.groups, name)
return s.saveGroups()
}
// UpdateGroup 更新主机组
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()
}
// 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)
}
// TestConnection 测试主机连接
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,
}
// 构建ansible命令
args := []string{
host.Name,
"-i", s.inventoryPath,
"-m", "ping",
"-u", host.Username,
}
// 认证方式:SSH Key 或 密码
if host.AuthType == "sshkey" && host.SSHKey != "" {
// SSH Key 认证
args = append(args, "--private-key", host.SSHKey)
} else if host.Password != "" {
// 密码认证
args = append(args, "--extra-vars", fmt.Sprintf("ansible_password=%s", host.Password))
}
// 如果端口不是22,通过extra-vars传递
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...)
// 通过环境变量禁用SSH主机密钥检查
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
}
// ExecuteCommand 执行单个命令
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
}
// runCommand 在主机上执行命令
func (s *AnsibleService) runCommand(hostName string, command string, timeout int) models.CommandResult {
start := time.Now()
result := models.CommandResult{
Host: hostName,
Success: false,
}
// 查找主机获取认证信息
var host *models.Host
for _, h := range s.hosts {
if h.Name == hostName {
host = h
break
}
}
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,
}
// 认证方式:SSH Key 或 密码
if host.AuthType == "sshkey" && host.SSHKey != "" {
args = append(args, "--private-key", host.SSHKey)
} else if host.Password != "" {
args = append(args, "--extra-vars", fmt.Sprintf("ansible_password=%s", host.Password))
}
// 如果端口不是22,通过extra-vars传递
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...)
// 通过环境变量禁用SSH主机密钥检查
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
}
// BatchExecute 批量执行命令
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
}
// updateTaskProgress 更新任务进度
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()
}
}
}
// ListTasks 获取任务列表
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
}
// GetTask 获取单个任务
func (s *AnsibleService) GetTask(id string) *models.TaskExecution {
s.taskLock.RLock()
defer s.taskLock.RUnlock()
return s.tasks[id]
}
// CancelTask 取消任务
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("任务不存在")
}
// ExecutePlaybook 执行Playbook
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
}
// runPlaybook 运行Playbook
func (s *AnsibleService) runPlaybook(task *models.TaskExecution, playbookPath string, req models.PlaybookExecutionRequest) {
var args []string
// 添加inventory
args = append(args, "-i", s.inventoryPath)
// 添加hosts限制
if len(req.Hosts) > 0 {
args = append(args, "-l", strings.Join(req.Hosts, ","))
}
// 添加extra-vars
if len(req.ExtraVars) > 0 {
varsJSON, _ := json.Marshal(req.ExtraVars)
args = append(args, "-e", string(varsJSON))
}
// 添加tags
if len(req.Tags) > 0 {
args = append(args, "-t", strings.Join(req.Tags, ","))
}
// 添加skip-tags
if len(req.SkipTags) > 0 {
args = append(args, "--skip-tags", strings.Join(req.SkipTags, ","))
}
// 添加verbose
if req.Verbose != "" {
args = append(args, "-"+req.Verbose)
}
// 显示文件差异
if req.Diff {
args = append(args, "-D")
}
// dry-run模式
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...)
}
// playbook路径放最后
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...)
}
// 实时写入日志的 Writer
sw := &syncWriter{buf: bytes.NewBuffer(nil)}
cmd.Stdout = sw
cmd.Stderr = sw
// 启动 goroutine 实时搬运日志到 task.Output
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) // 通知 goroutine 退出
// 最终同步一次完整日志
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()
}
// syncWriter 线程安全的 Writer
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()
}
// ListPlaybooks 列出可用Playbooks
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,
}
// 解析YAML获取描述和变量信息
data, err := os.ReadFile(playbookPath)
if err == nil {
// 尝试解析为playbook列表
var playEntries []map[string]interface{}
if yaml.Unmarshal(data, &playEntries) == nil && len(playEntries) > 0 {
first := playEntries[0]
// 提取注释中的描述(name字段)
if nameVal, ok := first["name"]; ok {
playbook.Description = fmt.Sprintf("%v", nameVal)
}
// 提取vars
if varsVal, ok := first["vars"]; ok {
if varsMap, ok := varsVal.(map[string]interface{}); ok {
playbook.Variables = varsMap
}
}
}
}
playbooks = append(playbooks, playbook)
}
}
return playbooks
}
// GetPlaybook 获取Playbook详情
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
// 简单解析YAML
if err := yaml.Unmarshal(data, &playbook); err != nil {
return nil, fmt.Errorf("Playbook解析失败")
}
return &playbook, nil
}
// WebSocketLogs WebSocket日志流
func (s *AnsibleService) WebSocketLogs(taskID string) (<-chan models.LogEntry, error) {
logChan := make(chan models.LogEntry, 100)
go func() {
defer close(logChan)
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.taskLock.RLock()
task, ok := s.tasks[taskID]
s.taskLock.RUnlock()
if !ok {
return
}
entry := models.LogEntry{
Time: time.Now().Format("15:04:05"),
Level: "info",
Host: "system",
Message: fmt.Sprintf("Progress: %d/%d", task.Progress, task.TotalHosts),
}
logChan <- entry
if task.Status == "completed" || task.Status == "failed" {
return
}
}
}
}()
return logChan, nil
}
// ParseAnsibleOutput 解析Ansible输出
func (s *AnsibleService) ParseAnsibleOutput(output string) (map[string]interface{}, error) {
var result map[string]interface{}
if err := json.Unmarshal([]byte(output), &result); err != nil {
return nil, err
}
return result, nil
}
// GetTaskOutput 获取任务输出
func (s *AnsibleService) GetTaskOutput(taskID string) string {
s.taskLock.RLock()
defer s.taskLock.RUnlock()
if task, ok := s.tasks[taskID]; ok {
return task.Output
}
return ""
}
// CreatePlaybook 创建Playbook(通过内容)
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)
}
// 验证YAML格式
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)
}
// DeletePlaybook 删除Playbook
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)
}
// UpdatePlaybook 更新Playbook内容
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)
}
// 验证YAML格式
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)
}
// GetPlaybookContent 获取Playbook原始内容
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
}
// CheckAnsibleInstalled 检查Ansible是否安装
func (s *AnsibleService) CheckAnsibleInstalled() bool {
cmd := exec.Command("ansible", "--version")
err := cmd.Run()
return err == nil
}
// GetInventoryPath 获取inventory路径
func (s *AnsibleService) GetInventoryPath() string {
return s.inventoryPath
}