package service import ( "encoding/json" "fmt" "mime" "sort" "strconv" "strings" "sync" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/copier" "github.com/1Panel-dev/1Panel/agent/utils/email" "github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/shirou/gopsutil/v4/disk" ) type AlertService struct{} var eeHiddenAlertTypes = []string{"licenseException", "panelUpdate", "panelPwdEndTime"} var communityAlertMethodTypeNames = map[string]string{ constant.WeCom: "WeCom", constant.DingTalk: "DingTalk", constant.FeiShu: "FeiShu", constant.SMS: "SMS", } type IAlertService interface { PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error) GetAlerts() ([]dto.AlertDTO, error) CreateAlert(create dto.AlertCreate, operator string) error UpdateAlert(req dto.AlertUpdate, operator string) error DeleteAlert(id uint) error GetAlert(id uint) (dto.AlertDTO, error) UpdateStatus(id uint, status string) error ExternalUpdateAlert(req dto.AlertCreate, operator string) error GetDisks() ([]dto.DiskDTO, error) PageAlertLogs(req dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) CleanAlertLogs() error GetClams() ([]dto.ClamDTO, error) GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error DeleteAlertConfig(id uint) error TestAlertConfig(req dto.AlertConfigTest) (bool, error) } func NewIAlertService() IAlertService { return &AlertService{} } func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO, error) { var ( opts []repo.DBOption result []dto.AlertDTO ) if global.CONF.Base.IsEnterprise { opts = append(opts, alertRepo.WithByTypeNotIn(eeHiddenAlertTypes)) } if search.Status != "" { opts = append(opts, repo.WithByStatus(search.Status)) } if search.Type != "" { opts = append(opts, alertRepo.WithByType(search.Type)) } opts = append(opts, repo.WithOrderDesc("created_at")) total, alerts, err := alertRepo.Page(search.Page, search.PageSize, opts...) if err != nil { return 0, nil, err } for _, item := range alerts { result = append(result, dto.AlertDTO{ ID: item.ID, Type: item.Type, Cycle: item.Cycle, Count: item.Count, Method: item.Method, Title: item.Title, Project: item.Project, Status: item.Status, SendCount: item.SendCount, AdvancedParams: item.AdvancedParams, CreateUser: item.CreateUser, UpdateUser: item.UpdateUser, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, }) } return total, result, err } func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) { var ( opts []repo.DBOption result []dto.AlertDTO ) opts = append(opts, repo.WithByStatus(constant.AlertEnable)) alerts, err := alertRepo.List(opts...) if err != nil { return nil, err } for _, item := range alerts { result = append(result, dto.AlertDTO{ ID: item.ID, Type: item.Type, Cycle: item.Cycle, Count: item.Count, Method: item.Method, Title: item.Title, Project: item.Project, Status: item.Status, SendCount: item.SendCount, AdvancedParams: item.AdvancedParams, CreateUser: item.CreateUser, UpdateUser: item.UpdateUser, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, }) } return result, err } func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error { if err := a.validateCommunityAlertMethod(create.Method); err != nil { return err } var alertID uint var alertInfo model.Alert if create.Project != "" { alertInfo, _ := alertRepo.Get(alertRepo.WithByType(create.Type), alertRepo.WithByProject(create.Project)) alertID = alertInfo.ID } else { alertInfo, _ := alertRepo.Get(alertRepo.WithByType(create.Type)) alertID = alertInfo.ID } if alertID != 0 { var upAlert dto.AlertUpdate if err := copier.Copy(&upAlert, &create); err != nil { return buserr.WithErr("ErrStructTransform", err) } upAlert.ID = alertID err := a.UpdateAlert(upAlert, operator) if err != nil { return err } } else { alertInfo.Status = constant.AlertEnable if err := copier.Copy(&alertInfo, &create); err != nil { return buserr.WithErr("ErrStructTransform", err) } alertInfo.CreateUser = operator alertInfo.UpdateUser = operator if err := alertRepo.Create(&alertInfo); err != nil { return err } NewIAlertTaskHelper().InitTask(alertInfo.Type) } return nil } func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error { if err := a.validateCommunityAlertMethod(req.Method); err != nil { return err } upMap := make(map[string]interface{}) upMap["id"] = req.ID upMap["type"] = req.Type upMap["cycle"] = req.Cycle upMap["count"] = req.Count upMap["method"] = req.Method upMap["title"] = req.Title upMap["project"] = req.Project upMap["status"] = req.Status upMap["send_count"] = req.SendCount upMap["advanced_params"] = req.AdvancedParams upMap["update_user"] = operator if err := alertRepo.Update(upMap, repo.WithByID(req.ID)); err != nil { return err } NewIAlertTaskHelper().InitTask(req.Type) return nil } func (a AlertService) DeleteAlert(id uint) error { alertInfo, _ := alertRepo.Get(repo.WithByID(id)) if alertInfo.ID == 0 { return buserr.New("ErrRecordNotFound") } err := alertRepo.Delete(repo.WithByID(id)) if err != nil { return err } alerts, err := a.GetAlerts() if err != nil { return err } if len(alerts) > 0 { NewIAlertTaskHelper().InitTask(alertInfo.Type) } else { NewIAlertTaskHelper().StopTask() } return nil } func (a AlertService) GetAlert(id uint) (dto.AlertDTO, error) { var res dto.AlertDTO alertInfo, err := alertRepo.Get(repo.WithByID(id)) if err != nil { return res, err } _ = copier.Copy(&res, &alertInfo) return res, nil } func (a AlertService) UpdateStatus(id uint, status string) error { alertInfo, _ := alertRepo.Get(repo.WithByID(id)) if alertInfo.ID == 0 { return buserr.New("ErrRecordNotFound") } err := alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID)) if err != nil { return err } alerts, err := a.GetAlerts() if err != nil { return err } if len(alerts) > 0 { NewIAlertTaskHelper().InitTask(alertInfo.Type) } else { NewIAlertTaskHelper().StopTask() } return nil } func (a AlertService) GetDisks() ([]dto.DiskDTO, error) { var disks []dto.DiskDTO excludes := map[string]struct{}{ "/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {}, "/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {}, } stdout, err := executeDiskCommand() if err != nil { return disks, nil } lines := strings.Split(stdout, "\n") var mounts []dto.AlertDiskInfo for _, line := range lines { fields := strings.Fields(line) if len(fields) < 7 { continue } mountPoint := strings.Join(fields[6:], " ") if shouldExclude(fields, mountPoint, excludes) { continue } mounts = append(mounts, dto.AlertDiskInfo{Type: fields[1], Device: fields[0], Mount: mountPoint}) } var ( wg sync.WaitGroup mu sync.Mutex ) wg.Add(len(mounts)) for i := 0; i < len(mounts); i++ { go func(timeoutCh <-chan time.Time, mount dto.AlertDiskInfo) { defer wg.Done() var itemData dto.DiskDTO itemData.Path = mount.Mount itemData.Type = mount.Type itemData.Device = mount.Device select { case <-timeoutCh: mu.Lock() disks = append(disks, itemData) mu.Unlock() global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount) default: state, err := disk.Usage(mount.Mount) if err != nil { mu.Lock() disks = append(disks, itemData) mu.Unlock() global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err) return } itemData.Total = state.Total itemData.Free = state.Free itemData.Used = state.Used itemData.UsedPercent = state.UsedPercent itemData.InodesTotal = state.InodesTotal itemData.InodesUsed = state.InodesUsed itemData.InodesFree = state.InodesFree itemData.InodesUsedPercent = state.InodesUsedPercent mu.Lock() disks = append(disks, itemData) mu.Unlock() } }(time.After(5*time.Second), mounts[i]) } wg.Wait() sort.Slice(disks, func(i, j int) bool { return disks[i].Path < disks[j].Path }) return disks, nil } func executeDiskCommand() (string, error) { cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second)) stdout, err := cmdMgr.RunWithStdout("df", "-hT", "-P") if err != nil { cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second)) stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P") } if err != nil { return stdout, err } var lines []string for _, line := range strings.Split(stdout, "\n") { if !strings.Contains(line, "/") || strings.Contains(line, "tmpfs") || strings.Contains(line, "snap/core") || strings.Contains(line, "udev") { continue } lines = append(lines, line) } if len(lines) == 0 { return "", nil } return strings.Join(lines, "\n"), nil } func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool { if strings.HasPrefix(mountPoint, "/snap") || len(strings.Split(mountPoint, "/")) > 10 { return true } if strings.TrimSpace(fields[1]) == "tmpfs" { return true } if strings.Contains(fields[2], "K") { return true } if strings.Contains(mountPoint, "docker") { return true } _, excluded := excludes[mountPoint] return excluded } func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) { var ( opts []repo.DBOption result []dto.AlertLogDTO ) if search.Status != "" { opts = append(opts, repo.WithByStatus(search.Status)) } if search.Count != 0 { opts = append(opts, alertRepo.WithByCount(search.Count)) } if !search.StartTime.IsZero() && !search.EndTime.IsZero() { opts = append(opts, repo.WithByCreatedAt(search.StartTime, search.EndTime)) } opts = append(opts, repo.WithOrderDesc("created_at")) total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...) if err != nil { return 0, nil, err } for _, item := range alerts { alertLogDTO, err := a.parseAlertLog(item) if err != nil { return 0, nil, err } result = append(result, alertLogDTO) } return total, result, err } func (a AlertService) parseAlertLog(item model.AlertLog) (dto.AlertLogDTO, error) { var alertDetail dto.AlertDetail var alertRule dto.AlertRule if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil { return dto.AlertLogDTO{}, err } if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil { return dto.AlertLogDTO{}, err } return dto.AlertLogDTO{ ID: item.ID, Count: item.Count, Type: item.Type, Status: item.Status, Method: item.Method, Message: item.Message, AlertId: item.AlertId, AlertDetail: alertDetail, AlertRule: alertRule, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, }, nil } func unmarshalAlertInfo(data string, v interface{}) error { if err := json.Unmarshal([]byte(data), v); err != nil { return fmt.Errorf("unmarshal alert info vars failed, err: %v", err) } return nil } func (a AlertService) CleanAlertLogs() error { return alertRepo.CleanAlertLogs() } func (a AlertService) GetClams() ([]dto.ClamDTO, error) { var clams []dto.ClamDTO clamList, err := clamRepo.List() for _, clam := range clamList { var clamDTO dto.ClamDTO clamDTO.ID = clam.ID clamDTO.Name = clam.Name clamDTO.Path = clam.Path clamDTO.Status = clam.Status clamDTO.UpdatedAt = clam.UpdatedAt clamDTO.CreatedAt = clam.CreatedAt clams = append(clams, clamDTO) } return clams, err } func (a AlertService) GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error) { var cronJobs []dto.CronJobDTO var ( opts []repo.DBOption ) if req.Status != "" { opts = append(opts, repo.WithByStatus(req.Status)) } if req.Type != "" { opts = append(opts, repo.WithByType(req.Type)) } cronjobList, err := cronjobRepo.List(opts...) for _, cronJob := range cronjobList { var cronJobDTO dto.CronJobDTO cronJobDTO.ID = cronJob.ID cronJobDTO.Name = cronJob.Name cronJobDTO.Status = cronJob.Status cronJobDTO.Type = cronJob.Type cronJobDTO.UpdatedAt = cronJob.UpdatedAt cronJobDTO.CreatedAt = cronJob.CreatedAt cronJobs = append(cronJobs, cronJobDTO) } return cronJobs, err } func (a AlertService) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error) { var ( opts []repo.DBOption configs []model.AlertConfig ) if len(req.ExcludeTypes) > 0 { opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes)) } opts = append(opts, repo.WithByStatus(constant.AlertEnable)) configs, err := alertRepo.AlertConfigList(opts...) return configs, err } func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) { opts := []repo.DBOption{ alertRepo.WithByTypeNotIn([]string{"common"}), repo.WithOrderDesc("created_at"), } if len(req.ExcludeTypes) > 0 { opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes)) } return alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...) } func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error { if err := a.validateCommunityAlertConfigType(req.Type); err != nil { return err } if err := a.checkAlertConfigDisplayNameUnique(req); err != nil { return err } if err := a.checkAlertConfigSMSPhoneUnique(req); err != nil { return err } if req.ID != 0 { upMap := make(map[string]interface{}) upMap["id"] = req.ID upMap["type"] = req.Type upMap["title"] = req.Title upMap["status"] = req.Status upMap["config"] = req.Config upMap["update_user"] = operator if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil { return err } } else { var alertConfig model.AlertConfig if err := copier.Copy(&alertConfig, &req); err != nil { return buserr.WithErr("ErrStructTransform", err) } alertConfig.CreateUser = operator alertConfig.UpdateUser = operator if err := alertRepo.CreateAlertConfig(&alertConfig); err != nil { return err } } return nil } func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error { if req.Type != constant.SMSConfig { return nil } phone := alertConfigSMSPhone(req.Config) configs, err := alertRepo.AlertConfigList(alertRepo.WithByType(req.Type)) if err != nil { return err } for _, config := range configs { if req.ID != 0 && config.ID == req.ID { continue } if alertConfigSMSPhone(config.Config) == phone { return buserr.New("ErrAlertConfigPhoneExist") } } return nil } func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error { displayName := alertConfigDisplayName(req.Type, req.Config) if displayName == "" { return nil } configs, err := alertRepo.AlertConfigList(alertRepo.WithByType(req.Type)) if err != nil { return err } for _, config := range configs { if req.ID != 0 && config.ID == req.ID { continue } if alertConfigDisplayName(config.Type, config.Config) == displayName { return buserr.New("ErrNameIsExist") } } return nil } func (a AlertService) validateCommunityAlertMethod(method string) error { if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" { return nil } if strings.TrimSpace(method) == "" { return nil } for _, item := range strings.Split(method, ",") { item = strings.TrimSpace(item) if item == "" { continue } if configID, err := strconv.ParseUint(item, 10, 64); err == nil { config, err := alertRepo.GetConfigById(uint(configID)) if err != nil { return err } if _, ok := communityAlertMethodTypeNames[config.Type]; ok { return buserr.WithErr("ErrAlertMethodNotSupported", nil) } continue } if _, ok := communityAlertMethodTypeNames[item]; ok { return buserr.WithErr("ErrAlertMethodNotSupported", nil) } } return nil } func (a AlertService) validateCommunityAlertConfigType(configType string) error { if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" { return nil } if _, ok := communityAlertMethodTypeNames[configType]; ok { return buserr.WithErr("ErrAlertMethodNotSupported", nil) } return nil } func alertConfigDisplayName(configType, configData string) string { switch configType { case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS: var cfg struct { DisplayName string `json:"displayName"` } if err := json.Unmarshal([]byte(configData), &cfg); err != nil { return "" } return strings.TrimSpace(cfg.DisplayName) default: return "" } } func alertConfigSMSPhone(configData string) string { var cfg struct { Phone string `json:"phone"` } if err := json.Unmarshal([]byte(configData), &cfg); err != nil { return "" } return strings.TrimSpace(cfg.Phone) } func (a AlertService) DeleteAlertConfig(id uint) error { _, err := alertRepo.GetConfigById(id) if err != nil { return err } usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id)) if err != nil { return err } if len(usedAlerts) > 0 { return buserr.New("ErrAlertConfigInUse") } return alertRepo.DeleteAlertConfig(repo.WithByID(id)) } func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) { username := req.UserName if username == "" { username = req.Sender } encodedDisplayName := mime.BEncoding.Encode("UTF-8", req.DisplayName) cfg := email.SMTPConfig{ Host: req.Host, Port: req.Port, Sender: req.Sender, Username: username, Password: req.Password, From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, req.Sender), Encryption: req.Encryption, Recipient: req.Recipient, } msg := email.EmailMessage{ Subject: i18n.GetMsgByKey("TestAlertTitle"), Body: i18n.GetMsgByKey("TestAlert"), IsHTML: false, } transport := xpack.MultiNodeProvider.LoadRequestTransport() if err := email.SendMail(cfg, msg, transport); err != nil { return false, err } return true, nil } func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error { if err := a.validateCommunityAlertMethod(updateAlert.Method); err != nil { return err } upMap := make(map[string]interface{}) var newStatus string if updateAlert.SendCount == 0 { newStatus = constant.AlertDisable } else { newStatus = constant.AlertEnable upMap["send_count"] = updateAlert.SendCount if updateAlert.Method != "" { upMap["method"] = updateAlert.Method } } upMap["status"] = newStatus alertInfo, _ := alertRepo.Get( alertRepo.WithByType(updateAlert.Type), alertRepo.WithByProject(updateAlert.Project), ) if alertInfo.ID > 0 { shouldUpdate := false if alertInfo.Status != newStatus { shouldUpdate = true } if val, ok := upMap["send_count"]; ok && val != alertInfo.SendCount { shouldUpdate = true } if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method { shouldUpdate = true } if shouldUpdate { if err := alertRepo.Update( upMap, alertRepo.WithByProject(updateAlert.Project), alertRepo.WithByType(updateAlert.Type), ); err != nil { return err } } } else { if updateAlert.Method != "" && updateAlert.Title != "" { updateAlert.Status = newStatus if err := a.CreateAlert(updateAlert, operator); err != nil { return err } } } return nil }