feat: v2.0 全面升级 - SSH密钥管理/模板中心/文件分发/全新UI
- 新增SSH密钥库管理(集中托管私钥、自动指纹识别) - 新增模板中心(24个预置模板,7大分类,一键创建Playbook) - 新增文件分发功能(内容/文件双模式,权限设置) - 新增系统信息接口(版本/主机数/运行时间) - 主机支持密码/SSH密钥双认证方式 - 全新暗色玻璃拟态UI(侧边栏导航、SSE实时日志流) - 修复UpdateHost ID丢失bug - 添加.gitignore排除敏感数据
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# 二进制文件
|
||||||
|
ansible-deploy
|
||||||
|
|
||||||
|
# 敏感数据 - 主机配置含密码
|
||||||
|
inventory/hosts.json
|
||||||
|
inventory/groups.json
|
||||||
|
inventory/hosts
|
||||||
|
|
||||||
|
# SSH私钥
|
||||||
|
inventory/ssh_keys/
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# 系统文件
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
# Ansible批量部署工具
|
# ⚡ Ansible Deploy Pro
|
||||||
|
|
||||||
基于 Go + Ansible 的批量运维部署系统,支持批量执行命令、Playbook管理、主机分组等功能。
|
基于 Go + Ansible 的企业级批量运维部署平台,支持 SSH 密钥管理、Playbook 模板中心、文件分发、实时日志流等功能。
|
||||||
|
|
||||||
## 功能特性
|
## ✨ 功能特性
|
||||||
|
|
||||||
- 🌐 **Web界面** - 简洁美观的Web管理界面
|
- 🎨 **现代化 Web 界面** - 暗色玻璃拟态设计,侧边栏导航,流畅动画
|
||||||
- 🖥️ **主机管理** - 支持添加、删除、编辑主机
|
- 🖥️ **主机管理** - 支持密码/SSH密钥双认证,连接测试,分组管理
|
||||||
- 📁 **分组管理** - 灵活的主机组管理
|
- 🔑 **SSH 密钥库** - 集中管理 SSH 私钥,自动指纹识别
|
||||||
- 💻 **命令执行** - 批量并行/串行执行命令
|
- 📜 **Playbook 管理** - 在线编辑、变量解析、执行选项配置
|
||||||
- 📜 **Playbook管理** - 预置Playbook模板,支持快速执行
|
- 🏪 **模板中心** - 20+ 预置模板(Docker/Nginx/MySQL/Redis/安全加固/监控等),一键创建
|
||||||
- 📊 **任务跟踪** - 实时任务进度监控
|
- 📦 **文件分发** - 批量分发文件/内容到远程主机
|
||||||
- 🔄 **自动刷新** - 数据自动同步
|
- 💻 **命令执行** - 批量并行/串行执行命令,快捷命令
|
||||||
|
- 📊 **任务中心** - SSE 实时日志流,进度追踪,任务取消
|
||||||
|
- 📈 **仪表盘** - 主机状态概览、任务统计、系统信息
|
||||||
|
|
||||||
## 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
### 1. 安装依赖
|
### 1. 安装依赖
|
||||||
|
|
||||||
@@ -25,72 +27,60 @@ pip install ansible
|
|||||||
|
|
||||||
# 安装 SSH
|
# 安装 SSH
|
||||||
apt install openssh-client # Debian/Ubuntu
|
apt install openssh-client # Debian/Ubuntu
|
||||||
yum install openssh-clients # CentOS/RHEL
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 构建项目
|
### 2. 构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /root/ansible-deploy
|
cd ansible-deploy
|
||||||
go mod tidy
|
go mod tidy
|
||||||
go build -o ansible-deploy cmd/main.go
|
go build -o ansible-deploy cmd/main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 配置SSH免密登录
|
### 3. 启动
|
||||||
|
|
||||||
```bash
|
|
||||||
# 生成SSH密钥
|
|
||||||
ssh-keygen -t rsa
|
|
||||||
|
|
||||||
# 复制到目标主机
|
|
||||||
ssh-copy-id user@hostname
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 启动服务
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 默认端口 8080
|
# 默认端口 8080
|
||||||
./ansible-deploy
|
./ansible-deploy
|
||||||
|
|
||||||
# 自定义端口
|
# 自定义端口和配置
|
||||||
./ansible-deploy -port 9000
|
./ansible-deploy -port 9000 -config /path/to/config.yaml
|
||||||
|
|
||||||
# 自定义配置
|
|
||||||
./ansible-deploy -config /path/to/config.yaml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. 访问Web界面
|
### 4. 访问
|
||||||
|
|
||||||
打开浏览器访问: `http://localhost:8080`
|
打开浏览器: `http://localhost:8080`
|
||||||
|
|
||||||
## 配置说明
|
## 📁 目录结构
|
||||||
|
|
||||||
配置文件位于 `config/config.yaml`:
|
```
|
||||||
|
ansible-deploy/
|
||||||
```yaml
|
├── cmd/
|
||||||
# Ansible路径
|
│ └── main.go # 主程序入口
|
||||||
ansible_path: /usr/bin/ansible
|
├── config/
|
||||||
|
│ └── config.yaml # 配置文件
|
||||||
# 资产清单目录
|
├── internal/
|
||||||
inventory_dir: ~/ansible-deploy/inventory
|
│ ├── handlers/
|
||||||
|
│ │ └── handlers.go # HTTP 处理器
|
||||||
# Playbook目录
|
│ ├── models/
|
||||||
playbook_dir: ~/ansible-deploy/playbooks
|
│ │ └── models.go # 数据模型
|
||||||
|
│ └── services/
|
||||||
# 日志目录
|
│ ├── ansible.go # Ansible 核心服务
|
||||||
log_dir: ~/ansible-deploy/logs
|
│ ├── config.go # 配置加载
|
||||||
|
│ └── templates.go # Playbook 模板库
|
||||||
# SSH超时(秒)
|
├── web/dist/
|
||||||
ssh_timeout: 30
|
│ └── index.html # 前端界面
|
||||||
|
├── playbooks/ # Playbook 目录
|
||||||
# 最大并发数
|
├── inventory/ # 资产清单
|
||||||
max_parallelism: 10
|
│ ├── hosts.json # 主机数据
|
||||||
|
│ ├── groups.json # 分组数据
|
||||||
|
│ └── ssh_keys/ # SSH 密钥存储
|
||||||
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## API接口
|
## 🔌 API 接口
|
||||||
|
|
||||||
### 主机管理
|
### 主机管理
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| GET | `/api/hosts` | 获取主机列表 |
|
| GET | `/api/hosts` | 获取主机列表 |
|
||||||
@@ -100,113 +90,78 @@ max_parallelism: 10
|
|||||||
| POST | `/api/hosts/test/:id` | 测试连接 |
|
| POST | `/api/hosts/test/:id` | 测试连接 |
|
||||||
|
|
||||||
### 主机组
|
### 主机组
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| GET | `/api/groups` | 获取组列表 |
|
| GET | `/api/groups` | 获取组列表 |
|
||||||
| POST | `/api/groups` | 创建组 |
|
| POST | `/api/groups` | 创建组 |
|
||||||
| PUT | `/api/groups/:name` | 更新组 |
|
|
||||||
| DELETE | `/api/groups/:name` | 删除组 |
|
| DELETE | `/api/groups/:name` | 删除组 |
|
||||||
|
|
||||||
### 命令执行
|
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| POST | `/api/command/execute` | 执行命令 |
|
|
||||||
| POST | `/api/command/batch` | 批量执行 |
|
|
||||||
|
|
||||||
### Playbook
|
### Playbook
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| GET | `/api/playbooks` | 列出 Playbook |
|
| GET | `/api/playbooks` | 列出 Playbook |
|
||||||
|
| POST | `/api/playbooks` | 创建 Playbook |
|
||||||
|
| GET | `/api/playbooks/:name/content` | 获取内容 |
|
||||||
|
| PUT | `/api/playbooks/:name` | 更新 Playbook |
|
||||||
|
| DELETE | `/api/playbooks/:name` | 删除 Playbook |
|
||||||
| POST | `/api/playbooks/execute` | 执行 Playbook |
|
| POST | `/api/playbooks/execute` | 执行 Playbook |
|
||||||
|
|
||||||
### 任务
|
### 模板中心
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| GET | `/api/tasks` | 获取任务列表 |
|
| GET | `/api/templates` | 列出模板 |
|
||||||
| GET | `/api/tasks/:id` | 获取任务详情 |
|
| GET | `/api/templates/categories` | 模板分类 |
|
||||||
|
| GET | `/api/templates/:id` | 模板详情 |
|
||||||
|
| POST | `/api/templates/deploy` | 从模板创建 |
|
||||||
|
|
||||||
|
### SSH 密钥
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/api/sshkeys` | 列出密钥 |
|
||||||
|
| POST | `/api/sshkeys` | 添加密钥 |
|
||||||
|
| DELETE | `/api/sshkeys/:name` | 删除密钥 |
|
||||||
|
|
||||||
|
### 文件分发
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/api/files/distribute` | 分发文件 |
|
||||||
|
|
||||||
|
### 任务
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/api/tasks` | 任务列表 |
|
||||||
|
| GET | `/api/tasks/:id` | 任务详情 |
|
||||||
|
| GET | `/api/tasks/:id/stream` | SSE 实时日志 |
|
||||||
| DELETE | `/api/tasks/:id` | 取消任务 |
|
| DELETE | `/api/tasks/:id` | 取消任务 |
|
||||||
|
|
||||||
## 使用示例
|
### 系统
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/api/system/info` | 系统信息 |
|
||||||
|
|
||||||
### 添加主机
|
## 📋 预置模板
|
||||||
|
|
||||||
```bash
|
| 分类 | 模板 | 说明 |
|
||||||
curl -X POST http://localhost:8080/api/hosts \
|
|------|------|------|
|
||||||
-H "Content-Type: application/json" \
|
| 🖥️ 系统管理 | 系统包更新 | 自动更新软件包 |
|
||||||
-d '{
|
| | 系统信息采集 | 采集 OS/硬件/网络信息 |
|
||||||
"name": "web-server-01",
|
| | 资源监控检查 | CPU/内存/磁盘告警 |
|
||||||
"ip": "192.168.1.100",
|
| | 日志清理 | 清理日志和临时文件 |
|
||||||
"port": 22,
|
| | 时间同步(NTP) | 配置 chrony 时间同步 |
|
||||||
"username": "root",
|
| 🌐 Web服务 | 部署 Nginx | 安装配置 Nginx + 反向代理 |
|
||||||
"password": "your-password",
|
| | 部署 Apache | 安装配置 Apache |
|
||||||
"groups": ["webservers"]
|
| 🗄️ 数据库 | 部署 MySQL | 安装 MySQL + 基础优化 |
|
||||||
}'
|
| | 部署 Redis | 安装 Redis + 内存配置 |
|
||||||
```
|
| | 部署 PostgreSQL | 安装 PostgreSQL |
|
||||||
|
| 🐳 容器化 | 部署 Docker | Docker CE + 镜像加速 |
|
||||||
### 批量执行命令
|
| | 部署 Portainer | 容器管理面板 |
|
||||||
|
| 🔧 DevOps | 部署 Node.js | Node.js + PM2 |
|
||||||
```bash
|
| | 部署 Python 应用 | Gunicorn + Systemd |
|
||||||
curl -X POST http://localhost:8080/api/command/batch \
|
| | 数据备份 | 定时备份 + 自动清理 |
|
||||||
-H "Content-Type: application/json" \
|
| 🔒 安全加固 | 安全加固 | SSH加固 + 防火墙 + fail2ban |
|
||||||
-d '{
|
| | SSL 证书部署 | Let's Encrypt 自动申请 |
|
||||||
"hosts": ["web1", "web2", "web3"],
|
| 📊 监控告警 | 部署 Node Exporter | Prometheus 指标采集 |
|
||||||
"command": "df -h",
|
| | 部署 Grafana | 可视化监控面板 |
|
||||||
"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
|
## License
|
||||||
|
|
||||||
|
|||||||
+19
-11
@@ -23,23 +23,17 @@ func init() {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// 加载配置
|
|
||||||
cfg, err := services.LoadConfig(configPath)
|
cfg, err := services.LoadConfig(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("配置加载失败: %v,使用默认配置", err)
|
log.Printf("配置加载失败: %v,使用默认配置", err)
|
||||||
cfg = services.DefaultConfig()
|
cfg = services.DefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化Ansible服务
|
|
||||||
ansibleService := services.NewAnsibleService(cfg)
|
ansibleService := services.NewAnsibleService(cfg)
|
||||||
|
|
||||||
// 初始化处理器
|
|
||||||
h := handlers.NewAnsibleHandler(ansibleService)
|
h := handlers.NewAnsibleHandler(ansibleService)
|
||||||
|
|
||||||
// 初始化Web服务
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
// CORS配置
|
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Header("Access-Control-Allow-Origin", "*")
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
@@ -51,11 +45,9 @@ func main() {
|
|||||||
c.Next()
|
c.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 静态文件
|
|
||||||
r.Static("/static", "./web/dist")
|
r.Static("/static", "./web/dist")
|
||||||
r.Static("/assets", "./web/dist/assets")
|
r.Static("/assets", "./web/dist/assets")
|
||||||
|
|
||||||
// API路由
|
|
||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
{
|
{
|
||||||
// 主机管理
|
// 主机管理
|
||||||
@@ -64,6 +56,7 @@ func main() {
|
|||||||
api.DELETE("/hosts/:id", h.DeleteHost)
|
api.DELETE("/hosts/:id", h.DeleteHost)
|
||||||
api.PUT("/hosts/:id", h.UpdateHost)
|
api.PUT("/hosts/:id", h.UpdateHost)
|
||||||
api.POST("/hosts/test/:id", h.TestConnection)
|
api.POST("/hosts/test/:id", h.TestConnection)
|
||||||
|
api.GET("/hosts/test/:id", h.TestConnection)
|
||||||
|
|
||||||
// 主机组管理
|
// 主机组管理
|
||||||
api.GET("/groups", h.ListGroups)
|
api.GET("/groups", h.ListGroups)
|
||||||
@@ -80,18 +73,34 @@ func main() {
|
|||||||
api.POST("/playbooks/execute", h.ExecutePlaybook)
|
api.POST("/playbooks/execute", h.ExecutePlaybook)
|
||||||
api.GET("/playbooks/:name", h.GetPlaybook)
|
api.GET("/playbooks/:name", h.GetPlaybook)
|
||||||
|
|
||||||
|
// Playbook模板
|
||||||
|
api.GET("/templates", h.ListTemplates)
|
||||||
|
api.GET("/templates/categories", h.ListTemplateCategories)
|
||||||
|
api.GET("/templates/:id", h.GetTemplate)
|
||||||
|
api.POST("/templates/deploy", h.CreateFromTemplate)
|
||||||
|
|
||||||
// 命令执行
|
// 命令执行
|
||||||
api.POST("/command/execute", h.ExecuteCommand)
|
api.POST("/command/execute", h.ExecuteCommand)
|
||||||
api.POST("/command/batch", h.BatchExecute)
|
api.POST("/command/batch", h.BatchExecute)
|
||||||
|
|
||||||
|
// 文件分发
|
||||||
|
api.POST("/files/distribute", h.DistributeFile)
|
||||||
|
|
||||||
|
// SSH密钥管理
|
||||||
|
api.GET("/sshkeys", h.ListSSHKeys)
|
||||||
|
api.POST("/sshkeys", h.AddSSHKey)
|
||||||
|
api.DELETE("/sshkeys/:name", h.DeleteSSHKey)
|
||||||
|
|
||||||
// 任务执行
|
// 任务执行
|
||||||
api.GET("/tasks", h.ListTasks)
|
api.GET("/tasks", h.ListTasks)
|
||||||
api.GET("/tasks/:id", h.GetTask)
|
api.GET("/tasks/:id", h.GetTask)
|
||||||
api.GET("/tasks/:id/stream", h.StreamTaskOutput)
|
api.GET("/tasks/:id/stream", h.StreamTaskOutput)
|
||||||
api.DELETE("/tasks/:id", h.CancelTask)
|
api.DELETE("/tasks/:id", h.CancelTask)
|
||||||
|
|
||||||
|
// 系统信息
|
||||||
|
api.GET("/system/info", h.GetSystemInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 前端路由 - 禁止缓存确保始终返回最新版本
|
|
||||||
r.GET("/", func(c *gin.Context) {
|
r.GET("/", func(c *gin.Context) {
|
||||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
c.Header("Pragma", "no-cache")
|
c.Header("Pragma", "no-cache")
|
||||||
@@ -99,12 +108,11 @@ func main() {
|
|||||||
c.File("./web/dist/index.html")
|
c.File("./web/dist/index.html")
|
||||||
})
|
})
|
||||||
|
|
||||||
// 创建必要目录
|
|
||||||
os.MkdirAll(cfg.InventoryDir, 0755)
|
os.MkdirAll(cfg.InventoryDir, 0755)
|
||||||
os.MkdirAll(cfg.PlaybookDir, 0755)
|
os.MkdirAll(cfg.PlaybookDir, 0755)
|
||||||
os.MkdirAll(cfg.LogDir, 0755)
|
os.MkdirAll(cfg.LogDir, 0755)
|
||||||
|
|
||||||
log.Printf("Ansible部署工具启动,监听端口: %s", port)
|
log.Printf("🚀 Ansible Deploy v2.0 启动,监听端口: %s", port)
|
||||||
if err := r.Run(":" + port); err != nil {
|
if err := r.Run(":" + port); err != nil {
|
||||||
log.Fatalf("服务启动失败: %v", err)
|
log.Fatalf("服务启动失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-12
@@ -1,16 +1,16 @@
|
|||||||
# Ansible Deploy 配置文件
|
# Ansible Deploy Pro 配置文件
|
||||||
|
|
||||||
# Ansible安装路径
|
# Ansible安装路径
|
||||||
ansible_path: /usr/bin/ansible
|
ansible_path: /usr/bin/ansible
|
||||||
|
|
||||||
# 资产清单目录
|
# 资产清单目录
|
||||||
inventory_dir: /root/ansible-deploy/inventory
|
inventory_dir: ./inventory
|
||||||
|
|
||||||
# Playbook目录
|
# Playbook目录
|
||||||
playbook_dir: /root/ansible-deploy/playbooks
|
playbook_dir: ./playbooks
|
||||||
|
|
||||||
# 日志目录
|
# 日志目录
|
||||||
log_dir: /root/ansible-deploy/logs
|
log_dir: ./logs
|
||||||
|
|
||||||
# SSH连接超时时间(秒)
|
# SSH连接超时时间(秒)
|
||||||
ssh_timeout: 30
|
ssh_timeout: 30
|
||||||
@@ -20,11 +20,3 @@ max_parallelism: 10
|
|||||||
|
|
||||||
# 输出格式 (json, yaml, plain)
|
# 输出格式 (json, yaml, plain)
|
||||||
callback_plugin: json
|
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
|
|
||||||
|
|||||||
+209
-310
@@ -19,314 +19,335 @@ func NewAnsibleHandler(svc *services.AnsibleService) *AnsibleHandler {
|
|||||||
return &AnsibleHandler{service: svc}
|
return &AnsibleHandler{service: svc}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListHosts 获取主机列表
|
// ===== 主机管理 =====
|
||||||
|
|
||||||
func (h *AnsibleHandler) ListHosts(c *gin.Context) {
|
func (h *AnsibleHandler) ListHosts(c *gin.Context) {
|
||||||
hosts := h.service.ListHosts()
|
hosts := h.service.ListHosts()
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": hosts})
|
||||||
"code": 0,
|
|
||||||
"msg": "success",
|
|
||||||
"data": hosts,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddHost 添加主机
|
|
||||||
func (h *AnsibleHandler) AddHost(c *gin.Context) {
|
func (h *AnsibleHandler) AddHost(c *gin.Context) {
|
||||||
var host models.Host
|
var host models.Host
|
||||||
if err := c.ShouldBindJSON(&host); err != nil {
|
if err := c.ShouldBindJSON(&host); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误: " + err.Error()})
|
||||||
"code": 400,
|
|
||||||
"msg": "参数错误: " + err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.service.AddHost(host); err != nil {
|
if err := h.service.AddHost(host); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
"code": 500,
|
|
||||||
"msg": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "主机添加成功"})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "主机添加成功",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteHost 删除主机
|
|
||||||
func (h *AnsibleHandler) DeleteHost(c *gin.Context) {
|
func (h *AnsibleHandler) DeleteHost(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
if err := h.service.DeleteHost(id); err != nil {
|
if err := h.service.DeleteHost(id); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
"code": 500,
|
|
||||||
"msg": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "主机删除成功"})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "主机删除成功",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateHost 更新主机
|
|
||||||
func (h *AnsibleHandler) UpdateHost(c *gin.Context) {
|
func (h *AnsibleHandler) UpdateHost(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
var host models.Host
|
var host models.Host
|
||||||
if err := c.ShouldBindJSON(&host); err != nil {
|
if err := c.ShouldBindJSON(&host); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误"})
|
||||||
"code": 400,
|
|
||||||
"msg": "参数错误",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.service.UpdateHost(id, host); err != nil {
|
if err := h.service.UpdateHost(id, host); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
"code": 500,
|
|
||||||
"msg": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "主机更新成功"})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "主机更新成功",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestConnection 测试连接
|
|
||||||
func (h *AnsibleHandler) TestConnection(c *gin.Context) {
|
func (h *AnsibleHandler) TestConnection(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
result, err := h.service.TestConnection(id)
|
result, err := h.service.TestConnection(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
"code": 500,
|
|
||||||
"msg": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": result})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "success",
|
|
||||||
"data": result,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListGroups 获取组列表
|
// ===== 主机组管理 =====
|
||||||
|
|
||||||
func (h *AnsibleHandler) ListGroups(c *gin.Context) {
|
func (h *AnsibleHandler) ListGroups(c *gin.Context) {
|
||||||
groups := h.service.ListGroups()
|
groups := h.service.ListGroups()
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": groups})
|
||||||
"code": 0,
|
|
||||||
"msg": "success",
|
|
||||||
"data": groups,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateGroup 创建组
|
|
||||||
func (h *AnsibleHandler) CreateGroup(c *gin.Context) {
|
func (h *AnsibleHandler) CreateGroup(c *gin.Context) {
|
||||||
var group models.HostGroup
|
var group models.HostGroup
|
||||||
if err := c.ShouldBindJSON(&group); err != nil {
|
if err := c.ShouldBindJSON(&group); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误"})
|
||||||
"code": 400,
|
|
||||||
"msg": "参数错误",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.service.CreateGroup(group); err != nil {
|
if err := h.service.CreateGroup(group); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
"code": 500,
|
|
||||||
"msg": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "组创建成功"})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "组创建成功",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteGroup 删除组
|
|
||||||
func (h *AnsibleHandler) DeleteGroup(c *gin.Context) {
|
func (h *AnsibleHandler) DeleteGroup(c *gin.Context) {
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
if err := h.service.DeleteGroup(name); err != nil {
|
if err := h.service.DeleteGroup(name); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
"code": 500,
|
|
||||||
"msg": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "组删除成功"})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "组删除成功",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateGroup 更新组
|
|
||||||
func (h *AnsibleHandler) UpdateGroup(c *gin.Context) {
|
func (h *AnsibleHandler) UpdateGroup(c *gin.Context) {
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
var group models.HostGroup
|
var group models.HostGroup
|
||||||
if err := c.ShouldBindJSON(&group); err != nil {
|
if err := c.ShouldBindJSON(&group); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误"})
|
||||||
"code": 400,
|
|
||||||
"msg": "参数错误",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.service.UpdateGroup(name, group); err != nil {
|
if err := h.service.UpdateGroup(name, group); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
"code": 500,
|
|
||||||
"msg": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "组更新成功"})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "组更新成功",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPlaybooks 列出Playbooks
|
// ===== Playbook管理 =====
|
||||||
|
|
||||||
func (h *AnsibleHandler) ListPlaybooks(c *gin.Context) {
|
func (h *AnsibleHandler) ListPlaybooks(c *gin.Context) {
|
||||||
playbooks := h.service.ListPlaybooks()
|
playbooks := h.service.ListPlaybooks()
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": playbooks})
|
||||||
"code": 0,
|
|
||||||
"msg": "success",
|
|
||||||
"data": playbooks,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPlaybook 获取Playbook详情
|
|
||||||
func (h *AnsibleHandler) GetPlaybook(c *gin.Context) {
|
func (h *AnsibleHandler) GetPlaybook(c *gin.Context) {
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
playbook, err := h.service.GetPlaybook(name)
|
playbook, err := h.service.GetPlaybook(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{
|
c.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": err.Error()})
|
||||||
"code": 404,
|
|
||||||
"msg": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": playbook})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "success",
|
|
||||||
"data": playbook,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecutePlaybook 执行Playbook
|
|
||||||
func (h *AnsibleHandler) ExecutePlaybook(c *gin.Context) {
|
func (h *AnsibleHandler) ExecutePlaybook(c *gin.Context) {
|
||||||
var req models.PlaybookExecutionRequest
|
var req models.PlaybookExecutionRequest
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误: " + err.Error()})
|
||||||
"code": 400,
|
|
||||||
"msg": "参数错误: " + err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
task, err := h.service.ExecutePlaybook(req)
|
task, err := h.service.ExecutePlaybook(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
"code": 500,
|
|
||||||
"msg": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "任务已启动", "taskId": task.ID})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "任务已启动",
|
|
||||||
"taskId": task.ID,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteCommand 执行命令
|
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创建成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
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删除成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
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})
|
||||||
|
}
|
||||||
|
|
||||||
|
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更新成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Playbook模板 =====
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) ListTemplates(c *gin.Context) {
|
||||||
|
templates := services.GetPlaybookTemplates()
|
||||||
|
category := c.Query("category")
|
||||||
|
if category != "" {
|
||||||
|
var filtered []services.PlaybookTemplate
|
||||||
|
for _, t := range templates {
|
||||||
|
if t.Category == category {
|
||||||
|
filtered = append(filtered, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templates = filtered
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": templates})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) ListTemplateCategories(c *gin.Context) {
|
||||||
|
cats := services.GetTemplateCategories()
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": cats})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) GetTemplate(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
for _, t := range services.GetPlaybookTemplates() {
|
||||||
|
if t.ID == id {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": t})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": "模板不存在"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) CreateFromTemplate(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
TemplateID string `json:"template_id" binding:"required"`
|
||||||
|
PlaybookName string `json:"playbook_name" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, t := range services.GetPlaybookTemplates() {
|
||||||
|
if t.ID == req.TemplateID {
|
||||||
|
if err := h.service.CreatePlaybook(req.PlaybookName, t.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创建成功"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": "模板不存在"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 命令执行 =====
|
||||||
|
|
||||||
func (h *AnsibleHandler) ExecuteCommand(c *gin.Context) {
|
func (h *AnsibleHandler) ExecuteCommand(c *gin.Context) {
|
||||||
var req models.CommandRequest
|
var req models.CommandRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误: " + err.Error()})
|
||||||
"code": 400,
|
|
||||||
"msg": "参数错误: " + err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := h.service.ExecuteCommand(req)
|
results, err := h.service.ExecuteCommand(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
"code": 500,
|
|
||||||
"msg": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": results})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "success",
|
|
||||||
"data": results,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchExecute 批量执行
|
|
||||||
func (h *AnsibleHandler) BatchExecute(c *gin.Context) {
|
func (h *AnsibleHandler) BatchExecute(c *gin.Context) {
|
||||||
var req models.CommandRequest
|
var req models.CommandRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误"})
|
||||||
"code": 400,
|
|
||||||
"msg": "参数错误",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result := h.service.BatchExecute(req)
|
result := h.service.BatchExecute(req)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "任务已启动", "data": result})
|
||||||
"code": 0,
|
|
||||||
"msg": "任务已启动",
|
|
||||||
"data": result,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListTasks 获取任务列表
|
// ===== 文件分发 =====
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) DistributeFile(c *gin.Context) {
|
||||||
|
var req models.FileDistributeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Content == "" && req.SourceFile == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "请提供文件内容或源文件路径"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task, err := h.service.DistributeFile(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})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== SSH密钥管理 =====
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) ListSSHKeys(c *gin.Context) {
|
||||||
|
keys := h.service.ListSSHKeys()
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": keys})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) AddSSHKey(c *gin.Context) {
|
||||||
|
var req models.SSHKeyCreateRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "参数错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.service.AddSSHKey(req.Name, req.PrivateKey); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "SSH密钥添加成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AnsibleHandler) DeleteSSHKey(c *gin.Context) {
|
||||||
|
name := c.Param("name")
|
||||||
|
if err := h.service.DeleteSSHKey(name); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "SSH密钥已删除"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 任务管理 =====
|
||||||
|
|
||||||
func (h *AnsibleHandler) ListTasks(c *gin.Context) {
|
func (h *AnsibleHandler) ListTasks(c *gin.Context) {
|
||||||
tasks := h.service.ListTasks()
|
tasks := h.service.ListTasks()
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": tasks})
|
||||||
"code": 0,
|
|
||||||
"msg": "success",
|
|
||||||
"data": tasks,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTask 获取任务详情
|
|
||||||
func (h *AnsibleHandler) GetTask(c *gin.Context) {
|
func (h *AnsibleHandler) GetTask(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
task := h.service.GetTask(id)
|
task := h.service.GetTask(id)
|
||||||
if task == nil {
|
if task == nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{
|
c.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": "任务不存在"})
|
||||||
"code": 404,
|
|
||||||
"msg": "任务不存在",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": task})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "success",
|
|
||||||
"data": task,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamTaskOutput SSE 流式推送任务日志
|
|
||||||
func (h *AnsibleHandler) StreamTaskOutput(c *gin.Context) {
|
func (h *AnsibleHandler) StreamTaskOutput(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
task := h.service.GetTask(id)
|
task := h.service.GetTask(id)
|
||||||
@@ -341,34 +362,26 @@ func (h *AnsibleHandler) StreamTaskOutput(c *gin.Context) {
|
|||||||
|
|
||||||
lastLen := 0
|
lastLen := 0
|
||||||
for {
|
for {
|
||||||
// 检查客户端是否断开
|
|
||||||
if c.Request.Context().Err() != nil {
|
if c.Request.Context().Err() != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
task := h.service.GetTask(id)
|
task := h.service.GetTask(id)
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
output := task.Output
|
output := task.Output
|
||||||
if len(output) > lastLen {
|
if len(output) > lastLen {
|
||||||
// 只发送增量
|
|
||||||
increment := output[lastLen:]
|
increment := output[lastLen:]
|
||||||
lastLen = len(output)
|
lastLen = len(output)
|
||||||
c.SSEvent("log", increment)
|
c.SSEvent("log", increment)
|
||||||
c.Writer.Flush()
|
c.Writer.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
if task.Status != "running" {
|
if task.Status != "running" {
|
||||||
// 任务完成,发送最终状态
|
|
||||||
c.SSEvent("status", task.Status)
|
c.SSEvent("status", task.Status)
|
||||||
c.SSEvent("error", task.Error)
|
c.SSEvent("error", task.Error)
|
||||||
c.Writer.Flush()
|
c.Writer.Flush()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等 500ms 再推送
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(500 * time.Millisecond):
|
case <-time.After(500 * time.Millisecond):
|
||||||
case <-c.Request.Context().Done():
|
case <-c.Request.Context().Done():
|
||||||
@@ -377,132 +390,18 @@ func (h *AnsibleHandler) StreamTaskOutput(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelTask 取消任务
|
|
||||||
func (h *AnsibleHandler) CancelTask(c *gin.Context) {
|
func (h *AnsibleHandler) CancelTask(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
if err := h.service.CancelTask(id); err != nil {
|
if err := h.service.CancelTask(id); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||||
"code": 500,
|
|
||||||
"msg": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "任务已取消"})
|
||||||
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 {
|
func (h *AnsibleHandler) GetSystemInfo(c *gin.Context) {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
info := h.service.GetSystemInfo()
|
||||||
"code": 400,
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": info})
|
||||||
"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连接",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-25
@@ -10,11 +10,12 @@ type Host struct {
|
|||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
SSHKey string `json:"ssh_key,omitempty"`
|
SSHKey string `json:"ssh_key,omitempty"` // SSH密钥名称或路径
|
||||||
AuthType string `json:"auth_type,omitempty"` // password 或 sshkey
|
AuthType string `json:"auth_type,omitempty"` // password 或 sshkey
|
||||||
Groups []string `json:"groups"`
|
Groups []string `json:"groups"`
|
||||||
Vars map[string]string `json:"vars,omitempty"`
|
Vars map[string]string `json:"vars,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
OS string `json:"os,omitempty"`
|
||||||
LastCheck time.Time `json:"last_check,omitempty"`
|
LastCheck time.Time `json:"last_check,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -25,24 +26,11 @@ type HostGroup struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Hosts []string `json:"hosts"`
|
Hosts []string `json:"hosts"`
|
||||||
HostList []Host `json:"host_list,omitempty"` // 组内主机的详细信息
|
HostList []Host `json:"host_list,omitempty"`
|
||||||
Vars map[string]string `json:"vars,omitempty"`
|
Vars map[string]string `json:"vars,omitempty"`
|
||||||
Children []string `json:"children,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定义
|
// Playbook Playbook定义
|
||||||
type Playbook struct {
|
type Playbook struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -69,7 +57,7 @@ type TaskExecution struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Playbook string `json:"playbook"`
|
Playbook string `json:"playbook"`
|
||||||
Hosts []string `json:"hosts"`
|
Hosts []string `json:"hosts"`
|
||||||
Status string `json:"status"` // pending, running, success, failed, cancelled
|
Status string `json:"status"`
|
||||||
StartTime time.Time `json:"start_time"`
|
StartTime time.Time `json:"start_time"`
|
||||||
EndTime time.Time `json:"end_time,omitempty"`
|
EndTime time.Time `json:"end_time,omitempty"`
|
||||||
Progress int `json:"progress"`
|
Progress int `json:"progress"`
|
||||||
@@ -112,15 +100,15 @@ type PlaybookExecutionRequest struct {
|
|||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Hosts []string `json:"hosts"`
|
Hosts []string `json:"hosts"`
|
||||||
ExtraVars map[string]interface{} `json:"extra_vars"`
|
ExtraVars map[string]interface{} `json:"extra_vars"`
|
||||||
Tags []string `json:"tags,omitempty"` // 只执行指定tags
|
Tags []string `json:"tags,omitempty"`
|
||||||
SkipTags []string `json:"skip_tags,omitempty"` // 跳过指定tags
|
SkipTags []string `json:"skip_tags,omitempty"`
|
||||||
Verbose string `json:"verbose,omitempty"` // v, vv, vvv, vvvv
|
Verbose string `json:"verbose,omitempty"`
|
||||||
Diff bool `json:"diff,omitempty"` // 显示文件差异
|
Diff bool `json:"diff,omitempty"`
|
||||||
Check bool `json:"check,omitempty"` // dry-run模式
|
Check bool `json:"check,omitempty"`
|
||||||
Become *bool `json:"become,omitempty"` // 是否提权,nil表示使用playbook默认
|
Become *bool `json:"become,omitempty"`
|
||||||
Forks int `json:"forks,omitempty"` // 并发数
|
Forks int `json:"forks,omitempty"`
|
||||||
Timeout int `json:"timeout,omitempty"` // 超时(秒)
|
Timeout int `json:"timeout,omitempty"`
|
||||||
ExtraArgs string `json:"extra_args,omitempty"` // 自定义额外参数
|
ExtraArgs string `json:"extra_args,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogEntry 日志条目
|
// LogEntry 日志条目
|
||||||
@@ -130,3 +118,43 @@ type LogEntry struct {
|
|||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSHKey SSH密钥
|
||||||
|
type SSHKey struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Fingerprint string `json:"fingerprint,omitempty"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHKeyCreateRequest 创建SSH密钥请求
|
||||||
|
type SSHKeyCreateRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
PrivateKey string `json:"private_key" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileDistributeRequest 文件分发请求
|
||||||
|
type FileDistributeRequest struct {
|
||||||
|
Hosts []string `json:"hosts" binding:"required"`
|
||||||
|
Content string `json:"content"` // 文件内容(与SourceFile二选一)
|
||||||
|
SourceFile string `json:"source_file"` // 服务器上的源文件路径
|
||||||
|
DestPath string `json:"dest_path" binding:"required"` // 目标路径
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Group string `json:"group"`
|
||||||
|
Mode string `json:"mode"` // 如 0644
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemInfo 系统信息
|
||||||
|
type SystemInfo struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
AnsiblePath string `json:"ansible_path"`
|
||||||
|
AnsibleVer string `json:"ansible_version"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
OS string `json:"os"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
HostCount int `json:"host_count"`
|
||||||
|
GroupCount int `json:"group_count"`
|
||||||
|
PlaybookCount int `json:"playbook_count"`
|
||||||
|
TaskCount int `json:"task_count"`
|
||||||
|
Uptime string `json:"uptime"`
|
||||||
|
}
|
||||||
|
|||||||
+295
-218
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"all": {
|
|
||||||
"name": "all",
|
|
||||||
"description": "所有主机",
|
|
||||||
"hosts": null
|
|
||||||
},
|
|
||||||
"ungrouped": {
|
|
||||||
"name": "ungrouped",
|
|
||||||
"description": "未分组主机",
|
|
||||||
"hosts": null
|
|
||||||
},
|
|
||||||
"zebu_user01": {
|
|
||||||
"name": "zebu_user01",
|
|
||||||
"description": "",
|
|
||||||
"hosts": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Ansible Inventory File
|
|
||||||
# Generated by ansible-deploy
|
|
||||||
|
|
||||||
[all]
|
|
||||||
nas ansible_host=10.168.1.209 ansible_user=root
|
|
||||||
|
|
||||||
[zebu_user01]
|
|
||||||
scmp48 ansible_host=172.16.11.46 ansible_user=root
|
|
||||||
scmp46 ansible_host=172.16.11.42 ansible_user=root
|
|
||||||
scmp47 ansible_host=172.16.11.44 ansible_user=root
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4d7a1f03",
|
|
||||||
"name": "scmp48",
|
|
||||||
"ip": "172.16.11.46",
|
|
||||||
"port": 22,
|
|
||||||
"username": "root",
|
|
||||||
"password": "STC#scmp%0818",
|
|
||||||
"groups": [
|
|
||||||
"zebu_user01"
|
|
||||||
],
|
|
||||||
"status": "online",
|
|
||||||
"last_check": "2026-05-13T18:27:19.272543838+08:00",
|
|
||||||
"created_at": "0001-01-01T00:00:00Z",
|
|
||||||
"updated_at": "2026-05-13T18:23:27.433461112+08:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "57884720",
|
|
||||||
"name": "scmp46",
|
|
||||||
"ip": "172.16.11.42",
|
|
||||||
"port": 22,
|
|
||||||
"username": "root",
|
|
||||||
"password": "STC#scmp%0818",
|
|
||||||
"groups": [
|
|
||||||
"zebu_user01"
|
|
||||||
],
|
|
||||||
"status": "online",
|
|
||||||
"last_check": "2026-05-13T18:30:38.103119534+08:00",
|
|
||||||
"created_at": "0001-01-01T00:00:00Z",
|
|
||||||
"updated_at": "2026-05-13T18:28:16.71131472+08:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "5150c740",
|
|
||||||
"name": "scmp47",
|
|
||||||
"ip": "172.16.11.44",
|
|
||||||
"port": 22,
|
|
||||||
"username": "root",
|
|
||||||
"password": "STC#scmp%0818",
|
|
||||||
"groups": [
|
|
||||||
"zebu_user01"
|
|
||||||
],
|
|
||||||
"status": "online",
|
|
||||||
"last_check": "2026-05-13T18:30:33.343216547+08:00",
|
|
||||||
"created_at": "0001-01-01T00:00:00Z",
|
|
||||||
"updated_at": "2026-05-13T18:28:08.656450224+08:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
- name: 部署Node Exporter
|
||||||
|
hosts: "{{ target_hosts | default('all') }}"
|
||||||
|
become: yes
|
||||||
|
vars:
|
||||||
|
exporter_version: "1.7.0"
|
||||||
|
exporter_port: 9100
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: 创建用户
|
||||||
|
user:
|
||||||
|
name: node_exporter
|
||||||
|
system: yes
|
||||||
|
shell: /usr/sbin/nologin
|
||||||
|
create_home: no
|
||||||
|
|
||||||
|
- name: 下载Node Exporter
|
||||||
|
get_url:
|
||||||
|
url: "https://github.com/prometheus/node_exporter/releases/download/v{{ exporter_version }}/node_exporter-{{ exporter_version }}.linux-amd64.tar.gz"
|
||||||
|
dest: /tmp/node_exporter.tar.gz
|
||||||
|
|
||||||
|
- name: 解压
|
||||||
|
unarchive:
|
||||||
|
src: /tmp/node_exporter.tar.gz
|
||||||
|
dest: /tmp
|
||||||
|
remote_src: yes
|
||||||
|
|
||||||
|
- name: 安装二进制
|
||||||
|
copy:
|
||||||
|
src: "/tmp/node_exporter-{{ exporter_version }}.linux-amd64/node_exporter"
|
||||||
|
dest: /usr/local/bin/node_exporter
|
||||||
|
mode: '0755'
|
||||||
|
remote_src: yes
|
||||||
|
|
||||||
|
- name: 创建Systemd服务
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Node Exporter
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=node_exporter
|
||||||
|
ExecStart=/usr/local/bin/node_exporter --web.listen-address=:{{ exporter_port }}
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
dest: /etc/systemd/system/node_exporter.service
|
||||||
|
|
||||||
|
- name: 启动服务
|
||||||
|
systemd:
|
||||||
|
name: node_exporter
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
|
daemon_reload: yes
|
||||||
|
|
||||||
|
- name: 验证
|
||||||
|
shell: curl -s http://localhost:{{ exporter_port }}/metrics | head -5
|
||||||
|
register: metrics
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: 显示状态
|
||||||
|
debug:
|
||||||
|
msg: "Node Exporter运行在端口 {{ exporter_port }}"
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
- name: 部署Python应用
|
||||||
|
hosts: "{{ target_hosts | default('all') }}"
|
||||||
|
become: yes
|
||||||
|
vars:
|
||||||
|
app_name: myapp
|
||||||
|
app_dir: /opt/myapp
|
||||||
|
python_version: python3
|
||||||
|
venv_dir: /opt/myapp/venv
|
||||||
|
gunicorn_port: 8000
|
||||||
|
gunicorn_workers: 4
|
||||||
|
app_user: www-data
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: 安装Python和依赖
|
||||||
|
package:
|
||||||
|
name:
|
||||||
|
- "{{ python_version }}"
|
||||||
|
- "{{ python_version }}-venv"
|
||||||
|
- python3-pip
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: 创建应用目录
|
||||||
|
file:
|
||||||
|
path: "{{ app_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ app_user }}"
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: 创建虚拟环境
|
||||||
|
shell: "{{ python_version }} -m venv {{ venv_dir }}"
|
||||||
|
args:
|
||||||
|
creates: "{{ venv_dir }}/bin/activate"
|
||||||
|
|
||||||
|
- name: 安装Gunicorn
|
||||||
|
pip:
|
||||||
|
name: gunicorn
|
||||||
|
virtualenv: "{{ venv_dir }}"
|
||||||
|
|
||||||
|
- name: 创建Systemd服务
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description={{ app_name }} Gunicorn Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User={{ app_user }}
|
||||||
|
WorkingDirectory={{ app_dir }}
|
||||||
|
ExecStart={{ venv_dir }}/bin/gunicorn --workers {{ gunicorn_workers }} --bind 0.0.0.0:{{ gunicorn_port }} app:app
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
dest: /etc/systemd/system/{{ app_name }}.service
|
||||||
|
notify: Restart App
|
||||||
|
|
||||||
|
- name: 启动服务
|
||||||
|
systemd:
|
||||||
|
name: "{{ app_name }}"
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
|
daemon_reload: yes
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: Restart App
|
||||||
|
systemd:
|
||||||
|
name: "{{ app_name }}"
|
||||||
|
state: restarted
|
||||||
|
daemon_reload: yes
|
||||||
Vendored
+1357
-1595
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user