mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-02-04 04:30:05 +00:00
refactor: simplify server & service manipulation (#993)
* refactor: simplify server & service manipulation * update * fix * update for nat, ddns & notification * chore * update cron * update dependencies * use of function iterators * update default dns servers
This commit is contained in:
@@ -56,10 +56,7 @@ func (a *authHandler) Check(ctx context.Context) (uint64, error) {
|
||||
return 0, status.Error(codes.Unauthenticated, "客户端 UUID 不合法")
|
||||
}
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
clientID, hasID := singleton.ServerUUIDToID[clientUUID]
|
||||
singleton.ServerLock.RUnlock()
|
||||
|
||||
clientID, hasID := singleton.ServerShared.UUIDToID(clientUUID)
|
||||
if !hasID {
|
||||
s := model.Server{UUID: clientUUID, Name: petname.Generate(2, "-"), Common: model.Common{
|
||||
UserID: userId,
|
||||
@@ -67,14 +64,9 @@ func (a *authHandler) Check(ctx context.Context) (uint64, error) {
|
||||
if err := singleton.DB.Create(&s).Error; err != nil {
|
||||
return 0, status.Error(codes.Unauthenticated, err.Error())
|
||||
}
|
||||
|
||||
model.InitServer(&s)
|
||||
|
||||
singleton.ServerLock.Lock()
|
||||
singleton.ServerList[s.ID] = &s
|
||||
singleton.ServerUUIDToID[clientUUID] = s.ID
|
||||
singleton.ServerLock.Unlock()
|
||||
|
||||
singleton.ReSortServer()
|
||||
singleton.ServerShared.Update(&s, clientUUID)
|
||||
|
||||
clientID = s.ID
|
||||
}
|
||||
|
||||
@@ -44,10 +44,8 @@ func (s *NezhaHandler) RequestTask(stream pb.NezhaService_RequestTaskServer) err
|
||||
return err
|
||||
}
|
||||
|
||||
singleton.ServerLock.Lock()
|
||||
singleton.ServerList[clientID].TaskStream = stream
|
||||
singleton.ServerLock.Unlock()
|
||||
|
||||
server, _ := singleton.ServerShared.Get(clientID)
|
||||
server.TaskStream = stream
|
||||
var result *pb.TaskResult
|
||||
for {
|
||||
result, err = stream.Recv()
|
||||
@@ -58,22 +56,18 @@ func (s *NezhaHandler) RequestTask(stream pb.NezhaService_RequestTaskServer) err
|
||||
switch result.GetType() {
|
||||
case model.TaskTypeCommand:
|
||||
// 处理上报的计划任务
|
||||
singleton.CronLock.RLock()
|
||||
cr := singleton.Crons[result.GetId()]
|
||||
singleton.CronLock.RUnlock()
|
||||
cr, _ := singleton.CronShared.Get(result.GetId())
|
||||
if cr != nil {
|
||||
// 保存当前服务器状态信息
|
||||
var curServer model.Server
|
||||
singleton.ServerLock.RLock()
|
||||
copier.Copy(&curServer, singleton.ServerList[clientID])
|
||||
singleton.ServerLock.RUnlock()
|
||||
copier.Copy(&curServer, server)
|
||||
if cr.PushSuccessful && result.GetSuccessful() {
|
||||
singleton.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", singleton.Localizer.T("Scheduled Task Executed Successfully"),
|
||||
cr.Name, singleton.ServerList[clientID].Name, result.GetData()), nil, &curServer)
|
||||
singleton.NotificationShared.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", singleton.Localizer.T("Scheduled Task Executed Successfully"),
|
||||
cr.Name, server.Name, result.GetData()), nil, &curServer)
|
||||
}
|
||||
if !result.GetSuccessful() {
|
||||
singleton.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", singleton.Localizer.T("Scheduled Task Executed Failed"),
|
||||
cr.Name, singleton.ServerList[clientID].Name, result.GetData()), nil, &curServer)
|
||||
singleton.NotificationShared.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", singleton.Localizer.T("Scheduled Task Executed Failed"),
|
||||
cr.Name, server.Name, result.GetData()), nil, &curServer)
|
||||
}
|
||||
singleton.DB.Model(cr).Updates(model.Cron{
|
||||
LastExecutedAt: time.Now().Add(time.Second * -1 * time.Duration(result.GetDelay())),
|
||||
@@ -81,16 +75,13 @@ func (s *NezhaHandler) RequestTask(stream pb.NezhaService_RequestTaskServer) err
|
||||
})
|
||||
}
|
||||
case model.TaskTypeReportConfig:
|
||||
singleton.ServerLock.RLock()
|
||||
if len(singleton.ServerList[clientID].ConfigCache) < 1 {
|
||||
if len(server.ConfigCache) < 1 {
|
||||
if !result.GetSuccessful() {
|
||||
singleton.ServerList[clientID].ConfigCache <- errors.New(result.Data)
|
||||
singleton.ServerLock.RUnlock()
|
||||
server.ConfigCache <- errors.New(result.Data)
|
||||
continue
|
||||
}
|
||||
singleton.ServerList[clientID].ConfigCache <- result.Data
|
||||
server.ConfigCache <- result.Data
|
||||
}
|
||||
singleton.ServerLock.RUnlock()
|
||||
default:
|
||||
if model.IsServiceSentinelNeeded(result.GetType()) {
|
||||
singleton.ServiceSentinelShared.Dispatch(singleton.ReportData{
|
||||
@@ -117,10 +108,7 @@ func (s *NezhaHandler) ReportSystemState(stream pb.NezhaService_ReportSystemStat
|
||||
}
|
||||
state := model.PB2State(state)
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
server, ok := singleton.ServerList[clientID]
|
||||
singleton.ServerLock.RUnlock()
|
||||
|
||||
server, ok := singleton.ServerShared.Get(clientID)
|
||||
if !ok || server == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -145,10 +133,7 @@ func (s *NezhaHandler) onReportSystemInfo(c context.Context, r *pb.Host) error {
|
||||
}
|
||||
host := model.PB2Host(r)
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
defer singleton.ServerLock.RUnlock()
|
||||
|
||||
server, ok := singleton.ServerList[clientID]
|
||||
server, ok := singleton.ServerShared.Get(clientID)
|
||||
if !ok || server == nil {
|
||||
return fmt.Errorf("server not found")
|
||||
}
|
||||
@@ -234,9 +219,7 @@ func (s *NezhaHandler) ReportGeoIP(c context.Context, r *pb.GeoIP) (*pb.GeoIP, e
|
||||
|
||||
joinedIP := geoip.IP.Join()
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
server, ok := singleton.ServerList[clientID]
|
||||
singleton.ServerLock.RUnlock()
|
||||
server, ok := singleton.ServerShared.Get(clientID)
|
||||
if !ok || server == nil {
|
||||
return nil, fmt.Errorf("server not found")
|
||||
}
|
||||
@@ -247,7 +230,7 @@ func (s *NezhaHandler) ReportGeoIP(c context.Context, r *pb.GeoIP) (*pb.GeoIP, e
|
||||
ipv4 := geoip.IP.IPv4Addr
|
||||
ipv6 := geoip.IP.IPv6Addr
|
||||
|
||||
providers, err := singleton.GetDDNSProvidersFromProfiles(server.DDNSProfiles, &ddns.IP{Ipv4Addr: ipv4, Ipv6Addr: ipv6})
|
||||
providers, err := singleton.DDNSShared.GetDDNSProvidersFromProfiles(server.DDNSProfiles, &model.IP{IPv4Addr: ipv4, IPv6Addr: ipv6})
|
||||
if err == nil {
|
||||
for _, provider := range providers {
|
||||
domains := server.OverrideDDNSDomains[provider.GetProfileID()]
|
||||
@@ -268,7 +251,7 @@ func (s *NezhaHandler) ReportGeoIP(c context.Context, r *pb.GeoIP) (*pb.GeoIP, e
|
||||
joinedIP != "" &&
|
||||
server.GeoIP.IP != geoip.IP {
|
||||
|
||||
singleton.SendNotification(singleton.Conf.IPChangeNotificationGroupID,
|
||||
singleton.NotificationShared.SendNotification(singleton.Conf.IPChangeNotificationGroupID,
|
||||
fmt.Sprintf(
|
||||
"[%s] %s, %s => %s",
|
||||
singleton.Localizer.T("IP Changed"),
|
||||
|
||||
@@ -132,15 +132,14 @@ func OnDeleteAlert(id []uint64) {
|
||||
func checkStatus() {
|
||||
AlertsLock.RLock()
|
||||
defer AlertsLock.RUnlock()
|
||||
ServerLock.RLock()
|
||||
defer ServerLock.RUnlock()
|
||||
m := ServerShared.GetList()
|
||||
|
||||
for _, alert := range Alerts {
|
||||
// 跳过未启用
|
||||
if !alert.Enabled() {
|
||||
continue
|
||||
}
|
||||
for _, server := range ServerList {
|
||||
for _, server := range m {
|
||||
// 监测点
|
||||
UserLock.RLock()
|
||||
var role uint8
|
||||
@@ -168,20 +167,20 @@ func checkStatus() {
|
||||
alertsPrevState[alert.ID][server.ID] = _RuleCheckFail
|
||||
message := fmt.Sprintf("[%s] %s(%s) %s", Localizer.T("Incident"),
|
||||
server.Name, IPDesensitize(server.GeoIP.IP.Join()), alert.Name)
|
||||
go SendTriggerTasks(alert.FailTriggerTasks, curServer.ID)
|
||||
go SendNotification(alert.NotificationGroupID, message, NotificationMuteLabel.ServerIncident(server.ID, alert.ID), &curServer)
|
||||
go CronShared.SendTriggerTasks(alert.FailTriggerTasks, curServer.ID)
|
||||
go NotificationShared.SendNotification(alert.NotificationGroupID, message, NotificationMuteLabel.ServerIncident(server.ID, alert.ID), &curServer)
|
||||
// 清除恢复通知的静音缓存
|
||||
UnMuteNotification(alert.NotificationGroupID, NotificationMuteLabel.ServerIncidentResolved(server.ID, alert.ID))
|
||||
NotificationShared.UnMuteNotification(alert.NotificationGroupID, NotificationMuteLabel.ServerIncidentResolved(server.ID, alert.ID))
|
||||
}
|
||||
} else {
|
||||
// 本次通过检查但上一次的状态为失败,则发送恢复通知
|
||||
if alertsPrevState[alert.ID][server.ID] == _RuleCheckFail {
|
||||
message := fmt.Sprintf("[%s] %s(%s) %s", Localizer.T("Resolved"),
|
||||
server.Name, IPDesensitize(server.GeoIP.IP.Join()), alert.Name)
|
||||
go SendTriggerTasks(alert.RecoverTriggerTasks, curServer.ID)
|
||||
go SendNotification(alert.NotificationGroupID, message, NotificationMuteLabel.ServerIncidentResolved(server.ID, alert.ID), &curServer)
|
||||
go CronShared.SendTriggerTasks(alert.RecoverTriggerTasks, curServer.ID)
|
||||
go NotificationShared.SendNotification(alert.NotificationGroupID, message, NotificationMuteLabel.ServerIncidentResolved(server.ID, alert.ID), &curServer)
|
||||
// 清除失败通知的静音缓存
|
||||
UnMuteNotification(alert.NotificationGroupID, NotificationMuteLabel.ServerIncident(server.ID, alert.ID))
|
||||
NotificationShared.UnMuteNotification(alert.NotificationGroupID, NotificationMuteLabel.ServerIncident(server.ID, alert.ID))
|
||||
}
|
||||
alertsPrevState[alert.ID][server.ID] = _RuleCheckPass
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
|
||||
@@ -16,36 +15,32 @@ import (
|
||||
pb "github.com/nezhahq/nezha/proto"
|
||||
)
|
||||
|
||||
var (
|
||||
Cron *cron.Cron
|
||||
Crons map[uint64]*model.Cron // [CronID] -> *model.Cron
|
||||
CronLock sync.RWMutex
|
||||
|
||||
CronList []*model.Cron
|
||||
)
|
||||
|
||||
func InitCronTask() {
|
||||
Cron = cron.New(cron.WithSeconds(), cron.WithLocation(Loc))
|
||||
Crons = make(map[uint64]*model.Cron)
|
||||
type CronClass struct {
|
||||
class[uint64, *model.Cron]
|
||||
*cron.Cron
|
||||
}
|
||||
|
||||
// loadCronTasks 加载计划任务
|
||||
func loadCronTasks() {
|
||||
InitCronTask()
|
||||
DB.Find(&CronList)
|
||||
func NewCronClass() *CronClass {
|
||||
cronx := cron.New(cron.WithSeconds(), cron.WithLocation(Loc))
|
||||
list := make(map[uint64]*model.Cron)
|
||||
|
||||
var sortedList []*model.Cron
|
||||
DB.Find(&sortedList)
|
||||
|
||||
var err error
|
||||
var notificationGroupList []uint64
|
||||
notificationMsgMap := make(map[uint64]*strings.Builder)
|
||||
for _, cron := range CronList {
|
||||
|
||||
for _, cron := range sortedList {
|
||||
// 触发任务类型无需注册
|
||||
if cron.TaskType == model.CronTypeTriggerTask {
|
||||
Crons[cron.ID] = cron
|
||||
list[cron.ID] = cron
|
||||
continue
|
||||
}
|
||||
// 注册计划任务
|
||||
cron.CronJobID, err = Cron.AddFunc(cron.Scheduler, CronTrigger(cron))
|
||||
cron.CronJobID, err = cronx.AddFunc(cron.Scheduler, CronTrigger(cron))
|
||||
if err == nil {
|
||||
Crons[cron.ID] = cron
|
||||
list[cron.ID] = cron
|
||||
} else {
|
||||
// 当前通知组首次出现 将其加入通知组列表并初始化通知组消息缓存
|
||||
if _, ok := notificationMsgMap[cron.NotificationGroupID]; !ok {
|
||||
@@ -56,61 +51,74 @@ func loadCronTasks() {
|
||||
notificationMsgMap[cron.NotificationGroupID].WriteString(fmt.Sprintf("%d,", cron.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// 向注册错误的计划任务所在通知组发送通知
|
||||
for _, gid := range notificationGroupList {
|
||||
notificationMsgMap[gid].WriteString(Localizer.T("] These tasks will not execute properly. Fix them in the admin dashboard."))
|
||||
SendNotification(gid, notificationMsgMap[gid].String(), nil)
|
||||
NotificationShared.SendNotification(gid, notificationMsgMap[gid].String(), nil)
|
||||
}
|
||||
cronx.Start()
|
||||
|
||||
return &CronClass{
|
||||
class: class[uint64, *model.Cron]{
|
||||
list: list,
|
||||
sortedList: sortedList,
|
||||
},
|
||||
Cron: cronx,
|
||||
}
|
||||
Cron.Start()
|
||||
}
|
||||
|
||||
func OnRefreshOrAddCron(c *model.Cron) {
|
||||
CronLock.Lock()
|
||||
defer CronLock.Unlock()
|
||||
crOld := Crons[c.ID]
|
||||
func (c *CronClass) Update(cr *model.Cron) {
|
||||
c.listMu.Lock()
|
||||
crOld := c.list[cr.ID]
|
||||
if crOld != nil && crOld.CronJobID != 0 {
|
||||
Cron.Remove(crOld.CronJobID)
|
||||
c.Cron.Remove(crOld.CronJobID)
|
||||
}
|
||||
|
||||
delete(Crons, c.ID)
|
||||
Crons[c.ID] = c
|
||||
delete(c.list, cr.ID)
|
||||
c.list[cr.ID] = cr
|
||||
c.listMu.Unlock()
|
||||
|
||||
c.sortList()
|
||||
}
|
||||
|
||||
func UpdateCronList() {
|
||||
CronLock.RLock()
|
||||
defer CronLock.RUnlock()
|
||||
func (c *CronClass) Delete(idList []uint64) {
|
||||
c.listMu.Lock()
|
||||
for _, id := range idList {
|
||||
cr := c.list[id]
|
||||
if cr != nil && cr.CronJobID != 0 {
|
||||
c.Cron.Remove(cr.CronJobID)
|
||||
}
|
||||
delete(c.list, id)
|
||||
}
|
||||
c.listMu.Unlock()
|
||||
|
||||
CronList = utils.MapValuesToSlice(Crons)
|
||||
slices.SortFunc(CronList, func(a, b *model.Cron) int {
|
||||
c.sortList()
|
||||
}
|
||||
|
||||
func (c *CronClass) sortList() {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
|
||||
sortedList := utils.MapValuesToSlice(c.list)
|
||||
slices.SortFunc(sortedList, func(a, b *model.Cron) int {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
})
|
||||
|
||||
c.sortedListMu.Lock()
|
||||
defer c.sortedListMu.Unlock()
|
||||
c.sortedList = sortedList
|
||||
}
|
||||
|
||||
func OnDeleteCron(id []uint64) {
|
||||
CronLock.Lock()
|
||||
defer CronLock.Unlock()
|
||||
for _, i := range id {
|
||||
cr := Crons[i]
|
||||
if cr != nil && cr.CronJobID != 0 {
|
||||
Cron.Remove(cr.CronJobID)
|
||||
}
|
||||
delete(Crons, i)
|
||||
}
|
||||
}
|
||||
|
||||
func ManualTrigger(c *model.Cron) {
|
||||
CronTrigger(c)()
|
||||
}
|
||||
|
||||
func SendTriggerTasks(taskIDs []uint64, triggerServer uint64) {
|
||||
CronLock.RLock()
|
||||
func (c *CronClass) SendTriggerTasks(taskIDs []uint64, triggerServer uint64) {
|
||||
c.listMu.RLock()
|
||||
var cronLists []*model.Cron
|
||||
for _, taskID := range taskIDs {
|
||||
if c, ok := Crons[taskID]; ok {
|
||||
if c, ok := c.list[taskID]; ok {
|
||||
cronLists = append(cronLists, c)
|
||||
}
|
||||
}
|
||||
CronLock.RUnlock()
|
||||
c.listMu.RUnlock()
|
||||
|
||||
// 依次调用CronTrigger发送任务
|
||||
for _, c := range cronLists {
|
||||
@@ -118,6 +126,10 @@ func SendTriggerTasks(taskIDs []uint64, triggerServer uint64) {
|
||||
}
|
||||
}
|
||||
|
||||
func ManualTrigger(cr *model.Cron) {
|
||||
CronTrigger(cr)()
|
||||
}
|
||||
|
||||
func CronTrigger(cr *model.Cron, triggerServer ...uint64) func() {
|
||||
crIgnoreMap := make(map[uint64]bool)
|
||||
for j := 0; j < len(cr.Servers); j++ {
|
||||
@@ -128,9 +140,7 @@ func CronTrigger(cr *model.Cron, triggerServer ...uint64) func() {
|
||||
if len(triggerServer) == 0 {
|
||||
return
|
||||
}
|
||||
ServerLock.RLock()
|
||||
defer ServerLock.RUnlock()
|
||||
if s, ok := ServerList[triggerServer[0]]; ok {
|
||||
if s, ok := ServerShared.Get(triggerServer[0]); ok {
|
||||
if s.TaskStream != nil {
|
||||
s.TaskStream.Send(&pb.Task{
|
||||
Id: cr.ID,
|
||||
@@ -141,15 +151,13 @@ func CronTrigger(cr *model.Cron, triggerServer ...uint64) func() {
|
||||
// 保存当前服务器状态信息
|
||||
curServer := model.Server{}
|
||||
copier.Copy(&curServer, s)
|
||||
SendNotification(cr.NotificationGroupID, Localizer.Tf("[Task failed] %s: server %s is offline and cannot execute the task", cr.Name, s.Name), nil, &curServer)
|
||||
NotificationShared.SendNotification(cr.NotificationGroupID, Localizer.Tf("[Task failed] %s: server %s is offline and cannot execute the task", cr.Name, s.Name), nil, &curServer)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ServerLock.RLock()
|
||||
defer ServerLock.RUnlock()
|
||||
for _, s := range ServerList {
|
||||
for _, s := range ServerShared.Range {
|
||||
if cr.Cover == model.CronCoverAll && crIgnoreMap[s.ID] {
|
||||
continue
|
||||
}
|
||||
@@ -166,7 +174,7 @@ func CronTrigger(cr *model.Cron, triggerServer ...uint64) func() {
|
||||
// 保存当前服务器状态信息
|
||||
curServer := model.Server{}
|
||||
copier.Copy(&curServer, s)
|
||||
SendNotification(cr.NotificationGroupID, Localizer.Tf("[Task failed] %s: server %s is offline and cannot execute the task", cr.Name, s.Name), nil, &curServer)
|
||||
NotificationShared.SendNotification(cr.NotificationGroupID, Localizer.Tf("[Task failed] %s: server %s is offline and cannot execute the task", cr.Name, s.Name), nil, &curServer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/libdns/cloudflare"
|
||||
tencentcloud "github.com/nezhahq/libdns-tencentcloud"
|
||||
@@ -16,67 +15,61 @@ import (
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
DDNSCache map[uint64]*model.DDNSProfile
|
||||
DDNSCacheLock sync.RWMutex
|
||||
DDNSList []*model.DDNSProfile
|
||||
DDNSListLock sync.RWMutex
|
||||
)
|
||||
type DDNSClass struct {
|
||||
class[uint64, *model.DDNSProfile]
|
||||
}
|
||||
|
||||
func initDDNS() {
|
||||
DB.Find(&DDNSList)
|
||||
DDNSCache = make(map[uint64]*model.DDNSProfile)
|
||||
for i := 0; i < len(DDNSList); i++ {
|
||||
DDNSCache[DDNSList[i].ID] = DDNSList[i]
|
||||
func NewDDNSClass() *DDNSClass {
|
||||
var sortedList []*model.DDNSProfile
|
||||
|
||||
DB.Find(&sortedList)
|
||||
list := make(map[uint64]*model.DDNSProfile, len(sortedList))
|
||||
for _, profile := range sortedList {
|
||||
list[profile.ID] = profile
|
||||
}
|
||||
|
||||
dc := &DDNSClass{
|
||||
class: class[uint64, *model.DDNSProfile]{
|
||||
list: list,
|
||||
sortedList: sortedList,
|
||||
},
|
||||
}
|
||||
|
||||
OnNameserverUpdate()
|
||||
return dc
|
||||
}
|
||||
|
||||
func OnDDNSUpdate(p *model.DDNSProfile) {
|
||||
DDNSCacheLock.Lock()
|
||||
defer DDNSCacheLock.Unlock()
|
||||
DDNSCache[p.ID] = p
|
||||
func (c *DDNSClass) Update(p *model.DDNSProfile) {
|
||||
c.listMu.Lock()
|
||||
c.list[p.ID] = p
|
||||
c.listMu.Unlock()
|
||||
|
||||
c.sortList()
|
||||
}
|
||||
|
||||
func OnDDNSDelete(id []uint64) {
|
||||
DDNSCacheLock.Lock()
|
||||
defer DDNSCacheLock.Unlock()
|
||||
|
||||
for _, i := range id {
|
||||
delete(DDNSCache, i)
|
||||
func (c *DDNSClass) Delete(idList []uint64) {
|
||||
c.listMu.Lock()
|
||||
for _, id := range idList {
|
||||
delete(c.list, id)
|
||||
}
|
||||
c.listMu.Unlock()
|
||||
|
||||
c.sortList()
|
||||
}
|
||||
|
||||
func UpdateDDNSList() {
|
||||
DDNSCacheLock.RLock()
|
||||
defer DDNSCacheLock.RUnlock()
|
||||
|
||||
DDNSListLock.Lock()
|
||||
defer DDNSListLock.Unlock()
|
||||
|
||||
DDNSList = utils.MapValuesToSlice(DDNSCache)
|
||||
slices.SortFunc(DDNSList, func(a, b *model.DDNSProfile) int {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func OnNameserverUpdate() {
|
||||
ddns2.InitDNSServers(Conf.DNSServers)
|
||||
}
|
||||
|
||||
func GetDDNSProvidersFromProfiles(profileId []uint64, ip *ddns2.IP) ([]*ddns2.Provider, error) {
|
||||
func (c *DDNSClass) GetDDNSProvidersFromProfiles(profileId []uint64, ip *model.IP) ([]*ddns2.Provider, error) {
|
||||
profiles := make([]*model.DDNSProfile, 0, len(profileId))
|
||||
DDNSCacheLock.RLock()
|
||||
|
||||
c.listMu.RLock()
|
||||
for _, id := range profileId {
|
||||
if profile, ok := DDNSCache[id]; ok {
|
||||
if profile, ok := c.list[id]; ok {
|
||||
profiles = append(profiles, profile)
|
||||
} else {
|
||||
DDNSCacheLock.RUnlock()
|
||||
c.listMu.RUnlock()
|
||||
return nil, fmt.Errorf("无法找到DDNS配置 ID %d", id)
|
||||
}
|
||||
}
|
||||
DDNSCacheLock.RUnlock()
|
||||
c.listMu.RUnlock()
|
||||
|
||||
providers := make([]*ddns2.Provider, 0, len(profiles))
|
||||
for _, profile := range profiles {
|
||||
@@ -100,3 +93,21 @@ func GetDDNSProvidersFromProfiles(profileId []uint64, ip *ddns2.IP) ([]*ddns2.Pr
|
||||
}
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func (c *DDNSClass) sortList() {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
|
||||
sortedList := utils.MapValuesToSlice(c.list)
|
||||
slices.SortFunc(sortedList, func(a, b *model.DDNSProfile) int {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
})
|
||||
|
||||
c.sortedListMu.Lock()
|
||||
defer c.sortedListMu.Unlock()
|
||||
c.sortedList = sortedList
|
||||
}
|
||||
|
||||
func OnNameserverUpdate() {
|
||||
ddns2.InitDNSServers(Conf.DNSServers)
|
||||
}
|
||||
|
||||
@@ -3,69 +3,89 @@ package singleton
|
||||
import (
|
||||
"cmp"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
NATCache = make(map[string]*model.NAT)
|
||||
NATCacheRwLock sync.RWMutex
|
||||
type NATClass struct {
|
||||
class[string, *model.NAT]
|
||||
|
||||
NATIDToDomain = make(map[uint64]string)
|
||||
NATList []*model.NAT
|
||||
NATListLock sync.RWMutex
|
||||
)
|
||||
idToDomain map[uint64]string
|
||||
}
|
||||
|
||||
func initNAT() {
|
||||
DB.Find(&NATList)
|
||||
NATCache = make(map[string]*model.NAT)
|
||||
for i := 0; i < len(NATList); i++ {
|
||||
NATCache[NATList[i].Domain] = NATList[i]
|
||||
NATIDToDomain[NATList[i].ID] = NATList[i].Domain
|
||||
func NewNATClass() *NATClass {
|
||||
var sortedList []*model.NAT
|
||||
|
||||
DB.Find(&sortedList)
|
||||
list := make(map[string]*model.NAT, len(sortedList))
|
||||
idToDomain := make(map[uint64]string, len(sortedList))
|
||||
for _, profile := range list {
|
||||
list[profile.Domain] = profile
|
||||
idToDomain[profile.ID] = profile.Domain
|
||||
}
|
||||
|
||||
return &NATClass{
|
||||
class: class[string, *model.NAT]{
|
||||
list: list,
|
||||
sortedList: sortedList,
|
||||
},
|
||||
idToDomain: idToDomain,
|
||||
}
|
||||
}
|
||||
|
||||
func OnNATUpdate(n *model.NAT) {
|
||||
NATCacheRwLock.Lock()
|
||||
defer NATCacheRwLock.Unlock()
|
||||
func (c *NATClass) Update(n *model.NAT) {
|
||||
c.listMu.Lock()
|
||||
|
||||
if oldDomain, ok := NATIDToDomain[n.ID]; ok && oldDomain != n.Domain {
|
||||
delete(NATCache, oldDomain)
|
||||
if oldDomain, ok := c.idToDomain[n.ID]; ok && oldDomain != n.Domain {
|
||||
delete(c.list, oldDomain)
|
||||
}
|
||||
|
||||
NATCache[n.Domain] = n
|
||||
NATIDToDomain[n.ID] = n.Domain
|
||||
c.list[n.Domain] = n
|
||||
c.idToDomain[n.ID] = n.Domain
|
||||
|
||||
c.listMu.Unlock()
|
||||
c.sortList()
|
||||
}
|
||||
|
||||
func OnNATDelete(id []uint64) {
|
||||
NATCacheRwLock.Lock()
|
||||
defer NATCacheRwLock.Unlock()
|
||||
func (c *NATClass) Delete(idList []uint64) {
|
||||
c.listMu.Lock()
|
||||
|
||||
for _, i := range id {
|
||||
if domain, ok := NATIDToDomain[i]; ok {
|
||||
delete(NATCache, domain)
|
||||
delete(NATIDToDomain, i)
|
||||
for _, id := range idList {
|
||||
if domain, ok := c.idToDomain[id]; ok {
|
||||
delete(c.list, domain)
|
||||
delete(c.idToDomain, id)
|
||||
}
|
||||
}
|
||||
|
||||
c.listMu.Unlock()
|
||||
c.sortList()
|
||||
}
|
||||
|
||||
func UpdateNATList() {
|
||||
NATCacheRwLock.RLock()
|
||||
defer NATCacheRwLock.RUnlock()
|
||||
func (c *NATClass) GetNATConfigByDomain(domain string) *model.NAT {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
|
||||
NATListLock.Lock()
|
||||
defer NATListLock.Unlock()
|
||||
return c.list[domain]
|
||||
}
|
||||
|
||||
NATList = utils.MapValuesToSlice(NATCache)
|
||||
slices.SortFunc(NATList, func(a, b *model.NAT) int {
|
||||
func (c *NATClass) GetDomain(id uint64) string {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
|
||||
return c.idToDomain[id]
|
||||
}
|
||||
|
||||
func (c *NATClass) sortList() {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
|
||||
sortedList := utils.MapValuesToSlice(c.list)
|
||||
slices.SortFunc(sortedList, func(a, b *model.NAT) int {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func GetNATConfigByDomain(domain string) *model.NAT {
|
||||
NATCacheRwLock.RLock()
|
||||
defer NATCacheRwLock.RUnlock()
|
||||
return NATCache[domain]
|
||||
c.sortedListMu.Lock()
|
||||
defer c.sortedListMu.Unlock()
|
||||
c.sortedList = sortedList
|
||||
}
|
||||
|
||||
@@ -16,217 +16,193 @@ const (
|
||||
firstNotificationDelay = time.Minute * 15
|
||||
)
|
||||
|
||||
// 通知方式
|
||||
var (
|
||||
NotificationList map[uint64]map[uint64]*model.Notification // [NotificationGroupID][NotificationID] -> model.Notification
|
||||
NotificationIDToGroups map[uint64]map[uint64]struct{} // [NotificationID] -> NotificationGroupID
|
||||
type NotificationClass struct {
|
||||
class[uint64, *model.Notification]
|
||||
|
||||
NotificationMap map[uint64]*model.Notification
|
||||
NotificationListSorted []*model.Notification
|
||||
NotificationGroup map[uint64]string // [NotificationGroupID] -> [NotificationGroupName]
|
||||
groupToIDList map[uint64]map[uint64]*model.Notification
|
||||
idToGroupList map[uint64]map[uint64]struct{}
|
||||
|
||||
NotificationsLock sync.RWMutex
|
||||
NotificationSortedLock sync.RWMutex
|
||||
NotificationGroupLock sync.RWMutex
|
||||
)
|
||||
|
||||
// InitNotification 初始化 GroupID <-> ID <-> Notification 的映射
|
||||
func initNotification() {
|
||||
NotificationList = make(map[uint64]map[uint64]*model.Notification)
|
||||
NotificationIDToGroups = make(map[uint64]map[uint64]struct{})
|
||||
NotificationGroup = make(map[uint64]string)
|
||||
groupList map[uint64]string
|
||||
groupMu sync.RWMutex
|
||||
}
|
||||
|
||||
// loadNotifications 从 DB 初始化通知方式相关参数
|
||||
func loadNotifications() {
|
||||
initNotification()
|
||||
func NewNotificationClass() *NotificationClass {
|
||||
var sortedList []*model.Notification
|
||||
|
||||
groupToIDList := make(map[uint64]map[uint64]*model.Notification)
|
||||
idToGroupList := make(map[uint64]map[uint64]struct{})
|
||||
|
||||
groupNotifications := make(map[uint64][]uint64)
|
||||
var ngn []model.NotificationGroupNotification
|
||||
if err := DB.Find(&ngn).Error; err != nil {
|
||||
panic(err)
|
||||
}
|
||||
DB.Find(&ngn)
|
||||
|
||||
for _, n := range ngn {
|
||||
groupNotifications[n.NotificationGroupID] = append(groupNotifications[n.NotificationGroupID], n.NotificationID)
|
||||
}
|
||||
|
||||
if err := DB.Find(&NotificationListSorted).Error; err != nil {
|
||||
panic(err)
|
||||
DB.Find(&sortedList)
|
||||
list := make(map[uint64]*model.Notification, len(sortedList))
|
||||
for _, n := range sortedList {
|
||||
list[n.ID] = n
|
||||
}
|
||||
|
||||
var groups []model.NotificationGroup
|
||||
DB.Find(&groups)
|
||||
groupList := make(map[uint64]string)
|
||||
for _, grp := range groups {
|
||||
NotificationGroup[grp.ID] = grp.Name
|
||||
}
|
||||
|
||||
NotificationMap = make(map[uint64]*model.Notification, len(NotificationListSorted))
|
||||
for i := range NotificationListSorted {
|
||||
NotificationMap[NotificationListSorted[i].ID] = NotificationListSorted[i]
|
||||
groupList[grp.ID] = grp.Name
|
||||
}
|
||||
|
||||
for gid, nids := range groupNotifications {
|
||||
NotificationList[gid] = make(map[uint64]*model.Notification)
|
||||
groupToIDList[gid] = make(map[uint64]*model.Notification)
|
||||
for _, nid := range nids {
|
||||
if n, ok := NotificationMap[nid]; ok {
|
||||
NotificationList[gid][n.ID] = n
|
||||
if n, ok := list[nid]; ok {
|
||||
groupToIDList[gid][n.ID] = n
|
||||
|
||||
if NotificationIDToGroups[n.ID] == nil {
|
||||
NotificationIDToGroups[n.ID] = make(map[uint64]struct{})
|
||||
if idToGroupList[n.ID] == nil {
|
||||
idToGroupList[n.ID] = make(map[uint64]struct{})
|
||||
}
|
||||
|
||||
NotificationIDToGroups[n.ID][gid] = struct{}{}
|
||||
idToGroupList[n.ID][gid] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nc := &NotificationClass{
|
||||
class: class[uint64, *model.Notification]{
|
||||
list: list,
|
||||
sortedList: sortedList,
|
||||
},
|
||||
groupToIDList: groupToIDList,
|
||||
idToGroupList: idToGroupList,
|
||||
groupList: groupList,
|
||||
}
|
||||
return nc
|
||||
}
|
||||
|
||||
func UpdateNotificationList() {
|
||||
NotificationsLock.RLock()
|
||||
defer NotificationsLock.RUnlock()
|
||||
func (c *NotificationClass) Update(n *model.Notification) {
|
||||
c.listMu.Lock()
|
||||
|
||||
NotificationSortedLock.Lock()
|
||||
defer NotificationSortedLock.Unlock()
|
||||
_, ok := c.list[n.ID]
|
||||
c.list[n.ID] = n
|
||||
|
||||
NotificationListSorted = utils.MapValuesToSlice(NotificationMap)
|
||||
slices.SortFunc(NotificationListSorted, func(a, b *model.Notification) int {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
})
|
||||
}
|
||||
|
||||
// OnRefreshOrAddNotificationGroup 刷新通知方式组相关参数
|
||||
func OnRefreshOrAddNotificationGroup(ng *model.NotificationGroup, ngn []uint64) {
|
||||
NotificationsLock.Lock()
|
||||
defer NotificationsLock.Unlock()
|
||||
|
||||
NotificationGroupLock.Lock()
|
||||
defer NotificationGroupLock.Unlock()
|
||||
var isEdit bool
|
||||
if _, ok := NotificationGroup[ng.ID]; ok {
|
||||
isEdit = true
|
||||
if ok {
|
||||
if gids, ok := c.idToGroupList[n.ID]; ok {
|
||||
for gid := range gids {
|
||||
c.groupToIDList[gid][n.ID] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !isEdit {
|
||||
AddNotificationGroupToList(ng, ngn)
|
||||
c.listMu.Unlock()
|
||||
c.sortList()
|
||||
}
|
||||
|
||||
func (c *NotificationClass) UpdateGroup(ng *model.NotificationGroup, ngn []uint64) {
|
||||
c.groupMu.Lock()
|
||||
defer c.groupMu.Unlock()
|
||||
|
||||
_, ok := c.groupList[ng.ID]
|
||||
c.groupList[ng.ID] = ng.Name
|
||||
|
||||
c.listMu.Lock()
|
||||
defer c.listMu.Unlock()
|
||||
if !ok {
|
||||
c.groupToIDList[ng.ID] = make(map[uint64]*model.Notification, len(ngn))
|
||||
for _, n := range ngn {
|
||||
if c.idToGroupList[n] == nil {
|
||||
c.idToGroupList[n] = make(map[uint64]struct{})
|
||||
}
|
||||
c.idToGroupList[n][ng.ID] = struct{}{}
|
||||
c.groupToIDList[ng.ID][n] = c.list[n]
|
||||
}
|
||||
} else {
|
||||
UpdateNotificationGroupInList(ng, ngn)
|
||||
}
|
||||
}
|
||||
|
||||
// AddNotificationGroupToList 添加通知方式组到map中
|
||||
func AddNotificationGroupToList(ng *model.NotificationGroup, ngn []uint64) {
|
||||
NotificationGroup[ng.ID] = ng.Name
|
||||
|
||||
NotificationList[ng.ID] = make(map[uint64]*model.Notification, len(ngn))
|
||||
|
||||
for _, n := range ngn {
|
||||
if NotificationIDToGroups[n] == nil {
|
||||
NotificationIDToGroups[n] = make(map[uint64]struct{})
|
||||
oldList := make(map[uint64]struct{})
|
||||
for nid := range c.groupToIDList[ng.ID] {
|
||||
oldList[nid] = struct{}{}
|
||||
}
|
||||
NotificationIDToGroups[n][ng.ID] = struct{}{}
|
||||
NotificationList[ng.ID][n] = NotificationMap[n]
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateNotificationGroupInList 在 map 中更新通知方式组
|
||||
func UpdateNotificationGroupInList(ng *model.NotificationGroup, ngn []uint64) {
|
||||
NotificationGroup[ng.ID] = ng.Name
|
||||
|
||||
oldList := make(map[uint64]struct{})
|
||||
for nid := range NotificationList[ng.ID] {
|
||||
oldList[nid] = struct{}{}
|
||||
}
|
||||
|
||||
NotificationList[ng.ID] = make(map[uint64]*model.Notification)
|
||||
for _, nid := range ngn {
|
||||
NotificationList[ng.ID][nid] = NotificationMap[nid]
|
||||
if NotificationIDToGroups[nid] == nil {
|
||||
NotificationIDToGroups[nid] = make(map[uint64]struct{})
|
||||
c.groupToIDList[ng.ID] = make(map[uint64]*model.Notification)
|
||||
for _, nid := range ngn {
|
||||
c.groupToIDList[ng.ID][nid] = c.list[nid]
|
||||
if c.idToGroupList[nid] == nil {
|
||||
c.idToGroupList[nid] = make(map[uint64]struct{})
|
||||
}
|
||||
c.idToGroupList[nid][ng.ID] = struct{}{}
|
||||
}
|
||||
NotificationIDToGroups[nid][ng.ID] = struct{}{}
|
||||
}
|
||||
|
||||
for oldID := range oldList {
|
||||
if _, ok := NotificationList[ng.ID][oldID]; !ok {
|
||||
delete(NotificationIDToGroups[oldID], ng.ID)
|
||||
if len(NotificationIDToGroups[oldID]) == 0 {
|
||||
delete(NotificationIDToGroups, oldID)
|
||||
for oldID := range oldList {
|
||||
if _, ok := c.groupToIDList[ng.ID][oldID]; !ok {
|
||||
delete(c.groupToIDList[oldID], ng.ID)
|
||||
if len(c.idToGroupList[oldID]) == 0 {
|
||||
delete(c.idToGroupList, oldID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateNotificationGroupInList 删除通知方式组
|
||||
func OnDeleteNotificationGroup(gids []uint64) {
|
||||
NotificationsLock.Lock()
|
||||
defer NotificationsLock.Unlock()
|
||||
func (c *NotificationClass) Delete(idList []uint64) {
|
||||
c.listMu.Lock()
|
||||
|
||||
for _, id := range idList {
|
||||
delete(c.list, id)
|
||||
// 如果绑定了通知组才删除
|
||||
if gids, ok := c.idToGroupList[id]; ok {
|
||||
for gid := range gids {
|
||||
delete(c.groupToIDList[gid], id)
|
||||
delete(c.idToGroupList, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.listMu.Unlock()
|
||||
c.sortList()
|
||||
}
|
||||
|
||||
func (c *NotificationClass) DeleteGroup(gids []uint64) {
|
||||
c.listMu.Lock()
|
||||
defer c.listMu.Unlock()
|
||||
c.groupMu.Lock()
|
||||
defer c.groupMu.Unlock()
|
||||
|
||||
for _, gid := range gids {
|
||||
delete(NotificationGroup, gid)
|
||||
delete(NotificationList, gid)
|
||||
delete(c.groupList, gid)
|
||||
delete(c.groupToIDList, gid)
|
||||
}
|
||||
}
|
||||
|
||||
// OnRefreshOrAddNotification 刷新通知方式相关参数
|
||||
func OnRefreshOrAddNotification(n *model.Notification) {
|
||||
NotificationsLock.Lock()
|
||||
defer NotificationsLock.Unlock()
|
||||
func (c *NotificationClass) GetGroupName(gid uint64) string {
|
||||
c.groupMu.RLock()
|
||||
defer c.groupMu.RUnlock()
|
||||
|
||||
var isEdit bool
|
||||
_, ok := NotificationMap[n.ID]
|
||||
if ok {
|
||||
isEdit = true
|
||||
}
|
||||
if !isEdit {
|
||||
AddNotificationToList(n)
|
||||
} else {
|
||||
UpdateNotificationInList(n)
|
||||
}
|
||||
return c.groupList[gid]
|
||||
}
|
||||
|
||||
// AddNotificationToList 添加通知方式到map中
|
||||
func AddNotificationToList(n *model.Notification) {
|
||||
NotificationMap[n.ID] = n
|
||||
func (c *NotificationClass) sortList() {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
|
||||
sortedList := utils.MapValuesToSlice(c.list)
|
||||
slices.SortFunc(sortedList, func(a, b *model.Notification) int {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
})
|
||||
|
||||
c.sortedListMu.Lock()
|
||||
defer c.sortedListMu.Unlock()
|
||||
c.sortedList = sortedList
|
||||
}
|
||||
|
||||
// UpdateNotificationInList 在 map 中更新通知方式
|
||||
func UpdateNotificationInList(n *model.Notification) {
|
||||
NotificationMap[n.ID] = n
|
||||
// 如果已经与通知组有绑定关系,更新
|
||||
if gids, ok := NotificationIDToGroups[n.ID]; ok {
|
||||
for gid := range gids {
|
||||
NotificationList[gid][n.ID] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnDeleteNotification 在map和表中删除通知方式
|
||||
func OnDeleteNotification(id []uint64) {
|
||||
NotificationsLock.Lock()
|
||||
defer NotificationsLock.Unlock()
|
||||
|
||||
for _, i := range id {
|
||||
delete(NotificationMap, i)
|
||||
// 如果绑定了通知组才删除
|
||||
if gids, ok := NotificationIDToGroups[i]; ok {
|
||||
for gid := range gids {
|
||||
delete(NotificationList[gid], i)
|
||||
delete(NotificationIDToGroups, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func UnMuteNotification(notificationGroupID uint64, muteLabel *string) {
|
||||
fullMuteLabel := *NotificationMuteLabel.AppendNotificationGroupName(muteLabel, notificationGroupID)
|
||||
func (c *NotificationClass) UnMuteNotification(notificationGroupID uint64, muteLabel *string) {
|
||||
fullMuteLabel := *NotificationMuteLabel.AppendNotificationGroupName(muteLabel, c.GetGroupName(notificationGroupID))
|
||||
Cache.Delete(fullMuteLabel)
|
||||
}
|
||||
|
||||
// SendNotification 向指定的通知方式组的所有通知方式发送通知
|
||||
func SendNotification(notificationGroupID uint64, desc string, muteLabel *string, ext ...*model.Server) {
|
||||
func (c *NotificationClass) SendNotification(notificationGroupID uint64, desc string, muteLabel *string, ext ...*model.Server) {
|
||||
if muteLabel != nil {
|
||||
// 将通知方式组名称加入静音标志
|
||||
muteLabel := *NotificationMuteLabel.AppendNotificationGroupName(muteLabel, notificationGroupID)
|
||||
muteLabel := *NotificationMuteLabel.AppendNotificationGroupName(muteLabel, c.GetGroupName(notificationGroupID))
|
||||
// 通知防骚扰策略
|
||||
var flag bool
|
||||
if cacheN, has := Cache.Get(muteLabel); has {
|
||||
@@ -259,12 +235,12 @@ func SendNotification(notificationGroupID uint64, desc string, muteLabel *string
|
||||
}
|
||||
}
|
||||
// 向该通知方式组的所有通知方式发出通知
|
||||
NotificationsLock.RLock()
|
||||
defer NotificationsLock.RUnlock()
|
||||
for _, n := range NotificationList[notificationGroupID] {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
for _, n := range c.groupToIDList[notificationGroupID] {
|
||||
log.Printf("NEZHA>> Try to notify %s", n.Name)
|
||||
}
|
||||
for _, n := range NotificationList[notificationGroupID] {
|
||||
for _, n := range c.groupToIDList[notificationGroupID] {
|
||||
ns := model.NotificationServerBundle{
|
||||
Notification: n,
|
||||
Server: nil,
|
||||
@@ -300,10 +276,8 @@ func (_NotificationMuteLabel) ServerIncidentResolved(alertId uint64, serverId ui
|
||||
return &label
|
||||
}
|
||||
|
||||
func (_NotificationMuteLabel) AppendNotificationGroupName(label *string, notificationGroupID uint64) *string {
|
||||
NotificationGroupLock.RLock()
|
||||
defer NotificationGroupLock.RUnlock()
|
||||
newLabel := fmt.Sprintf("%s:%s", *label, NotificationGroup[notificationGroupID])
|
||||
func (_NotificationMuteLabel) AppendNotificationGroupName(label *string, notificationGroupName string) *string {
|
||||
newLabel := fmt.Sprintf("%s:%s", *label, notificationGroupName)
|
||||
return &newLabel
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
var (
|
||||
OnlineUserMap = make(map[string]*model.OnlineUser)
|
||||
OnlineUserMapLock = new(sync.Mutex)
|
||||
OnlineUserMapLock sync.Mutex
|
||||
)
|
||||
|
||||
func AddOnlineUser(connId string, user *model.OnlineUser) {
|
||||
|
||||
@@ -3,71 +3,101 @@ package singleton
|
||||
import (
|
||||
"cmp"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
ServerList map[uint64]*model.Server // [ServerID] -> model.Server
|
||||
ServerUUIDToID map[string]uint64 // [ServerUUID] -> ServerID
|
||||
ServerLock sync.RWMutex
|
||||
type ServerClass struct {
|
||||
class[uint64, *model.Server]
|
||||
|
||||
SortedServerList []*model.Server // 用于存储服务器列表的 slice,按照服务器 ID 排序
|
||||
SortedServerListForGuest []*model.Server
|
||||
SortedServerLock sync.RWMutex
|
||||
)
|
||||
uuidToID map[string]uint64
|
||||
|
||||
func InitServer() {
|
||||
ServerList = make(map[uint64]*model.Server)
|
||||
ServerUUIDToID = make(map[string]uint64)
|
||||
sortedListForGuest []*model.Server
|
||||
}
|
||||
|
||||
// loadServers 加载服务器列表并根据ID排序
|
||||
func loadServers() {
|
||||
InitServer()
|
||||
func NewServerClass() *ServerClass {
|
||||
sc := &ServerClass{
|
||||
class: class[uint64, *model.Server]{
|
||||
list: make(map[uint64]*model.Server),
|
||||
},
|
||||
uuidToID: make(map[string]uint64),
|
||||
}
|
||||
|
||||
var servers []model.Server
|
||||
DB.Find(&servers)
|
||||
for _, s := range servers {
|
||||
innerS := s
|
||||
model.InitServer(&innerS)
|
||||
ServerList[innerS.ID] = &innerS
|
||||
ServerUUIDToID[innerS.UUID] = innerS.ID
|
||||
sc.list[innerS.ID] = &innerS
|
||||
sc.uuidToID[innerS.UUID] = innerS.ID
|
||||
}
|
||||
ReSortServer()
|
||||
sc.sortList()
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
// ReSortServer 根据服务器ID 对服务器列表进行排序(ID越大越靠前)
|
||||
func ReSortServer() {
|
||||
ServerLock.RLock()
|
||||
defer ServerLock.RUnlock()
|
||||
SortedServerLock.Lock()
|
||||
defer SortedServerLock.Unlock()
|
||||
func (c *ServerClass) Update(s *model.Server, uuid string) {
|
||||
c.listMu.Lock()
|
||||
|
||||
SortedServerList = utils.MapValuesToSlice(ServerList)
|
||||
c.list[s.ID] = s
|
||||
if uuid != "" {
|
||||
c.uuidToID[uuid] = s.ID
|
||||
}
|
||||
|
||||
c.listMu.Unlock()
|
||||
|
||||
c.sortList()
|
||||
}
|
||||
|
||||
func (c *ServerClass) Delete(idList []uint64) {
|
||||
c.listMu.Lock()
|
||||
|
||||
for _, id := range idList {
|
||||
serverUUID := c.list[id].UUID
|
||||
delete(c.uuidToID, serverUUID)
|
||||
delete(c.list, id)
|
||||
}
|
||||
|
||||
c.listMu.Unlock()
|
||||
|
||||
c.sortList()
|
||||
}
|
||||
|
||||
func (c *ServerClass) GetSortedListForGuest() []*model.Server {
|
||||
c.sortedListMu.RLock()
|
||||
defer c.sortedListMu.RUnlock()
|
||||
|
||||
return slices.Clone(c.sortedListForGuest)
|
||||
}
|
||||
|
||||
func (c *ServerClass) UUIDToID(uuid string) (id uint64, ok bool) {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
|
||||
id, ok = c.uuidToID[uuid]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ServerClass) sortList() {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
c.sortedListMu.Lock()
|
||||
defer c.sortedListMu.Unlock()
|
||||
|
||||
c.sortedList = utils.MapValuesToSlice(c.list)
|
||||
// 按照服务器 ID 排序的具体实现(ID越大越靠前)
|
||||
slices.SortStableFunc(SortedServerList, func(a, b *model.Server) int {
|
||||
slices.SortStableFunc(c.sortedList, func(a, b *model.Server) int {
|
||||
if a.DisplayIndex == b.DisplayIndex {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
}
|
||||
return cmp.Compare(b.DisplayIndex, a.DisplayIndex)
|
||||
})
|
||||
|
||||
SortedServerListForGuest = make([]*model.Server, 0, len(SortedServerList))
|
||||
for _, s := range SortedServerList {
|
||||
c.sortedListForGuest = make([]*model.Server, 0, len(c.sortedList))
|
||||
for _, s := range c.sortedList {
|
||||
if !s.HideForGuest {
|
||||
SortedServerListForGuest = append(SortedServerListForGuest, s)
|
||||
c.sortedListForGuest = append(c.sortedListForGuest, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func OnServerDelete(sid []uint64) {
|
||||
ServerLock.Lock()
|
||||
defer ServerLock.Unlock()
|
||||
for _, id := range sid {
|
||||
serverUUID := ServerList[id].UUID
|
||||
delete(ServerUUIDToID, serverUUID)
|
||||
delete(ServerList, id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,26 @@ package singleton
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"iter"
|
||||
"log"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
pb "github.com/nezhahq/nezha/proto"
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
const (
|
||||
_CurrentStatusSize = 30 // 统计 15 分钟内的数据为当前状态
|
||||
)
|
||||
|
||||
var ServiceSentinelShared *ServiceSentinel
|
||||
|
||||
type serviceResponseItem struct {
|
||||
model.ServiceResponseItem
|
||||
|
||||
@@ -39,26 +41,68 @@ type _TodayStatsOfService struct {
|
||||
Delay float32 // 今日平均延迟
|
||||
}
|
||||
|
||||
/*
|
||||
使用缓存 channel,处理上报的 Service 请求结果,然后判断是否需要报警
|
||||
需要记录上一次的状态信息
|
||||
|
||||
加锁顺序:serviceResponseDataStoreLock > monthlyStatusLock > servicesLock
|
||||
*/
|
||||
type ServiceSentinel struct {
|
||||
// 服务监控任务上报通道
|
||||
serviceReportChannel chan ReportData // 服务状态汇报管道
|
||||
// 服务监控任务调度通道
|
||||
dispatchBus chan<- *model.Service
|
||||
|
||||
serviceResponseDataStoreLock sync.RWMutex
|
||||
serviceStatusToday map[uint64]*_TodayStatsOfService // [service_id] -> _TodayStatsOfService
|
||||
serviceCurrentStatusIndex map[uint64]*indexStore // [service_id] -> 该监控ID对应的 serviceCurrentStatusData 的最新索引下标
|
||||
serviceCurrentStatusData map[uint64][]*pb.TaskResult // [service_id] -> []model.ServiceHistory
|
||||
serviceResponseDataStoreCurrentUp map[uint64]uint64 // [service_id] -> 当前服务在线计数
|
||||
serviceResponseDataStoreCurrentDown map[uint64]uint64 // [service_id] -> 当前服务离线计数
|
||||
serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [service_id] -> 当前服务离线计数
|
||||
serviceResponsePing map[uint64]map[uint64]*pingStore // [service_id] -> ClientID -> delay
|
||||
lastStatus map[uint64]uint8
|
||||
tlsCertCache map[uint64]string
|
||||
|
||||
servicesLock sync.RWMutex
|
||||
serviceListLock sync.RWMutex
|
||||
services map[uint64]*model.Service
|
||||
serviceList []*model.Service
|
||||
|
||||
// 30天数据缓存
|
||||
monthlyStatusLock sync.Mutex
|
||||
monthlyStatus map[uint64]*serviceResponseItem
|
||||
|
||||
// references
|
||||
serverc *ServerClass
|
||||
notificationc *NotificationClass
|
||||
crc *CronClass
|
||||
}
|
||||
|
||||
// NewServiceSentinel 创建服务监控器
|
||||
func NewServiceSentinel(serviceSentinelDispatchBus chan<- model.Service) {
|
||||
ServiceSentinelShared = &ServiceSentinel{
|
||||
func NewServiceSentinel(serviceSentinelDispatchBus chan<- *model.Service, sc *ServerClass, nc *NotificationClass, crc *CronClass) (*ServiceSentinel, error) {
|
||||
ss := &ServiceSentinel{
|
||||
serviceReportChannel: make(chan ReportData, 200),
|
||||
serviceStatusToday: make(map[uint64]*_TodayStatsOfService),
|
||||
serviceCurrentStatusIndex: make(map[uint64]*indexStore),
|
||||
serviceCurrentStatusData: make(map[uint64][]*pb.TaskResult),
|
||||
lastStatus: make(map[uint64]int),
|
||||
lastStatus: make(map[uint64]uint8),
|
||||
serviceResponseDataStoreCurrentUp: make(map[uint64]uint64),
|
||||
serviceResponseDataStoreCurrentDown: make(map[uint64]uint64),
|
||||
serviceResponseDataStoreCurrentAvgDelay: make(map[uint64]float32),
|
||||
serviceResponsePing: make(map[uint64]map[uint64]*pingStore),
|
||||
Services: make(map[uint64]*model.Service),
|
||||
services: make(map[uint64]*model.Service),
|
||||
tlsCertCache: make(map[uint64]string),
|
||||
// 30天数据缓存
|
||||
monthlyStatus: make(map[uint64]*serviceResponseItem),
|
||||
dispatchBus: serviceSentinelDispatchBus,
|
||||
|
||||
serverc: sc,
|
||||
notificationc: nc,
|
||||
crc: crc,
|
||||
}
|
||||
// 加载历史记录
|
||||
ServiceSentinelShared.loadServiceHistory()
|
||||
ss.loadServiceHistory()
|
||||
|
||||
year, month, day := time.Now().Date()
|
||||
today := time.Date(year, month, day, 0, 0, 0, 0, Loc)
|
||||
@@ -71,56 +115,25 @@ func NewServiceSentinel(serviceSentinelDispatchBus chan<- model.Service) {
|
||||
for i := 0; i < len(mhs); i++ {
|
||||
totalDelay[mhs[i].ServiceID] += mhs[i].AvgDelay
|
||||
totalDelayCount[mhs[i].ServiceID]++
|
||||
ServiceSentinelShared.serviceStatusToday[mhs[i].ServiceID].Up += int(mhs[i].Up)
|
||||
ServiceSentinelShared.monthlyStatus[mhs[i].ServiceID].TotalUp += mhs[i].Up
|
||||
ServiceSentinelShared.serviceStatusToday[mhs[i].ServiceID].Down += int(mhs[i].Down)
|
||||
ServiceSentinelShared.monthlyStatus[mhs[i].ServiceID].TotalDown += mhs[i].Down
|
||||
ss.serviceStatusToday[mhs[i].ServiceID].Up += int(mhs[i].Up)
|
||||
ss.monthlyStatus[mhs[i].ServiceID].TotalUp += mhs[i].Up
|
||||
ss.serviceStatusToday[mhs[i].ServiceID].Down += int(mhs[i].Down)
|
||||
ss.monthlyStatus[mhs[i].ServiceID].TotalDown += mhs[i].Down
|
||||
}
|
||||
for id, delay := range totalDelay {
|
||||
ServiceSentinelShared.serviceStatusToday[id].Delay = delay / float32(totalDelayCount[id])
|
||||
ss.serviceStatusToday[id].Delay = delay / float32(totalDelayCount[id])
|
||||
}
|
||||
|
||||
// 启动服务监控器
|
||||
go ServiceSentinelShared.worker()
|
||||
go ss.worker()
|
||||
|
||||
// 每日将游标往后推一天
|
||||
_, err := Cron.AddFunc("0 0 0 * * *", ServiceSentinelShared.refreshMonthlyServiceStatus)
|
||||
_, err := crc.AddFunc("0 0 0 * * *", ss.refreshMonthlyServiceStatus)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
使用缓存 channel,处理上报的 Service 请求结果,然后判断是否需要报警
|
||||
需要记录上一次的状态信息
|
||||
|
||||
加锁顺序:serviceResponseDataStoreLock > monthlyStatusLock > servicesLock
|
||||
*/
|
||||
type ServiceSentinel struct {
|
||||
// 服务监控任务上报通道
|
||||
serviceReportChannel chan ReportData // 服务状态汇报管道
|
||||
// 服务监控任务调度通道
|
||||
dispatchBus chan<- model.Service
|
||||
|
||||
serviceResponseDataStoreLock sync.RWMutex
|
||||
serviceStatusToday map[uint64]*_TodayStatsOfService // [service_id] -> _TodayStatsOfService
|
||||
serviceCurrentStatusIndex map[uint64]*indexStore // [service_id] -> 该监控ID对应的 serviceCurrentStatusData 的最新索引下标
|
||||
serviceCurrentStatusData map[uint64][]*pb.TaskResult // [service_id] -> []model.ServiceHistory
|
||||
serviceResponseDataStoreCurrentUp map[uint64]uint64 // [service_id] -> 当前服务在线计数
|
||||
serviceResponseDataStoreCurrentDown map[uint64]uint64 // [service_id] -> 当前服务离线计数
|
||||
serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [service_id] -> 当前服务离线计数
|
||||
serviceResponsePing map[uint64]map[uint64]*pingStore // [service_id] -> ClientID -> delay
|
||||
lastStatus map[uint64]int
|
||||
tlsCertCache map[uint64]string
|
||||
|
||||
ServicesLock sync.RWMutex
|
||||
ServiceListLock sync.RWMutex
|
||||
Services map[uint64]*model.Service
|
||||
ServiceList []*model.Service
|
||||
|
||||
// 30天数据缓存
|
||||
monthlyStatusLock sync.Mutex
|
||||
monthlyStatus map[uint64]*serviceResponseItem
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
type indexStore struct {
|
||||
@@ -169,14 +182,14 @@ func (ss *ServiceSentinel) Dispatch(r ReportData) {
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) UpdateServiceList() {
|
||||
ss.ServicesLock.RLock()
|
||||
defer ss.ServicesLock.RUnlock()
|
||||
ss.servicesLock.RLock()
|
||||
defer ss.servicesLock.RUnlock()
|
||||
|
||||
ss.ServiceListLock.Lock()
|
||||
defer ss.ServiceListLock.Unlock()
|
||||
ss.serviceListLock.Lock()
|
||||
defer ss.serviceListLock.Unlock()
|
||||
|
||||
ss.ServiceList = utils.MapValuesToSlice(ss.Services)
|
||||
slices.SortFunc(ss.ServiceList, func(a, b *model.Service) int {
|
||||
ss.serviceList = utils.MapValuesToSlice(ss.services)
|
||||
slices.SortFunc(ss.serviceList, func(a, b *model.Service) int {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
})
|
||||
}
|
||||
@@ -190,25 +203,25 @@ func (ss *ServiceSentinel) loadServiceHistory() {
|
||||
}
|
||||
|
||||
for i := 0; i < len(services); i++ {
|
||||
task := *services[i]
|
||||
task := services[i]
|
||||
// 通过cron定时将服务监控任务传递给任务调度管道
|
||||
services[i].CronJobID, err = Cron.AddFunc(task.CronSpec(), func() {
|
||||
services[i].CronJobID, err = ss.crc.AddFunc(task.CronSpec(), func() {
|
||||
ss.dispatchBus <- task
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ss.Services[services[i].ID] = services[i]
|
||||
ss.services[services[i].ID] = services[i]
|
||||
ss.serviceCurrentStatusData[services[i].ID] = make([]*pb.TaskResult, _CurrentStatusSize)
|
||||
ss.serviceStatusToday[services[i].ID] = &_TodayStatsOfService{}
|
||||
}
|
||||
ss.ServiceList = services
|
||||
ss.serviceList = services
|
||||
|
||||
year, month, day := time.Now().Date()
|
||||
today := time.Date(year, month, day, 0, 0, 0, 0, Loc)
|
||||
|
||||
for i := 0; i < len(services); i++ {
|
||||
ServiceSentinelShared.monthlyStatus[services[i].ID] = &serviceResponseItem{
|
||||
ss.monthlyStatus[services[i].ID] = &serviceResponseItem{
|
||||
service: services[i],
|
||||
ServiceResponseItem: model.ServiceResponseItem{
|
||||
Delay: &[30]float32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
@@ -227,38 +240,38 @@ func (ss *ServiceSentinel) loadServiceHistory() {
|
||||
if dayIndex < 0 {
|
||||
continue
|
||||
}
|
||||
ServiceSentinelShared.monthlyStatus[mhs[i].ServiceID].Delay[dayIndex] = (ServiceSentinelShared.monthlyStatus[mhs[i].ServiceID].Delay[dayIndex]*float32(delayCount[dayIndex]) + mhs[i].AvgDelay) / float32(delayCount[dayIndex]+1)
|
||||
ss.monthlyStatus[mhs[i].ServiceID].Delay[dayIndex] = (ss.monthlyStatus[mhs[i].ServiceID].Delay[dayIndex]*float32(delayCount[dayIndex]) + mhs[i].AvgDelay) / float32(delayCount[dayIndex]+1)
|
||||
delayCount[dayIndex]++
|
||||
ServiceSentinelShared.monthlyStatus[mhs[i].ServiceID].Up[dayIndex] += int(mhs[i].Up)
|
||||
ServiceSentinelShared.monthlyStatus[mhs[i].ServiceID].TotalUp += mhs[i].Up
|
||||
ServiceSentinelShared.monthlyStatus[mhs[i].ServiceID].Down[dayIndex] += int(mhs[i].Down)
|
||||
ServiceSentinelShared.monthlyStatus[mhs[i].ServiceID].TotalDown += mhs[i].Down
|
||||
ss.monthlyStatus[mhs[i].ServiceID].Up[dayIndex] += int(mhs[i].Up)
|
||||
ss.monthlyStatus[mhs[i].ServiceID].TotalUp += mhs[i].Up
|
||||
ss.monthlyStatus[mhs[i].ServiceID].Down[dayIndex] += int(mhs[i].Down)
|
||||
ss.monthlyStatus[mhs[i].ServiceID].TotalDown += mhs[i].Down
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) OnServiceUpdate(m model.Service) error {
|
||||
func (ss *ServiceSentinel) Update(m *model.Service) error {
|
||||
ss.serviceResponseDataStoreLock.Lock()
|
||||
defer ss.serviceResponseDataStoreLock.Unlock()
|
||||
ss.monthlyStatusLock.Lock()
|
||||
defer ss.monthlyStatusLock.Unlock()
|
||||
ss.ServicesLock.Lock()
|
||||
defer ss.ServicesLock.Unlock()
|
||||
ss.servicesLock.Lock()
|
||||
defer ss.servicesLock.Unlock()
|
||||
|
||||
var err error
|
||||
// 写入新任务
|
||||
m.CronJobID, err = Cron.AddFunc(m.CronSpec(), func() {
|
||||
m.CronJobID, err = ss.crc.AddFunc(m.CronSpec(), func() {
|
||||
ss.dispatchBus <- m
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ss.Services[m.ID] != nil {
|
||||
if ss.services[m.ID] != nil {
|
||||
// 停掉旧任务
|
||||
Cron.Remove(ss.Services[m.ID].CronJobID)
|
||||
ss.crc.Remove(ss.services[m.ID].CronJobID)
|
||||
} else {
|
||||
// 新任务初始化数据
|
||||
ss.monthlyStatus[m.ID] = &serviceResponseItem{
|
||||
service: &m,
|
||||
service: m,
|
||||
ServiceResponseItem: model.ServiceResponseItem{
|
||||
Delay: &[30]float32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
Up: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
@@ -269,17 +282,17 @@ func (ss *ServiceSentinel) OnServiceUpdate(m model.Service) error {
|
||||
ss.serviceStatusToday[m.ID] = &_TodayStatsOfService{}
|
||||
}
|
||||
// 更新这个任务
|
||||
ss.Services[m.ID] = &m
|
||||
ss.services[m.ID] = m
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) OnServiceDelete(ids []uint64) {
|
||||
func (ss *ServiceSentinel) Delete(ids []uint64) {
|
||||
ss.serviceResponseDataStoreLock.Lock()
|
||||
defer ss.serviceResponseDataStoreLock.Unlock()
|
||||
ss.monthlyStatusLock.Lock()
|
||||
defer ss.monthlyStatusLock.Unlock()
|
||||
ss.ServicesLock.Lock()
|
||||
defer ss.ServicesLock.Unlock()
|
||||
ss.servicesLock.Lock()
|
||||
defer ss.servicesLock.Unlock()
|
||||
|
||||
for _, id := range ids {
|
||||
delete(ss.serviceCurrentStatusIndex, id)
|
||||
@@ -292,24 +305,24 @@ func (ss *ServiceSentinel) OnServiceDelete(ids []uint64) {
|
||||
delete(ss.serviceStatusToday, id)
|
||||
|
||||
// 停掉定时任务
|
||||
Cron.Remove(ss.Services[id].CronJobID)
|
||||
delete(ss.Services, id)
|
||||
ss.crc.Remove(ss.services[id].CronJobID)
|
||||
delete(ss.services, id)
|
||||
|
||||
delete(ss.monthlyStatus, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) LoadStats() map[uint64]*serviceResponseItem {
|
||||
ss.ServicesLock.RLock()
|
||||
defer ss.ServicesLock.RUnlock()
|
||||
ss.servicesLock.RLock()
|
||||
defer ss.servicesLock.RUnlock()
|
||||
ss.serviceResponseDataStoreLock.RLock()
|
||||
defer ss.serviceResponseDataStoreLock.RUnlock()
|
||||
ss.monthlyStatusLock.Lock()
|
||||
defer ss.monthlyStatusLock.Unlock()
|
||||
|
||||
// 刷新最新一天的数据
|
||||
for k := range ss.Services {
|
||||
ss.monthlyStatus[k].service = ss.Services[k]
|
||||
for k := range ss.services {
|
||||
ss.monthlyStatus[k].service = ss.services[k]
|
||||
v := ss.serviceStatusToday[k]
|
||||
|
||||
// 30 天在线率,
|
||||
@@ -354,14 +367,52 @@ func (ss *ServiceSentinel) CopyStats() map[uint64]model.ServiceResponseItem {
|
||||
return sri
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) Get(id uint64) (s *model.Service, ok bool) {
|
||||
ss.servicesLock.RLock()
|
||||
defer ss.servicesLock.RUnlock()
|
||||
|
||||
s, ok = ss.services[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) GetList() map[uint64]*model.Service {
|
||||
ss.servicesLock.RLock()
|
||||
defer ss.servicesLock.RUnlock()
|
||||
|
||||
return maps.Clone(ss.services)
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) GetSortedList() []*model.Service {
|
||||
ss.serviceListLock.RLock()
|
||||
defer ss.serviceListLock.RUnlock()
|
||||
|
||||
return slices.Clone(ss.serviceList)
|
||||
}
|
||||
|
||||
func (ss *ServiceSentinel) CheckPermission(c *gin.Context, idList iter.Seq[uint64]) bool {
|
||||
ss.servicesLock.RLock()
|
||||
defer ss.servicesLock.RUnlock()
|
||||
|
||||
for id := range idList {
|
||||
if s, ok := ss.services[id]; ok {
|
||||
if !s.HasPermission(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// worker 服务监控的实际工作流程
|
||||
func (ss *ServiceSentinel) worker() {
|
||||
// 从服务状态汇报管道获取汇报的服务数据
|
||||
for r := range ss.serviceReportChannel {
|
||||
if ss.Services[r.Data.GetId()] == nil || ss.Services[r.Data.GetId()].ID == 0 {
|
||||
css, _ := ss.Get(r.Data.GetId())
|
||||
if css == nil || css.ID == 0 {
|
||||
log.Printf("NEZHA>> Incorrect service monitor report %+v", r)
|
||||
continue
|
||||
}
|
||||
css = nil
|
||||
mh := r.Data
|
||||
if mh.Type == model.TaskTypeTCPPing || mh.Type == model.TaskTypeICMPPing {
|
||||
serviceTcpMap, ok := ss.serviceResponsePing[mh.GetId()]
|
||||
@@ -454,79 +505,20 @@ func (ss *ServiceSentinel) worker() {
|
||||
}
|
||||
}
|
||||
|
||||
cs, _ := ss.Get(mh.GetId())
|
||||
m := ss.serverc.GetList()
|
||||
// 延迟报警
|
||||
if mh.Delay > 0 {
|
||||
ss.ServicesLock.RLock()
|
||||
if ss.Services[mh.GetId()].LatencyNotify {
|
||||
notificationGroupID := ss.Services[mh.GetId()].NotificationGroupID
|
||||
minMuteLabel := NotificationMuteLabel.ServiceLatencyMin(mh.GetId())
|
||||
maxMuteLabel := NotificationMuteLabel.ServiceLatencyMax(mh.GetId())
|
||||
if mh.Delay > ss.Services[mh.GetId()].MaxLatency {
|
||||
// 延迟超过最大值
|
||||
ServerLock.RLock()
|
||||
reporterServer := ServerList[r.Reporter]
|
||||
msg := Localizer.Tf("[Latency] %s %2f > %2f, Reporter: %s", ss.Services[mh.GetId()].Name, mh.Delay, ss.Services[mh.GetId()].MaxLatency, reporterServer.Name)
|
||||
go SendNotification(notificationGroupID, msg, minMuteLabel)
|
||||
ServerLock.RUnlock()
|
||||
} else if mh.Delay < ss.Services[mh.GetId()].MinLatency {
|
||||
// 延迟低于最小值
|
||||
ServerLock.RLock()
|
||||
reporterServer := ServerList[r.Reporter]
|
||||
msg := Localizer.Tf("[Latency] %s %2f < %2f, Reporter: %s", ss.Services[mh.GetId()].Name, mh.Delay, ss.Services[mh.GetId()].MinLatency, reporterServer.Name)
|
||||
go SendNotification(notificationGroupID, msg, maxMuteLabel)
|
||||
ServerLock.RUnlock()
|
||||
} else {
|
||||
// 正常延迟, 清除静音缓存
|
||||
UnMuteNotification(notificationGroupID, minMuteLabel)
|
||||
UnMuteNotification(notificationGroupID, maxMuteLabel)
|
||||
}
|
||||
}
|
||||
ss.ServicesLock.RUnlock()
|
||||
delayCheck(&r, ss.notificationc, m, cs, mh)
|
||||
}
|
||||
|
||||
// 状态变更报警+触发任务执行
|
||||
if stateCode == StatusDown || stateCode != ss.lastStatus[mh.GetId()] {
|
||||
ss.ServicesLock.Lock()
|
||||
lastStatus := ss.lastStatus[mh.GetId()]
|
||||
// 存储新的状态值
|
||||
ss.lastStatus[mh.GetId()] = stateCode
|
||||
|
||||
// 判断是否需要发送通知
|
||||
isNeedSendNotification := ss.Services[mh.GetId()].Notify && (lastStatus != 0 || stateCode == StatusDown)
|
||||
if isNeedSendNotification {
|
||||
ServerLock.RLock()
|
||||
|
||||
reporterServer := ServerList[r.Reporter]
|
||||
notificationGroupID := ss.Services[mh.GetId()].NotificationGroupID
|
||||
notificationMsg := Localizer.Tf("[%s] %s Reporter: %s, Error: %s", StatusCodeToString(stateCode), ss.Services[mh.GetId()].Name, reporterServer.Name, mh.Data)
|
||||
muteLabel := NotificationMuteLabel.ServiceStateChanged(mh.GetId())
|
||||
|
||||
// 状态变更时,清除静音缓存
|
||||
if stateCode != lastStatus {
|
||||
UnMuteNotification(notificationGroupID, muteLabel)
|
||||
}
|
||||
|
||||
go SendNotification(notificationGroupID, notificationMsg, muteLabel)
|
||||
ServerLock.RUnlock()
|
||||
}
|
||||
|
||||
// 判断是否需要触发任务
|
||||
isNeedTriggerTask := ss.Services[mh.GetId()].EnableTriggerTask && lastStatus != 0
|
||||
if isNeedTriggerTask {
|
||||
ServerLock.RLock()
|
||||
reporterServer := ServerList[r.Reporter]
|
||||
ServerLock.RUnlock()
|
||||
|
||||
if stateCode == StatusGood && lastStatus != stateCode {
|
||||
// 当前状态正常 前序状态非正常时 触发恢复任务
|
||||
go SendTriggerTasks(ss.Services[mh.GetId()].RecoverTriggerTasks, reporterServer.ID)
|
||||
} else if lastStatus == StatusGood && lastStatus != stateCode {
|
||||
// 前序状态正常 当前状态非正常时 触发失败任务
|
||||
go SendTriggerTasks(ss.Services[mh.GetId()].FailTriggerTasks, reporterServer.ID)
|
||||
}
|
||||
}
|
||||
|
||||
ss.ServicesLock.Unlock()
|
||||
notifyCheck(&r, ss.notificationc, ss.crc, m, cs, mh, lastStatus, stateCode)
|
||||
}
|
||||
ss.serviceResponseDataStoreLock.Unlock()
|
||||
|
||||
@@ -538,22 +530,18 @@ func (ss *ServiceSentinel) worker() {
|
||||
!strings.HasSuffix(mh.Data, "EOF") &&
|
||||
!strings.HasSuffix(mh.Data, "timed out") {
|
||||
errMsg = mh.Data
|
||||
ss.ServicesLock.RLock()
|
||||
if ss.Services[mh.GetId()].Notify {
|
||||
if cs.Notify {
|
||||
muteLabel := NotificationMuteLabel.ServiceTLS(mh.GetId(), "network")
|
||||
go SendNotification(ss.Services[mh.GetId()].NotificationGroupID, Localizer.Tf("[TLS] Fetch cert info failed, Reporter: %s, Error: %s", ss.Services[mh.GetId()].Name, errMsg), muteLabel)
|
||||
go ss.notificationc.SendNotification(cs.NotificationGroupID, Localizer.Tf("[TLS] Fetch cert info failed, Reporter: %s, Error: %s", cs.Name, errMsg), muteLabel)
|
||||
}
|
||||
ss.ServicesLock.RUnlock()
|
||||
|
||||
}
|
||||
} else {
|
||||
// 清除网络错误静音缓存
|
||||
UnMuteNotification(ss.Services[mh.GetId()].NotificationGroupID, NotificationMuteLabel.ServiceTLS(mh.GetId(), "network"))
|
||||
ss.notificationc.UnMuteNotification(cs.NotificationGroupID, NotificationMuteLabel.ServiceTLS(mh.GetId(), "network"))
|
||||
|
||||
var newCert = strings.Split(mh.Data, "|")
|
||||
if len(newCert) > 1 {
|
||||
ss.ServicesLock.Lock()
|
||||
enableNotify := ss.Services[mh.GetId()].Notify
|
||||
enableNotify := cs.Notify
|
||||
|
||||
// 首次获取证书信息时,缓存证书信息
|
||||
if ss.tlsCertCache[mh.GetId()] == "" {
|
||||
@@ -571,9 +559,8 @@ func (ss *ServiceSentinel) worker() {
|
||||
ss.tlsCertCache[mh.GetId()] = mh.Data
|
||||
}
|
||||
|
||||
notificationGroupID := ss.Services[mh.GetId()].NotificationGroupID
|
||||
serviceName := ss.Services[mh.GetId()].Name
|
||||
ss.ServicesLock.Unlock()
|
||||
notificationGroupID := cs.NotificationGroupID
|
||||
serviceName := cs.Name
|
||||
|
||||
// 需要发送提醒
|
||||
if enableNotify {
|
||||
@@ -588,7 +575,7 @@ func (ss *ServiceSentinel) worker() {
|
||||
// 静音规则: 服务id+证书过期时间
|
||||
// 用于避免多个监测点对相同证书同时报警
|
||||
muteLabel := NotificationMuteLabel.ServiceTLS(mh.GetId(), fmt.Sprintf("expire_%s", expiresTimeStr))
|
||||
go SendNotification(notificationGroupID, fmt.Sprintf("[TLS] %s %s", serviceName, errMsg), muteLabel)
|
||||
go ss.notificationc.SendNotification(notificationGroupID, fmt.Sprintf("[TLS] %s %s", serviceName, errMsg), muteLabel)
|
||||
}
|
||||
|
||||
// 证书变更提醒
|
||||
@@ -598,7 +585,7 @@ func (ss *ServiceSentinel) worker() {
|
||||
oldCert[0], expiresOld.Format("2006-01-02 15:04:05"), newCert[0], expiresNew.Format("2006-01-02 15:04:05"))
|
||||
|
||||
// 证书变更后会自动更新缓存,所以不需要静音
|
||||
go SendNotification(notificationGroupID, fmt.Sprintf("[TLS] %s %s", serviceName, errMsg), nil)
|
||||
go ss.notificationc.SendNotification(notificationGroupID, fmt.Sprintf("[TLS] %s %s", serviceName, errMsg), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,6 +593,63 @@ func (ss *ServiceSentinel) worker() {
|
||||
}
|
||||
}
|
||||
|
||||
func delayCheck(r *ReportData, nc *NotificationClass, m map[uint64]*model.Server, ss *model.Service, mh *pb.TaskResult) {
|
||||
if !ss.LatencyNotify {
|
||||
return
|
||||
}
|
||||
|
||||
notificationGroupID := ss.NotificationGroupID
|
||||
minMuteLabel := NotificationMuteLabel.ServiceLatencyMin(mh.GetId())
|
||||
maxMuteLabel := NotificationMuteLabel.ServiceLatencyMax(mh.GetId())
|
||||
if mh.Delay > ss.MaxLatency {
|
||||
// 延迟超过最大值
|
||||
reporterServer := m[r.Reporter]
|
||||
msg := Localizer.Tf("[Latency] %s %2f > %2f, Reporter: %s", ss.Name, mh.Delay, ss.MaxLatency, reporterServer.Name)
|
||||
go nc.SendNotification(notificationGroupID, msg, minMuteLabel)
|
||||
} else if mh.Delay < ss.MinLatency {
|
||||
// 延迟低于最小值
|
||||
reporterServer := m[r.Reporter]
|
||||
msg := Localizer.Tf("[Latency] %s %2f < %2f, Reporter: %s", ss.Name, mh.Delay, ss.MinLatency, reporterServer.Name)
|
||||
go nc.SendNotification(notificationGroupID, msg, maxMuteLabel)
|
||||
} else {
|
||||
// 正常延迟, 清除静音缓存
|
||||
nc.UnMuteNotification(notificationGroupID, minMuteLabel)
|
||||
nc.UnMuteNotification(notificationGroupID, maxMuteLabel)
|
||||
}
|
||||
}
|
||||
|
||||
func notifyCheck(r *ReportData, nc *NotificationClass, crc *CronClass, m map[uint64]*model.Server,
|
||||
ss *model.Service, mh *pb.TaskResult, lastStatus, stateCode uint8) {
|
||||
// 判断是否需要发送通知
|
||||
isNeedSendNotification := ss.Notify && (lastStatus != 0 || stateCode == StatusDown)
|
||||
if isNeedSendNotification {
|
||||
reporterServer := m[r.Reporter]
|
||||
notificationGroupID := ss.NotificationGroupID
|
||||
notificationMsg := Localizer.Tf("[%s] %s Reporter: %s, Error: %s", StatusCodeToString(stateCode), ss.Name, reporterServer.Name, mh.Data)
|
||||
muteLabel := NotificationMuteLabel.ServiceStateChanged(mh.GetId())
|
||||
|
||||
// 状态变更时,清除静音缓存
|
||||
if stateCode != lastStatus {
|
||||
nc.UnMuteNotification(notificationGroupID, muteLabel)
|
||||
}
|
||||
|
||||
go nc.SendNotification(notificationGroupID, notificationMsg, muteLabel)
|
||||
}
|
||||
|
||||
// 判断是否需要触发任务
|
||||
isNeedTriggerTask := ss.EnableTriggerTask && lastStatus != 0
|
||||
if isNeedTriggerTask {
|
||||
reporterServer := m[r.Reporter]
|
||||
if stateCode == StatusGood && lastStatus != stateCode {
|
||||
// 当前状态正常 前序状态非正常时 触发恢复任务
|
||||
go crc.SendTriggerTasks(ss.RecoverTriggerTasks, reporterServer.ID)
|
||||
} else if lastStatus == StatusGood && lastStatus != stateCode {
|
||||
// 前序状态正常 当前状态非正常时 触发失败任务
|
||||
go crc.SendTriggerTasks(ss.FailTriggerTasks, reporterServer.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
StatusNoData
|
||||
@@ -614,7 +658,7 @@ const (
|
||||
StatusDown
|
||||
)
|
||||
|
||||
func GetStatusCode[T float32 | uint64](percent T) int {
|
||||
func GetStatusCode[T constraints.Float | constraints.Integer](percent T) uint8 {
|
||||
if percent == 0 {
|
||||
return StatusNoData
|
||||
}
|
||||
@@ -627,7 +671,7 @@ func GetStatusCode[T float32 | uint64](percent T) int {
|
||||
return StatusDown
|
||||
}
|
||||
|
||||
func StatusCodeToString(statusCode int) string {
|
||||
func StatusCodeToString(statusCode uint8) string {
|
||||
switch statusCode {
|
||||
case StatusNoData:
|
||||
return Localizer.T("No Data")
|
||||
|
||||
@@ -2,9 +2,14 @@ package singleton
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"iter"
|
||||
"log"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/driver/sqlite"
|
||||
@@ -23,6 +28,13 @@ var (
|
||||
Loc *time.Location
|
||||
FrontendTemplates []model.FrontendTemplate
|
||||
DashboardBootTime = uint64(time.Now().Unix())
|
||||
|
||||
ServerShared *ServerClass
|
||||
ServiceSentinelShared *ServiceSentinel
|
||||
DDNSShared *DDNSClass
|
||||
NotificationShared *NotificationClass
|
||||
NATShared *NATClass
|
||||
CronShared *CronClass
|
||||
)
|
||||
|
||||
//go:embed frontend-templates.yaml
|
||||
@@ -40,13 +52,13 @@ func InitTimezoneAndCache() {
|
||||
|
||||
// LoadSingleton 加载子服务并执行
|
||||
func LoadSingleton() {
|
||||
initUser() // 加载用户ID绑定表
|
||||
initI18n() // 加载本地化服务
|
||||
loadNotifications() // 加载通知服务
|
||||
loadServers() // 加载服务器列表
|
||||
loadCronTasks() // 加载定时任务
|
||||
initNAT()
|
||||
initDDNS()
|
||||
initUser() // 加载用户ID绑定表
|
||||
initI18n() // 加载本地化服务
|
||||
NotificationShared = NewNotificationClass() // 加载通知服务
|
||||
ServerShared = NewServerClass() // 加载服务器列表
|
||||
CronShared = NewCronClass() // 加载定时任务
|
||||
NATShared = NewNATClass()
|
||||
DDNSShared = NewDDNSClass()
|
||||
}
|
||||
|
||||
// InitFrontendTemplates 从内置文件中加载FrontendTemplates
|
||||
@@ -90,12 +102,13 @@ func InitDBFromPath(path string) {
|
||||
|
||||
// RecordTransferHourlyUsage 对流量记录进行打点
|
||||
func RecordTransferHourlyUsage() {
|
||||
ServerLock.Lock()
|
||||
defer ServerLock.Unlock()
|
||||
ServerShared.listMu.RLock()
|
||||
defer ServerShared.listMu.RUnlock()
|
||||
|
||||
now := time.Now()
|
||||
nowTrimSeconds := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
|
||||
var txs []model.Transfer
|
||||
for id, server := range ServerList {
|
||||
for id, server := range ServerShared.list {
|
||||
tx := model.Transfer{
|
||||
ServerID: id,
|
||||
In: utils.Uint64SubInt64(server.State.NetInTransfer, server.PrevTransferInSnapshot),
|
||||
@@ -171,3 +184,58 @@ func IPDesensitize(ip string) string {
|
||||
}
|
||||
return utils.IPDesensitize(ip)
|
||||
}
|
||||
|
||||
type class[K comparable, V model.CommonInterface] struct {
|
||||
list map[K]V
|
||||
listMu sync.RWMutex
|
||||
|
||||
sortedList []V
|
||||
sortedListMu sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *class[K, V]) Get(id K) (s V, ok bool) {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
|
||||
s, ok = c.list[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *class[K, V]) GetList() map[K]V {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
|
||||
return maps.Clone(c.list)
|
||||
}
|
||||
|
||||
func (c *class[K, V]) GetSortedList() []V {
|
||||
c.sortedListMu.RLock()
|
||||
defer c.sortedListMu.RUnlock()
|
||||
|
||||
return slices.Clone(c.sortedList)
|
||||
}
|
||||
|
||||
func (c *class[K, V]) Range(fn func(k K, v V) bool) {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
|
||||
for k, v := range c.list {
|
||||
if !fn(k, v) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *class[K, V]) CheckPermission(ctx *gin.Context, idList iter.Seq[K]) bool {
|
||||
c.listMu.RLock()
|
||||
defer c.listMu.RUnlock()
|
||||
|
||||
for id := range idList {
|
||||
if s, ok := c.list[id]; ok {
|
||||
if !s.HasPermission(ctx) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -65,12 +65,11 @@ func OnUserDelete(id []uint64, errorFunc func(string, ...interface{}) error) err
|
||||
crons, servers []uint64
|
||||
)
|
||||
|
||||
slist := ServerShared.GetSortedList()
|
||||
clist := CronShared.GetSortedList()
|
||||
for _, uid := range id {
|
||||
err := DB.Transaction(func(tx *gorm.DB) error {
|
||||
CronLock.RLock()
|
||||
crons = model.FindByUserID(CronList, uid)
|
||||
CronLock.RUnlock()
|
||||
|
||||
crons = model.FindByUserID(clist, uid)
|
||||
cron = len(crons) > 0
|
||||
if cron {
|
||||
if err := tx.Unscoped().Delete(&model.Cron{}, "id in (?)", crons).Error; err != nil {
|
||||
@@ -78,10 +77,7 @@ func OnUserDelete(id []uint64, errorFunc func(string, ...interface{}) error) err
|
||||
}
|
||||
}
|
||||
|
||||
SortedServerLock.RLock()
|
||||
servers = model.FindByUserID(SortedServerList, uid)
|
||||
SortedServerLock.RUnlock()
|
||||
|
||||
servers = model.FindByUserID(slist, uid)
|
||||
server = len(servers) > 0
|
||||
if server {
|
||||
if err := tx.Unscoped().Delete(&model.Server{}, "id in (?)", servers).Error; err != nil {
|
||||
@@ -107,7 +103,7 @@ func OnUserDelete(id []uint64, errorFunc func(string, ...interface{}) error) err
|
||||
}
|
||||
|
||||
if cron {
|
||||
OnDeleteCron(crons)
|
||||
CronShared.Delete(crons)
|
||||
}
|
||||
|
||||
if server {
|
||||
@@ -122,21 +118,12 @@ func OnUserDelete(id []uint64, errorFunc func(string, ...interface{}) error) err
|
||||
}
|
||||
}
|
||||
AlertsLock.Unlock()
|
||||
OnServerDelete(servers)
|
||||
ServerShared.Delete(servers)
|
||||
}
|
||||
|
||||
secret := UserInfoMap[uid].AgentSecret
|
||||
delete(AgentSecretToUserId, secret)
|
||||
delete(UserInfoMap, uid)
|
||||
}
|
||||
|
||||
if cron {
|
||||
UpdateCronList()
|
||||
}
|
||||
|
||||
if server {
|
||||
ReSortServer()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user