feat(alert): Support panel login/ssh login/license exception and node exception alerts (#10034)

#9635
This commit is contained in:
2025-08-19 13:41:15 +08:00 committed by GitHub
parent 4e6ebb53ac
commit 0dca3fc997
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1369 additions and 691 deletions

View file

@ -36,40 +36,43 @@ type AlertSearch struct {
}
type AlertDTO struct {
ID uint `json:"id"`
Type string `json:"type"`
Cycle uint `json:"cycle"`
Count uint `json:"count"`
Method string `json:"method"`
Title string `json:"title"`
Project string `json:"project"`
Status string `json:"status"`
SendCount uint `json:"sendCount"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID uint `json:"id"`
Type string `json:"type"`
Cycle uint `json:"cycle"`
Count uint `json:"count"`
Method string `json:"method"`
Title string `json:"title"`
Project string `json:"project"`
Status string `json:"status"`
SendCount uint `json:"sendCount"`
AdvancedParams string `json:"advancedParams"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type AlertCreate struct {
Type string `json:"type" validate:"required"`
Cycle uint `json:"cycle"`
Count uint `json:"count"`
Method string `json:"method" validate:"required"`
Title string `json:"title"`
Project string `json:"project"`
Status string `json:"status"`
SendCount uint `json:"sendCount"`
Type string `json:"type" validate:"required"`
Cycle uint `json:"cycle"`
Count uint `json:"count"`
Method string `json:"method" validate:"required"`
Title string `json:"title"`
Project string `json:"project"`
Status string `json:"status"`
SendCount uint `json:"sendCount"`
AdvancedParams string `json:"advancedParams"`
}
type AlertUpdate struct {
ID uint `json:"id" validate:"required"`
Type string `json:"type"`
Cycle uint `json:"cycle"`
Count uint `json:"count"`
Method string `json:"method"`
Title string `json:"title"`
Project string `json:"project"`
Status string `json:"status"`
SendCount uint `json:"sendCount"`
ID uint `json:"id" validate:"required"`
Type string `json:"type"`
Cycle uint `json:"cycle"`
Count uint `json:"count"`
Method string `json:"method"`
Title string `json:"title"`
Project string `json:"project"`
Status string `json:"status"`
SendCount uint `json:"sendCount"`
AdvancedParams string `json:"advancedParams"`
}
type DeleteRequest struct {

View file

@ -3,14 +3,15 @@ package model
type Alert struct {
BaseModel
Title string `gorm:"type:varchar(256);not null" json:"title"`
Type string `gorm:"type:varchar(64);not null" json:"type"`
Cycle uint `gorm:"type:integer;not null" json:"cycle"`
Count uint `gorm:"type:integer;not null" json:"count"`
Project string `gorm:"type:varchar(64)" json:"project"`
Status string `gorm:"type:varchar(64);not null" json:"status"`
Method string `gorm:"type:varchar(64);not null" json:"method"`
SendCount uint `gorm:"type:integer" json:"sendCount"`
Title string `gorm:"type:varchar(256);not null" json:"title"`
Type string `gorm:"type:varchar(64);not null" json:"type"`
Cycle uint `gorm:"type:integer;not null" json:"cycle"`
Count uint `gorm:"type:integer;not null" json:"count"`
Project string `gorm:"type:varchar(64)" json:"project"`
Status string `gorm:"type:varchar(64);not null" json:"status"`
Method string `gorm:"type:varchar(64);not null" json:"method"`
SendCount uint `gorm:"type:integer" json:"sendCount"`
AdvancedParams string `gorm:"type:longText" json:"advancedParam"`
}
type AlertTask struct {
@ -43,3 +44,12 @@ type AlertConfig struct {
Status string `gorm:"type:varchar(64);not null" json:"status"`
Config string `gorm:"type:varchar(256);not null" json:"config"`
}
type LoginLog struct {
BaseModel
IP string `json:"ip"`
Address string `json:"address"`
Agent string `json:"agent"`
Status string `json:"status"`
Message string `json:"message"`
}

View file

@ -70,17 +70,18 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
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,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
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,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
})
}
@ -100,17 +101,18 @@ func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) {
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,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
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,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
})
}
@ -165,6 +167,7 @@ func (a AlertService) UpdateAlert(req dto.AlertUpdate) error {
upMap["project"] = req.Project
upMap["status"] = req.Status
upMap["send_count"] = req.SendCount
upMap["advanced_params"] = req.AdvancedParams
if err := alertRepo.Update(upMap, repo.WithByID(req.ID)); err != nil {
return err

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,165 @@
package service
import (
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"strings"
)
type AlertSender struct {
alert dto.AlertDTO
quotaType string
}
func NewAlertSender(alert dto.AlertDTO, quotaType string) *AlertSender {
return &AlertSender{
alert: alert,
quotaType: quotaType,
}
}
func (s *AlertSender) Send(quota string, params []dto.Param) {
methods := strings.Split(s.alert.Method, ",")
for _, method := range methods {
method = strings.TrimSpace(method)
switch method {
case constant.SMS:
s.sendSMS(quota, params)
case constant.Email:
s.sendEmail(quota, params)
}
}
}
func (s *AlertSender) ResourceSend(quota string, params []dto.Param) {
methods := strings.Split(s.alert.Method, ",")
for _, method := range methods {
method = strings.TrimSpace(method)
switch method {
case constant.SMS:
s.sendResourceSMS(quota, params)
case constant.Email:
s.sendResourceEmail(quota, params)
}
}
}
func (s *AlertSender) sendSMS(quota string, params []dto.Param) {
if !alertUtil.CheckSMSSendLimit(constant.SMS) {
return
}
totalCount, isValid := s.canSendAlert(constant.SMS)
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
}
_ = xpack.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS)
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS)
global.LOG.Infof("%s alert sms push successful", s.alert.Type)
}
func (s *AlertSender) sendEmail(quota string, params []dto.Param) {
totalCount, isValid := s.canSendAlert(constant.Email)
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email),
}
transport := xpack.LoadRequestTransport()
_ = alertUtil.CreateEmailAlertLog(create, s.alert, params, transport)
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email)
global.LOG.Infof("%s alert email push successful", s.alert.Type)
}
func (s *AlertSender) sendResourceSMS(quota string, params []dto.Param) {
if !alertUtil.CheckSMSSendLimit(constant.SMS) {
return
}
todayCount, isValid := s.canResourceSendAlert(constant.SMS)
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: todayCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
}
if err := xpack.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS); err != nil {
global.LOG.Errorf("failed to send SMS alert: %v", err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS)
global.LOG.Infof("%s alert sms push successful", s.alert.Type)
}
func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) {
todayCount, isValid := s.canResourceSendAlert(constant.Email)
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: todayCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email),
}
transport := xpack.LoadRequestTransport()
if err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport); err != nil {
global.LOG.Errorf("failed to send Email alert: %v", err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email)
global.LOG.Infof("%s alert email push successful", s.alert.Type)
}
func (s *AlertSender) canSendAlert(method string) (uint, bool) {
todayCount, totalCount, err := alertRepo.LoadTaskCount(s.alert.Type, s.quotaType, method)
if err != nil {
global.LOG.Errorf("error getting task count: %v", err)
return totalCount, false
}
if todayCount >= 1 || s.alert.SendCount <= totalCount {
return totalCount, false
}
return totalCount, true
}
func (s *AlertSender) canResourceSendAlert(method string) (uint, bool) {
todayCount, _, err := alertRepo.LoadTaskCount(s.alert.Type, s.quotaType, method)
if err != nil {
global.LOG.Errorf("error getting task count: %v", err)
return todayCount, false
}
if s.alert.SendCount <= todayCount {
return todayCount, false
}
return todayCount, true
}

