From d7a3ba607b7e1e47feb32b5f03da75db00a879ec Mon Sep 17 00:00:00 2001 From: naiba Date: Tue, 19 Jan 2021 09:59:04 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=8A=20v0.3.0=20=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=EF=BC=88=E5=AE=9A=E6=9C=9F=E5=A4=87=E4=BB=BD?= =?UTF-8?q?=E7=AD=89=E5=9C=BA=E6=99=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dashboard.yml | 4 +- .goreleaser.yml | 6 +- Dockerfile.dashboard => Dockerfile | 5 +- README.md | 6 +- cmd/agent/main.go | 21 +++++-- cmd/dashboard/controller/controller.go | 2 +- cmd/dashboard/controller/member_api.go | 84 ++++++++++++++++++++++++- cmd/dashboard/controller/member_page.go | 10 +++ cmd/dashboard/controller/oauth2.go | 5 +- cmd/dashboard/main.go | 52 +++++++++++++-- go.mod | 2 +- go.sum | 4 +- model/cron.go | 27 ++++++++ model/monitor.go | 7 ++- model/user.go | 5 +- pkg/mygin/mygin.go | 1 + pkg/utils/utils.go | 42 +++++++++++++ resource/static/main.js | 17 +++++ resource/template/common/footer.html | 2 +- resource/template/common/menu.html | 1 + resource/template/component/cron.html | 45 +++++++++++++ resource/template/dashboard/cron.html | 57 +++++++++++++++++ service/dao/dao.go | 20 +++++- service/rpc/nezha.go | 30 +++++++-- 24 files changed, 417 insertions(+), 38 deletions(-) rename Dockerfile.dashboard => Dockerfile (77%) create mode 100644 model/cron.go create mode 100644 pkg/utils/utils.go create mode 100644 resource/template/component/cron.html create mode 100644 resource/template/dashboard/cron.html diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml index 60be756..d703898 100644 --- a/.github/workflows/dashboard.yml +++ b/.github/workflows/dashboard.yml @@ -11,7 +11,7 @@ on: - "cmd/agent/**" - ".github/workflows/agent.yml" - ".goreleaser.yml" - - ".github/ISSUE_TEMPLATE/*" + - ".github/ISSUE_TEMPLATE/**" jobs: deploy: @@ -25,5 +25,5 @@ jobs: - name: Build and push dasbboard image run: | go test -v ./... - docker build -t ghcr.io/${{ github.repository_owner }}/nezha-dashboard -f Dockerfile.dashboard . + docker build -t ghcr.io/${{ github.repository_owner }}/nezha-dashboard -f Dockerfile . docker push ghcr.io/${{ github.repository_owner }}/nezha-dashboard diff --git a/.goreleaser.yml b/.goreleaser.yml index 0158479..4342d76 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -10,12 +10,14 @@ builds: - windows - darwin goarch: - - amd64 + - arm - arm64 - 386 - - arm + - amd64 - mips - mips64 + gomips: + - softfloat main: ./cmd/agent binary: nezha-agent checksum: diff --git a/Dockerfile.dashboard b/Dockerfile similarity index 77% rename from Dockerfile.dashboard rename to Dockerfile index f025279..4e5ffb2 100644 --- a/Dockerfile.dashboard +++ b/Dockerfile @@ -6,9 +6,12 @@ COPY . . RUN cd cmd/dashboard && go build -o app -ldflags="-s -w" FROM alpine:latest +ENV TZ="Asia/Shanghai" RUN apk --no-cache --no-progress add \ ca-certificates \ - tzdata + tzdata && \ + cp "/usr/share/zoneinfo/$TZ" /etc/localtime && \ + echo "$TZ" > /etc/timezone WORKDIR /dashboard COPY ./resource ./resource COPY --from=binarybuilder /dashboard/cmd/dashboard/app ./app diff --git a/README.md b/README.md index 457cc1c..aaac334 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,11 @@ URL 里面也可放置占位符,请求时会进行简单的字符串替换。 ## 变更日志 -最新:`dashboard 0.2.6` `agent 0.2.5`,只记录最后一次更新导致必须更新面板的说明。 +最新:`dashboard 0.3.0` `agent 0.3.0`,只记录最后一次更新导致必须更新面板的说明。 + +- `dashboard 0.3.0` `agent 0.3.0` **重大更新** + + 增加了定时任务功能,可以定时在 Agent 上执行脚本(应用于定期备份、重启服务等计划运维场景) - `dashboard 0.2.0` `agent 0.2.0` **重大更新** diff --git a/cmd/agent/main.go b/cmd/agent/main.go index f529fc7..b2fab17 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "os" + "os/exec" "strings" "time" @@ -62,7 +63,7 @@ func doSelfUpdate() { updateCh <- struct{}{} }() v := semver.MustParse(version) - log.Println("check update", v) + log.Println("Check update", v) latest, err := selfupdate.UpdateSelf(v, "naiba/nezha") if err != nil { log.Println("Binary update failed:", err) @@ -84,7 +85,6 @@ func init() { func main() { // 来自于 GoReleaser 的版本号 dao.Version = version - rootCmd.PersistentFlags().StringVarP(&server, "server", "s", "localhost:5555", "客户端ID") rootCmd.PersistentFlags().StringVarP(&clientID, "id", "i", "", "客户端ID") rootCmd.PersistentFlags().StringVarP(&clientSecret, "secret", "p", "", "客户端Secret") @@ -174,7 +174,7 @@ func doTask(task *pb.Task) { result.Id = task.GetId() result.Type = task.GetType() switch task.GetType() { - case model.MonitorTypeHTTPGET: + case model.TaskTypeHTTPGET: start := time.Now() resp, err := httpClient.Get(task.GetData()) if err == nil { @@ -196,7 +196,7 @@ func doTask(task *pb.Task) { } else { result.Data = err.Error() } - case model.MonitorTypeICMPPing: + case model.TaskTypeICMPPing: pinger, err := ping.NewPinger(task.GetData()) if err == nil { pinger.SetPrivileged(true) @@ -210,7 +210,7 @@ func doTask(task *pb.Task) { } else { result.Data = err.Error() } - case model.MonitorTypeTCPPing: + case model.TaskTypeTCPPing: start := time.Now() conn, err := net.DialTimeout("tcp", task.GetData(), time.Second*10) if err == nil { @@ -221,6 +221,17 @@ func doTask(task *pb.Task) { } else { result.Data = err.Error() } + case model.TaskTypeCommand: + startedAt := time.Now() + cmd := exec.Command(task.GetData()) + output, err := cmd.Output() + result.Delay = float32(time.Now().Sub(startedAt).Seconds()) + if err != nil { + result.Data = fmt.Sprintf("%s\n%s", string(output), err.Error()) + } else { + result.Data = string(output) + result.Successful = true + } default: log.Printf("Unknown action: %v", task) } diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index d25ef7b..484dc07 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -22,7 +22,7 @@ func ServeWeb(port uint) { r.Use(mygin.RecordPath) r.SetFuncMap(template.FuncMap{ "tf": func(t time.Time) string { - return t.Format("2006年1月2号") + return t.Format("2006年1月2号 15:04:05") }, "safe": func(s string) template.HTML { return template.HTML(s) diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go index 41d0f59..11d934d 100644 --- a/cmd/dashboard/controller/member_api.go +++ b/cmd/dashboard/controller/member_api.go @@ -9,10 +9,12 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/naiba/com" + "github.com/robfig/cron/v3" "github.com/naiba/nezha/model" "github.com/naiba/nezha/pkg/mygin" + "github.com/naiba/nezha/pkg/utils" + pb "github.com/naiba/nezha/proto" "github.com/naiba/nezha/service/alertmanager" "github.com/naiba/nezha/service/dao" ) @@ -34,6 +36,7 @@ func (ma *memberAPI) serve() { mr.POST("/logout", ma.logout) mr.POST("/server", ma.addOrEditServer) mr.POST("/monitor", ma.addOrEditMonitor) + mr.POST("/cron", ma.addOrEditCron) mr.POST("/notification", ma.addOrEditNotification) mr.POST("/alert-rule", ma.addOrEditAlertRule) mr.POST("/setting", ma.updateSetting) @@ -70,6 +73,17 @@ func (ma *memberAPI) delete(c *gin.Context) { if err == nil { err = dao.DB.Delete(&model.MonitorHistory{}, "monitor_id = ?", id).Error } + case "cron": + + err = dao.DB.Delete(&model.Cron{}, "id = ?", id).Error + if err == nil { + dao.CronLock.RLock() + defer dao.CronLock.RUnlock() + if dao.Crons[id].CronID != 0 { + dao.Cron.Remove(dao.Crons[id].CronID) + } + delete(dao.Crons, id) + } case "alert-rule": err = dao.DB.Delete(&model.AlertRule{}, "id = ?", id).Error if err == nil { @@ -109,7 +123,7 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) { s.ID = sf.ID s.Tag = sf.Tag if sf.ID == 0 { - s.Secret = com.MD5(fmt.Sprintf("%s%s%d", time.Now(), sf.Name, admin.ID)) + s.Secret = utils.MD5(fmt.Sprintf("%s%s%d", time.Now(), sf.Name, admin.ID)) s.Secret = s.Secret[:10] err = dao.DB.Create(&s).Error } else { @@ -179,6 +193,72 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) { }) } +type cronForm struct { + ID uint64 + Name string + Scheduler string + Command string + ServersRaw string + PushSuccessful string +} + +func (ma *memberAPI) addOrEditCron(c *gin.Context) { + var cf cronForm + var cr model.Cron + err := c.ShouldBindJSON(&cf) + if err == nil { + cr.Name = cf.Name + cr.Scheduler = cf.Scheduler + cr.Command = cf.Command + cr.ServersRaw = cf.ServersRaw + cr.PushSuccessful = cf.PushSuccessful == "on" + cr.ID = cf.ID + err = json.Unmarshal([]byte(cf.ServersRaw), &cr.Servers) + } + if err == nil { + _, err = cron.ParseStandard(cr.Scheduler) + } + if err == nil { + if cf.ID == 0 { + err = dao.DB.Create(&cr).Error + } else { + err = dao.DB.Save(&cr).Error + } + } + + if err != nil { + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusBadRequest, + Message: fmt.Sprintf("请求错误:%s", err), + }) + return + } + + if cr.CronID != 0 { + dao.Cron.Remove(cr.CronID) + } + + cr.CronID, err = dao.Cron.AddFunc(cr.Scheduler, func() { + dao.ServerLock.RLock() + defer dao.ServerLock.RUnlock() + for j := 0; j < len(cr.Servers); j++ { + if dao.ServerList[cr.Servers[j]].TaskStream != nil { + dao.ServerList[cr.Servers[j]].TaskStream.Send(&pb.Task{ + Id: cr.ID, + Data: cr.Command, + Type: model.TaskTypeCommand, + }) + } else { + alertmanager.SendNotification(fmt.Sprintf("计划任务:%s,服务器:%d 离线,无法执行。", cr.Name, cr.Servers[j])) + } + } + }) + + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusOK, + }) +} + type notificationForm struct { ID uint64 Name string diff --git a/cmd/dashboard/controller/member_page.go b/cmd/dashboard/controller/member_page.go index e155557..66ab61b 100644 --- a/cmd/dashboard/controller/member_page.go +++ b/cmd/dashboard/controller/member_page.go @@ -24,6 +24,7 @@ func (mp *memberPage) serve() { })) mr.GET("/server", mp.server) mr.GET("/monitor", mp.monitor) + mr.GET("/cron", mp.cron) mr.GET("/notification", mp.notification) mr.GET("/setting", mp.setting) } @@ -46,6 +47,15 @@ func (mp *memberPage) monitor(c *gin.Context) { })) } +func (mp *memberPage) cron(c *gin.Context) { + var crons []model.Cron + dao.DB.Find(&crons) + c.HTML(http.StatusOK, "dashboard/cron", mygin.CommonEnvironment(c, gin.H{ + "Title": "计划任务", + "Crons": crons, + })) +} + func (mp *memberPage) notification(c *gin.Context) { var nf []model.Notification dao.DB.Find(&nf) diff --git a/cmd/dashboard/controller/oauth2.go b/cmd/dashboard/controller/oauth2.go index 8682e6d..a97ec6f 100644 --- a/cmd/dashboard/controller/oauth2.go +++ b/cmd/dashboard/controller/oauth2.go @@ -6,14 +6,13 @@ import ( "net/http" "strings" - "github.com/naiba/com" - "github.com/gin-gonic/gin" GitHubAPI "github.com/google/go-github/github" "golang.org/x/oauth2" "github.com/naiba/nezha/model" "github.com/naiba/nezha/pkg/mygin" + "github.com/naiba/nezha/pkg/utils" "github.com/naiba/nezha/service/dao" ) @@ -28,7 +27,7 @@ func (oa *oauth2controller) serve() { } func (oa *oauth2controller) login(c *gin.Context) { - state := com.RandomString(6) + state := utils.RandStringBytesMaskImprSrcUnsafe(6) dao.Cache.Set(fmt.Sprintf("%s%s", model.CtxKeyOauth2State, c.ClientIP()), state, 0) url := oa.oauth2Config.AuthCodeURL(state, oauth2.AccessTypeOnline) c.Redirect(http.StatusFound, url) diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index b4a8048..131fed3 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "time" "github.com/patrickmn/go-cache" @@ -10,13 +11,13 @@ import ( "github.com/naiba/nezha/cmd/dashboard/controller" "github.com/naiba/nezha/cmd/dashboard/rpc" "github.com/naiba/nezha/model" + pb "github.com/naiba/nezha/proto" "github.com/naiba/nezha/service/alertmanager" "github.com/naiba/nezha/service/dao" ) func init() { var err error - dao.ServerList = make(map[uint64]*model.Server) dao.Conf = &model.Config{} err = dao.Conf.Read("data/config.yaml") if err != nil { @@ -30,14 +31,26 @@ func init() { dao.DB = dao.DB.Debug() } dao.Cache = cache.New(5*time.Minute, 10*time.Minute) - initDB() + initSystem() } -func initDB() { +func initSystem() { dao.DB.AutoMigrate(model.Server{}, model.User{}, model.Notification{}, model.AlertRule{}, model.Monitor{}, - model.MonitorHistory{}) - // load cache + model.MonitorHistory{}, model.Cron{}) + + loadServers() //加载服务器列表 + loadCrons() //加载计划任务 + + // 清理旧数据 + dao.Cron.AddFunc("* 3 * * *", cleanMonitorHistory) +} + +func cleanMonitorHistory() { + dao.DB.Delete(&model.MonitorHistory{}, "created_at < ?", time.Now().AddDate(0, -1, 0)) +} + +func loadServers() { var servers []model.Server dao.DB.Find(&servers) for _, s := range servers { @@ -49,6 +62,35 @@ func initDB() { dao.ReSortServer() } +func loadCrons() { + var crons []model.Cron + dao.DB.Find(&crons) + var err error + for i := 0; i < len(crons); i++ { + cr := crons[i] + cr.CronID, err = dao.Cron.AddFunc(cr.Scheduler, func() { + dao.ServerLock.RLock() + defer dao.ServerLock.RUnlock() + for j := 0; j < len(cr.Servers); j++ { + if dao.ServerList[cr.Servers[j]].TaskStream != nil { + dao.ServerList[cr.Servers[j]].TaskStream.Send(&pb.Task{ + Id: cr.ID, + Data: cr.Command, + Type: model.TaskTypeCommand, + }) + } else { + alertmanager.SendNotification(fmt.Sprintf("计划任务:%s,服务器:%d 离线,无法执行。", cr.Name, cr.Servers[j])) + } + } + }) + if err != nil { + panic(err) + } + dao.Crons[cr.ID] = &cr + } + dao.Cron.Start() +} + func main() { go controller.ServeWeb(dao.Conf.HTTPPort) go rpc.ServeRPC(5555) diff --git a/go.mod b/go.mod index 4ecd257..2ac360e 100644 --- a/go.mod +++ b/go.mod @@ -12,11 +12,11 @@ require ( github.com/golang/protobuf v1.4.2 github.com/google/go-github v17.0.0+incompatible github.com/gorilla/websocket v1.4.2 - github.com/naiba/com v0.0.0-20191104074000-318339dc72a5 github.com/onsi/ginkgo v1.7.0 // indirect github.com/onsi/gomega v1.4.3 // indirect github.com/p14yground/go-github-selfupdate v0.0.0-20201212172144-81a03b17860d github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil/v3 v3.20.11 github.com/spf13/cobra v0.0.5 github.com/spf13/viper v1.7.1 diff --git a/go.sum b/go.sum index 667f7d3..571dcce 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/naiba/com v0.0.0-20191104074000-318339dc72a5 h1:NYtQRl/K+QXUTLdamg3nHkdI1qGXhN+4B8ZdGktAaVo= -github.com/naiba/com v0.0.0-20191104074000-318339dc72a5/go.mod h1:gU4KduAcIb9RnoirznFwMcPjeSyYQhvXYL9NWEWUMYI= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= @@ -287,6 +285,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= diff --git a/model/cron.go b/model/cron.go new file mode 100644 index 0000000..d8ef23f --- /dev/null +++ b/model/cron.go @@ -0,0 +1,27 @@ +package model + +import ( + "encoding/json" + "time" + + "github.com/robfig/cron/v3" + "gorm.io/gorm" +) + +type Cron struct { + Common + Name string + Scheduler string //分钟 小时 天 月 星期 + Command string + Servers []uint64 `gorm:"-"` + PushSuccessful bool // 推送成功的通知 + LastExecutedAt time.Time // 最后一次执行时间 + LastResult bool // 最后一次执行结果 + + CronID cron.EntryID `gorn:"-"` + ServersRaw string +} + +func (c *Cron) AfterFind(tx *gorm.DB) error { + return json.Unmarshal([]byte(c.ServersRaw), &c.Servers) +} diff --git a/model/monitor.go b/model/monitor.go index a01b9f1..7b56253 100644 --- a/model/monitor.go +++ b/model/monitor.go @@ -6,9 +6,10 @@ import ( const ( _ = iota - MonitorTypeHTTPGET - MonitorTypeICMPPing - MonitorTypeTCPPing + TaskTypeHTTPGET + TaskTypeICMPPing + TaskTypeTCPPing + TaskTypeCommand ) type Monitor struct { diff --git a/model/user.go b/model/user.go index 1e01a1a..cbd0caa 100644 --- a/model/user.go +++ b/model/user.go @@ -5,7 +5,8 @@ import ( "time" "github.com/google/go-github/github" - "github.com/naiba/com" + + "github.com/naiba/nezha/pkg/utils" ) type User struct { @@ -44,6 +45,6 @@ func NewUserFromGitHub(gu *github.User) User { } func (u *User) IssueNewToken() { - u.Token = com.MD5(fmt.Sprintf("%d%d%s", time.Now().UnixNano(), u.ID, u.Login)) + u.Token = utils.MD5(fmt.Sprintf("%d%d%s", time.Now().UnixNano(), u.ID, u.Login)) u.TokenExpired = time.Now().AddDate(0, 2, 0) } diff --git a/pkg/mygin/mygin.go b/pkg/mygin/mygin.go index 7af4cca..81caed1 100644 --- a/pkg/mygin/mygin.go +++ b/pkg/mygin/mygin.go @@ -15,6 +15,7 @@ var adminPage = map[string]bool{ "/monitor": true, "/setting": true, "/notification": true, + "/cron": true, } func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H { diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..735eed6 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,42 @@ +package utils + +import ( + "crypto/md5" + "encoding/hex" + "math/rand" + "time" + "unsafe" +) + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +const ( + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return *(*string)(unsafe.Pointer(&b)) +} + +func MD5(plantext string) string { + hash := md5.New() + hash.Write([]byte(plantext)) + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/resource/static/main.js b/resource/static/main.js index 00f35a9..ee24ca7 100644 --- a/resource/static/main.js +++ b/resource/static/main.js @@ -129,6 +129,23 @@ function addOrEditMonitor(monitor) { showFormModal('.monitor.modal', '#monitorForm', '/api/monitor') } +function addOrEditCron(cron) { + const modal = $('.cron.modal') + modal.children('.header').text((cron ? '修改' : '添加') + '计划任务') + modal.find('.positive.button').html(cron ? '修改' : '添加') + modal.find('input[name=ID]').val(cron ? cron.ID : null) + modal.find('input[name=Name]').val(cron ? cron.Name : null) + modal.find('input[name=Scheduler]').val(cron ? cron.Scheduler : null) + modal.find('input[name=ServersRaw]').val(cron ? cron.ServersRaw : '[]') + modal.find('textarea[name=Command]').val(cron ? cron.Command : null) + if (cron && cron.PushSuccessful) { + modal.find('.ui.push-successful.checkbox').checkbox('set checked') + } else { + modal.find('.ui.push-successful.checkbox').checkbox('set unchecked') + } + showFormModal('.cron.modal', '#cronForm', '/api/cron') +} + function deleteRequest(api) { $.ajax({ url: api, diff --git a/resource/template/common/footer.html b/resource/template/common/footer.html index 01291fb..74de7bc 100644 --- a/resource/template/common/footer.html +++ b/resource/template/common/footer.html @@ -8,7 +8,7 @@ - + diff --git a/resource/template/common/menu.html b/resource/template/common/menu.html index 635f348..330e207 100644 --- a/resource/template/common/menu.html +++ b/resource/template/common/menu.html @@ -7,6 +7,7 @@ {{if .IsAdminPage}} 服务器 服务监控 + 计划任务 通知 设置 {{else}} diff --git a/resource/template/component/cron.html b/resource/template/component/cron.html new file mode 100644 index 0000000..42cae30 --- /dev/null +++ b/resource/template/component/cron.html @@ -0,0 +1,45 @@ +{{define "component/cron"}} + +{{end}} \ No newline at end of file diff --git a/resource/template/dashboard/cron.html b/resource/template/dashboard/cron.html new file mode 100644 index 0000000..733a822 --- /dev/null +++ b/resource/template/dashboard/cron.html @@ -0,0 +1,57 @@ +{{define "dashboard/cron"}} +{{template "common/header" .}} +{{template "common/menu" .}} +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + {{range $cron := .Crons}} + + + + + + + + + + + + {{end}} + +
ID备注计划命令成功推送执行者最后执行最后结果管理
{{$cron.ID}}{{$cron.Name}}{{$cron.Scheduler}}{{$cron.Command}}{{$cron.PushSuccessful}}{{$cron.ServersRaw}}{{$cron.LastExecutedAt|tf}}{{$cron.LastResult}} +
+ + +
+
+
+
+{{template "component/cron"}} +{{template "common/footer" .}} +{{end}} \ No newline at end of file diff --git a/service/dao/dao.go b/service/dao/dao.go index 0a11083..8abc536 100644 --- a/service/dao/dao.go +++ b/service/dao/dao.go @@ -3,8 +3,10 @@ package dao import ( "sort" "sync" + "time" "github.com/patrickmn/go-cache" + "github.com/robfig/cron/v3" "gorm.io/gorm" "github.com/naiba/nezha/model" @@ -21,13 +23,29 @@ var Cache *cache.Cache var DB *gorm.DB +// 服务器监控、状态相关 var ServerList map[uint64]*model.Server var ServerLock sync.RWMutex var SortedServerList []*model.Server var SortedServerLock sync.RWMutex -var Version = "v0.2.6" +// 计划任务相关 +var CronLock sync.RWMutex +var Crons map[uint64]*model.Cron +var Cron *cron.Cron + +var Version = "v0.3.0" + +func init() { + shanghai, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + panic(err) + } + Cron = cron.New(cron.WithLocation(shanghai)) + Crons = make(map[uint64]*model.Cron) + ServerList = make(map[uint64]*model.Server) +} func ReSortServer() { ServerLock.RLock() diff --git a/service/rpc/nezha.go b/service/rpc/nezha.go index a16e7e3..4fcc2ef 100644 --- a/service/rpc/nezha.go +++ b/service/rpc/nezha.go @@ -18,10 +18,11 @@ type NezhaHandler struct { func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Receipt, error) { var err error - if _, err = s.Auth.Check(c); err != nil { + var clientID uint64 + if clientID, err = s.Auth.Check(c); err != nil { return nil, err } - if r.GetType() == model.MonitorTypeHTTPGET { + if r.GetType() == model.TaskTypeHTTPGET { // SSL 证书报警 var errMsg string if strings.HasPrefix(r.GetData(), "SSL证书错误:") { @@ -54,10 +55,27 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece alertmanager.SendNotification(fmt.Sprintf("服务监控:%s %s", monitor.Name, errMsg)) } } - // 存入历史记录 - mh := model.PB2MonitorHistory(r) - if err := dao.DB.Create(&mh).Error; err != nil { - return nil, err + if r.GetType() == model.TaskTypeCommand { + // 处理上报的计划任务 + dao.CronLock.RLock() + cr := dao.Crons[r.GetId()] + dao.CronLock.RUnlock() + if cr.PushSuccessful && r.GetSuccessful() { + alertmanager.SendNotification(fmt.Sprintf("成功计划任务:%s ,服务器:%d,日志:\n%s", cr.Name, clientID, r.GetData())) + } + if !r.GetSuccessful() { + alertmanager.SendNotification(fmt.Sprintf("失败计划任务:%s ,服务器:%d,日志:\n%s", cr.Name, clientID, r.GetData())) + } + dao.DB.Model(cr).Updates(model.Cron{ + LastExecutedAt: time.Now().Add(time.Second * -1 * time.Duration(r.GetDelay())), + LastResult: r.GetSuccessful(), + }) + } else { + // 存入历史记录 + mh := model.PB2MonitorHistory(r) + if err := dao.DB.Create(&mh).Error; err != nil { + return nil, err + } } return &pb.Receipt{Proced: true}, nil }