feat: v2.0 全面升级 - SSH密钥管理/模板中心/文件分发/全新UI

- 新增SSH密钥库管理(集中托管私钥、自动指纹识别)
- 新增模板中心(24个预置模板,7大分类,一键创建Playbook)
- 新增文件分发功能(内容/文件双模式,权限设置)
- 新增系统信息接口(版本/主机数/运行时间)
- 主机支持密码/SSH密钥双认证方式
- 全新暗色玻璃拟态UI(侧边栏导航、SSE实时日志流)
- 修复UpdateHost ID丢失bug
- 添加.gitignore排除敏感数据
This commit is contained in:
cnbugs
2026-07-30 22:49:13 +08:00
parent bb9d12d311
commit 1e6ac95cdb
14 changed files with 4011 additions and 2569 deletions
+17
View File
@@ -0,0 +1,17 @@
# 二进制文件
ansible-deploy
# 敏感数据 - 主机配置含密码
inventory/hosts.json
inventory/groups.json
inventory/hosts
# SSH私钥
inventory/ssh_keys/
# 日志
logs/
# 系统文件
.DS_Store
*.log
+104 -149
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"`
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-17
View File
@@ -1,17 +0,0 @@
{
"all": {
"name": "all",
"description": "所有主机",
"hosts": null
},
"ungrouped": {
"name": "ungrouped",
"description": "未分组主机",
"hosts": null
},
"zebu_user01": {
"name": "zebu_user01",
"description": "",
"hosts": null
}
}
-10
View File
@@ -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
-62
View File
@@ -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"
}
]
+65
View File
@@ -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 }}"
+70
View File
@@ -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
+1426 -1664
View File
File diff suppressed because it is too large Load Diff