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 ecb8afd732
commit bdb7d54505
15 changed files with 4011 additions and 2569 deletions
+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})
}