diff --git a/cmd/dashboard/controller/alertrule.go b/cmd/dashboard/controller/alertrule.go index 37a1877..953cdf4 100644 --- a/cmd/dashboard/controller/alertrule.go +++ b/cmd/dashboard/controller/alertrule.go @@ -64,13 +64,12 @@ func createAlertRule(c *gin.Context) (uint64, error) { enable := arf.Enable r.TriggerMode = arf.TriggerMode r.Enable = &enable - r.ID = arf.ID if err := singleton.DB.Create(&r).Error; err != nil { return 0, newGormError("%v", err) } - singleton.OnRefreshOrAddAlert(r) + singleton.OnRefreshOrAddAlert(&r) return r.ID, nil } @@ -115,13 +114,12 @@ func updateAlertRule(c *gin.Context) (any, error) { enable := arf.Enable r.TriggerMode = arf.TriggerMode r.Enable = &enable - r.ID = arf.ID if err := singleton.DB.Save(&r).Error; err != nil { return 0, newGormError("%v", err) } - singleton.OnRefreshOrAddAlert(r) + singleton.OnRefreshOrAddAlert(&r) return r.ID, nil } @@ -143,7 +141,7 @@ func batchDeleteAlertRule(c *gin.Context) (any, error) { return nil, err } - if err := singleton.DB.Unscoped().Delete(&model.DDNSProfile{}, "id in (?)", ar).Error; err != nil { + if err := singleton.DB.Unscoped().Delete(&model.AlertRule{}, "id in (?)", ar).Error; err != nil { return nil, newGormError("%v", err) } diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index 2d0e673..bf5c8cc 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -97,12 +97,23 @@ func routers(r *gin.Engine) { auth.PATCH("/alert-rule/:id", commonHandler(updateAlertRule)) auth.POST("/batch-delete/alert-rule", commonHandler(batchDeleteAlertRule)) + auth.GET("/cron", commonHandler(listCron)) + auth.POST("/cron", commonHandler(createCron)) + auth.PATCH("/cron/:id", commonHandler(updateCron)) + auth.GET("/cron/:id/manual", commonHandler(manualTriggerCron)) + auth.POST("/batch-delete/cron", commonHandler(batchDeleteCron)) + auth.GET("/ddns", commonHandler(listDDNS)) auth.GET("/ddns/providers", commonHandler(listProviders)) auth.POST("/ddns", commonHandler(createDDNS)) auth.PATCH("/ddns/:id", commonHandler(updateDDNS)) auth.POST("/batch-delete/ddns", commonHandler(batchDeleteDDNS)) + auth.GET("/nat", commonHandler(listNAT)) + auth.POST("/nat", commonHandler(createNAT)) + auth.PATCH("/nat/:id", commonHandler(updateNAT)) + auth.POST("/batch-delete/nat", commonHandler(batchDeleteNAT)) + r.NoRoute(fallbackToFrontend) } diff --git a/cmd/dashboard/controller/cron.go b/cmd/dashboard/controller/cron.go new file mode 100644 index 0000000..d67986f --- /dev/null +++ b/cmd/dashboard/controller/cron.go @@ -0,0 +1,193 @@ +package controller + +import ( + "errors" + "fmt" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/jinzhu/copier" + + "github.com/naiba/nezha/model" + "github.com/naiba/nezha/service/singleton" +) + +// List schedule tasks +// @Summary List schedule tasks +// @Security BearerAuth +// @Schemes +// @Description List schedule tasks +// @Tags auth required +// @Produce json +// @Success 200 {object} model.CommonResponse[[]model.Cron] +// @Router /cron [get] +func listCron(c *gin.Context) ([]*model.Cron, error) { + singleton.CronLock.RLock() + defer singleton.CronLock.RUnlock() + + var cr []*model.Cron + if err := copier.Copy(&cr, &singleton.CronList); err != nil { + return nil, err + } + return cr, nil +} + +// Create new schedule task +// @Summary Create new schedule task +// @Security BearerAuth +// @Schemes +// @Description Create new schedule task +// @Tags auth required +// @Accept json +// @param request body model.CronForm true "CronForm" +// @Produce json +// @Success 200 {object} model.CommonResponse[uint64] +// @Router /cron [post] +func createCron(c *gin.Context) (uint64, error) { + var cf model.CronForm + var cr model.Cron + + if err := c.ShouldBindJSON(&cf); err != nil { + return 0, err + } + + cr.TaskType = cf.TaskType + cr.Name = cf.Name + cr.Scheduler = cf.Scheduler + cr.Command = cf.Command + cr.Servers = cf.Servers + cr.PushSuccessful = cf.PushSuccessful + cr.NotificationGroupID = cf.NotificationGroupID + cr.Cover = cf.Cover + + if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger { + return 0, errors.New("计划任务类型不得使用触发服务器执行方式") + } + + // 对于计划任务类型,需要更新CronJob + var err error + if cf.TaskType == model.CronTypeCronTask { + if cr.CronJobID, err = singleton.Cron.AddFunc(cr.Scheduler, singleton.CronTrigger(&cr)); err != nil { + return 0, err + } + } + + if err = singleton.DB.Create(&cr).Error; err != nil { + return 0, newGormError("%v", err) + } + + singleton.OnRefreshOrAddCron(&cr) + singleton.UpdateCronList() + return cr.ID, nil +} + +// Update schedule task +// @Summary Update schedule task +// @Security BearerAuth +// @Schemes +// @Description Update schedule task +// @Tags auth required +// @Accept json +// @param id path uint true "Task ID" +// @param request body model.CronForm true "CronForm" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /cron/{id} [patch] +func updateCron(c *gin.Context) (any, error) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return nil, err + } + + var cf model.CronForm + if err := c.ShouldBindJSON(&cf); err != nil { + return 0, err + } + + var cr model.Cron + if err := singleton.DB.First(&cr, id).Error; err != nil { + return nil, fmt.Errorf("task id %d does not exist", id) + } + + cr.TaskType = cf.TaskType + cr.Name = cf.Name + cr.Scheduler = cf.Scheduler + cr.Command = cf.Command + cr.Servers = cf.Servers + cr.PushSuccessful = cf.PushSuccessful + cr.NotificationGroupID = cf.NotificationGroupID + cr.Cover = cf.Cover + + if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger { + return nil, errors.New("计划任务类型不得使用触发服务器执行方式") + } + + // 对于计划任务类型,需要更新CronJob + if cf.TaskType == model.CronTypeCronTask { + if cr.CronJobID, err = singleton.Cron.AddFunc(cr.Scheduler, singleton.CronTrigger(&cr)); err != nil { + return nil, err + } + } + + if err = singleton.DB.Save(&cr).Error; err != nil { + return nil, newGormError("%v", err) + } + + singleton.OnRefreshOrAddCron(&cr) + singleton.UpdateCronList() + return nil, nil +} + +// Trigger schedule task +// @Summary Trigger schedule task +// @Security BearerAuth +// @Schemes +// @Description Trigger schedule task +// @Tags auth required +// @Accept json +// @param id path uint true "Task ID" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /cron/{id}/manual [get] +func manualTriggerCron(c *gin.Context) (any, error) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return nil, err + } + + var cr model.Cron + if err := singleton.DB.First(&cr, id).Error; err != nil { + return nil, fmt.Errorf("task id %d does not exist", id) + } + + singleton.ManualTrigger(&cr) + return nil, nil +} + +// Batch delete schedule tasks +// @Summary Batch delete schedule tasks +// @Security BearerAuth +// @Schemes +// @Description Batch delete schedule tasks +// @Tags auth required +// @Accept json +// @param request body []uint64 true "id list" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /batch-delete/cron [post] +func batchDeleteCron(c *gin.Context) (any, error) { + var cr []uint64 + + if err := c.ShouldBindJSON(&cr); err != nil { + return nil, err + } + + if err := singleton.DB.Unscoped().Delete(&model.Cron{}, "id in (?)", cr).Error; err != nil { + return nil, newGormError("%v", err) + } + + singleton.OnDeleteCron(cr) + return nil, nil +} diff --git a/cmd/dashboard/controller/ddns.go b/cmd/dashboard/controller/ddns.go index 16c9c3d..35c7d8d 100644 --- a/cmd/dashboard/controller/ddns.go +++ b/cmd/dashboard/controller/ddns.go @@ -128,7 +128,6 @@ func updateDDNS(c *gin.Context) (any, error) { } p.Name = df.Name - p.ID = id enableIPv4 := df.EnableIPv4 enableIPv6 := df.EnableIPv6 p.EnableIPv4 = &enableIPv4 diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go index 4e3e3a9..b3c0128 100644 --- a/cmd/dashboard/controller/member_api.go +++ b/cmd/dashboard/controller/member_api.go @@ -58,12 +58,6 @@ func (ma *memberAPI) delete(c *gin.Context) { var err error switch c.Param("model") { - case "nat": - err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error - if err == nil { - singleton.OnNATUpdate() - } - case "cron": err = singleton.DB.Unscoped().Delete(&model.Cron{}, "id = ?", id).Error if err == nil { @@ -139,12 +133,6 @@ func (ma *memberAPI) addOrEditCron(c *gin.Context) { err = tx.Save(&cr).Error } } - if err == nil { - // 对于计划任务类型,需要更新CronJob - if cf.TaskType == model.CronTypeCronTask { - cr.CronJobID, err = singleton.Cron.AddFunc(cr.Scheduler, singleton.CronTrigger(cr)) - } - } if err == nil { err = tx.Commit().Error } else { @@ -183,7 +171,7 @@ func (ma *memberAPI) manualTrigger(c *gin.Context) { return } - singleton.ManualTrigger(cr) + //singleton.ManualTrigger(cr) c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, @@ -452,7 +440,7 @@ func (ma *memberAPI) addOrEditNAT(c *gin.Context) { }) return } - singleton.OnNATUpdate() + //singleton.OnNATUpdate() c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, }) @@ -538,7 +526,7 @@ func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) { }) return } - singleton.OnRefreshOrAddAlert(r) + //singleton.OnRefreshOrAddAlert(r) c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, }) diff --git a/cmd/dashboard/controller/nat.go b/cmd/dashboard/controller/nat.go new file mode 100644 index 0000000..ed6f23f --- /dev/null +++ b/cmd/dashboard/controller/nat.go @@ -0,0 +1,138 @@ +package controller + +import ( + "fmt" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/jinzhu/copier" + + "github.com/naiba/nezha/model" + "github.com/naiba/nezha/service/singleton" +) + +// List NAT Profiles +// @Summary List NAT profiles +// @Schemes +// @Description List NAT profiles +// @Security BearerAuth +// @Tags auth required +// @Produce json +// @Success 200 {object} model.CommonResponse[[]model.NAT] +// @Router /nat [get] +func listNAT(c *gin.Context) ([]*model.NAT, error) { + var n []*model.NAT + + singleton.NATCacheRwLock.RLock() + defer singleton.NATCacheRwLock.RUnlock() + + if err := copier.Copy(&n, &singleton.NATList); err != nil { + return nil, err + } + + return n, nil +} + +// Add NAT profile +// @Summary Add NAT profile +// @Security BearerAuth +// @Schemes +// @Description Add NAT profile +// @Tags auth required +// @Accept json +// @param request body model.NATForm true "NAT Request" +// @Produce json +// @Success 200 {object} model.CommonResponse[uint64] +// @Router /nat [post] +func createNAT(c *gin.Context) (uint64, error) { + var nf model.NATForm + var n model.NAT + + if err := c.ShouldBindJSON(&nf); err != nil { + return 0, err + } + + n.Name = nf.Name + n.Domain = nf.Domain + n.Host = nf.Host + n.ServerID = nf.ServerID + + if err := singleton.DB.Create(&n).Error; err != nil { + return 0, newGormError("%v", err) + } + + singleton.OnNATUpdate(&n) + singleton.UpdateNATList() + return n.ID, nil +} + +// Edit NAT profile +// @Summary Edit NAT profile +// @Security BearerAuth +// @Schemes +// @Description Edit NAT profile +// @Tags auth required +// @Accept json +// @param id path uint true "Profile ID" +// @param request body model.NATForm true "NAT Request" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /nat/{id} [patch] +func updateNAT(c *gin.Context) (any, error) { + idStr := c.Param("id") + + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return nil, err + } + + var nf model.NATForm + if err := c.ShouldBindJSON(&nf); err != nil { + return nil, err + } + + var n model.NAT + if err = singleton.DB.First(&n, id).Error; err != nil { + return nil, fmt.Errorf("profile id %d does not exist", id) + } + + n.Name = nf.Name + n.Domain = nf.Domain + n.Host = nf.Host + n.ServerID = nf.ServerID + + if err := singleton.DB.Save(&n).Error; err != nil { + return 0, newGormError("%v", err) + } + + singleton.OnNATUpdate(&n) + singleton.UpdateNATList() + return nil, nil +} + +// Batch delete NAT configurations +// @Summary Batch delete NAT configurations +// @Security BearerAuth +// @Schemes +// @Description Batch delete NAT configurations +// @Tags auth required +// @Accept json +// @param request body []uint64 true "id list" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /batch-delete/nat [post] +func batchDeleteNAT(c *gin.Context) (any, error) { + var n []uint64 + + if err := c.ShouldBindJSON(&n); err != nil { + return nil, err + } + + if err := singleton.DB.Unscoped().Delete(&model.NAT{}, "id in (?)", n).Error; err != nil { + return nil, newGormError("%v", err) + } + + singleton.OnNATDelete(n) + singleton.UpdateNATList() + return nil, nil +} diff --git a/cmd/dashboard/controller/notification.go b/cmd/dashboard/controller/notification.go index e582d3a..a46c019 100644 --- a/cmd/dashboard/controller/notification.go +++ b/cmd/dashboard/controller/notification.go @@ -57,7 +57,7 @@ func createNotification(c *gin.Context) (uint64, error) { n.URL = nf.URL verifySSL := nf.VerifySSL n.VerifySSL = &verifySSL - n.ID = nf.ID + ns := model.NotificationServerBundle{ Notification: &n, Server: nil, @@ -115,7 +115,7 @@ func updateNotification(c *gin.Context) (any, error) { n.URL = nf.URL verifySSL := nf.VerifySSL n.VerifySSL = &verifySSL - n.ID = nf.ID + ns := model.NotificationServerBundle{ Notification: &n, Server: nil, diff --git a/cmd/dashboard/controller/notification_group.go b/cmd/dashboard/controller/notification_group.go index b1ce076..d8bdf88 100644 --- a/cmd/dashboard/controller/notification_group.go +++ b/cmd/dashboard/controller/notification_group.go @@ -17,7 +17,7 @@ import ( // @Schemes // @Description List notification group // @Security BearerAuth -// @Tags common +// @Tags auth required // @Produce json // @Success 200 {object} model.CommonResponse[[]model.NotificationGroupResponseItem] // @Router /notification-group [get] diff --git a/cmd/dashboard/controller/server.go b/cmd/dashboard/controller/server.go index 85566ca..3b195aa 100644 --- a/cmd/dashboard/controller/server.go +++ b/cmd/dashboard/controller/server.go @@ -62,7 +62,6 @@ func updateServer(c *gin.Context) (any, error) { s.Name = sf.Name s.DisplayIndex = sf.DisplayIndex - s.ID = id s.Note = sf.Note s.PublicNote = sf.PublicNote s.HideForGuest = sf.HideForGuest diff --git a/model/alertrule.go b/model/alertrule.go index c375fd8..e1fafcd 100644 --- a/model/alertrule.go +++ b/model/alertrule.go @@ -61,55 +61,55 @@ func (r *AlertRule) Enabled() bool { return r.Enable != nil && *r.Enable } -// Snapshot 对传入的Server进行该报警规则下所有type的检查 返回包含每项检查结果的空接口 -func (r *AlertRule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) []interface{} { - var point []interface{} - for i := 0; i < len(r.Rules); i++ { - point = append(point, r.Rules[i].Snapshot(cycleTransferStats, server, db)) +// Snapshot 对传入的Server进行该报警规则下所有type的检查 返回每项检查结果 +func (r *AlertRule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) []bool { + point := make([]bool, 0, len(r.Rules)) + for _, rule := range r.Rules { + point = append(point, rule.Snapshot(cycleTransferStats, server, db)) } return point } -// Check 传入包含当前报警规则下所有type检查结果的空接口 返回报警持续时间与是否通过报警检查(通过则返回true) -func (r *AlertRule) Check(points [][]interface{}) (int, bool) { - var maxNum int // 报警持续时间 - var count int // 检查未通过的个数 - for i := 0; i < len(r.Rules); i++ { - if r.Rules[i].IsTransferDurationRule() { +// Check 传入包含当前报警规则下所有type检查结果 返回报警持续时间与是否通过报警检查(通过则返回true) +func (r *AlertRule) Check(points [][]bool) (maxDuration int, passed bool) { + failCount := 0 // 检查未通过的个数 + + for i, rule := range r.Rules { + if rule.IsTransferDurationRule() { // 循环区间流量报警 - if maxNum < 1 { - maxNum = 1 + if maxDuration < 1 { + maxDuration = 1 } for j := len(points[i]) - 1; j >= 0; j-- { - if points[i][j] != nil { - count++ + if !points[i][j] { + failCount++ break } } } else { // 常规报警 - total := 0.0 - fail := 0.0 - num := int(r.Rules[i].Duration) - if num > maxNum { - maxNum = num + duration := int(rule.Duration) + if duration > maxDuration { + maxDuration = duration } - if len(points) < num { + if len(points) < duration { continue } - for j := len(points) - 1; j >= 0 && len(points)-num <= j; j-- { + + total, fail := 0.0, 0.0 + for j := len(points) - duration; j < len(points); j++ { total++ - if points[j][i] != nil { + if !points[j][i] { fail++ } } // 当70%以上的采样点未通过规则判断时 才认为当前检查未通过 if fail/total > 0.7 { - count++ + failCount++ break } } } // 仅当所有检查均未通过时 返回false - return maxNum, count != len(r.Rules) + return maxDuration, failCount != len(r.Rules) } diff --git a/model/alertrule_api.go b/model/alertrule_api.go index 8c93b3c..194a8ef 100644 --- a/model/alertrule_api.go +++ b/model/alertrule_api.go @@ -1,7 +1,6 @@ package model type AlertRuleForm struct { - ID uint64 `json:"id"` Name string `json:"name"` Rules []Rule `json:"rules"` FailTriggerTasks []uint64 `json:"fail_trigger_tasks"` // 失败时触发的任务id diff --git a/model/cron.go b/model/cron.go index 6db2602..f3a59ba 100644 --- a/model/cron.go +++ b/model/cron.go @@ -18,19 +18,19 @@ const ( type Cron struct { Common - Name string - TaskType uint8 `gorm:"default:0"` // 0:计划任务 1:触发任务 - Scheduler string //分钟 小时 天 月 星期 - Command string - Servers []uint64 `gorm:"-"` - PushSuccessful bool // 推送成功的通知 - NotificationGroupID uint64 // 指定通知方式的分组 - LastExecutedAt time.Time // 最后一次执行时间 - LastResult bool // 最后一次执行结果 - Cover uint8 // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器 2:由触发该计划任务的服务器执行) + Name string `json:"name,omitempty"` + TaskType uint8 `gorm:"default:0" json:"task_type,omitempty"` // 0:计划任务 1:触发任务 + Scheduler string `json:"scheduler,omitempty"` // 分钟 小时 天 月 星期 + Command string `json:"command,omitempty"` + Servers []uint64 `gorm:"-" json:"servers,omitempty"` + PushSuccessful bool `json:"push_successful,omitempty"` // 推送成功的通知 + NotificationGroupID uint64 `json:"notification_group_id,omitempty"` // 指定通知方式的分组 + LastExecutedAt time.Time `json:"last_executed_at,omitempty"` // 最后一次执行时间 + LastResult bool `json:"last_result,omitempty"` // 最后一次执行结果 + Cover uint8 `json:"cover,omitempty"` // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器 2:由触发该计划任务的服务器执行) - CronJobID cron.EntryID `gorm:"-"` - ServersRaw string + CronJobID cron.EntryID `gorm:"-" json:"cron_job_id,omitempty"` + ServersRaw string `json:"-"` } func (c *Cron) AfterFind(tx *gorm.DB) error { diff --git a/model/cron_api.go b/model/cron_api.go new file mode 100644 index 0000000..c468b8d --- /dev/null +++ b/model/cron_api.go @@ -0,0 +1,13 @@ +package model + +type CronForm struct { + ID uint64 `json:"id,omitempty"` + TaskType uint8 `json:"task_type,omitempty"` // 0:计划任务 1:触发任务 + Name string `json:"name,omitempty"` + Scheduler string `json:"scheduler,omitempty"` + Command string `json:"command,omitempty"` + Servers []uint64 `json:"servers,omitempty"` + Cover uint8 `json:"cover,omitempty"` + PushSuccessful bool `json:"push_successful,omitempty"` + NotificationGroupID uint64 `json:"notification_group_id,omitempty"` +} diff --git a/model/ddns_api.go b/model/ddns_api.go index e33b176..dba3a51 100644 --- a/model/ddns_api.go +++ b/model/ddns_api.go @@ -1,7 +1,6 @@ package model type DDNSForm struct { - ID uint64 `json:"id,omitempty"` MaxRetries uint64 `json:"max_retries,omitempty"` EnableIPv4 bool `json:"enable_ipv4,omitempty"` EnableIPv6 bool `json:"enable_ipv6,omitempty"` diff --git a/model/nat_api.go b/model/nat_api.go new file mode 100644 index 0000000..7f7665f --- /dev/null +++ b/model/nat_api.go @@ -0,0 +1,8 @@ +package model + +type NATForm struct { + Name string `json:"name,omitempty"` + ServerID uint64 `json:"server_id,omitempty"` + Host string `json:"host,omitempty"` + Domain string `json:"domain,omitempty"` +} diff --git a/model/notification.go b/model/notification.go index 4b4a9ca..992c85f 100644 --- a/model/notification.go +++ b/model/notification.go @@ -41,18 +41,6 @@ type Notification struct { VerifySSL *bool `json:"verify_ssl,omitempty"` } -type NotificationForm struct { - ID uint64 `json:"id,omitempty"` - Name string `json:"name,omitempty"` - URL string `json:"url,omitempty"` - RequestMethod int `json:"request_method,omitempty"` - RequestType int `json:"request_type,omitempty"` - RequestHeader string `json:"request_header,omitempty"` - RequestBody string `json:"request_body,omitempty"` - VerifySSL bool `json:"verify_ssl,omitempty"` - SkipCheck bool `json:"skip_check,omitempty"` -} - func (ns *NotificationServerBundle) reqURL(message string) string { n := ns.Notification return ns.replaceParamsInString(n.URL, message, func(msg string) string { diff --git a/model/notification_api.go b/model/notification_api.go new file mode 100644 index 0000000..d5fde9e --- /dev/null +++ b/model/notification_api.go @@ -0,0 +1,12 @@ +package model + +type NotificationForm struct { + Name string `json:"name,omitempty"` + URL string `json:"url,omitempty"` + RequestMethod int `json:"request_method,omitempty"` + RequestType int `json:"request_type,omitempty"` + RequestHeader string `json:"request_header,omitempty"` + RequestBody string `json:"request_body,omitempty"` + VerifySSL bool `json:"verify_ssl,omitempty"` + SkipCheck bool `json:"skip_check,omitempty"` +} diff --git a/model/rule.go b/model/rule.go index de3706a..d796080 100644 --- a/model/rule.go +++ b/model/rule.go @@ -34,8 +34,8 @@ type Rule struct { Ignore map[uint64]bool `json:"ignore,omitempty"` // 覆盖范围的排除 // 只作为缓存使用,记录下次该检测的时间 - NextTransferAt map[uint64]time.Time `json:"-"` - LastCycleStatus map[uint64]interface{} `json:"-"` + NextTransferAt map[uint64]time.Time `json:"-"` + LastCycleStatus map[uint64]bool `json:"-"` } func percentage(used, total uint64) float64 { @@ -45,15 +45,15 @@ func percentage(used, total uint64) float64 { return float64(used) * 100 / float64(total) } -// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil -func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) interface{} { +// Snapshot 未通过规则返回 false, 通过返回 true +func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) bool { // 监控全部但是排除了此服务器 if u.Cover == RuleCoverAll && u.Ignore[server.ID] { - return nil + return true } // 忽略全部但是指定监控了此服务器 if u.Cover == RuleCoverIgnoreAll && !u.Ignore[server.ID] { - return nil + return true } // 循环区间流量检测 · 短期无需重复检测 @@ -147,13 +147,13 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, u.NextTransferAt = make(map[uint64]time.Time) } if u.LastCycleStatus == nil { - u.LastCycleStatus = make(map[uint64]interface{}) + u.LastCycleStatus = make(map[uint64]bool) } u.NextTransferAt[server.ID] = time.Now().Add(time.Second * time.Duration(seconds)) if (u.Max > 0 && src > u.Max) || (u.Min > 0 && src < u.Min) { - u.LastCycleStatus[server.ID] = struct{}{} + u.LastCycleStatus[server.ID] = false } else { - u.LastCycleStatus[server.ID] = nil + u.LastCycleStatus[server.ID] = true } if cycleTransferStats.ServerName[server.ID] != server.Name { cycleTransferStats.ServerName[server.ID] = server.Name @@ -166,12 +166,12 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, } if u.Type == "offline" && float64(time.Now().Unix())-src > 6 { - return struct{}{} + return false } else if (u.Max > 0 && src > u.Max) || (u.Min > 0 && src < u.Min) { - return struct{}{} + return false } - return nil + return true } // IsTransferDurationRule 判断该规则是否属于周期流量规则 属于则返回true diff --git a/service/singleton/alertsentinel.go b/service/singleton/alertsentinel.go index ea63a9f..d0fd576 100644 --- a/service/singleton/alertsentinel.go +++ b/service/singleton/alertsentinel.go @@ -26,9 +26,9 @@ type NotificationHistory struct { var ( AlertsLock sync.RWMutex Alerts []*model.AlertRule - alertsStore map[uint64]map[uint64][][]interface{} // [alert_id][server_id] -> 对应报警规则的检查结果 - alertsPrevState map[uint64]map[uint64]uint8 // [alert_id][server_id] -> 对应报警规则的上一次报警状态 - AlertsCycleTransferStatsStore map[uint64]*model.CycleTransferStats // [alert_id] -> 对应报警规则的周期流量统计 + alertsStore map[uint64]map[uint64][][]bool // [alert_id][server_id] -> 对应报警规则的检查结果 + alertsPrevState map[uint64]map[uint64]uint8 // [alert_id][server_id] -> 对应报警规则的上一次报警状态 + AlertsCycleTransferStatsStore map[uint64]*model.CycleTransferStats // [alert_id] -> 对应报警规则的周期流量统计 ) // addCycleTransferStatsInfo 向AlertsCycleTransferStatsStore中添加周期流量报警统计信息 @@ -59,7 +59,7 @@ func addCycleTransferStatsInfo(alert *model.AlertRule) { // AlertSentinelStart 报警器启动 func AlertSentinelStart() { - alertsStore = make(map[uint64]map[uint64][][]interface{}) + alertsStore = make(map[uint64]map[uint64][][]bool) alertsPrevState = make(map[uint64]map[uint64]uint8) AlertsCycleTransferStatsStore = make(map[uint64]*model.CycleTransferStats) AlertsLock.Lock() @@ -67,7 +67,7 @@ func AlertSentinelStart() { panic(err) } for _, alert := range Alerts { - alertsStore[alert.ID] = make(map[uint64][][]interface{}) + alertsStore[alert.ID] = make(map[uint64][][]bool) alertsPrevState[alert.ID] = make(map[uint64]uint8) addCycleTransferStatsInfo(alert) } @@ -91,7 +91,7 @@ func AlertSentinelStart() { } } -func OnRefreshOrAddAlert(alert model.AlertRule) { +func OnRefreshOrAddAlert(alert *model.AlertRule) { AlertsLock.Lock() defer AlertsLock.Unlock() delete(alertsStore, alert.ID) @@ -99,17 +99,17 @@ func OnRefreshOrAddAlert(alert model.AlertRule) { var isEdit bool for i := 0; i < len(Alerts); i++ { if Alerts[i].ID == alert.ID { - Alerts[i] = &alert + Alerts[i] = alert isEdit = true } } if !isEdit { - Alerts = append(Alerts, &alert) + Alerts = append(Alerts, alert) } - alertsStore[alert.ID] = make(map[uint64][][]interface{}) + alertsStore[alert.ID] = make(map[uint64][][]bool) alertsPrevState[alert.ID] = make(map[uint64]uint8) delete(AlertsCycleTransferStatsStore, alert.ID) - addCycleTransferStatsInfo(&alert) + addCycleTransferStatsInfo(alert) } func OnDeleteAlert(id []uint64) { diff --git a/service/singleton/crontask.go b/service/singleton/crontask.go index 09104b9..652c199 100644 --- a/service/singleton/crontask.go +++ b/service/singleton/crontask.go @@ -3,6 +3,7 @@ package singleton import ( "bytes" "fmt" + "slices" "sync" "github.com/jinzhu/copier" @@ -15,8 +16,10 @@ import ( var ( Cron *cron.Cron - Crons map[uint64]*model.Cron // [CrondID] -> *model.Cron + Crons map[uint64]*model.Cron // [CronID] -> *model.Cron CronLock sync.RWMutex + + CronList []*model.Cron ) func InitCronTask() { @@ -27,29 +30,28 @@ func InitCronTask() { // loadCronTasks 加载计划任务 func loadCronTasks() { InitCronTask() - var crons []model.Cron - DB.Find(&crons) + DB.Find(&CronList) var err error var notificationGroupList []uint64 notificationMsgMap := make(map[uint64]*bytes.Buffer) - for i := 0; i < len(crons); i++ { + for i := 0; i < len(CronList); i++ { // 触发任务类型无需注册 - if crons[i].TaskType == model.CronTypeTriggerTask { - Crons[crons[i].ID] = &crons[i] + if CronList[i].TaskType == model.CronTypeTriggerTask { + Crons[CronList[i].ID] = CronList[i] continue } // 注册计划任务 - crons[i].CronJobID, err = Cron.AddFunc(crons[i].Scheduler, CronTrigger(crons[i])) + CronList[i].CronJobID, err = Cron.AddFunc(CronList[i].Scheduler, CronTrigger(CronList[i])) if err == nil { - Crons[crons[i].ID] = &crons[i] + Crons[CronList[i].ID] = CronList[i] } else { // 当前通知组首次出现 将其加入通知组列表并初始化通知组消息缓存 - if _, ok := notificationMsgMap[crons[i].NotificationGroupID]; !ok { - notificationGroupList = append(notificationGroupList, crons[i].NotificationGroupID) - notificationMsgMap[crons[i].NotificationGroupID] = bytes.NewBufferString("") - notificationMsgMap[crons[i].NotificationGroupID].WriteString("调度失败的计划任务:[") + if _, ok := notificationMsgMap[CronList[i].NotificationGroupID]; !ok { + notificationGroupList = append(notificationGroupList, CronList[i].NotificationGroupID) + notificationMsgMap[CronList[i].NotificationGroupID] = bytes.NewBufferString("") + notificationMsgMap[CronList[i].NotificationGroupID].WriteString("调度失败的计划任务:[") } - notificationMsgMap[crons[i].NotificationGroupID].WriteString(fmt.Sprintf("%d,", crons[i].ID)) + notificationMsgMap[CronList[i].NotificationGroupID].WriteString(fmt.Sprintf("%d,", CronList[i].ID)) } } // 向注册错误的计划任务所在通知组发送通知 @@ -60,7 +62,49 @@ func loadCronTasks() { Cron.Start() } -func ManualTrigger(c model.Cron) { +func OnRefreshOrAddCron(c *model.Cron) { + CronLock.Lock() + defer CronLock.Unlock() + crOld := Crons[c.ID] + if crOld != nil && crOld.CronJobID != 0 { + Cron.Remove(crOld.CronJobID) + } + + delete(Crons, c.ID) + Crons[c.ID] = c +} + +func UpdateCronList() { + CronLock.RLock() + defer CronLock.RUnlock() + + CronList = make([]*model.Cron, 0, len(Crons)) + for _, c := range Crons { + CronList = append(CronList, c) + } + slices.SortFunc(CronList, func(a, b *model.Cron) int { + if a.ID < b.ID { + return -1 + } else if a.ID == b.ID { + return 0 + } + return 1 + }) +} + +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)() } @@ -76,11 +120,11 @@ func SendTriggerTasks(taskIDs []uint64, triggerServer uint64) { // 依次调用CronTrigger发送任务 for _, c := range cronLists { - go CronTrigger(*c, triggerServer)() + go CronTrigger(c, triggerServer)() } } -func CronTrigger(cr model.Cron, triggerServer ...uint64) func() { +func CronTrigger(cr *model.Cron, triggerServer ...uint64) func() { crIgnoreMap := make(map[uint64]bool) for j := 0; j < len(cr.Servers); j++ { crIgnoreMap[cr.Servers[j]] = true diff --git a/service/singleton/ddns.go b/service/singleton/ddns.go index b2461b2..f2ccb73 100644 --- a/service/singleton/ddns.go +++ b/service/singleton/ddns.go @@ -21,16 +21,14 @@ var ( ) func initDDNS() { - var ddns []*model.DDNSProfile - DB.Find(&ddns) + DB.Find(&DDNSList) DDNSCacheLock.Lock() DDNSCache = make(map[uint64]*model.DDNSProfile) - for i := 0; i < len(ddns); i++ { - DDNSCache[ddns[i].ID] = ddns[i] + for i := 0; i < len(DDNSList); i++ { + DDNSCache[DDNSList[i].ID] = DDNSList[i] } DDNSCacheLock.Unlock() - UpdateDDNSList() OnNameserverUpdate() } diff --git a/service/singleton/nat.go b/service/singleton/nat.go index 5a2b7cb..fe27eac 100644 --- a/service/singleton/nat.go +++ b/service/singleton/nat.go @@ -1,31 +1,75 @@ package singleton import ( + "slices" "sync" "github.com/naiba/nezha/model" ) -var natCache = make(map[string]*model.NAT) -var natCacheRwLock = new(sync.RWMutex) +var ( + NATCache = make(map[string]*model.NAT) + NATCacheRwLock sync.RWMutex + + NATIDToDomain = make(map[uint64]string) + NATList []*model.NAT +) func initNAT() { - OnNATUpdate() -} - -func OnNATUpdate() { - natCacheRwLock.Lock() - defer natCacheRwLock.Unlock() - var nats []*model.NAT - DB.Find(&nats) - natCache = make(map[string]*model.NAT) - for i := 0; i < len(nats); i++ { - natCache[nats[i].Domain] = nats[i] + DB.Find(&NATList) + NATCacheRwLock.Lock() + defer NATCacheRwLock.Unlock() + 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 GetNATConfigByDomain(domain string) *model.NAT { - natCacheRwLock.RLock() - defer natCacheRwLock.RUnlock() - return natCache[domain] +func OnNATUpdate(n *model.NAT) { + NATCacheRwLock.Lock() + defer NATCacheRwLock.Unlock() + + if oldDomain, ok := NATIDToDomain[n.ID]; ok && oldDomain != n.Domain { + delete(NATCache, oldDomain) + } + + NATCache[n.Domain] = n + NATIDToDomain[n.ID] = n.Domain +} + +func OnNATDelete(id []uint64) { + NATCacheRwLock.Lock() + defer NATCacheRwLock.Unlock() + + for _, i := range id { + if domain, ok := NATIDToDomain[i]; ok { + delete(NATCache, domain) + delete(NATIDToDomain, i) + } + } +} + +func UpdateNATList() { + NATCacheRwLock.RLock() + defer NATCacheRwLock.RUnlock() + + NATList = make([]*model.NAT, 0, len(NATCache)) + for _, n := range NATCache { + NATList = append(NATList, n) + } + slices.SortFunc(NATList, func(a, b *model.NAT) int { + if a.ID < b.ID { + return -1 + } else if a.ID == b.ID { + return 0 + } + return 1 + }) +} + +func GetNATConfigByDomain(domain string) *model.NAT { + NATCacheRwLock.RLock() + defer NATCacheRwLock.RUnlock() + return NATCache[domain] } diff --git a/service/singleton/notification.go b/service/singleton/notification.go index 16fe79c..5d28ceb 100644 --- a/service/singleton/notification.go +++ b/service/singleton/notification.go @@ -49,14 +49,13 @@ func loadNotifications() { groupNotifications[n.NotificationGroupID] = append(groupNotifications[n.NotificationGroupID], n.NotificationID) } - var notifications []model.Notification - if err := DB.Find(¬ifications).Error; err != nil { + if err := DB.Find(&NotificationListSorted).Error; err != nil { panic(err) } - NotificationMap = make(map[uint64]*model.Notification, len(notifications)) - for i := range notifications { - NotificationMap[notifications[i].ID] = ¬ifications[i] + NotificationMap = make(map[uint64]*model.Notification, len(NotificationListSorted)) + for i := range NotificationListSorted { + NotificationMap[NotificationListSorted[i].ID] = NotificationListSorted[i] } for gid, nids := range groupNotifications { @@ -75,7 +74,6 @@ func loadNotifications() { } NotificationsLock.Unlock() - UpdateNotificationList() } func UpdateNotificationList() { diff --git a/service/singleton/server.go b/service/singleton/server.go index 3404271..844b20b 100644 --- a/service/singleton/server.go +++ b/service/singleton/server.go @@ -45,8 +45,8 @@ func ReSortServer() { SortedServerLock.Lock() defer SortedServerLock.Unlock() - SortedServerList = []*model.Server{} - SortedServerListForGuest = []*model.Server{} + SortedServerList = make([]*model.Server, 0, len(ServerList)) + var SortedServerListForGuest []*model.Server for _, s := range ServerList { SortedServerList = append(SortedServerList, s) if !s.HideForGuest {