View file

@ -449,4 +449,8 @@ SSLAlert: "There are {{ .num }} SSL certificates on your 1Panel that will expire
DiskUsedAlert: "Your 1Panel disk '{{ .name }}' has used {{ .used }}. Please log in to the panel for details."
ResourceAlert: "The average {{ .name }} usage over {{ .time }} minutes on your 1Panel is {{ .used }}. Please log in to the panel for details."
PanelVersionAlert: "A new version of 1Panel is available. Please log in to the panel to upgrade."
PanelPwdExpirationAlert: "Your 1Panel password will expire in {{ .day }} days. Please log in to the panel for details."
PanelPwdExpirationAlert: "Your 1Panel password will expire in {{ .day }} days. Please log in to the panel for details."
CommonAlert: "Your 1Panel, {{ .msg }}, please log in to the panel for details."
NodeExceptionAlert: "Your 1Panel, {{ .num }} nodes are abnormal, please log in to the panel for details."
LicenseExceptionAlert: "Your 1Panel, {{ .num }} licenses are abnormal, please log in to the panel for details."
SSHAndPanelLoginAlert: "Your 1Panel, abnormal panel {{ .name }} login from {{ .ip }}, please log in to the panel for details."

View file

@ -449,4 +449,8 @@ SSLAlert: "1Panel にある {{ .num }} 枚のSSL証明書が {{ .day }} 日後
DiskUsedAlert: "1Panel のディスク '{{ .name }}' は {{ .used }} 使用されています。詳細はパネルでご確認ください。"
ResourceAlert: "1Panel の {{ .time }} 分間の平均 {{ .name }} 使用率は {{ .used }} です。詳細はパネルでご確認ください。"
PanelVersionAlert: "1Panel に新しいバージョンが利用可能です。アップグレードはパネルから行ってください。"
PanelPwdExpirationAlert: "1Panel のパスワードは {{ .day }} 日後に期限切れとなります。詳細はパネルでご確認ください。"
PanelPwdExpirationAlert: "1Panel のパスワードは {{ .day }} 日後に期限切れとなります。詳細はパネルでご確認ください。"
CommonAlert: "お使いの1Panel、{{ .msg }}、詳細はパネルにログインしてご確認ください。"
NodeExceptionAlert: "お使いの1Panel、{{ .num }}個のノードに異常があります。詳細はパネルにログインしてご確認ください。"
LicenseExceptionAlert: "お使いの1Panel、{{ .num }}個のライセンスに異常があります。詳細はパネルにログインしてご確認ください。"
SSHAndPanelLoginAlert: "お使いの1Panel、パネル{{ .name }}が{{ .ip }}から異常ログインしました。詳細はパネルにログインしてご確認ください。"

View file

@ -449,4 +449,8 @@ SSLAlert: "1Panel 에 있는 {{ .num }}개의 SSL 인증서가 {{ .day }}일 후
DiskUsedAlert: "1Panel 디스크 '{{ .name }}'의 사용량은 {{ .used }}입니다. 자세한 내용은 패널에서 확인하세요."
ResourceAlert: "1Panel 의 평균 {{ .time }}분 동안 {{ .name }} 사용률은 {{ .used }}입니다. 자세한 내용은 패널에서 확인하세요."
PanelVersionAlert: "1Panel 의 새로운 버전이 이용 가능합니다. 패널에 로그인하여 업그레이드하세요."
PanelPwdExpirationAlert: "1Panel 비밀번호가 {{ .day }}일 후에 만료됩니다. 자세한 내용은 패널에서 확인하세요."
PanelPwdExpirationAlert: "1Panel 비밀번호가 {{ .day }}일 후에 만료됩니다. 자세한 내용은 패널에서 확인하세요."
CommonAlert: "귀하의 1Panel, {{ .msg }}. 자세한 내용은 패널에 로그인하여 확인하세요."
NodeExceptionAlert: "귀하의 1Panel, {{ .num }}개의 노드에 이상이 있습니다. 자세한 내용은 패널에 로그인하여 확인하세요."
LicenseExceptionAlert: "귀하의 1Panel, {{ .num }}개의 라이선스에 이상이 있습니다. 자세한 내용은 패널에 로그인하여 확인하세요."
SSHAndPanelLoginAlert: "귀하의 1Panel, 패널 {{ .name }}이(가) {{ .ip }}에서 비정상 로그인했습니다. 자세한 내용은 패널에 로그인하여 확인하세요."

View file

@ -448,4 +448,8 @@ SSLAlert: "Terdapat {{ .num }} sijil SSL dalam 1Panel anda yang akan tamat dalam
DiskUsedAlert: "Cakera '{{ .name }}' dalam 1Panel anda telah menggunakan {{ .used }}. Sila log masuk ke panel untuk maklumat lanjut."
ResourceAlert: "Penggunaan purata {{ .name }} selama {{ .time }} minit dalam 1Panel anda ialah {{ .used }}. Sila log masuk ke panel untuk maklumat lanjut."
PanelVersionAlert: "Versi baru 1Panel tersedia. Sila log masuk ke panel untuk menaik taraf."
PanelPwdExpirationAlert: "Kata laluan 1Panel anda akan tamat dalam {{ .day }} hari. Sila log masuk ke panel untuk maklumat lanjut."
PanelPwdExpirationAlert: "Kata laluan 1Panel anda akan tamat dalam {{ .day }} hari. Sila log masuk ke panel untuk maklumat lanjut."
CommonAlert: "1Panel anda, {{ .msg }}, sila log masuk ke panel untuk maklumat lanjut."
NodeExceptionAlert: "1Panel anda, {{ .num }} nod bermasalah, sila log masuk ke panel untuk maklumat lanjut."
LicenseExceptionAlert: "1Panel anda, {{ .num }} lesen bermasalah, sila log masuk ke panel untuk maklumat lanjut."
SSHAndPanelLoginAlert: "1Panel anda, log masuk panel {{ .name }} yang tidak normal dari {{ .ip }}, sila log masuk ke panel untuk maklumat lanjut."

View file

@ -449,4 +449,8 @@ SSLAlert: "{{ .num }} certificados SSL do seu 1Panel expirarão em {{ .day }} di
DiskUsedAlert: "O disco '{{ .name }}' do seu 1Panel está com uso de {{ .used }}. Acesse o painel para mais detalhes."
ResourceAlert: "O uso médio de {{ .name }} em {{ .time }} minutos no seu 1Panel é de {{ .used }}. Acesse o painel para mais detalhes."
PanelVersionAlert: "Uma nova versão do 1Panel está disponível. Acesse o painel para atualizá-lo."
PanelPwdExpirationAlert: "A senha do 1Panel expirará em {{ .day }} dias. Acesse o painel para mais detalhes."
PanelPwdExpirationAlert: "A senha do 1Panel expirará em {{ .day }} dias. Acesse o painel para mais detalhes."
CommonAlert: "Seu 1Panel, {{ .msg }}. Para mais detalhes, faça login no painel."
NodeExceptionAlert: "Seu 1Panel, {{ .num }} nós estão com problemas. Para mais detalhes, faça login no painel."
LicenseExceptionAlert: "Seu 1Panel, {{ .num }} licenças estão com problemas. Para mais detalhes, faça login no painel."
SSHAndPanelLoginAlert: "Seu 1Panel, login anormal no painel {{ .name }} a partir de {{ .ip }}. Para mais detalhes, faça login no painel."

View file

