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,96 +1,86 @@
|
||||
# Ansible批量部署工具
|
||||
# ⚡ Ansible Deploy Pro
|
||||
|
||||
基于 Go + Ansible 的批量运维部署系统,支持批量执行命令、Playbook管理、主机分组等功能。
|
||||
基于 Go + Ansible 的企业级批量运维部署平台,支持 SSH 密钥管理、Playbook 模板中心、文件分发、实时日志流等功能。
|
||||
|
||||
## 功能特性
|
||||
## ✨ 功能特性
|
||||
|
||||
- 🌐 **Web界面** - 简洁美观的Web管理界面
|
||||
- 🖥️ **主机管理** - 支持添加、删除、编辑主机
|
||||
- 📁 **分组管理** - 灵活的主机组管理
|
||||
- 💻 **命令执行** - 批量并行/串行执行命令
|
||||
- 📜 **Playbook管理** - 预置Playbook模板,支持快速执行
|
||||
- 📊 **任务跟踪** - 实时任务进度监控
|
||||
- 🔄 **自动刷新** - 数据自动同步
|
||||
- 🎨 **现代化 Web 界面** - 暗色玻璃拟态设计,侧边栏导航,流畅动画
|
||||
- 🖥️ **主机管理** - 支持密码/SSH密钥双认证,连接测试,分组管理
|
||||
- 🔑 **SSH 密钥库** - 集中管理 SSH 私钥,自动指纹识别
|
||||
- 📜 **Playbook 管理** - 在线编辑、变量解析、执行选项配置
|
||||
- 🏪 **模板中心** - 20+ 预置模板(Docker/Nginx/MySQL/Redis/安全加固/监控等),一键创建
|
||||
- 📦 **文件分发** - 批量分发文件/内容到远程主机
|
||||
- 💻 **命令执行** - 批量并行/串行执行命令,快捷命令
|
||||
- 📊 **任务中心** - SSE 实时日志流,进度追踪,任务取消
|
||||
- 📈 **仪表盘** - 主机状态概览、任务统计、系统信息
|
||||
|
||||
## 快速开始
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装Go
|
||||
# 安装 Go
|
||||
curl -fsSL https://go.dev/dl/go1.21.linux-amd64.tar.gz | tar -C /usr/local -xzf -
|
||||
|
||||
# 安装Ansible
|
||||
# 安装 Ansible
|
||||
pip install ansible
|
||||
|
||||
# 安装SSH
|
||||
# 安装 SSH
|
||||
apt install openssh-client # Debian/Ubuntu
|
||||
yum install openssh-clients # CentOS/RHEL
|
||||
```
|
||||
|
||||
### 2. 构建项目
|
||||
### 2. 构建
|
||||
|
||||
```bash
|
||||
cd /root/ansible-deploy
|
||||
cd ansible-deploy
|
||||
go mod tidy
|
||||
go build -o ansible-deploy cmd/main.go
|
||||
```
|
||||
|
||||
### 3. 配置SSH免密登录
|
||||
### 3. 启动
|
||||
|
||||
```bash
|
||||
# 生成SSH密钥
|
||||
ssh-keygen -t rsa
|
||||
|
||||
# 复制到目标主机
|
||||
ssh-copy-id user@hostname
|
||||
```
|
||||
|
||||
### 4. 启动服务
|
||||
|
||||
```bash
|
||||
# 默认端口8080
|
||||
# 默认端口 8080
|
||||
./ansible-deploy
|
||||
|
||||
# 自定义端口
|
||||
./ansible-deploy -port 9000
|
||||
|
||||
# 自定义配置
|
||||
./ansible-deploy -config /path/to/config.yaml
|
||||
# 自定义端口和配置
|
||||
./ansible-deploy -port 9000 -config /path/to/config.yaml
|
||||
```
|
||||
|
||||
### 5. 访问Web界面
|
||||
### 4. 访问
|
||||
|
||||
打开浏览器访问: `http://localhost:8080`
|
||||
打开浏览器: `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
|
||||
```
|
||||
ansible-deploy/
|
||||
├── cmd/
|
||||
│ └── main.go # 主程序入口
|
||||
├── config/
|
||||
│ └── config.yaml # 配置文件
|
||||
├── internal/
|
||||
│ ├── handlers/
|
||||
│ │ └── handlers.go # HTTP 处理器
|
||||
│ ├── models/
|
||||
│ │ └── models.go # 数据模型
|
||||
│ └── services/
|
||||
│ ├── ansible.go # Ansible 核心服务
|
||||
│ ├── config.go # 配置加载
|
||||
│ └── templates.go # Playbook 模板库
|
||||
├── web/dist/
|
||||
│ └── index.html # 前端界面
|
||||
├── playbooks/ # Playbook 目录
|
||||
├── inventory/ # 资产清单
|
||||
│ ├── hosts.json # 主机数据
|
||||
│ ├── groups.json # 分组数据
|
||||
│ └── ssh_keys/ # SSH 密钥存储
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## API接口
|
||||
## 🔌 API 接口
|
||||
|
||||
### 主机管理
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/hosts` | 获取主机列表 |
|
||||
@@ -100,113 +90,78 @@ max_parallelism: 10
|
||||
| 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/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 |
|
||||
|
||||
### 模板中心
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/templates` | 列出模板 |
|
||||
| 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` | 任务列表 |
|
||||
| GET | `/api/tasks/:id` | 任务详情 |
|
||||
| GET | `/api/tasks/:id/stream` | SSE 实时日志 |
|
||||
| DELETE | `/api/tasks/:id` | 取消任务 |
|
||||
|
||||
## 使用示例
|
||||
### 系统
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/system/info` | 系统信息 |
|
||||
|
||||
### 添加主机
|
||||
## 📋 预置模板
|
||||
|
||||
```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
|
||||
| 分类 | 模板 | 说明 |
|
||||
|------|------|------|
|
||||
| 🖥️ 系统管理 | 系统包更新 | 自动更新软件包 |
|
||||
| | 系统信息采集 | 采集 OS/硬件/网络信息 |
|
||||
| | 资源监控检查 | CPU/内存/磁盘告警 |
|
||||
| | 日志清理 | 清理日志和临时文件 |
|
||||
| | 时间同步(NTP) | 配置 chrony 时间同步 |
|
||||
| 🌐 Web服务 | 部署 Nginx | 安装配置 Nginx + 反向代理 |
|
||||
| | 部署 Apache | 安装配置 Apache |
|
||||
| 🗄️ 数据库 | 部署 MySQL | 安装 MySQL + 基础优化 |
|
||||
| | 部署 Redis | 安装 Redis + 内存配置 |
|
||||
| | 部署 PostgreSQL | 安装 PostgreSQL |
|
||||
| 🐳 容器化 | 部署 Docker | Docker CE + 镜像加速 |
|
||||
| | 部署 Portainer | 容器管理面板 |
|
||||
| 🔧 DevOps | 部署 Node.js | Node.js + PM2 |
|
||||
| | 部署 Python 应用 | Gunicorn + Systemd |
|
||||
| | 数据备份 | 定时备份 + 自动清理 |
|
||||
| 🔒 安全加固 | 安全加固 | SSH加固 + 防火墙 + fail2ban |
|
||||
| | SSL 证书部署 | Let's Encrypt 自动申请 |
|
||||
| 📊 监控告警 | 部署 Node Exporter | Prometheus 指标采集 |
|
||||
| | 部署 Grafana | 可视化监控面板 |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+20
-12
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
var (
|
||||
configPath string
|
||||
port string
|
||||
port string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -23,23 +23,17 @@ func init() {
|
||||
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")
|
||||
@@ -51,11 +45,9 @@ func main() {
|
||||
c.Next()
|
||||
})
|
||||
|
||||
// 静态文件
|
||||
r.Static("/static", "./web/dist")
|
||||
r.Static("/assets", "./web/dist/assets")
|
||||
|
||||
// API路由
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 主机管理
|
||||
@@ -64,6 +56,7 @@ func main() {
|
||||
api.DELETE("/hosts/:id", h.DeleteHost)
|
||||
api.PUT("/hosts/:id", h.UpdateHost)
|
||||
api.POST("/hosts/test/:id", h.TestConnection)
|
||||
api.GET("/hosts/test/:id", h.TestConnection)
|
||||
|
||||
// 主机组管理
|
||||
api.GET("/groups", h.ListGroups)
|
||||
@@ -80,18 +73,34 @@ func main() {
|
||||
api.POST("/playbooks/execute", h.ExecutePlaybook)
|
||||
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/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/:id", h.GetTask)
|
||||
api.GET("/tasks/:id/stream", h.StreamTaskOutput)
|
||||
api.DELETE("/tasks/:id", h.CancelTask)
|
||||
|
||||
// 系统信息
|
||||
api.GET("/system/info", h.GetSystemInfo)
|
||||
}
|
||||
|
||||
// 前端路由 - 禁止缓存确保始终返回最新版本
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Header("Pragma", "no-cache")
|
||||
@@ -99,12 +108,11 @@ func main() {
|
||||
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)
|
||||
log.Printf("🚀 Ansible Deploy v2.0 启动,监听端口: %s", port)
|
||||
if err := r.Run(":" + port); err != nil {
|
||||
log.Fatalf("服务启动失败: %v", err)
|
||||
}
|
||||
|
||||
+4
-12
@@ -1,16 +1,16 @@
|
||||
# Ansible Deploy 配置文件
|
||||
# Ansible Deploy Pro 配置文件
|
||||
|
||||
# Ansible安装路径
|
||||
ansible_path: /usr/bin/ansible
|
||||
|
||||
# 资产清单目录
|
||||
inventory_dir: /root/ansible-deploy/inventory
|
||||
inventory_dir: ./inventory
|
||||
|
||||
# Playbook目录
|
||||
playbook_dir: /root/ansible-deploy/playbooks
|
||||
playbook_dir: ./playbooks
|
||||
|
||||
# 日志目录
|
||||
log_dir: /root/ansible-deploy/logs
|
||||
log_dir: ./logs
|
||||
|
||||
# SSH连接超时时间(秒)
|
||||
ssh_timeout: 30
|
||||
@@ -20,11 +20,3 @@ 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
|
||||
|
||||
+209
-310
@@ -19,314 +19,335 @@ 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,
|
||||
})
|
||||
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(),
|
||||
})
|
||||
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(),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||
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) {
|
||||
id := c.Param("id")
|
||||
if err := h.service.DeleteHost(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||
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) {
|
||||
id := c.Param("id")
|
||||
var host models.Host
|
||||
if err := c.ShouldBindJSON(&host); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "参数错误",
|
||||
})
|
||||
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(),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||
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) {
|
||||
id := c.Param("id")
|
||||
result, err := h.service.TestConnection(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||
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) {
|
||||
groups := h.service.ListGroups()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": groups,
|
||||
})
|
||||
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": "参数错误",
|
||||
})
|
||||
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(),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||
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) {
|
||||
name := c.Param("name")
|
||||
if err := h.service.DeleteGroup(name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"msg": err.Error(),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||
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) {
|
||||
name := c.Param("name")
|
||||
var group models.HostGroup
|
||||
if err := c.ShouldBindJSON(&group); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "参数错误",
|
||||
})
|
||||
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(),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||
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) {
|
||||
playbooks := h.service.ListPlaybooks()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": playbooks,
|
||||
})
|
||||
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(),
|
||||
})
|
||||
c.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": err.Error()})
|
||||
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) {
|
||||
var req models.PlaybookExecutionRequest
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "参数错误: " + err.Error(),
|
||||
})
|
||||
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(),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||
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) {
|
||||
var req models.CommandRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "参数错误: " + err.Error(),
|
||||
})
|
||||
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(),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||
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) {
|
||||
var req models.CommandRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "参数错误",
|
||||
})
|
||||
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,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{"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) {
|
||||
tasks := h.service.ListTasks()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": tasks,
|
||||
})
|
||||
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": "任务不存在",
|
||||
})
|
||||
c.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": "任务不存在"})
|
||||
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) {
|
||||
id := c.Param("id")
|
||||
task := h.service.GetTask(id)
|
||||
@@ -341,34 +362,26 @@ func (h *AnsibleHandler) StreamTaskOutput(c *gin.Context) {
|
||||
|
||||
lastLen := 0
|
||||
for {
|
||||
// 检查客户端是否断开
|
||||
if c.Request.Context().Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
task := h.service.GetTask(id)
|
||||
if task == nil {
|
||||
return
|
||||
}
|
||||
|
||||
output := task.Output
|
||||
if len(output) > lastLen {
|
||||
// 只发送增量
|
||||
increment := output[lastLen:]
|
||||
lastLen = len(output)
|
||||
c.SSEvent("log", increment)
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
if task.Status != "running" {
|
||||
// 任务完成,发送最终状态
|
||||
c.SSEvent("status", task.Status)
|
||||
c.SSEvent("error", task.Error)
|
||||
c.Writer.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
// 等 500ms 再推送
|
||||
select {
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
case <-c.Request.Context().Done():
|
||||
@@ -377,132 +390,18 @@ func (h *AnsibleHandler) StreamTaskOutput(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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(),
|
||||
})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": err.Error()})
|
||||
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 {
|
||||
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连接",
|
||||
})
|
||||
func (h *AnsibleHandler) GetSystemInfo(c *gin.Context) {
|
||||
info := h.service.GetSystemInfo()
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": info})
|
||||
}
|
||||
|
||||
+104
-76
@@ -4,53 +4,41 @@ 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"`
|
||||
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"` // SSH密钥名称或路径
|
||||
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"`
|
||||
Status string `json:"status"`
|
||||
OS string `json:"os,omitempty"`
|
||||
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"` // 组内主机的详细信息
|
||||
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"`
|
||||
Children []string `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
// Playbook Playbook定义
|
||||
type Playbook struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Description string `json:"description"`
|
||||
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"`
|
||||
Hosts string `json:"hosts"`
|
||||
Tasks []Task `json:"tasks"`
|
||||
}
|
||||
|
||||
// Task 任务定义
|
||||
@@ -65,62 +53,62 @@ type Task struct {
|
||||
|
||||
// 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"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Playbook string `json:"playbook"`
|
||||
Hosts []string `json:"hosts"`
|
||||
Status string `json:"status"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
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"` // 自定义额外参数
|
||||
Name string `json:"name" binding:"required"`
|
||||
Hosts []string `json:"hosts"`
|
||||
ExtraVars map[string]interface{} `json:"extra_vars"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
SkipTags []string `json:"skip_tags,omitempty"`
|
||||
Verbose string `json:"verbose,omitempty"`
|
||||
Diff bool `json:"diff,omitempty"`
|
||||
Check bool `json:"check,omitempty"`
|
||||
Become *bool `json:"become,omitempty"`
|
||||
Forks int `json:"forks,omitempty"`
|
||||
Timeout int `json:"timeout,omitempty"`
|
||||
ExtraArgs string `json:"extra_args,omitempty"`
|
||||
}
|
||||
|
||||
// LogEntry 日志条目
|
||||
@@ -130,3 +118,43 @@ type LogEntry struct {
|
||||
Host string `json:"host"`
|
||||
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"`
|
||||
}
|
||||
|
||||
+334
-257
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
+1426
-1664
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user