feat: 1) ListGroups返回组内主机详细信息 2) 前端刷新间隔改为5分钟 3) 命令执行和Playbook页面主机组支持展开显示组内主机
Šī revīzija ir iekļauta:
+213
@@ -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
|
||||||
Izpildāmais fails
Binārs
Bināro failu nav iespējams attēlot.
+110
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
+34
@@ -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
|
||||||
|
)
|
||||||
+86
@@ -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=
|
||||||
@@ -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连接",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"all": {
|
||||||
|
"name": "all",
|
||||||
|
"description": "所有主机",
|
||||||
|
"hosts": null
|
||||||
|
},
|
||||||
|
"ungrouped": {
|
||||||
|
"name": "ungrouped",
|
||||||
|
"description": "未分组主机",
|
||||||
|
"hosts": null
|
||||||
|
},
|
||||||
|
"zebu_user01": {
|
||||||
|
"name": "zebu_user01",
|
||||||
|
"description": "",
|
||||||
|
"hosts": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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 }}"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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 "$@"
|
||||||
ārējs
+1639
Failā izmaiņas netiks attēlotas, jo tās ir par lielu
Ielādēt izmaiņas
Atsaukties uz šo jaunā problēmā
Block a user