mirror of
https://github.com/nezhahq/nezha.git
synced 2025-02-02 01:28:13 -05:00
🔊 v0.3.0 计划任务(定期备份等场景)
This commit is contained in:
parent
f20a501ab4
commit
d7a3ba607b
4
.github/workflows/dashboard.yml
vendored
4
.github/workflows/dashboard.yml
vendored
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
@ -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` **重大更新**
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
2
go.mod
2
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
|
||||
|
4
go.sum
4
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=
|
||||
|
27
model/cron.go
Normal file
27
model/cron.go
Normal file
@ -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)
|
||||
}
|
@ -6,9 +6,10 @@ import (
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
MonitorTypeHTTPGET
|
||||
MonitorTypeICMPPing
|
||||
MonitorTypeTCPPing
|
||||
TaskTypeHTTPGET
|
||||
TaskTypeICMPPing
|
||||
TaskTypeTCPPing
|
||||
TaskTypeCommand
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
42
pkg/utils/utils.go
Normal file
42
pkg/utils/utils.go
Normal file
@ -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<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
func RandStringBytesMaskImprSrcUnsafe(n int) string {
|
||||
var src = rand.NewSource(time.Now().UnixNano())
|
||||
b := make([]byte, n)
|
||||
|
||||
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 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))
|
||||
}
|
@ -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 ? '修改<i class="edit icon"></i>' : '添加<i class="add icon"></i>')
|
||||
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,
|
||||
|
@ -8,7 +8,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.1/dist/semantic.min.js"></script>
|
||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.min.js"></script>
|
||||
<script src="/static/main.js?v202101132218"></script>
|
||||
<script src="/static/main.js?v202101190958"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -7,6 +7,7 @@
|
||||
{{if .IsAdminPage}}
|
||||
<a class="item{{if eq .MatchedPath " /server"}} active{{end}}" href="/server">服务器</a>
|
||||
<a class="item{{if eq .MatchedPath " /monitor"}} active{{end}}" href="/monitor">服务监控</a>
|
||||
<a class="item{{if eq .MatchedPath " /cron"}} active{{end}}" href="/cron">计划任务</a>
|
||||
<a class="item{{if eq .MatchedPath " /notification"}} active{{end}}" href="/notification">通知</a>
|
||||
<a class="item{{if eq .MatchedPath " /setting"}} active{{end}}" href="/setting">设置</a>
|
||||
{{else}}
|
||||
|
45
resource/template/component/cron.html
Normal file
45
resource/template/component/cron.html
Normal file
@ -0,0 +1,45 @@
|
||||
{{define "component/cron"}}
|
||||
<div class="ui tiny cron modal transition hidden">
|
||||
<div class="header">添加计划任务</div>
|
||||
<div class="content">
|
||||
<form id="cronForm" class="ui form">
|
||||
<input type="hidden" name="ID">
|
||||
<div class="field">
|
||||
<label>备注</label>
|
||||
<input type="text" name="Name" placeholder="备份">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>计划</label>
|
||||
<input type="text" name="Scheduler" placeholder="* * 3 * *(每天3点)">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>命令</label>
|
||||
<textarea name="Command"></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>执行服务器ID列表</label>
|
||||
<input type="text" name="ServersRaw" placeholder="[10,7,19]">
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui push-successful checkbox">
|
||||
<input name="PushSuccessful" type="checkbox" tabindex="0" class="hidden">
|
||||
<label>推送成功的消息</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="ui warning message">
|
||||
<p>
|
||||
计划的格式为:<code>* * * * *</code> 分 时 天 月 星期,详情见 <a
|
||||
href="https://pkg.go.dev/github.com/robfig/cron/v3#hdr-CRON_Expression_Format" target="_blank">计划表达式格式</a><br>
|
||||
命令:Shell 命令,就像写脚本一样就可以,如果遇到 xxx 命令找不到,可能是 <code>PATH</code> 环境变量的问题,<code>source ~/.bashrc</code>
|
||||
或者使用绝对路径执行。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" actions">
|
||||
<div class="ui negative button">取消</div>
|
||||
<button class="ui positive right labeled icon button">确认<i class="checkmark icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
57
resource/template/dashboard/cron.html
Normal file
57
resource/template/dashboard/cron.html
Normal file
@ -0,0 +1,57 @@
|
||||
{{define "dashboard/cron"}}
|
||||
{{template "common/header" .}}
|
||||
{{template "common/menu" .}}
|
||||
<div class="nb-container">
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
<div class="right floated right aligned twelve wide column">
|
||||
<button class="ui right labeled positive icon button" onclick="addOrEditCron()"><i class="add icon"></i>
|
||||
添加计划任务
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>备注</th>
|
||||
<th>计划</th>
|
||||
<th>命令</th>
|
||||
<th>成功推送</th>
|
||||
<th>执行者</th>
|
||||
<th>最后执行</th>
|
||||
<th>最后结果</th>
|
||||
<th>管理</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $cron := .Crons}}
|
||||
<tr>
|
||||
<td>{{$cron.ID}}</td>
|
||||
<td>{{$cron.Name}}</td>
|
||||
<td>{{$cron.Scheduler}}</td>
|
||||
<td>{{$cron.Command}}</td>
|
||||
<td>{{$cron.PushSuccessful}}</td>
|
||||
<td>{{$cron.ServersRaw}}</td>
|
||||
<td>{{$cron.LastExecutedAt|tf}}</td>
|
||||
<td>{{$cron.LastResult}}</td>
|
||||
<td>
|
||||
<div class="ui mini icon buttons">
|
||||
<button class="ui button" onclick="addOrEditCron({{$cron}})">
|
||||
<i class="edit icon"></i>
|
||||
</button>
|
||||
<button class="ui button"
|
||||
onclick="showConfirm('删除计划任务','确认删除此计划任务?',deleteRequest,'/api/cron/'+{{$cron.ID}})">
|
||||
<i class="delete icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{template "component/cron"}}
|
||||
{{template "common/footer" .}}
|
||||
{{end}}
|
@ -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()
|
||||
|
@ -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,11 +55,28 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece
|
||||
alertmanager.SendNotification(fmt.Sprintf("服务监控:%s %s", monitor.Name, errMsg))
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user