feat(audit): F1+F2 告警静默规则标记完成 + 审计日志CSV导出实现
F1 告警静默规则 [done]: - 后端 alert_silence.go: CRUD + Toggle (onetime/daily/weekly 三模式) - 告警引擎 isSilenced 集成 (alert_engine.go) - 前端 SilenceRuleTab + SilenceDialog - 路由 /alerts/silences 已注册 F2 审计日志导出 [done]: - 重写 ExportAuditLogs: 真正的 CSV 导出 (encoding/csv + UTF-8 BOM) - 支持 module/action/username/startTime/endTime 筛选 - 导出限制 10000 条防溢出,自动记录审计日志 - JSON 格式向后兼容 (format=json) - 前端 exportLogs 传递查询参数 同时包含累积的前期工作: - 目录重构 internal/ -> controller/model/router (v0.55-v0.57) - 后端分层架构 dto/repository/service/utils - 审计模块全功能 (终端审计/操作审计/登录日志) - asciicast v2 录制格式 - SFTP 文件浏览器 - StatStrip 紧凑统计条 - 在线会话自动刷新 - LiveMonitor WebSocket 诊断修复 验证: go build ✅ | npm run build ✅ | Docker 部署 ✅ | 10 回归端点 ✅ | CSV 导出 ✅ | 静默 CRUD ✅
This commit is contained in:
1 parent
e57718998a
commit
77c234b060
314 files changed
+10050
-4065
No files matched your search
@@ -0,0 +1,309 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/model"
|
||||
"ezops/pkg/remoteexec"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RemoteExecHandler 远程执行处理器 — Go 原生 SSH 批量执行,替代 Ansible // Logan.Li
|
||||
type RemoteExecHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewRemoteExecHandler 创建远程执行处理器
|
||||
func NewRemoteExecHandler(db *gorm.DB) *RemoteExecHandler {
|
||||
return &RemoteExecHandler{db: db}
|
||||
}
|
||||
|
||||
// ==================== 远程命令执行 ====================
|
||||
|
||||
// BatchExecRequest 批量执行请求
|
||||
type BatchExecRequest struct {
|
||||
ServerIDs []uint `json:"server_ids" binding:"required"` // 目标服务器 ID 列表
|
||||
Command string `json:"command" binding:"required"` // 要执行的命令
|
||||
Timeout int `json:"timeout"` // 超时(秒),默认 300
|
||||
}
|
||||
|
||||
// BatchExec 批量远程执行命令 // Logan.Li
|
||||
// POST /api/v1/remote-exec/execute
|
||||
func (h *RemoteExecHandler) BatchExec(c *gin.Context) {
|
||||
var req BatchExecRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 查询目标服务器
|
||||
var servers []model.Server
|
||||
if err := h.db.Where("id IN ?", req.ServerIDs).Find(&servers).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询服务器失败"})
|
||||
return
|
||||
}
|
||||
|
||||
if len(servers) == 0 {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "未找到目标服务器"})
|
||||
return
|
||||
}
|
||||
|
||||
// 构建 SSH 配置
|
||||
configs := make([]remoteexec.SSHConfig, 0, len(servers))
|
||||
for _, s := range servers {
|
||||
cfg, err := buildSSHConfig(h.db, s)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400, Message: fmt.Sprintf("服务器 %s 认证配置错误: %v", s.Hostname, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
configs = append(configs, cfg)
|
||||
}
|
||||
|
||||
timeout := req.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 300
|
||||
}
|
||||
|
||||
// 并发执行
|
||||
results := remoteexec.BatchExec(configs, req.Command, time.Duration(timeout)*time.Second, 10)
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: results})
|
||||
}
|
||||
|
||||
// ==================== 文件批量上传 ====================
|
||||
|
||||
// BatchUploadRequest 批量上传请求
|
||||
type BatchUploadRequest struct {
|
||||
ServerIDs []uint `json:"server_ids" binding:"required"` // 目标服务器 ID 列表
|
||||
LocalPath string `json:"local_path" binding:"required"` // AIOps 主机上的文件路径
|
||||
RemotePath string `json:"remote_path" binding:"required"` // 远程目标路径
|
||||
}
|
||||
|
||||
// BatchUpload 批量上传文件到远程服务器 // Logan.Li
|
||||
// POST /api/v1/remote-exec/upload
|
||||
func (h *RemoteExecHandler) BatchUpload(c *gin.Context) {
|
||||
var req BatchUploadRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.LocalPath, "/") {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "local_path 必须是绝对路径"})
|
||||
return
|
||||
}
|
||||
|
||||
// 查询目标服务器
|
||||
var servers []model.Server
|
||||
if err := h.db.Where("id IN ?", req.ServerIDs).Find(&servers).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询服务器失败"})
|
||||
return
|
||||
}
|
||||
|
||||
if len(servers) == 0 {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "未找到目标服务器"})
|
||||
return
|
||||
}
|
||||
|
||||
// 构建 SSH 配置
|
||||
configs := make([]remoteexec.SSHConfig, 0, len(servers))
|
||||
for _, s := range servers {
|
||||
cfg, err := buildSSHConfig(h.db, s)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400, Message: fmt.Sprintf("服务器 %s 认证配置错误: %v", s.Hostname, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
configs = append(configs, cfg)
|
||||
}
|
||||
|
||||
// 并发上传
|
||||
results := remoteexec.BatchUpload(configs, req.LocalPath, req.RemotePath, 5)
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: results})
|
||||
}
|
||||
|
||||
// ==================== 主机匹配 ====================
|
||||
|
||||
// MatchHosts 预览匹配的主机列表(从资产服务器表查询)// Logan.Li
|
||||
// GET /api/v1/remote-exec/hosts?pattern=web*&match_type=prefix
|
||||
func (h *RemoteExecHandler) MatchHosts(c *gin.Context) {
|
||||
pattern := c.Query("pattern")
|
||||
matchType := c.DefaultQuery("match_type", "contains")
|
||||
|
||||
query := h.db.Model(&model.Server{})
|
||||
|
||||
if pattern != "" {
|
||||
switch matchType {
|
||||
case "prefix":
|
||||
query = query.Where("hostname LIKE ? OR ip LIKE ?", pattern+"%", pattern+"%")
|
||||
case "regex":
|
||||
query = query.Where("hostname REGEXP ? OR ip REGEXP ?", pattern, pattern)
|
||||
default: // contains
|
||||
query = query.Where("hostname LIKE ? OR ip LIKE ? OR tags LIKE ?",
|
||||
"%"+pattern+"%", "%"+pattern+"%", "%"+pattern+"%")
|
||||
}
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []model.Server
|
||||
query.Order("hostname").Find(&items)
|
||||
|
||||
// 补充 HasAuth 字段
|
||||
for i := range items {
|
||||
items[i].HasAuth = items[i].Password != "" || items[i].SSHKey != ""
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: items, Total: total}})
|
||||
}
|
||||
|
||||
// ==================== 单机命令执行 ====================
|
||||
|
||||
// ExecOnServer 在指定服务器上执行命令并返回结果
|
||||
// POST /api/v1/remote-exec/server/:id/exec
|
||||
func (h *RemoteExecHandler) ExecOnServer(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的服务器 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Command string `json:"command" binding:"required"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var server model.Server
|
||||
if err := h.db.First(&server, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "服务器不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := buildSSHConfig(h.db, server)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "认证配置错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
timeout := req.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 300
|
||||
}
|
||||
|
||||
result, err := remoteexec.Exec(cfg, req.Command, time.Duration(timeout)*time.Second)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error(), Data: result})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
// buildSSHConfig 从 Server 模型构建 SSHConfig。
|
||||
// 优先使用 server 自带的 Password/SSHKey 字段,
|
||||
// 如果 server 关联了 credential_id,则从凭证表解密获取。
|
||||
func buildSSHConfig(db *gorm.DB, server model.Server) (remoteexec.SSHConfig, error) {
|
||||
cfg := remoteexec.SSHConfig{
|
||||
Host: server.IP,
|
||||
Port: server.Port,
|
||||
User: server.Username,
|
||||
}
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 22
|
||||
}
|
||||
if cfg.User == "" {
|
||||
cfg.User = "root"
|
||||
}
|
||||
|
||||
// 优先使用 server 自带的认证信息
|
||||
if server.AuthType == "key" && server.SSHKey != "" {
|
||||
cfg.AuthType = remoteexec.AuthKey
|
||||
cfg.KeyData = server.SSHKey
|
||||
return cfg, nil
|
||||
}
|
||||
if server.Password != "" {
|
||||
cfg.AuthType = remoteexec.AuthPassword
|
||||
cfg.Password = server.Password
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// 从凭证表查找关联的 SSH 凭据
|
||||
// Server 模型目前没有 CredentialID 字段,通过 tags 或 name 匹配
|
||||
// 未来可扩展 server.credential_id FK
|
||||
|
||||
return cfg, fmt.Errorf("服务器 %s 未配置 SSH 认证(密码或密钥)", server.Hostname)
|
||||
}
|
||||
|
||||
// ==================== 定时任务远程执行(对接 CronJob target_type) ====================
|
||||
|
||||
// ExecuteCronOnTargets 根据 CronJob 的 target_type 在目标主机上执行脚本。
|
||||
// 由 automation_cron.go 调用的公共函数。
|
||||
func ExecuteCronOnTargets(db *gorm.DB, job model.CronJob, scriptType, content string, timeoutSec int) ([]remoteexec.ExecResult, error) {
|
||||
// 解析目标服务器
|
||||
var targetIDs []uint
|
||||
if job.Targets != "" {
|
||||
json.Unmarshal([]byte(job.Targets), &targetIDs)
|
||||
}
|
||||
|
||||
if len(targetIDs) == 0 {
|
||||
return nil, fmt.Errorf("cron job %s has no targets", job.Name)
|
||||
}
|
||||
|
||||
// 查询服务器
|
||||
var servers []model.Server
|
||||
switch job.TargetType {
|
||||
case "server":
|
||||
db.Where("id IN ?", targetIDs).Find(&servers)
|
||||
case "group":
|
||||
db.Where("id IN ?", targetIDs).Find(&servers)
|
||||
case "tag":
|
||||
// 标签匹配 — 通过 tags JSON 字段模糊查询
|
||||
tagStr := fmt.Sprintf("%v", targetIDs)
|
||||
db.Where("tags LIKE ?", "%"+tagStr+"%").Find(&servers)
|
||||
default:
|
||||
db.Where("id IN ?", targetIDs).Find(&servers)
|
||||
}
|
||||
|
||||
if len(servers) == 0 {
|
||||
return nil, fmt.Errorf("no target servers found for cron job %s", job.Name)
|
||||
}
|
||||
|
||||
// 构建 SSH 配置
|
||||
configs := make([]remoteexec.SSHConfig, 0, len(servers))
|
||||
for _, s := range servers {
|
||||
cfg, err := buildSSHConfig(db, s)
|
||||
if err != nil {
|
||||
continue // 跳过认证失败的主机
|
||||
}
|
||||
configs = append(configs, cfg)
|
||||
}
|
||||
|
||||
if len(configs) == 0 {
|
||||
return nil, fmt.Errorf("all target servers have auth errors for cron job %s", job.Name)
|
||||
}
|
||||
|
||||
// 并发执行
|
||||
if timeoutSec <= 0 {
|
||||
timeoutSec = 300
|
||||
}
|
||||
results := remoteexec.BatchExec(configs, content, time.Duration(timeoutSec)*time.Second, 5)
|
||||
return results, nil
|
||||
}
|
||||
Reference in new issue
Block a user