@ -449,4 +449,8 @@ SSLAlert: "На вашем 1Panel {{ .num }} SSL-сертификатов ист
DiskUsedAlert: "Диск '{{ .name }}' на вашем 1Panel использует {{ .used }}. Подробности смотрите в панели."
ResourceAlert: "Средняя загрузка {{ .name }} за {{ .time }} минут составляет {{ .used }}. Подробности смотрите в панели."
PanelVersionAlert: "Доступна новая версия 1Panel. Обновитесь через панель."
PanelPwdExpirationAlert: "Пароль для 1Panel истекает через {{ .day }} дней. Подробности смотрите в панели."
PanelPwdExpirationAlert: "Пароль для 1Panel истекает через {{ .day }} дней. Подробности смотрите в панели."
CommonAlert: "Ваш 1Panel, {{ .msg }}. Подробности смотрите, войдя в панель."
NodeExceptionAlert: "Ваш 1Panel, {{ .num }} узлов работают неправильно. Подробности смотрите, войдя в панель."
LicenseExceptionAlert: "Ваш 1Panel, {{ .num }} лицензий работают неправильно. Подробности смотрите, войдя в панель."
SSHAndPanelLoginAlert: "Ваш 1Panel, обнаружен аномальный вход в панель {{ .name }} с {{ .ip }}. Подробности смотрите, войдя в панель."

View file

@ -448,3 +448,7 @@ DiskUsedAlert: "1Panel diski '{{ .name }}' {{ .used }} kullanıldı. Detaylar i
ResourceAlert: "1Panel üzerinde ortalama {{ .time }} dakikalık {{ .name }} kullanım oranı {{ .used }}. Detaylar için panele giriş yapın."
PanelVersionAlert: "1Panel için yeni bir sürüm mevcut. Güncellemek için panele giriş yapın."
PanelPwdExpirationAlert: "1Panel şifreniz {{ .day }} gün içinde sona erecek. Detaylar için panele giriş yapın."
CommonAlert: "1Panel'iniz, {{ .msg }}. Detaylar için panele giriş yapınız."
NodeExceptionAlert: "1Panel'iniz, {{ .num }} düğümde sorun var. Detaylar için panele giriş yapınız."
LicenseExceptionAlert: "1Panel'iniz, {{ .num }} lisansda sorun var. Detaylar için panele giriş yapınız."
SSHAndPanelLoginAlert: "1Panel'iniz, {{ .ip }} adresinden {{ .name }} paneline anormal giriş tespit edildi. Detaylar için panele giriş yapınız."

View file

@ -449,3 +449,7 @@ DiskUsedAlert: "您的 1Panel 面板磁碟 '{{ .name }}' 已使用 {{ .used }}
ResourceAlert: "您的 1Panel 面板於 {{ .time }} 分鐘內的平均 {{ .name }} 使用率為 {{ .used }},詳情請登入面板查看。"
PanelVersionAlert: "您的 1Panel 面板有可升級的新版本,詳情請登入面板查看。"
PanelPwdExpirationAlert: "您的 1Panel 面板密碼將於 {{ .day }} 天後到期,詳情請登入面板查看。"
CommonAlert: "您的 1Panel 面板,{{ .msg }},詳情請登入面板查看。"
NodeExceptionAlert: "您的 1Panel 面板,{{ .num }} 個節點存在異常,詳情請登入面板查看。"
LicenseExceptionAlert: "您的 1Panel 面板,{{ .num }} 個許可證存在異常,詳情請登入面板查看。"
SSHAndPanelLoginAlert: "您的 1Panel 面板,面板{{ .name }}從 {{ .ip }} 登錄異常,詳情請登入面板查看。"

View file

@ -449,4 +449,8 @@ SSLAlert: "您的 1Panel 面板,有 {{ .num }} 张SSL证书将在 {{ .day }}
DiskUsedAlert: "您的 1Panel 面板,磁盘 {{ .name }} 已使用 {{ .used }},详情请登录面板查看。"
ResourceAlert: "您的 1Panel 面板,平均 {{ .time }} 分钟内的 {{ .name }} 使用率为 {{ .used }},详情请登录面板查看。"
PanelVersionAlert: "您的 1Panel 面板,有最新面板版本可供升级,详情请登录面板查看。"
PanelPwdExpirationAlert: "您的 1Panel 面板,面板密码将在 {{ .day }} 天后到期,详情请登录面板查看。"
PanelPwdExpirationAlert: "您的 1Panel 面板,面板密码将在 {{ .day }} 天后到期,详情请登录面板查看。"
CommonAlert: "您的 1Panel 面板,{{ .msg }},详情请登录面板查看。"
NodeExceptionAlert: "您的 1Panel 面板,{{ .num }} 个节点存在异常,详情请登录面板查看。"
LicenseExceptionAlert: "您的 1Panel 面板,{{ .num }} 个许可证存在异常,详情请登录面板查看。"
SSHAndPanelLoginAlert: "您的 1Panel 面板,面板{{ .name }}登录{{ .ip }}异常,详情请登录面板查看。"

View file

@ -35,6 +35,7 @@ func InitAgentDB() {
migrations.AddMethodToAlertTask,
migrations.UpdateMcpServer,
migrations.InitCronjobGroup,
migrations.AddColumnToAlert,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View file

@ -446,3 +446,18 @@ var InitCronjobGroup = &gormigrate.Migration{
return nil
},
}
var AddColumnToAlert = &gormigrate.Migration{
ID: "20250729-add-column-to-alert",
Migrate: func(tx *gorm.DB) error {
if err := global.AlertDB.AutoMigrate(&model.Alert{}); err != nil {
return err
}
if err := global.AlertDB.Model(&model.Alert{}).
Where("advanced_params IS NULL").
Update("advanced_params", "").Error; err != nil {
return err
}
return nil
},
}

View file

@ -2,21 +2,24 @@ package alert
import (
"encoding/json"
"errors"
"fmt"
"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/email"
"github.com/jinzhu/copier"
"net/http"
"os"
"os/exec"
"regexp"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/global"
)
var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp"}
@ -66,7 +69,7 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [
Encryption: emailInfo.Encryption,
Recipient: emailInfo.Recipient,
}
content := alert.Title
content := i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title})
if GetEmailContent(alert.Type, params) != "" {
content = GetEmailContent(alert.Type, params)
}
@ -191,7 +194,7 @@ func CreateAlertParams(param string) []dto.Param {
var checkTaskMutex sync.Mutex
func CheckTaskFrequency(method string) bool {
func CheckSMSSendLimit(method string) bool {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMSConfig))
if err != nil {
@ -204,8 +207,8 @@ func CheckTaskFrequency(method string) bool {
}
limitCount := cfg.AlertDailyNum
checkTaskMutex.Lock()
todayCount, err := alertRepo.GetLicensePushCount(method)
defer checkTaskMutex.Unlock()
todayCount, err := alertRepo.GetLicensePushCount(method)
if err != nil {
global.LOG.Errorf("error getting license push count info, err: %v", err)
return false
@ -312,6 +315,18 @@ func GetEmailContent(alertType string, params []dto.Param) string {
return i18n.GetMsgWithMap("CronJobFailedAlert", map[string]interface{}{"name": getValueByIndex(params, "1")})
case "clams":
return i18n.GetMsgWithMap("ClamAlert", map[string]interface{}{"num": getValueByIndex(params, "1")})
case "panelLogin":
return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")})
case "sshLogin":
return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")})
case "panelIpLogin":
return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")})
case "sshIpLogin":
return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")})
case "nodeException":
return i18n.GetMsgWithMap("NodeExceptionAlert", map[string]interface{}{"num": getValueByIndex(params, "1")})
case "licenseException":
return i18n.GetMsgWithMap("LicenseExceptionAlert", map[string]interface{}{"num": getValueByIndex(params, "1")})
default:
return ""
}
@ -325,3 +340,162 @@ func getValueByIndex(params []dto.Param, index string) string {
}
return ""
}
func CountRecentFailedLoginLogs(minutes uint, failCount uint) (int, bool, error) {
now := time.Now()
startTime := now.Add(-time.Duration(minutes) * time.Minute)
db := global.CoreDB.Model(&model.LoginLog{})
var count int64
err := db.Where("created_at >= ? AND status = ?", startTime, constant.StatusFailed).
Count(&count).Error
if err != nil {
return 0, false, err
}
return int(count), int(count) > int(failCount), nil
}
func FindRecentSuccessLoginsNotInWhitelist(minutes int, whitelist []string) ([]model.LoginLog, error) {
now := time.Now()
startTime := now.Add(-time.Duration(minutes) * time.Minute)
whitelistMap := make(map[string]struct{})
for _, ip := range whitelist {
whitelistMap[ip] = struct{}{}
}
var logs []model.LoginLog
err := global.CoreDB.Model(&model.LoginLog{}).
Where("created_at >= ? AND status = ?", startTime, constant.StatusSuccess).
Find(&logs).Error
if err != nil {
return nil, err
}
var abnormalLogs []model.LoginLog
for _, log := range logs {
if _, ok := whitelistMap[log.IP]; !ok {
abnormalLogs = append(abnormalLogs, log)
}
}
return abnormalLogs, nil
}
func CountRecentFailedSSHLog(minutes uint, maxAllowed uint) (int, bool, error) {
lines, err := grepSSHLog("Failed password")
if err != nil {
return 0, false, err
}
thresholdTime := time.Now().Add(-time.Duration(minutes) * time.Minute)
count := 0
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
t, err := parseLogTime(line)
if err != nil {
continue
}
if t.After(thresholdTime) {
count++
}
}
return count, count > int(maxAllowed), nil
}
func FindRecentSuccessLoginNotInWhitelist(minutes int, whitelist []string) ([]string, error) {
lines, err := grepSSHLog("Accepted password")
if err != nil {
return nil, err
}
thresholdTime := time.Now().Add(-time.Duration(minutes) * time.Minute)
var abnormalLogins []string
whitelistMap := make(map[string]struct{}, len(whitelist))
for _, ip := range whitelist {
whitelistMap[ip] = struct{}{}
}
ipRegex := regexp.MustCompile(`from\s+([0-9.]+)\s+port\s+(\d+)`)
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
t, err := parseLogTime(line)
if err != nil || t.Before(thresholdTime) {
continue
}
match := ipRegex.FindStringSubmatch(line)
if len(match) >= 2 {
ip := match[1]
if _, ok := whitelistMap[ip]; !ok {
abnormalLogins = append(abnormalLogins, fmt.Sprintf("%s-%s", ip, t.Format("2006-01-02 15:04:05")))
}
}
}
return abnormalLogins, nil
}
func findGrepPath() (string, error) {
path, err := exec.LookPath("grep")
if err != nil {
return "", fmt.Errorf("grep not found in PATH: %w", err)
}
return path, nil
}
func grepSSHLog(keyword string) ([]string, error) {
logFiles := []string{"/var/log/secure", "/var/log/auth.log"}
var results []string
grepPath, err := findGrepPath()
if err != nil {
panic(err)
}
for _, logFile := range logFiles {
if _, err := os.Stat(logFile); err != nil {
continue
}
cmd := exec.Command(grepPath, "-a", keyword, logFile)
output, err := cmd.Output()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
if exitErr.ExitCode() == 1 {
continue
}
}
return nil, fmt.Errorf("read log file fail [%s]: %w", logFile, err)
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
results = append(results, line)
}
}
}
return results, nil
}
func parseLogTime(line string) (time.Time, error) {
if len(line) < 15 {
return time.Time{}, errors.New("log line time is incorrect")
}
timeStr := line[:15]
parsedTime, err := time.ParseInLocation("Jan 2 15:04:05", timeStr, time.Local)
if err != nil {
return time.Time{}, err
}
return parsedTime.AddDate(time.Now().Year(), 0, 0), nil
}

View file

@ -31,7 +31,7 @@ func PushAlert(pushAlert dto.PushAlert) error {
m = strings.TrimSpace(m)
switch m {
case constant.SMS:
if !alertUtil.CheckTaskFrequency(constant.SMS) {
if !alertUtil.CheckSMSSendLimit(constant.SMS) {
continue
}
todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.SMS)
@ -43,7 +43,7 @@ func PushAlert(pushAlert dto.PushAlert) error {
AlertId: alert.ID,
Count: todayCount + 1,
}
_ = xpack.CreateTaskScanSMSAlertLog(alert, create, pushAlert, constant.SMS)
_ = xpack.CreateTaskScanSMSAlertLog(alert, alert.Type, create, pushAlert, constant.SMS)
alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.SMS)
case constant.Email:
todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.Email)

View file

