Files
aiops/backend/controller/remote_exec.go
T
attacker 77c234b060 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 ✅
2026-06-25 20:16:30 +08:00

310 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}