告警中心 - 新增统一告警(alert_unified)、监控告警规则(monitor_alert_rule)、指标目录(alert_metric_catalog) - 通知策略与消息分发(alert_notify_policy/dispatch/message)、静默规则 - 告警引擎按阈值/SNMP 拆分(alert_engine_monitor/snmp) 监控采集 - SNMP 常量/模板与历史指标(snmp templates/constants、snmp_metrics_history、snmp_collect) - 中间件监控 Redis 采集与种子(middleware_monitor_redis/seed) - 站点探测 pkg(siteprobe) + 站点视图(site_view) 自动化任务 - cron 调度器(cronscheduler)、脚本运行器(scriptrunner)、系统任务(systask)及运行记录(cron_job_run、system_task) 审计与凭证 - 审计设置(audit_setting) + 历史清理(auditcleaner) 前端 - 监控页面(host/middleware/site/snmp)拆分为子模块并大屏化 - 告警中心多 Tab(规则/指标目录/通知策略/静默)、任务管理组件重构、Agent CLI Markdown 渲染 - 移除旧脚本卡片/详情/日志组件,统一为 ScriptTable/ScriptCodeEditor/TerminalOutput Co-Authored-By: Claude <noreply@anthropic.com>
543 lines
14 KiB
Go
543 lines
14 KiB
Go
// Package service 提供业务逻辑层 — 编排 repository、处理加密/校验/转换
|
|
// Handler 不直接调用 repository,通过 service 调用
|
|
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
|
|
"ezops/dto"
|
|
"ezops/model"
|
|
"ezops/repository"
|
|
"ezops/pkg/crypto"
|
|
ezredis "ezops/pkg/redis"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ━━━ Server Service ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
type ServerService struct {
|
|
repo *repository.ServerRepository
|
|
cache ezredis.Cache
|
|
crypt *crypto.SecretBox
|
|
}
|
|
|
|
func NewServerService(repo *repository.ServerRepository, cache ezredis.Cache, crypt *crypto.SecretBox) *ServerService {
|
|
return &ServerService{repo: repo, cache: cache, crypt: crypt}
|
|
}
|
|
|
|
// ListServers 获取服务器列表
|
|
func (s *ServerService) ListServers(keyword, status, regionID string, page, pageSize int) ([]model.Server, int64, error) {
|
|
list, total, err := s.repo.List(keyword, status, regionID, page, pageSize)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
// 填充计算字段
|
|
for i := range list {
|
|
list[i].HasAuth = list[i].Password != "" || list[i].SSHKey != ""
|
|
}
|
|
return list, total, nil
|
|
}
|
|
|
|
// GetServer 获取服务器详情
|
|
func (s *ServerService) GetServer(id uint) (*model.Server, error) {
|
|
server, err := s.repo.GetByID(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
server.HasAuth = server.Password != "" || server.SSHKey != ""
|
|
return server, nil
|
|
}
|
|
|
|
// CreateServer 创建服务器 — 加密凭证
|
|
func (s *ServerService) CreateServer(req *dto.CreateServerRequest) (*model.Server, error) {
|
|
srv := &model.Server{
|
|
Hostname: req.Hostname,
|
|
IP: req.IP,
|
|
PublicIP: req.PublicIP,
|
|
Port: req.Port,
|
|
Username: req.Username,
|
|
AuthType: req.AuthType,
|
|
OS: req.OS,
|
|
OSVersion: req.OSVersion,
|
|
Arch: req.Arch,
|
|
CPU: req.CPU,
|
|
Memory: req.Memory,
|
|
Disk: req.Disk,
|
|
RegionID: req.RegionID,
|
|
CloudID: req.CloudID,
|
|
Tags: req.Tags,
|
|
Description: req.Description,
|
|
}
|
|
if req.Port == 0 {
|
|
srv.Port = 22
|
|
}
|
|
if req.Username == "" {
|
|
srv.Username = "root"
|
|
}
|
|
if req.AuthType == "" {
|
|
srv.AuthType = "password"
|
|
}
|
|
|
|
// 加密密码和 SSH 密钥
|
|
if req.Password != "" {
|
|
enc, err := s.crypt.Encrypt(req.Password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("加密密码失败: %w", err)
|
|
}
|
|
srv.Password = enc
|
|
}
|
|
if req.SSHKey != "" {
|
|
enc, err := s.crypt.Encrypt(req.SSHKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("加密 SSH 密钥失败: %w", err)
|
|
}
|
|
srv.SSHKey = enc
|
|
}
|
|
|
|
if err := s.repo.Create(srv); err != nil {
|
|
return nil, err
|
|
}
|
|
return srv, nil
|
|
}
|
|
|
|
// UpdateServer 部分更新服务器
|
|
func (s *ServerService) UpdateServer(id uint, req *dto.UpdateServerRequest) error {
|
|
updates := make(map[string]interface{})
|
|
|
|
if req.Hostname != nil {
|
|
updates["hostname"] = *req.Hostname
|
|
}
|
|
if req.IP != nil {
|
|
updates["ip"] = *req.IP
|
|
}
|
|
if req.PublicIP != nil {
|
|
updates["public_ip"] = *req.PublicIP
|
|
}
|
|
if req.Port != nil {
|
|
updates["port"] = *req.Port
|
|
}
|
|
if req.Username != nil {
|
|
updates["username"] = *req.Username
|
|
}
|
|
if req.AuthType != nil {
|
|
updates["auth_type"] = *req.AuthType
|
|
}
|
|
if req.OS != nil {
|
|
updates["os"] = *req.OS
|
|
}
|
|
if req.OSVersion != nil {
|
|
updates["os_version"] = *req.OSVersion
|
|
}
|
|
if req.Arch != nil {
|
|
updates["arch"] = *req.Arch
|
|
}
|
|
if req.CPU != nil {
|
|
updates["cpu"] = *req.CPU
|
|
}
|
|
if req.Memory != nil {
|
|
updates["memory"] = *req.Memory
|
|
}
|
|
if req.Disk != nil {
|
|
updates["disk"] = *req.Disk
|
|
}
|
|
if req.Status != nil {
|
|
updates["status"] = *req.Status
|
|
}
|
|
if req.RegionID != nil {
|
|
updates["region_id"] = *req.RegionID
|
|
}
|
|
if req.CloudID != nil {
|
|
updates["cloud_id"] = *req.CloudID
|
|
}
|
|
if req.Tags != nil {
|
|
updates["tags"] = *req.Tags
|
|
}
|
|
if req.Description != nil {
|
|
updates["description"] = *req.Description
|
|
}
|
|
|
|
// 加密凭据字段
|
|
if req.Password != nil && *req.Password != "" {
|
|
enc, err := s.crypt.Encrypt(*req.Password)
|
|
if err != nil {
|
|
return fmt.Errorf("加密密码失败: %w", err)
|
|
}
|
|
updates["password"] = enc
|
|
}
|
|
if req.SSHKey != nil && *req.SSHKey != "" {
|
|
enc, err := s.crypt.Encrypt(*req.SSHKey)
|
|
if err != nil {
|
|
return fmt.Errorf("加密 SSH 密钥失败: %w", err)
|
|
}
|
|
updates["ssh_key"] = enc
|
|
}
|
|
|
|
if len(updates) == 0 {
|
|
return nil
|
|
}
|
|
return s.repo.UpdateFields(id, updates)
|
|
}
|
|
|
|
// DeleteServer 删除服务器
|
|
func (s *ServerService) DeleteServer(id uint) error {
|
|
return s.repo.Delete(id)
|
|
}
|
|
|
|
// GetDashboardStats 获取仪表盘统计
|
|
func (s *ServerService) GetDashboardStats() (*model.DashboardStats, error) {
|
|
var stats model.DashboardStats
|
|
|
|
stats.ServerStats.Total = s.repo.CountByStatus("")
|
|
stats.ServerStats.Running = s.repo.CountByStatus("running")
|
|
stats.ServerStats.Stopped = s.repo.CountByStatus("stopped")
|
|
stats.ServerStats.Maintenance = s.repo.CountByStatus("maintenance")
|
|
stats.ServerStats.Error = s.repo.CountByStatus("error")
|
|
|
|
stats.ServerStats.AvgCPU = math.Round(s.repo.AvgMetric("cpu_usage")*100) / 100
|
|
stats.ServerStats.AvgMemory = math.Round(s.repo.AvgMetric("memory_usage")*100) / 100
|
|
stats.ServerStats.AvgDisk = math.Round(s.repo.AvgMetric("disk_usage")*100) / 100
|
|
|
|
return &stats, nil
|
|
}
|
|
|
|
// ━━━ Credential Service ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
type CredentialService struct {
|
|
repo *repository.CredentialRepository
|
|
crypt *crypto.SecretBox
|
|
}
|
|
|
|
func NewCredentialService(repo *repository.CredentialRepository, crypt *crypto.SecretBox) *CredentialService {
|
|
return &CredentialService{repo: repo, crypt: crypt}
|
|
}
|
|
|
|
// List 分页查询
|
|
func (s *CredentialService) List(keyword, secretType string, page, pageSize int) ([]model.Credential, int64, error) {
|
|
return s.repo.List(keyword, secretType, page, pageSize)
|
|
}
|
|
|
|
// GetByID 获取详情
|
|
func (s *CredentialService) GetByID(id uint) (*model.Credential, error) {
|
|
return s.repo.GetByID(id)
|
|
}
|
|
|
|
// buildCredentialContent 根据请求构建加密内容
|
|
func (s *CredentialService) buildCredentialContent(req *dto.CreateCredentialRequest) (string, string, error) {
|
|
switch req.SecretType {
|
|
case "ssh":
|
|
data := map[string]interface{}{
|
|
"private_key": req.SSHPrivateKey,
|
|
"public_key": req.SSHPublicKey,
|
|
"ssh_user": req.SSHUser,
|
|
"ssh_port": req.SSHPort,
|
|
"password": req.SSHPassword,
|
|
}
|
|
b, _ := json.Marshal(data)
|
|
return string(b), fmt.Sprintf("SSH %s@%d", req.SSHUser, req.SSHPort), nil
|
|
case "docker":
|
|
data := map[string]string{
|
|
"server": req.DockerServer,
|
|
"username": req.DockerUsername,
|
|
"password": req.DockerPassword,
|
|
}
|
|
b, _ := json.Marshal(data)
|
|
return string(b), req.DockerServer, nil
|
|
case "tls":
|
|
data := map[string]string{"cert": req.TLSCert, "key": req.TLSKey, "ca": req.TLSCA}
|
|
b, _ := json.Marshal(data)
|
|
return string(b), "TLS 证书已配置", nil
|
|
case "password":
|
|
return req.PlainPassword, "密码", nil
|
|
case "env":
|
|
return req.EnvVars, "环境变量", nil
|
|
default:
|
|
return "", "", fmt.Errorf("不支持的凭据类型: %s", req.SecretType)
|
|
}
|
|
}
|
|
|
|
// Create 创建凭据
|
|
func (s *CredentialService) Create(req *dto.CreateCredentialRequest) (*model.Credential, error) {
|
|
content, usage, err := s.buildCredentialContent(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
encContent, err := s.crypt.Encrypt(content)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("加密失败: %w", err)
|
|
}
|
|
|
|
cred := &model.Credential{
|
|
Name: req.Name,
|
|
SecretType: req.SecretType,
|
|
Content: encContent,
|
|
Summary: usage,
|
|
Description: req.Description,
|
|
}
|
|
if err := s.repo.Create(cred); err != nil {
|
|
return nil, err
|
|
}
|
|
return cred, nil
|
|
}
|
|
|
|
// buildUpdateContent 构建更新凭据的加密内容
|
|
func (s *CredentialService) buildUpdateContent(req *dto.UpdateCredentialRequest, existingSecretType string) (string, string, error) {
|
|
st := existingSecretType
|
|
switch st {
|
|
case "ssh":
|
|
data := map[string]interface{}{}
|
|
if req.SSHPrivateKey != nil {
|
|
data["private_key"] = *req.SSHPrivateKey
|
|
}
|
|
if req.SSHPublicKey != nil {
|
|
data["public_key"] = *req.SSHPublicKey
|
|
}
|
|
if req.SSHUser != nil {
|
|
data["ssh_user"] = *req.SSHUser
|
|
}
|
|
if req.SSHPort != nil {
|
|
data["ssh_port"] = *req.SSHPort
|
|
}
|
|
if req.SSHPassword != nil {
|
|
data["password"] = *req.SSHPassword
|
|
}
|
|
if len(data) == 0 {
|
|
return "", "", nil
|
|
}
|
|
b, _ := json.Marshal(data)
|
|
return string(b), "", nil
|
|
case "docker":
|
|
data := map[string]string{}
|
|
if req.DockerServer != nil {
|
|
data["server"] = *req.DockerServer
|
|
}
|
|
if req.DockerUsername != nil {
|
|
data["username"] = *req.DockerUsername
|
|
}
|
|
if req.DockerPassword != nil {
|
|
data["password"] = *req.DockerPassword
|
|
}
|
|
b, _ := json.Marshal(data)
|
|
return string(b), "", nil
|
|
case "tls":
|
|
data := map[string]string{}
|
|
if req.TLSCert != nil {
|
|
data["cert"] = *req.TLSCert
|
|
}
|
|
if req.TLSKey != nil {
|
|
data["key"] = *req.TLSKey
|
|
}
|
|
if req.TLSCA != nil {
|
|
data["ca"] = *req.TLSCA
|
|
}
|
|
b, _ := json.Marshal(data)
|
|
return string(b), "", nil
|
|
case "password":
|
|
if req.PlainPassword != nil {
|
|
return *req.PlainPassword, "", nil
|
|
}
|
|
return "", "", nil
|
|
case "env":
|
|
if req.EnvVars != nil {
|
|
return *req.EnvVars, "", nil
|
|
}
|
|
return "", "", nil
|
|
}
|
|
return "", "", nil
|
|
}
|
|
|
|
// Update 更新凭据
|
|
func (s *CredentialService) Update(id uint, req *dto.UpdateCredentialRequest) error {
|
|
existing, err := s.repo.GetByID(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
content, _, err := s.buildUpdateContent(req, existing.SecretType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
updates := make(map[string]interface{})
|
|
if req.Name != nil {
|
|
updates["name"] = *req.Name
|
|
}
|
|
if req.Description != nil {
|
|
updates["description"] = *req.Description
|
|
}
|
|
if content != "" {
|
|
enc, err := s.crypt.Encrypt(content)
|
|
if err != nil {
|
|
return fmt.Errorf("加密失败: %w", err)
|
|
}
|
|
updates["secret_data"] = enc
|
|
}
|
|
|
|
if len(updates) == 0 {
|
|
return nil
|
|
}
|
|
return s.repo.UpdateFields(id, updates)
|
|
}
|
|
|
|
// Delete 删除凭据
|
|
func (s *CredentialService) Delete(id uint) error {
|
|
return s.repo.Delete(id)
|
|
}
|
|
|
|
// ━━━ Alert Service ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
type AlertContactService struct {
|
|
repo *repository.AlertContactRepository
|
|
}
|
|
|
|
func NewAlertContactService(repo *repository.AlertContactRepository) *AlertContactService {
|
|
return &AlertContactService{repo: repo}
|
|
}
|
|
|
|
func (s *AlertContactService) List(keyword string, page, pageSize int) ([]model.AlertContact, int64, error) {
|
|
return s.repo.List(keyword, page, pageSize)
|
|
}
|
|
|
|
func (s *AlertContactService) GetByID(id uint) (*model.AlertContact, error) {
|
|
return s.repo.GetByID(id)
|
|
}
|
|
|
|
func (s *AlertContactService) Create(req *dto.CreateAlertContactRequest) (*model.AlertContact, error) {
|
|
c := &model.AlertContact{
|
|
Name: req.Name,
|
|
ChannelType: req.ChannelType,
|
|
WebhookURL: req.WebhookURL,
|
|
Phone: req.Phone,
|
|
Email: req.Email,
|
|
}
|
|
if err := s.repo.Create(c); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func (s *AlertContactService) Update(id uint, req *dto.UpdateAlertContactRequest) error {
|
|
updates := make(map[string]interface{})
|
|
if req.Name != nil {
|
|
updates["name"] = *req.Name
|
|
}
|
|
if req.ChannelType != nil {
|
|
updates["channel_type"] = *req.ChannelType
|
|
}
|
|
if req.WebhookURL != nil {
|
|
updates["webhook_url"] = *req.WebhookURL
|
|
}
|
|
if req.Phone != nil {
|
|
updates["phone"] = *req.Phone
|
|
}
|
|
if req.Email != nil {
|
|
updates["email"] = *req.Email
|
|
}
|
|
if len(updates) == 0 {
|
|
return nil
|
|
}
|
|
// AlertContact 没有 UpdateFields,直接 Save
|
|
c, err := s.repo.GetByID(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if req.Name != nil {
|
|
c.Name = *req.Name
|
|
}
|
|
if req.ChannelType != nil {
|
|
c.ChannelType = *req.ChannelType
|
|
}
|
|
if req.WebhookURL != nil {
|
|
c.WebhookURL = *req.WebhookURL
|
|
}
|
|
if req.Phone != nil {
|
|
c.Phone = *req.Phone
|
|
}
|
|
if req.Email != nil {
|
|
c.Email = *req.Email
|
|
}
|
|
return s.repo.Update(c)
|
|
}
|
|
|
|
func (s *AlertContactService) Delete(id uint) error {
|
|
return s.repo.Delete(id)
|
|
}
|
|
|
|
// AlertContactGroup Service
|
|
type AlertContactGroupService struct {
|
|
repo *repository.AlertContactGroupRepository
|
|
}
|
|
|
|
func NewAlertContactGroupService(repo *repository.AlertContactGroupRepository) *AlertContactGroupService {
|
|
return &AlertContactGroupService{repo: repo}
|
|
}
|
|
|
|
func (s *AlertContactGroupService) List(keyword string, page, pageSize int) ([]model.AlertContactGroup, int64, error) {
|
|
return s.repo.List(keyword, page, pageSize)
|
|
}
|
|
|
|
func (s *AlertContactGroupService) GetByID(id uint) (*model.AlertContactGroup, error) {
|
|
return s.repo.GetByID(id)
|
|
}
|
|
|
|
func (s *AlertContactGroupService) Create(req *dto.CreateAlertContactGroupRequest) (*model.AlertContactGroup, error) {
|
|
g := &model.AlertContactGroup{GroupName: req.GroupName, Notes: req.Notes}
|
|
if len(req.ContactIDs) > 0 {
|
|
g.ContactIDs = toJSONString(req.ContactIDs)
|
|
}
|
|
if err := s.repo.Create(g); err != nil {
|
|
return nil, err
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
func (s *AlertContactGroupService) Update(id uint, req *dto.UpdateAlertContactGroupRequest) error {
|
|
updates := make(map[string]interface{})
|
|
if req.GroupName != nil {
|
|
updates["group_name"] = *req.GroupName
|
|
}
|
|
if req.Notes != nil {
|
|
updates["notes"] = *req.Notes
|
|
}
|
|
if req.ContactIDs != nil {
|
|
updates["contact_ids"] = toJSONString(*req.ContactIDs)
|
|
}
|
|
if len(updates) == 0 {
|
|
return nil
|
|
}
|
|
return s.repo.UpdateFields(id, updates)
|
|
}
|
|
|
|
func (s *AlertContactGroupService) Delete(id uint) error {
|
|
return s.repo.Delete(id)
|
|
}
|
|
|
|
func toJSONString(v interface{}) string {
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|
|
|
|
// ━━━ Config Helper — 管理所有 Service 的依赖注入 ━━━━
|
|
|
|
type Services struct {
|
|
Server *ServerService
|
|
Credential *CredentialService
|
|
AlertContact *AlertContactService
|
|
AlertContactGroup *AlertContactGroupService
|
|
}
|
|
|
|
// NewServices 初始化所有服务
|
|
func NewServices(db *gorm.DB, cache ezredis.Cache, masterKey string) *Services {
|
|
crypt := crypto.NewSecretBox(masterKey)
|
|
|
|
return &Services{
|
|
Server: NewServerService(repository.NewServerRepository(db), cache, crypt),
|
|
Credential: NewCredentialService(repository.NewCredentialRepository(db), crypt),
|
|
AlertContact: NewAlertContactService(repository.NewAlertContactRepository(db)),
|
|
AlertContactGroup: NewAlertContactGroupService(repository.NewAlertContactGroupRepository(db)),
|
|
}
|
|
}
|