@ -53,14 +53,22 @@ func IsUseCustomApp() bool {
return false
}
func CreateTaskScanSMSAlertLog(alert dto.AlertDTO, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error {
func CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error {
return nil
}
func CreateSMSAlertLog(info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error {
func CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error {
return nil
}
func GetLicenseErrorAlert() (uint, error) {
return 0, nil
}
func GetNodeErrorAlert() (uint, error) {
return 0, nil
}
func LoadRequestTransport() *http.Transport {
return &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},

View file

@ -13,6 +13,7 @@ export namespace Alert {
status: string;
sendCount: number;
sendMethod: string[];
advancedParams: string;
}
export interface AlertDetail {

View file

@ -3583,7 +3583,7 @@ const message = {
cpuUseExceedAvgHelper: 'The average cpu usage within the specified time exceeds the specified value',
memoryUseExceedAvgHelper: 'The average memory usage within the specified time exceeds the specified value',
loadUseExceedAvgHelper: 'The average load usage within the specified time exceeds the specified value',
resourceAlertRulesHelper: 'Note: Continuous alerts within 30 minutes will send only one SMS',
resourceAlertRulesHelper: 'Note: Continuous alerts within 30 minutes will send only one',
specifiedTime: 'Specified Time',
deleteTitle: 'Delete Alert',
deleteMsg: 'Are you sure you want to delete the alert task?',
@ -3715,6 +3715,19 @@ const message = {
portHelper: 'SSL usually uses 465, TLS usually uses 587',
sslHelper: 'If the SMTP port is 465, SSL is usually required',
tlsHelper: 'If the SMTP port is 587, TLS is usually required',
triggerCondition: 'Trigger Condition',
loginFail: ' login failures within',
nodeException: 'Node Exception Alert',
licenseException: 'License Exception Alert',
panelLogin: 'Panel Login Exception Alert',
sshLogin: 'SSH Login Exception Alert',
panelIpLogin: 'Panel Login IP Exception Alert',
sshIpLogin: 'SSH Login IP Exception Alert',
ipWhiteListHelper: 'IPs in the whitelist are not restricted by any rules',
nodeExceptionRule: 'Node exception alert, sent {0} times per day',
licenseExceptionRule: 'License exception alert, sent {0} times per day',
panelLoginRule: 'Panel login alert, sent {0} times per day',
sshLoginRule: 'SSH login alert, sent {0} times per day',
},
theme: {
lingXiaGold: 'Ling Xia Gold',

View file

@ -3466,7 +3466,7 @@ const message = {
cpuUseExceedAvgHelper: '指定時間内の平均CPU使用率が指定した値を超過',
memoryUseExceedAvgHelper: '指定時間内の平均メモリ使用率が指定した値を超過',
loadUseExceedAvgHelper: '指定時間内の平均負荷使用率が指定した値を超過',
resourceAlertRulesHelper: '注意30分以内に連続してアラートが発生した場合SMSは1回だけ送信されます',
resourceAlertRulesHelper: '注意30分以内に連続してアラートが発生した場合は1回だけ送信されます',
specifiedTime: '指定時間',
deleteTitle: 'アラートを削除',
deleteMsg: 'アラートタスクを削除してもよろしいですか',
@ -3594,6 +3594,19 @@ const message = {
portHelper: 'SSLは通常465TLSは通常587',
sslHelper: 'SMTPポートが465の場合通常はSSLが必要です',
tlsHelper: 'SMTPポートが587の場合通常はTLSが必要です',
triggerCondition: 'トリガー条件',
loginFail: '以内にログイン失敗',
nodeException: 'ノード異常アラート',
licenseException: 'ライセンス異常アラート',
panelLogin: 'パネルログイン異常アラート',
sshLogin: 'SSHログイン異常アラート',
panelIpLogin: 'パネルログインIP異常アラート',
sshIpLogin: 'SSHログインIP異常アラート',
ipWhiteListHelper: 'ホワイトリスト内のIPはいかなるルールの制限も受けません',
nodeExceptionRule: 'ノード異常アラートは1日あたり{0}回送信',
licenseExceptionRule: 'ライセンス異常アラートは1日あたり{0}回送信',
panelLoginRule: 'パネルログインアラートは1日あたり{0}回送信',
sshLoginRule: 'SSHログインアラートは1日あたり{0}回送信',
},
theme: {
lingXiaGold: '凌霞金',

View file

@ -3404,7 +3404,7 @@ const message = {
cpuUseExceedAvgHelper: '지정된 시간 내의 평균 CPU 사용량이 지정된 값을 초과함',
memoryUseExceedAvgHelper: '지정된 시간 내의 평균 메모리 사용량이 지정된 값을 초과함',
loadUseExceedAvgHelper: '지정된 시간 내의 평균 부하 사용량이 지정된 값을 초과함',
resourceAlertRulesHelper: '참고: 30 내에 연속적인 알림은 SMS 번만 발송됩니다',
resourceAlertRulesHelper: '참고: 30 내에 연속적인 알림은 번만 발송됩니다',
specifiedTime: '지정된 시간',
deleteTitle: '알림 삭제',
deleteMsg: '알림 작업을 삭제하시겠습니까?',
@ -3529,6 +3529,19 @@ const message = {
portHelper: 'SSL 일반적으로 465, TLS 587',
sslHelper: 'SMTP 포트가 465 이면 일반적으로 SSL 필요합니다',
tlsHelper: 'SMTP 포트가 587 이면 일반적으로 TLS 필요합니다',
triggerCondition: '트리거 조건',
loginFail: ' 이내 로그인 실패',
nodeException: '노드 이상 알림',
licenseException: '라이선스 이상 알림',
panelLogin: '패널 로그인 이상 알림',
sshLogin: 'SSH 로그인 이상 알림',
panelIpLogin: '패널 로그인 IP 이상 알림',
sshIpLogin: 'SSH 로그인 IP 이상 알림',
ipWhiteListHelper: '화이트리스트에 있는 IP는 어떠한 규칙의 제한도 받지 않습니다',
nodeExceptionRule: '노드 이상 알림은 하루 {0} 전송',
licenseExceptionRule: '라이선스 이상 알림은 하루 {0} 전송',
panelLoginRule: '패널 로그인 알림은 하루 {0} 전송',
sshLoginRule: 'SSH 로그인 알림은 하루 {0} 전송',
},
theme: {
lingXiaGold: '링샤 골드',

View file

@ -3545,7 +3545,7 @@ const message = {
cpuUseExceedAvgHelper: 'Penggunaan CPU purata dalam masa tertentu melebihi nilai yang ditetapkan',
memoryUseExceedAvgHelper: 'Penggunaan memori purata dalam masa tertentu melebihi nilai yang ditetapkan',
loadUseExceedAvgHelper: 'Penggunaan beban purata dalam masa tertentu melebihi nilai yang ditetapkan',
resourceAlertRulesHelper: 'Nota: Amaran berterusan dalam masa 30 minit hanya akan menghantar satu SMS',
resourceAlertRulesHelper: 'Nota: Amaran berterusan dalam masa 30 minit hanya akan menghantar satu',
specifiedTime: 'Masa Tertentu',
deleteTitle: 'Padam Amaran',
deleteMsg: 'Adakah anda pasti ingin memadam tugas amaran?',
@ -3679,6 +3679,19 @@ const message = {
portHelper: 'SSL biasanya 465, TLS biasanya 587',
sslHelper: 'Jika port SMTP ialah 465, SSL biasanya diperlukan',
tlsHelper: 'Jika port SMTP ialah 587, TLS biasanya diperlukan',
triggerCondition: 'Syarat Pencetus',
loginFail: ' kegagalan log masuk dalam',
nodeException: 'Amaran Kerosakan Nod',
licenseException: 'Amaran Kerosakan Lesen',
panelLogin: 'Amaran Log Masuk Panel Tidak Normal',
sshLogin: 'Amaran Log Masuk SSH Tidak Normal',
panelIpLogin: 'Amaran IP Log Masuk Panel Tidak Normal',
sshIpLogin: 'Amaran IP Log Masuk SSH Tidak Normal',
ipWhiteListHelper: 'IP dalam senarai putih tidak tertakluk kepada sebarang peraturan',
nodeExceptionRule: 'Amaran kerosakan nod, dihantar {0} kali sehari',
licenseExceptionRule: 'Amaran kerosakan lesen, dihantar {0} kali sehari',
panelLoginRule: 'Amaran log masuk panel, dihantar {0} kali sehari',
sshLoginRule: 'Amaran log masuk SSH, dihantar {0} kali sehari',
},
theme: {
lingXiaGold: 'Ling Xia Emas',

View file

@ -3554,7 +3554,7 @@ const message = {
cpuUseExceedAvgHelper: 'O uso médio da CPU dentro do tempo especificado excede o valor especificado',
memoryUseExceedAvgHelper: 'O uso médio da memória dentro do tempo especificado excede o valor especificado',
loadUseExceedAvgHelper: 'O uso médio da carga dentro do tempo especificado excede o valor especificado',
resourceAlertRulesHelper: 'Nota: Alertas contínuos em 30 minutos enviarão apenas um SMS',
resourceAlertRulesHelper: 'Nota: Alertas contínuos em 30 minutos enviarão apenas um',
specifiedTime: 'Hora Especificada',
deleteTitle: 'Excluir Alerta',
deleteMsg: 'Tem certeza de que deseja excluir a tarefa de alerta?',
@ -3687,6 +3687,19 @@ const message = {
portHelper: 'SSL geralmente usa 465, TLS geralmente usa 587',
sslHelper: 'Se a porta SMTP for 465, normalmente é necessário SSL',
tlsHelper: 'Se a porta SMTP for 587, normalmente é necessário TLS',
triggerCondition: 'Condição de Disparo',
loginFail: ' falhas de login em',
nodeException: 'Alerta de Exceção de ',
licenseException: 'Alerta de Exceção de Licença',
panelLogin: 'Alerta de Exceção de Login no Painel',
sshLogin: 'Alerta de Exceção de Login SSH',
panelIpLogin: 'Alerta de Exceção de IP de Login no Painel',
sshIpLogin: 'Alerta de Exceção de IP de Login SSH',
ipWhiteListHelper: 'IPs na lista branca não estão sujeitos a nenhuma regra',
nodeExceptionRule: 'Alerta de exceção de , enviado {0} vezes por dia',
licenseExceptionRule: 'Alerta de exceção de licença, enviado {0} vezes por dia',
panelLoginRule: 'Alerta de login no painel, enviado {0} vezes por dia',
sshLoginRule: 'Alerta de login SSH, enviado {0} vezes por dia',
},
theme: {
lingXiaGold: 'Ling Xia Gold',

View file

@ -3540,7 +3540,7 @@ const message = {
cpuUseExceedAvgHelper: 'Среднее использование процессора за указанное время превышает заданное значение',
memoryUseExceedAvgHelper: 'Среднее использование памяти за указанное время превышает заданное значение',
loadUseExceedAvgHelper: 'Средняя нагрузка за указанное время превышает заданное значение',
resourceAlertRulesHelper: 'Примечание: Непрерывные уведомления в течение 30 минут отправят только одно SMS',
resourceAlertRulesHelper: 'Примечание: Непрерывные уведомления в течение 30 минут отправят только одно',
specifiedTime: 'Указанное Время',
deleteTitle: 'Удалить Уведомление',
deleteMsg: 'Вы уверены, что хотите удалить задачу уведомления?',
@ -3678,6 +3678,19 @@ const message = {
portHelper: 'SSL обычно использует 465, TLS 587',
sslHelper: 'Если порт SMTP 465, обычно требуется SSL',
tlsHelper: 'Если порт SMTP 587, обычно требуется TLS',
triggerCondition: 'Условие срабатывания',
loginFail: ' неудачных попыток входа в течение',
nodeException: 'Оповещение о сбое узла',
licenseException: 'Оповещение о сбое лицензии',
panelLogin: 'Оповещение о сбое входа в панель',
sshLogin: 'Оповещение о сбое входа по SSH',
panelIpLogin: 'Оповещение о сбое IP входа в панель',
sshIpLogin: 'Оповещение о сбое IP входа по SSH',
ipWhiteListHelper: 'IP-адреса в белом списке не подпадают под действие каких-либо правил',
nodeExceptionRule: 'Оповещение о сбое узла, отправляется {0} раз в день',
licenseExceptionRule: 'Оповещение о сбое лицензии, отправляется {0} раз в день',
panelLoginRule: 'Оповещение о входе в панель, отправляется {0} раз в день',
sshLoginRule: 'Оповещение о входе по SSH, отправляется {0} раз в день',
},
theme: {
lingXiaGold: 'Лин Ся Золотой',

View file

@ -3622,7 +3622,7 @@ const message = {
cpuUseExceedAvgHelper: 'Belirtilen süre içinde ortalama CPU kullanımı belirtilen değeri aşar',
memoryUseExceedAvgHelper: 'Belirtilen süre içinde ortalama bellek kullanımı belirtilen değeri aşar',
loadUseExceedAvgHelper: 'Belirtilen süre içinde ortalama yük kullanımı belirtilen değeri aşar',
resourceAlertRulesHelper: 'Not: 30 dakika içinde sürekli uyarılar yalnızca bir SMS gönderir',
resourceAlertRulesHelper: 'Not: 30 dakika içinde sürekli uyarılar yalnızca bir gönderir',
specifiedTime: 'Belirtilen Süre',
deleteTitle: 'Uyarıyı Sil',
deleteMsg: 'Uyarı görevini silmek istediğinizden emin misiniz?',
@ -3757,6 +3757,19 @@ const message = {
portHelper: 'SSL genellikle 465, TLS genellikle 587',
sslHelper: 'SMTP portu 465 ise genellikle SSL gerekir',
tlsHelper: 'SMTP portu 587 ise genellikle TLS gerekir',
triggerCondition: 'Tetikleme Koşulu',
loginFail: ' içinde oturum açma başarısızlığı',
nodeException: 'Düğüm Hatası Uyarısı',
licenseException: 'Lisans Hatası Uyarısı',
panelLogin: 'Panel Girişi Hatası Uyarısı',
sshLogin: 'SSH Girişi Hatası Uyarısı',
panelIpLogin: 'Panel Girişi IP Hatası Uyarısı',
sshIpLogin: 'SSH Girişi IP Hatası Uyarısı',
ipWhiteListHelper: 'Beyaz listedeki IPler herhangi bir kuralla kısıtlanmaz',
nodeExceptionRule: 'Düğüm hatası uyarısı, günde {0} kez gönderilir',
licenseExceptionRule: 'Lisans hatası uyarısı, günde {0} kez gönderilir',
panelLoginRule: 'Panel girişi uyarısı, günde {0} kez gönderilir',
sshLoginRule: 'SSH girişi uyarısı, günde {0} kez gönderilir',
},
theme: {
lingXiaGold: 'Ling Xia Altın',

View file

@ -3337,7 +3337,7 @@ const message = {
cpuUseExceedAvgHelper: '指定時間內 CPU 平均使用率超過指定值',
memoryUseExceedAvgHelper: '指定時間內記憶體平均使用率超過指定值',
loadUseExceedAvgHelper: '指定時間內負載平均使用率超過指定值',
resourceAlertRulesHelper: '注意30分鐘內持續告警只發送一次簡訊',
resourceAlertRulesHelper: '注意30分鐘內持續告警只發送一次',
specifiedTime: '指定時間',
deleteTitle: '删除告警',
deleteMsg: '是否確認删除告警任務',
@ -3460,6 +3460,19 @@ const message = {
portHelper: 'SSL 通常為 465TLS 通常為 587',
sslHelper: ' SMTP 連接埠為 465通常需要啟用 SSL',
tlsHelper: ' SMTP 連接埠為 587通常需要啟用 TLS',
triggerCondition: '觸發條件',
loginFail: '登入失敗',
nodeException: '節點異常告警',
licenseException: '許可證異常告警',
panelLogin: '面板登入異常告警',
sshLogin: 'SSH 登入異常告警',
panelIpLogin: '面板登入 IP 異常告警',
sshIpLogin: 'SSH 登入 IP 異常告警',
ipWhiteListHelper: '白名單中的 IP 不受任何規則限制',
nodeExceptionRule: '節點異常告警每天發送 {0} ',
licenseExceptionRule: '許可證異常告警每天發送 {0} ',
panelLoginRule: '面板登入告警每天發送 {0} ',
sshLoginRule: 'SSH 登入告警每天發送 {0} ',
},
theme: {
lingXiaGold: '凌霞金',

View file

@ -3306,7 +3306,7 @@ const message = {
cpuUseExceedAvgHelper: '指定时间内 CPU 平均使用率超过指定值',
memoryUseExceedAvgHelper: '指定时间内内存平均使用率超过指定值',
loadUseExceedAvgHelper: '指定时间内负载平均使用率超过指定值',
resourceAlertRulesHelper: '注意30 分钟内持续告警只发送一次短信',
resourceAlertRulesHelper: '注意30 分钟内持续告警只发送一次',
specifiedTime: '指定时间',
deleteTitle: '删除告警',
deleteMsg: '是否确认删除告警任务',
@ -3430,6 +3430,19 @@ const message = {
portHelper: 'SSL 通常为465TLS 通常为587',
sslHelper: '如果 SMTP 端口是 465通常需要启用 SSL',
tlsHelper: '如果 SMTP 端口是 587通常需要启用 TLS',
triggerCondition: '触发条件',
loginFail: '登录失败',
nodeException: '节点异常告警',
licenseException: '许可证异常告警',
panelLogin: '面板登录异常告警',
sshLogin: 'SSH 登录异常告警',
panelIpLogin: '面板登录 IP 异常告警',
sshIpLogin: 'SSH 登录 IP 异常告警',
ipWhiteListHelper: '白名单中的 IP 不受任何规则限制',
nodeExceptionRule: '节点异常告警每天发送 {0} ',
licenseExceptionRule: '许可证异常告警每天发送 {0} ',
panelLoginRule: '面板登录告警每天发送 {0} ',
sshLoginRule: 'SSH 登录告警告警每天发送 {0} ',
},
theme: {
lingXiaGold: '凌霞金',

View file

@ -675,14 +675,12 @@ let fileTypes = {
text: ['.iso', '.tiff', '.exe', '.so', '.bz', '.dmg', '.apk', '.pptx', '.ppt', '.xlsb'],
};
export const getFileType = (extension: string) => {
let type = 'text';
Object.entries(fileTypes).forEach(([key, extensions]) => {
if (extensions.includes(extension.toLowerCase())) {
type = key;
}
});
return type;
export const getFileType = (extension?: string) => {
const ext = extension?.toLowerCase();
if (!ext) return 'text';
const match = Object.entries(fileTypes).find((extensions) => extensions.includes(ext));
return match ? match[0] : 'text';
};
export const newUUID = () => {

View file

@ -20,7 +20,11 @@
<template v-if="isMaster">
<el-option value="panelPwdEndTime" :label="$t('xpack.alert.panelPwdEndTime')" />
<el-option value="panelUpdate" :label="$t('xpack.alert.panelUpdate')" />
<el-option value="nodeException" :label="$t('xpack.alert.nodeException')" />
<el-option value="licenseException" :label="$t('xpack.alert.licenseException')" />
<el-option value="panelLogin" :label="$t('xpack.alert.panelLogin')" />
</template>
<el-option value="sshLogin" :label="$t('xpack.alert.sshLogin')" />
<el-option value="clams" :label="$t('xpack.alert.clams')" />
<el-option value="shell" :label="$t('xpack.alert.cronjob') + '-' + $t('cronjob.shell')" />
<el-option value="app" :label="$t('xpack.alert.cronjob') + '-' + $t('cronjob.app')" />
@ -238,6 +242,10 @@ const formatRule = (row: Alert.AlertInfo) => {
cutWebsiteLog: () => t('xpack.alert.cronJobCutWebsiteLogRule', [row.sendCount]),
clean: () => t('xpack.alert.cronJobCleanRule', [row.sendCount]),
ntp: () => t('xpack.alert.cronJobNtpRule', [row.sendCount]),
nodeException: () => t('xpack.alert.nodeExceptionRule', [row.sendCount]),
licenseException: () => t('xpack.alert.licenseExceptionRule', [row.sendCount]),
panelLogin: () => t('xpack.alert.panelLoginRule', [row.sendCount]),
sshLogin: () => t('xpack.alert.sshLoginRule', [row.sendCount]),
};
return ruleTemplates[row.type] ? ruleTemplates[row.type]() : '';

View file

@ -16,18 +16,14 @@
v-model="dialogData.rowData!.type"
:disabled="dialogData.title === 'edit'"
>
<el-option value="ssl" :label="$t('xpack.alert.ssl')" />
<el-option value="siteEndTime" :label="$t('xpack.alert.siteEndTime')" />
<template v-if="isMaster">
<el-option value="panelPwdEndTime" :label="$t('xpack.alert.panelPwdEndTime')" />
<el-option value="panelUpdate" :label="$t('xpack.alert.panelUpdate')" />
<template v-for="item in allTaskOptions">
<el-option
:key="item.value"
v-if="item.show"
:value="item.value"
:label="$t(item.label)"
/>
</template>
<el-option value="clams" :label="$t('xpack.alert.clams')" />
<el-option value="cronJob" :label="$t('xpack.alert.cronjob')" />
<el-option value="cpu" :label="$t('xpack.alert.cpu')" />
<el-option value="memory" :label="$t('xpack.alert.memory')" />
<el-option value="load" :label="$t('xpack.alert.load')" />
<el-option value="disk" :label="$t('xpack.alert.disk')" />
</el-select>
<span
class="input-help"
@ -271,6 +267,43 @@
</span>
</el-form-item>
<el-form-item
:label="$t('xpack.alert.triggerCondition')"
v-if="ipTypes.includes(dialogData.rowData!.type)"
prop="count"
>
<div class="flex items-center flex-row md:flex-nowrap flex-wrap justify-between gap-x-3 w-full">
<el-form-item prop="cycle">
<el-input v-model.number="dialogData.rowData!.cycle" :max="200">
<template #append>{{ $t('commons.units.minute') }}</template>
</el-input>
</el-form-item>
<span class="whitespace-nowrap input-help w-[4.5rem]">
{{ $t('xpack.alert.loginFail') }}
</span>
<el-form-item prop="count">
<el-input v-model.number="dialogData.rowData!.count">
<template #append>{{ $t('commons.units.time') }}</template>
</el-input>
</el-form-item>
</div>
</el-form-item>
<el-form-item
:label="$t('setting.ipWhiteList')"
prop="advancedParams"
v-if="ipTypes.includes(dialogData.rowData!.type)"
>
<el-input
type="textarea"
:placeholder="$t('setting.ipWhiteListEgs')"
:rows="4"
v-model="dialogData.rowData!.advancedParams"
/>
<span class="input-help">{{ $t('xpack.alert.ipWhiteListHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('xpack.alert.sendCount')" prop="sendCount">
<el-input v-model.number="dialogData.rowData!.sendCount" />
<span class="input-help">
@ -297,7 +330,7 @@
</el-form-item>
<span class="input-help">
{{
avgTypes.includes(dialogData.rowData!.type) || diskTypes.includes(dialogData.rowData!.type)
intervalTypes.includes(dialogData.rowData!.type)
? $t('xpack.alert.resourceAlertRulesHelper')
: ''
}}
@ -333,6 +366,7 @@ import { getSettingInfo } from '@/api/modules/setting';
import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
import { routerToName } from '@/utils/router';
import { checkCidr, checkCidrV6, checkIpV4V6 } from '@/utils/util';
const globalStore = GlobalStore();
const { isMaster } = storeToRefs(globalStore);
@ -357,7 +391,10 @@ type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const timeTypes = ['ssl', 'siteEndTime', 'panelPwdEndTime'];
const avgTypes = ['cpu', 'memory', 'load'];
const ipTypes = ['sshLogin', 'panelLogin'];
const noParamTypes = ['panelUpdate'];
const intervalTypes = ['cpu', 'memory', 'load', 'disk', 'sshLogin', 'panelLogin', 'nodeException', 'licenseException'];
const diskTypes = ['disk'];
const cronjobTypes = [
'shell',
@ -395,8 +432,26 @@ const rules = reactive({
count: [Rules.requiredInput, Rules.integerNumber, { validator: checkCount, trigger: 'blur' }],
sendCount: [Rules.requiredInput, Rules.integerNumber, { validator: checkSendCount, trigger: 'blur' }],
sendMethod: [Rules.requiredSelect],
advancedParams: [{ required: false, validator: checkIPs, trigger: 'blur' }],
});
const allTaskOptions = [
{ value: 'ssl', label: 'xpack.alert.ssl', show: true },
{ value: 'siteEndTime', label: 'xpack.alert.siteEndTime', show: true },
{ value: 'panelPwdEndTime', label: 'xpack.alert.panelPwdEndTime', show: isMaster.value },
{ value: 'panelUpdate', label: 'xpack.alert.panelUpdate', show: isMaster.value },
{ value: 'nodeException', label: 'xpack.alert.nodeException', show: isMaster.value },
{ value: 'licenseException', label: 'xpack.alert.licenseException', show: isMaster.value },
{ value: 'panelLogin', label: 'xpack.alert.panelLogin', show: isMaster.value },
{ value: 'sshLogin', label: 'xpack.alert.sshLogin', show: true },
{ value: 'clams', label: 'xpack.alert.clams', show: true },
{ value: 'cronJob', label: 'xpack.alert.cronjob', show: true },
{ value: 'cpu', label: 'xpack.alert.cpu', show: true },
{ value: 'memory', label: 'xpack.alert.memory', show: true },
{ value: 'load', label: 'xpack.alert.load', show: true },
{ value: 'disk', label: 'xpack.alert.disk', show: true },
];
function checkCycle(rule: any, value: any, callback: any) {
if (value === '') {
callback();
@ -406,6 +461,11 @@ function checkCycle(rule: any, value: any, callback: any) {
if (!regex.test(value)) {
return callback(new Error(i18n.global.t('commons.rule.numberRange', [1, 60])));
}
} else if (ipTypes.includes(dialogData.value.rowData.type)) {
const regex = /^(?:[1-9]|[1-5][0-9]|200)$/;
if (!regex.test(value)) {
return callback(new Error(i18n.global.t('commons.rule.numberRange', [1, 200])));
}
} else {
const regex = /^(?:[1-9]|[12][0-9]|30)$/;
if (!regex.test(value)) {
@ -419,7 +479,7 @@ function checkCount(rule: any, value: any, callback: any) {
if (value === '') {
callback();
}
if (avgTypes.includes(dialogData.value.rowData.type)) {
if (avgTypes.includes(dialogData.value.rowData.type) || ipTypes.includes(dialogData.value.rowData.type)) {
const regex = /^(?:[1-9]|[1-9][0-9]|100)$/;
if (!regex.test(value)) {
return callback(new Error(i18n.global.t('commons.rule.numberRange', [1, 100])));
@ -464,6 +524,29 @@ function checkSendCount(rule: any, value: any, callback: any) {
callback();
}
function checkIPs(rule: any, value: any, callback: any) {
if (typeof value === 'string' && value.trim() !== '') {
let addr = value.split('\n');
for (const item of addr) {
if (item === '') {
continue;
}
if (item.indexOf('/') !== -1) {
if (item.indexOf(':') !== -1) {
if (checkCidrV6(item)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
} else if (checkCidr(item)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
} else if (checkIpV4V6(item)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
}
}
callback();
}
const initOptions = (type: string, subType: string) => {
if (type === 'ssl') {
loadSSLs();
@ -512,6 +595,10 @@ const changeType = () => {
cutWebsiteLog: 0,
clean: 0,
ntp: 0,
sshLogin: 30,
panelLogin: 30,
nodeException: 0,
licenseException: 0,
};
const typeToCountMap = {
@ -535,6 +622,10 @@ const changeType = () => {
cutWebsiteLog: 0,
clean: 0,
ntp: 0,
sshLogin: 3,
panelLogin: 3,
nodeException: 0,
licenseException: 0,
};
const typeToProjectMap = {
ssl: 'all',
@ -557,6 +648,10 @@ const changeType = () => {
cutWebsiteLog: '',
clean: '',
ntp: '',
sshLogin: 'all',
panelLogin: 'all',
nodeException: 'all',
licenseException: 'all',
};
const rowData = dialogData.value.rowData;
@ -656,6 +751,10 @@ const formatTitle = (row: Alert.AlertInfo) => {
cutWebsiteLog: () => t('xpack.alert.cronJobCutWebsiteLogTitle', [formatCronJobName(Number(row.project))]),
clean: () => t('xpack.alert.cronJobCleanTitle', [formatCronJobName(Number(row.project))]),
ntp: () => t('xpack.alert.cronJobNtpTitle', [formatCronJobName(Number(row.project))]),
nodeException: () => t('xpack.alert.nodeException'),
licenseException: () => t('xpack.alert.licenseException'),
panelLogin: () => t('xpack.alert.panelLogin'),
sshLogin: () => t('xpack.alert.sshLogin'),
};
return titleTemplates[row.type] ? titleTemplates[row.type]() : '';
@ -709,7 +808,7 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
emit('search');
visible.value = false;
} finally {
loading.value = false; //
loading.value = false;
}
});
};

View file

@ -98,7 +98,18 @@ const isProductPro = ref(false);
const loading = ref(false);
const data = ref();
const isOffline = ref('Disable');
const resourceTypes = ['cpu', 'memory', 'load', 'disk'];
const resourceTypes = [
'cpu',
'memory',
'load',
'disk',
'nodeException',
'licenseException',
'panelLogin',
'sshLogin',
'panelIpLogin',
'sshIpLogin',
];
const paginationConfig = reactive({
cacheSizeKey: 'alert-log-page-size',
currentPage: 1,
@ -125,7 +136,10 @@ const buttons = [
syncAlert(row);
},
disabled: (row: Alert.AlertLog) => {
return row.method != 'sms' && row.status != 'PushSuccess' && row.status != 'SyncError';
return (
(row.method != 'sms' && row.status != 'PushSuccess' && row.status != 'SyncError') ||
row.status == 'Success'
);
},
},
];
@ -188,6 +202,12 @@ const formatMessage = (row: Alert.AlertInfo) => {
cutWebsiteLog: () => t('xpack.alert.cronJobCutWebsiteLogTitle', [row.project]),
clean: () => t('xpack.alert.cronJobCleanTitle', [row.project]),
ntp: () => t('xpack.alert.cronJobNtpTitle', [row.project]),
nodeException: () => t('xpack.alert.nodeException'),
licenseException: () => t('xpack.alert.licenseException'),
panelLogin: () => t('xpack.alert.panelLogin'),
sshLogin: () => t('xpack.alert.sshLogin'),
panelIpLogin: () => t('xpack.alert.panelIpLogin'),
sshIpLogin: () => t('xpack.alert.sshIpLogin'),
};
let type = row.type === 'cronJob' ? row.subType : row.type;
return messageTemplates[type] ? messageTemplates[type]() : '';

View file

@ -19,8 +19,8 @@
v-model="noticeTimeRange"
class="!w-[235px] mx-1 mt-1"
is-range
:start-placeholder="$t('xpack.commons.search.timeStart')"
:end-placeholder="$t('xpack.commons.search.结束时间')"
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
/>
<span class="input-help ml-2">
{{
@ -37,8 +37,8 @@
v-model="resourceTimeRange"
class="!w-[235px] mx-1 mt-1"
is-range
:start-placeholder="$t('xpack.commons.search.timeStart')"
:end-placeholder="$t('xpack.commons.search.结束时间')"
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
/>
<span class="input-help ml-2">
{{
@ -123,7 +123,18 @@ const config = ref<Alert.AlertConfigInfo>({
status: '',
config: '',
});
const resourceValue = ref(['clams', 'cronJob', 'cpu', 'memory', 'load', 'disk']);
const resourceValue = ref([
'clams',
'cronJob',
'cpu',
'memory',
'load',
'disk',
'nodeException',
'licenseException',
'panelLogin',
'sshLogin',
]);
const noticeDefaultTime: [Date, Date] = [new Date(0, 0, 1, 8, 0, 0), new Date(0, 0, 1, 23, 59, 59)];
const resourceDefaultTime: [Date, Date] = [new Date(0, 0, 1, 0, 0, 0), new Date(0, 0, 1, 23, 59, 59)];
const noticeTimeRange = ref(noticeDefaultTime);
@ -140,6 +151,10 @@ const generateData = (): Option[] => {
data.push({ key: 'memory', label: i18n.global.t('xpack.alert.memory'), disabled: false });
data.push({ key: 'load', label: i18n.global.t('xpack.alert.load'), disabled: false });
data.push({ key: 'disk', label: i18n.global.t('xpack.alert.disk'), disabled: false });
data.push({ key: 'nodeException', label: i18n.global.t('xpack.alert.nodeException'), disabled: false });
data.push({ key: 'licenseException', label: i18n.global.t('xpack.alert.licenseException'), disabled: false });
data.push({ key: 'panelLogin', label: i18n.global.t('xpack.alert.panelLogin'), disabled: false });
data.push({ key: 'sshLogin', label: i18n.global.t('xpack.alert.sshLogin'), disabled: false });
return data;
};