commit 9c1f44e91a3ff439366bd76a20078699a863a1f0 Author: Hermes Agent Date: Wed May 13 17:57:41 2026 +0800 feat: 1) ListGroups返回组内主机详细信息 2) 前端刷新间隔改为5分钟 3) 命令执行和Playbook页面主机组支持展开显示组内主机 diff --git a/README.md b/README.md new file mode 100644 index 0000000..baa4b38 --- /dev/null +++ b/README.md @@ -0,0 +1,213 @@ +# Ansible批量部署工具 + +基于 Go + Ansible 的批量运维部署系统,支持批量执行命令、Playbook管理、主机分组等功能。 + +## 功能特性 + +- 🌐 **Web界面** - 简洁美观的Web管理界面 +- 🖥️ **主机管理** - 支持添加、删除、编辑主机 +- 📁 **分组管理** - 灵活的主机组管理 +- 💻 **命令执行** - 批量并行/串行执行命令 +- 📜 **Playbook管理** - 预置Playbook模板,支持快速执行 +- 📊 **任务跟踪** - 实时任务进度监控 +- 🔄 **自动刷新** - 数据自动同步 + +## 快速开始 + +### 1. 安装依赖 + +```bash +# 安装Go +curl -fsSL https://go.dev/dl/go1.21.linux-amd64.tar.gz | tar -C /usr/local -xzf - + +# 安装Ansible +pip install ansible + +# 安装SSH +apt install openssh-client # Debian/Ubuntu +yum install openssh-clients # CentOS/RHEL +``` + +### 2. 构建项目 + +```bash +cd /root/ansible-deploy +go mod tidy +go build -o ansible-deploy cmd/main.go +``` + +### 3. 配置SSH免密登录 + +```bash +# 生成SSH密钥 +ssh-keygen -t rsa + +# 复制到目标主机 +ssh-copy-id user@hostname +``` + +### 4. 启动服务 + +```bash +# 默认端口8080 +./ansible-deploy + +# 自定义端口 +./ansible-deploy -port 9000 + +# 自定义配置 +./ansible-deploy -config /path/to/config.yaml +``` + +### 5. 访问Web界面 + +打开浏览器访问: `http://localhost:8080` + +## 配置说明 + +配置文件位于 `config/config.yaml`: + +```yaml +# Ansible路径 +ansible_path: /usr/bin/ansible + +# 资产清单目录 +inventory_dir: ~/ansible-deploy/inventory + +# Playbook目录 +playbook_dir: ~/ansible-deploy/playbooks + +# 日志目录 +log_dir: ~/ansible-deploy/logs + +# SSH超时(秒) +ssh_timeout: 30 + +# 最大并发数 +max_parallelism: 10 +``` + +## API接口 + +### 主机管理 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/hosts` | 获取主机列表 | +| POST | `/api/hosts` | 添加主机 | +| PUT | `/api/hosts/:id` | 更新主机 | +| DELETE | `/api/hosts/:id` | 删除主机 | +| POST | `/api/hosts/test/:id` | 测试连接 | + +### 主机组 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/groups` | 获取组列表 | +| POST | `/api/groups` | 创建组 | +| PUT | `/api/groups/:name` | 更新组 | +| DELETE | `/api/groups/:name` | 删除组 | + +### 命令执行 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/command/execute` | 执行命令 | +| POST | `/api/command/batch` | 批量执行 | + +### Playbook + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/playbooks` | 列出Playbook | +| POST | `/api/playbooks/execute` | 执行Playbook | + +### 任务 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/tasks` | 获取任务列表 | +| GET | `/api/tasks/:id` | 获取任务详情 | +| DELETE | `/api/tasks/:id` | 取消任务 | + +## 使用示例 + +### 添加主机 + +```bash +curl -X POST http://localhost:8080/api/hosts \ + -H "Content-Type: application/json" \ + -d '{ + "name": "web-server-01", + "ip": "192.168.1.100", + "port": 22, + "username": "root", + "password": "your-password", + "groups": ["webservers"] + }' +``` + +### 批量执行命令 + +```bash +curl -X POST http://localhost:8080/api/command/batch \ + -H "Content-Type: application/json" \ + -d '{ + "hosts": ["web1", "web2", "web3"], + "command": "df -h", + "parallel": true + }' +``` + +### 执行Playbook + +```bash +curl -X POST http://localhost:8080/api/playbooks/execute \ + -H "Content-Type: application/json" \ + -d '{ + "name": "update-packages", + "hosts": ["all"], + "extra_vars": {} + }' +``` + +## 预置Playbook + +| 文件 | 说明 | +|------|------| +| `update-packages.yml` | 更新系统包 | +| `deploy-docker.yml` | 安装Docker | +| `deploy-nginx.yml` | 部署Nginx | +| `check-system.yml` | 系统信息检查 | + +## 目录结构 + +``` +ansible-deploy/ +├── cmd/ +│ └── main.go # 主程序入口 +├── config/ +│ └── config.yaml # 配置文件 +├── internal/ +│ ├── handlers/ # HTTP处理器 +│ ├── models/ # 数据模型 +│ └── services/ # 业务逻辑 +├── web/ +│ └── dist/ +│ └── index.html # 前端页面 +├── playbooks/ # Playbook目录 +├── scripts/ +│ └── install.sh # 安装脚本 +└── README.md +``` + +## 注意事项 + +1. **SSH免密** - 建议配置SSH密钥对实现免密登录 +2. **权限** - 部分操作需要sudo权限,确保用户有sudo权限 +3. **防火墙** - 确保SSH端口开放 +4. **Python** - Ansible需要目标主机安装Python + +## License + +MIT License diff --git a/ansible-deploy b/ansible-deploy new file mode 100755 index 0000000..c1a286c Binary files /dev/null and b/ansible-deploy differ diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..d9d46be --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,110 @@ +package main + +import ( + "flag" + "log" + "os" + + "github.com/ansible-deploy/internal/handlers" + "github.com/ansible-deploy/internal/services" + "github.com/gin-gonic/gin" +) + +var ( + configPath string + port string +) + +func init() { + flag.StringVar(&configPath, "config", "config/config.yaml", "配置文件路径") + flag.StringVar(&port, "port", "8080", "服务端口") +} + +func main() { + flag.Parse() + + // 加载配置 + cfg, err := services.LoadConfig(configPath) + if err != nil { + log.Printf("配置加载失败: %v,使用默认配置", err) + cfg = services.DefaultConfig() + } + + // 初始化Ansible服务 + ansibleService := services.NewAnsibleService(cfg) + + // 初始化处理器 + h := handlers.NewAnsibleHandler(ansibleService) + + // 初始化Web服务 + r := gin.Default() + + // CORS配置 + r.Use(func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + c.Next() + }) + + // 静态文件 + r.Static("/static", "./web/dist") + r.Static("/assets", "./web/dist/assets") + + // API路由 + api := r.Group("/api") + { + // 主机管理 + api.GET("/hosts", h.ListHosts) + api.POST("/hosts", h.AddHost) + api.DELETE("/hosts/:id", h.DeleteHost) + api.PUT("/hosts/:id", h.UpdateHost) + api.POST("/hosts/test/:id", h.TestConnection) + + // 主机组管理 + api.GET("/groups", h.ListGroups) + api.POST("/groups", h.CreateGroup) + api.DELETE("/groups/:name", h.DeleteGroup) + api.PUT("/groups/:name", h.UpdateGroup) + + // Playbook管理 + api.GET("/playbooks", h.ListPlaybooks) + api.POST("/playbooks", h.CreatePlaybook) + api.GET("/playbooks/:name/content", h.GetPlaybookContent) + api.PUT("/playbooks/:name", h.UpdatePlaybook) + api.DELETE("/playbooks/:name", h.DeletePlaybook) + api.POST("/playbooks/execute", h.ExecutePlaybook) + api.GET("/playbooks/:name", h.GetPlaybook) + + // 命令执行 + api.POST("/command/execute", h.ExecuteCommand) + api.POST("/command/batch", h.BatchExecute) + + // 任务执行 + api.GET("/tasks", h.ListTasks) + api.GET("/tasks/:id", h.GetTask) + api.DELETE("/tasks/:id", h.CancelTask) + } + + // 前端路由 - 禁止缓存确保始终返回最新版本 + r.GET("/", func(c *gin.Context) { + c.Header("Cache-Control", "no-cache, no-store, must-revalidate") + c.Header("Pragma", "no-cache") + c.Header("Expires", "0") + c.File("./web/dist/index.html") + }) + + // 创建必要目录 + os.MkdirAll(cfg.InventoryDir, 0755) + os.MkdirAll(cfg.PlaybookDir, 0755) + os.MkdirAll(cfg.LogDir, 0755) + + log.Printf("Ansible部署工具启动,监听端口: %s", port) + if err := r.Run(":" + port); err != nil { + log.Fatalf("服务启动失败: %v", err) + } +} diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..ba247ab --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,30 @@ +# Ansible Deploy 配置文件 + +# Ansible安装路径 +ansible_path: /usr/bin/ansible + +# 资产清单目录 +inventory_dir: /root/ansible-deploy/inventory + +# Playbook目录 +playbook_dir: /root/ansible-deploy/playbooks + +# 日志目录 +log_dir: /root/ansible-deploy/logs + +# SSH连接超时时间(秒) +ssh_timeout: 30 + +# 最大并发数 +max_parallelism: 10 + +# 输出格式 (json, yaml, plain) +callback_plugin: json + +# SSH连接选项 +ssh_options: + strict_host_key_checking: no + user_known_hosts_file: /dev/null + connect_timeout: 10 + password_authentication: yes + key_authentication: yes diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7046ca6 --- /dev/null +++ b/go.mod @@ -0,0 +1,34 @@ +module github.com/ansible-deploy + +go 1.21 + +require ( + github.com/gin-gonic/gin v1.9.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bfe325c --- /dev/null +++ b/go.sum @@ -0,0 +1,86 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go new file mode 100644 index 0000000..51625f5 --- /dev/null +++ b/internal/handlers/handlers.go @@ -0,0 +1,456 @@ +package handlers + +import ( + "net/http" + + "github.com/ansible-deploy/internal/models" + "github.com/ansible-deploy/internal/services" + "github.com/gin-gonic/gin" +) + +// AnsibleHandler API处理器 +type AnsibleHandler struct { + service *services.AnsibleService +} + +// NewAnsibleHandler 创建处理器 +func NewAnsibleHandler(svc *services.AnsibleService) *AnsibleHandler { + return &AnsibleHandler{service: svc} +} + +// ListHosts 获取主机列表 +func (h *AnsibleHandler) ListHosts(c *gin.Context) { + hosts := h.service.ListHosts() + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": hosts, + }) +} + +// AddHost 添加主机 +func (h *AnsibleHandler) AddHost(c *gin.Context) { + var host models.Host + if err := c.ShouldBindJSON(&host); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误: " + err.Error(), + }) + return + } + + if err := h.service.AddHost(host); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "主机添加成功", + }) +} + +// DeleteHost 删除主机 +func (h *AnsibleHandler) DeleteHost(c *gin.Context) { + id := c.Param("id") + if err := h.service.DeleteHost(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "主机删除成功", + }) +} + +// UpdateHost 更新主机 +func (h *AnsibleHandler) UpdateHost(c *gin.Context) { + id := c.Param("id") + var host models.Host + if err := c.ShouldBindJSON(&host); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误", + }) + return + } + + if err := h.service.UpdateHost(id, host); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "主机更新成功", + }) +} + +// TestConnection 测试连接 +func (h *AnsibleHandler) TestConnection(c *gin.Context) { + id := c.Param("id") + result, err := h.service.TestConnection(id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": result, + }) +} + +// ListGroups 获取组列表 +func (h *AnsibleHandler) ListGroups(c *gin.Context) { + groups := h.service.ListGroups() + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": groups, + }) +} + +// CreateGroup 创建组 +func (h *AnsibleHandler) CreateGroup(c *gin.Context) { + var group models.HostGroup + if err := c.ShouldBindJSON(&group); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误", + }) + return + } + + if err := h.service.CreateGroup(group); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "组创建成功", + }) +} + +// DeleteGroup 删除组 +func (h *AnsibleHandler) DeleteGroup(c *gin.Context) { + name := c.Param("name") + if err := h.service.DeleteGroup(name); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "组删除成功", + }) +} + +// UpdateGroup 更新组 +func (h *AnsibleHandler) UpdateGroup(c *gin.Context) { + name := c.Param("name") + var group models.HostGroup + if err := c.ShouldBindJSON(&group); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误", + }) + return + } + + if err := h.service.UpdateGroup(name, group); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "组更新成功", + }) +} + +// ListPlaybooks 列出Playbooks +func (h *AnsibleHandler) ListPlaybooks(c *gin.Context) { + playbooks := h.service.ListPlaybooks() + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": playbooks, + }) +} + +// GetPlaybook 获取Playbook详情 +func (h *AnsibleHandler) GetPlaybook(c *gin.Context) { + name := c.Param("name") + playbook, err := h.service.GetPlaybook(name) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": playbook, + }) +} + +// ExecutePlaybook 执行Playbook +func (h *AnsibleHandler) ExecutePlaybook(c *gin.Context) { + var req models.PlaybookExecutionRequest + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误: " + err.Error(), + }) + return + } + + task, err := h.service.ExecutePlaybook(req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "任务已启动", + "taskId": task.ID, + }) +} + +// ExecuteCommand 执行命令 +func (h *AnsibleHandler) ExecuteCommand(c *gin.Context) { + var req models.CommandRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误: " + err.Error(), + }) + return + } + + results, err := h.service.ExecuteCommand(req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": results, + }) +} + +// BatchExecute 批量执行 +func (h *AnsibleHandler) BatchExecute(c *gin.Context) { + var req models.CommandRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误", + }) + return + } + + result := h.service.BatchExecute(req) + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "任务已启动", + "data": result, + }) +} + +// ListTasks 获取任务列表 +func (h *AnsibleHandler) ListTasks(c *gin.Context) { + tasks := h.service.ListTasks() + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": tasks, + }) +} + +// GetTask 获取任务详情 +func (h *AnsibleHandler) GetTask(c *gin.Context) { + id := c.Param("id") + task := h.service.GetTask(id) + if task == nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "msg": "任务不存在", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": task, + }) +} + +// CancelTask 取消任务 +func (h *AnsibleHandler) CancelTask(c *gin.Context) { + id := c.Param("id") + if err := h.service.CancelTask(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "任务已取消", + }) +} + +// CreatePlaybook 创建Playbook +func (h *AnsibleHandler) CreatePlaybook(c *gin.Context) { + var req struct { + Name string `json:"name" binding:"required"` + Content string `json:"content"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误: " + err.Error(), + }) + return + } + + if req.Content == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "Playbook内容不能为空", + }) + return + } + + if err := h.service.CreatePlaybook(req.Name, req.Content); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "Playbook创建成功", + }) +} + +// DeletePlaybook 删除Playbook +func (h *AnsibleHandler) DeletePlaybook(c *gin.Context) { + name := c.Param("name") + if err := h.service.DeletePlaybook(name); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "Playbook删除成功", + }) +} + +// GetPlaybookContent 获取Playbook内容 +func (h *AnsibleHandler) GetPlaybookContent(c *gin.Context) { + name := c.Param("name") + content, err := h.service.GetPlaybookContent(name) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "success", + "data": content, + }) +} + +// UpdatePlaybook 更新Playbook +func (h *AnsibleHandler) UpdatePlaybook(c *gin.Context) { + name := c.Param("name") + var req struct { + Content string `json:"content" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "msg": "参数错误", + }) + return + } + + if err := h.service.UpdatePlaybook(name, req.Content); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "msg": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "Playbook更新成功", + }) +} + +// WebSocketLogs WebSocket日志 +func (h *AnsibleHandler) WebSocketLogs(c *gin.Context) { + taskID := c.Param("taskId") + _ = taskID + // WebSocket实现需要单独处理,这里返回提示 + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "WebSocket连接", + }) +} diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..48aeda8 --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,132 @@ +package models + +import "time" + +// Host 主机信息 +type Host struct { + ID string `json:"id"` + Name string `json:"name"` + IP string `json:"ip"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password,omitempty"` + SSHKey string `json:"ssh_key,omitempty"` + AuthType string `json:"auth_type,omitempty"` // password 或 sshkey + Groups []string `json:"groups"` + Vars map[string]string `json:"vars,omitempty"` + Status string `json:"status"` + LastCheck time.Time `json:"last_check,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// HostGroup 主机组 +type HostGroup struct { + Name string `json:"name"` + Description string `json:"description"` + Hosts []string `json:"hosts"` + HostList []Host `json:"host_list,omitempty"` // 组内主机的详细信息 + Vars map[string]string `json:"vars,omitempty"` + Children []string `json:"children,omitempty"` +} + +// Inventory 资产清单 +type Inventory struct { + All *InventoryGroup `yaml:"all"` + Ungrouped *InventoryGroup `yaml:"ungrouped,omitempty"` +} + +// InventoryGroup 资产组 +type InventoryGroup struct { + Children map[string]*InventoryGroup `yaml:"children,omitempty"` + Hosts map[string]Host `yaml:"hosts,omitempty"` + Vars map[string]interface{} `yaml:"vars,omitempty"` +} + +// Playbook Playbook定义 +type Playbook struct { + Name string `json:"name"` + Path string `json:"path"` + Description string `json:"description"` + Variables map[string]interface{} `json:"variables,omitempty"` + Hosts string `json:"hosts"` + Tasks []Task `json:"tasks"` +} + +// Task 任务定义 +type Task struct { + Name string `json:"name"` + Module string `json:"module"` + Args map[string]interface{} `json:"args,omitempty"` + When string `json:"when,omitempty"` + Loop []interface{} `json:"loop,omitempty"` + LoopVar string `json:"loop_var,omitempty"` +} + +// TaskExecution 任务执行记录 +type TaskExecution struct { + ID string `json:"id"` + Name string `json:"name"` + Playbook string `json:"playbook"` + Hosts []string `json:"hosts"` + Status string `json:"status"` // pending, running, success, failed, cancelled + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time,omitempty"` + Progress int `json:"progress"` + TotalHosts int `json:"total_hosts"` + SuccessHosts int `json:"success_hosts"` + FailedHosts int `json:"failed_hosts"` + Output string `json:"output,omitempty"` + Error string `json:"error,omitempty"` +} + +// CommandRequest 命令执行请求 +type CommandRequest struct { + Hosts []string `json:"hosts" binding:"required"` + Command string `json:"command" binding:"required"` + Parallel bool `json:"parallel"` + Timeout int `json:"timeout"` +} + +// CommandResult 命令执行结果 +type CommandResult struct { + Host string `json:"host"` + Success bool `json:"success"` + Output string `json:"output"` + Error string `json:"error,omitempty"` + ExitCode int `json:"exit_code"` + Duration int64 `json:"duration_ms"` +} + +// BatchCommandResult 批量命令结果 +type BatchCommandResult struct { + TaskID string `json:"task_id"` + Total int `json:"total"` + Success int `json:"success"` + Failed int `json:"failed"` + Results []CommandResult `json:"results"` +} + +// PlaybookExecutionRequest Playbook执行请求 +type PlaybookExecutionRequest struct { + Name string `json:"name" binding:"required"` + Hosts []string `json:"hosts"` + ExtraVars map[string]interface{} `json:"extra_vars"` + Tags []string `json:"tags,omitempty"` // 只执行指定tags + SkipTags []string `json:"skip_tags,omitempty"` // 跳过指定tags + Verbose string `json:"verbose,omitempty"` // v, vv, vvv, vvvv + Diff bool `json:"diff,omitempty"` // 显示文件差异 + Check bool `json:"check,omitempty"` // dry-run模式 + Become *bool `json:"become,omitempty"` // 是否提权,nil表示使用playbook默认 + Forks int `json:"forks,omitempty"` // 并发数 + Timeout int `json:"timeout,omitempty"` // 超时(秒) + ExtraArgs string `json:"extra_args,omitempty"` // 自定义额外参数 +} + +// LogEntry 日志条目 +type LogEntry struct { + Time string `json:"time"` + Level string `json:"level"` + Host string `json:"host"` + Message string `json:"message"` +} diff --git a/internal/services/ansible.go b/internal/services/ansible.go new file mode 100644 index 0000000..b422d3a --- /dev/null +++ b/internal/services/ansible.go @@ -0,0 +1,893 @@ +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() + + return svc +} + +// 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 { + hcopy := h + if hcopy.ID == "" { + hcopy.ID = s.generateID() + } + if hcopy.Port == 0 { + hcopy.Port = 22 + } + if hcopy.Username == "" { + hcopy.Username = "root" + } + if hcopy.Status == "" { + hcopy.Status = "pending" + } + s.hosts[hcopy.ID] = &hcopy + } + // 保存以持久化补全的字段 + 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 { + hosts = append(hosts, *h) + } + + 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 + // 展开组内主机的详细信息 + var hostList []models.Host + for _, hName := range g.Hosts { + for _, h := range s.hosts { + if h.Name == hName { + hostList = append(hostList, *h) + break + } + } + } + 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...) + } + + var output bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = &output + + err := cmd.Run() + + task.EndTime = time.Now() + if err != nil { + task.Status = "failed" + task.Error = err.Error() + } else { + task.Status = "success" + } + task.Output = output.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 +} diff --git a/internal/services/config.go b/internal/services/config.go new file mode 100644 index 0000000..8ec8a03 --- /dev/null +++ b/internal/services/config.go @@ -0,0 +1,62 @@ +package services + +import ( + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +// Config 配置 +type Config struct { + AnsiblePath string `yaml:"ansible_path"` + InventoryDir string `yaml:"inventory_dir"` + PlaybookDir string `yaml:"playbook_dir"` + LogDir string `yaml:"log_dir"` + SSHTimeout int `yaml:"ssh_timeout"` + MaxParallelism int `yaml:"max_parallelism"` + CallbackPlugin string `yaml:"callback_plugin"` +} + +// DefaultConfig 默认配置 +func DefaultConfig() *Config { + home, _ := os.UserHomeDir() + return &Config{ + AnsiblePath: "/usr/bin/ansible", + InventoryDir: filepath.Join(home, "ansible-deploy/inventory"), + PlaybookDir: filepath.Join(home, "ansible-deploy/playbooks"), + LogDir: filepath.Join(home, "ansible-deploy/logs"), + SSHTimeout: 30, + MaxParallelism: 10, + CallbackPlugin: "json", + } +} + +// LoadConfig 加载配置文件 +func LoadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return DefaultConfig(), err + } + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + + // 设置默认值 + if cfg.AnsiblePath == "" { + cfg.AnsiblePath = "/usr/bin/ansible" + } + if cfg.InventoryDir == "" { + cfg.InventoryDir = filepath.Join(os.Getenv("HOME"), "ansible-deploy/inventory") + } + if cfg.PlaybookDir == "" { + cfg.PlaybookDir = filepath.Join(os.Getenv("HOME"), "ansible-deploy/playbooks") + } + if cfg.LogDir == "" { + cfg.LogDir = filepath.Join(os.Getenv("HOME"), "ansible-deploy/logs") + } + + return &cfg, nil +} diff --git a/inventory/groups.json b/inventory/groups.json new file mode 100644 index 0000000..67b2bc0 --- /dev/null +++ b/inventory/groups.json @@ -0,0 +1,17 @@ +{ + "all": { + "name": "all", + "description": "所有主机", + "hosts": null + }, + "ungrouped": { + "name": "ungrouped", + "description": "未分组主机", + "hosts": null + }, + "zebu_user01": { + "name": "zebu_user01", + "description": "", + "hosts": null + } +} \ No newline at end of file diff --git a/inventory/hosts b/inventory/hosts new file mode 100644 index 0000000..7c7854b --- /dev/null +++ b/inventory/hosts @@ -0,0 +1,11 @@ +# Ansible Inventory File +# Generated by ansible-deploy + +[ungrouped] + scmp47 ansible_host=172.16.11.44 ansible_user=root + +[all] + nas ansible_host=10.168.1.209 ansible_user=root + +[zebu_user01] + scmp46 ansible_host=172.16.11.42 ansible_user=root diff --git a/inventory/hosts.json b/inventory/hosts.json new file mode 100644 index 0000000..f08f294 --- /dev/null +++ b/inventory/hosts.json @@ -0,0 +1,44 @@ +[ + { + "id": "c3e976cb", + "name": "scmp46", + "ip": "172.16.11.42", + "port": 22, + "username": "root", + "password": "STC#scmp%0818", + "groups": [ + "zebu_user01" + ], + "status": "online", + "last_check": "2026-05-13T17:43:47.863884597+08:00", + "created_at": "2026-05-13T16:19:53.979745105+08:00", + "updated_at": "2026-05-13T16:19:53.979745185+08:00" + }, + { + "id": "5ef41aa3", + "name": "scmp47", + "ip": "172.16.11.44", + "port": 22, + "username": "root", + "groups": [], + "status": "pending", + "last_check": "0001-01-01T00:00:00Z", + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "2026-05-13T17:40:47.833904109+08:00" + }, + { + "id": "706f8ce7", + "name": "nas", + "ip": "10.168.1.209", + "port": 22, + "username": "root", + "password": "WXJwxj91612!!", + "groups": [ + "all" + ], + "status": "online", + "last_check": "2026-05-13T17:34:10.808052527+08:00", + "created_at": "2026-05-13T16:03:45.265250935+08:00", + "updated_at": "2026-05-13T16:03:45.265251013+08:00" + } +] \ No newline at end of file diff --git a/playbooks/check-cpu-mem-disk.yml b/playbooks/check-cpu-mem-disk.yml new file mode 100644 index 0000000..7c863c6 --- /dev/null +++ b/playbooks/check-cpu-mem-disk.yml @@ -0,0 +1,53 @@ +--- +- name: 检测主机 CPU、内存、磁盘占用 + hosts: all + gather_facts: yes + vars: + warn_threshold: 80 + crit_threshold: 90 + + tasks: + - name: 获取 CPU 使用率 + shell: | + top -bn1 | grep "Cpu(s)" | awk '{print "cpu_usage:" $2}' | cut -d'%' -f1 + register: cpu_result + + - name: 获取内存使用率 + shell: | + free | grep Mem | awk '{printf "memory_used:%.0f\nmemory_total:%.0f\n", $3, $2}' + register: mem_result + + - name: 获取磁盘使用率 + shell: | + df -h | grep -E '/$|/data' | awk '{print $1 ":" $5}' + register: disk_result + + - name: 格式化输出 + set_fact: + cpu_usage: "{{ cpu_result.stdout_lines[0].split(':')[1] | trim }}" + memory_used_mb: "{{ (mem_result.stdout_lines[0].split(':')[1] | trim | float / 1024) | round | int }}" + memory_total_mb: "{{ (mem_result.stdout_lines[1].split(':')[1] | trim | float / 1024) | round | int }}" + disk_usage: "{{ disk_result.stdout_lines }}" + + - name: 显示检测结果 + debug: + msg: | + ================== 主机检测报告 ================== + 主机名: {{ inventory_hostname }} + CPU 使用率: {{ cpu_usage }}% + 内存使用: {{ memory_used_mb }} MB / {{ memory_total_mb }} MB + 磁盘使用: + {% for disk in disk_usage %} + - {{ disk }} + {% endfor %} + ================================================= + + - name: 告警判断 + fail: + msg: "{{ inventory_hostname }} CPU 使用率超过 {{ crit_threshold }}%: {{ cpu_usage }}%" + when: cpu_usage | int >= crit_threshold + + - name: 警告判断 + warn: + msg: "{{ inventory_hostname }} CPU 使用率超过 {{ warn_threshold }}%: {{ cpu_usage }}%" + when: cpu_usage | int >= warn_threshold and cpu_usage | int < crit_threshold diff --git a/playbooks/check-system.yml b/playbooks/check-system.yml new file mode 100644 index 0000000..308b3d8 --- /dev/null +++ b/playbooks/check-system.yml @@ -0,0 +1,38 @@ +--- +# 系统信息检查 +- name: System Information + hosts: "{{ target_hosts | default('all') }}" + gather_facts: yes + + tasks: + - name: Display OS info + debug: + msg: "OS: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}" + + - name: Display hostname + debug: + msg: "Hostname: {{ ansible_facts['hostname'] }}" + + - name: Display IP addresses + debug: + msg: "IP: {{ ansible_facts['default_ipv4']['address'] }}" + + - name: Display memory info + debug: + msg: "Memory: {{ (ansible_facts['memtotal_mb'] / 1024) | round(2) }} GB" + + - name: Display CPU info + debug: + msg: "CPU: {{ ansible_facts['processor_vcpus'] }} vCPUs" + + - name: Display disk space + debug: + msg: "Disk: {{ ansible_facts['mounts'][0]['size_total'] | default(0) | int / 1024 / 1024 / 1024 | round(2) }} GB" + + - name: Uptime + command: uptime -s + register: uptime + + - name: Show uptime + debug: + msg: "Uptime since: {{ uptime.stdout }}" diff --git a/playbooks/deploy-docker.yml b/playbooks/deploy-docker.yml new file mode 100644 index 0000000..c6040ba --- /dev/null +++ b/playbooks/deploy-docker.yml @@ -0,0 +1,51 @@ +--- +# 安装Docker +- name: Install Docker + hosts: "{{ target_hosts | default('all') }}" + become: yes + vars: + docker_version: latest + + tasks: + - name: Install required packages + apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + state: present + when: ansible_os_family == "Debian" + + - name: Add Docker GPG key + apt_key: + url: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg + state: present + + - name: Add Docker repository + apt_repository: + repo: "deb [arch=amd64] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable" + state: present + when: ansible_os_family == "Debian" + + - name: Install Docker + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-compose-plugin + state: present + + - name: Start Docker service + service: + name: docker + state: started + enabled: yes + + - name: Add user to docker group + user: + name: "{{ ansible_user }}" + groups: docker + append: yes diff --git a/playbooks/deploy-nginx.yml b/playbooks/deploy-nginx.yml new file mode 100644 index 0000000..4435d02 --- /dev/null +++ b/playbooks/deploy-nginx.yml @@ -0,0 +1,53 @@ +--- +# 部署Nginx +- name: Deploy Nginx + hosts: "{{ target_hosts | default('all') }}" + become: yes + vars: + nginx_worker_processes: auto + nginx_worker_connections: 1024 + server_name: localhost + + tasks: + - name: Install Nginx + apt: + name: nginx + state: present + when: ansible_os_family == "Debian" + + - name: Install Nginx + yum: + name: nginx + state: present + when: ansible_os_family == "RedHat" + + - name: Configure Nginx + template: + src: templates/nginx.conf.j2 + dest: /etc/nginx/nginx.conf + mode: '0644' + notify: Restart Nginx + + - name: Create site configuration + template: + src: templates/site.conf.j2 + dest: /etc/nginx/conf.d/{{ server_name }}.conf + mode: '0644' + notify: Reload Nginx + + - name: Start Nginx + service: + name: nginx + state: started + enabled: yes + + handlers: + - name: Restart Nginx + service: + name: nginx + state: restarted + + - name: Reload Nginx + service: + name: nginx + state: reloaded diff --git a/playbooks/update-packages.yml b/playbooks/update-packages.yml new file mode 100644 index 0000000..8699522 --- /dev/null +++ b/playbooks/update-packages.yml @@ -0,0 +1,30 @@ +--- +# 更新系统包 +- name: Update System Packages + hosts: all + become: yes + vars: + update_cache: yes + + tasks: + - name: Update apt cache + apt: + update_cache: yes + when: ansible_os_family == "Debian" + + - name: Upgrade all packages + apt: + upgrade: dist + autoremove: yes + when: ansible_os_family == "Debian" + + - name: Update yum cache + yum: + update_cache: yes + when: ansible_os_family == "RedHat" + + - name: Upgrade all packages + yum: + name: "*" + state: latest + when: ansible_os_family == "RedHat" diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..84e8cb7 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +# Ansible Deploy 部署脚本 + +set -e + +echo "=== Ansible Deploy 批量部署工具 ===" + +# 检查依赖 +check_dependencies() { + echo "检查依赖..." + + # 检查Go + if ! command -v go &> /dev/null; then + echo "错误: Go未安装" + exit 1 + fi + echo "✓ Go已安装: $(go version)" + + # 检查Ansible + if ! command -v ansible &> /dev/null; then + echo "警告: Ansible未安装,是否安装? (y/n)" + read -r answer + if [ "$answer" = "y" ]; then + pip install ansible + else + echo "警告: Ansible未安装,部分功能可能不可用" + fi + else + echo "✓ Ansible已安装: $(ansible --version | head -1)" + fi + + # 检查SSH + if ! command -v ssh &> /dev/null; then + echo "错误: SSH未安装" + exit 1 + fi + echo "✓ SSH已安装" +} + +# 创建目录结构 +create_dirs() { + echo "创建目录结构..." + mkdir -p ~/ansible-deploy/{inventory,playbooks,logs} + echo "✓ 目录已创建" +} + +# 初始化配置文件 +init_config() { + echo "初始化配置文件..." + + # 复制配置 + if [ ! -f ~/ansible-deploy/config.yaml ]; then + cp config/config.yaml ~/ansible-deploy/config.yaml + echo "✓ 配置文件已创建" + fi + + # 创建默认inventory + if [ ! -f ~/ansible-deploy/inventory/hosts ]; then + cat > ~/ansible-deploy/inventory/hosts << 'EOF' +# Ansible Inventory File +# 添加你的主机信息,格式: +# server1 ansible_host=192.168.1.10 ansible_user=root + +[webservers] +web1 ansible_host=192.168.1.10 ansible_user=root +web2 ansible_host=192.168.1.11 ansible_user=root + +[dbservers] +db1 ansible_host=192.168.1.20 ansible_user=root + +[all:vars] +ansible_python_interpreter=/usr/bin/python3 +EOF + echo "✓ Inventory文件已创建" + fi +} + +# 构建项目 +build_project() { + echo "构建项目..." + cd /root/ansible-deploy + go build -o ansible-deploy cmd/main.go + echo "✓ 构建成功: ansible-deploy" +} + +# 安装 +install_project() { + echo "安装..." + cp ansible-deploy /usr/local/bin/ + chmod +x /usr/local/bin/ansible-deploy + echo "✓ 安装成功" +} + +# 主函数 +main() { + check_dependencies + create_dirs + init_config + build_project + install_project + + echo "" + echo "=== 安装完成 ===" + echo "启动服务: ansible-deploy -port 8080" + echo "配置文件: ~/ansible-deploy/config.yaml" + echo "访问地址: http://localhost:8080" +} + +main "$@" diff --git a/web/dist/index.html b/web/dist/index.html new file mode 100644 index 0000000..65e206a --- /dev/null +++ b/web/dist/index.html @@ -0,0 +1,1639 @@ + + + + + + Ansible批量部署工具 + + + +
+
+

⚡ Ansible批量部署工具

+ +
+
+ +
+ +
+
+
+

主机总数

+
0
+
+
+

在线主机

+
0
+
+
+

离线主机

+
0
+
+
+

运行中任务

+
0
+
+
+ +
+

📝 最近任务

+ + + + + + + + + + + +
任务名称状态进度开始时间
+
+
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +