diff --git a/data/sessions/.gitkeep b/data/sessions/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/models/host.go b/models/host.go index 8bf15d7..a3f1fda 100644 --- a/models/host.go +++ b/models/host.go @@ -5,8 +5,6 @@ import ( "github.com/go-xorm/xorm" ) - - // 主机 type Host struct { Id int16 `xorm:"smallint pk autoincr"` diff --git a/models/migration.go b/models/migration.go index b44138b..2818b46 100644 --- a/models/migration.go +++ b/models/migration.go @@ -12,8 +12,9 @@ func (migration *Migration) Exec(dbName string) error { if !isDatabaseExist(dbName) { return errors.New("数据库不存在") } + setting := new(Setting) tables := []interface{}{ - &User{}, &Task{}, &TaskLog{}, &Host{}, + &User{}, &Task{}, &TaskLog{}, &Host{}, setting, } for _, table := range tables { exist, err:= Db.IsTableExist(table) @@ -28,6 +29,10 @@ func (migration *Migration) Exec(dbName string) error { return err } } + err := setting.InitBasicField() + if err != nil { + return err + } return nil } diff --git a/models/setting.go b/models/setting.go new file mode 100644 index 0000000..0aceca7 --- /dev/null +++ b/models/setting.go @@ -0,0 +1,44 @@ +package models + +type Setting struct { + Id int16 `xorm:"smallint pk autoincr"` + Code string `xorm:"varchar(32) notnull"` + Key string `xorm:"varchar(64) notnull"` + Value string `xorm:"varchar(4096) notnull default '' "` +} + +const SlackCode = "slack" +const SlackKey = "url" + +// 初始化基本字段 邮件、slack等 +func (setting *Setting) InitBasicField() (error) { + setting.Code = "slack"; + setting.Key = "url" + setting.Value = "" + _, err := Db.Insert(setting) + + return err +} + +func (setting *Setting) SlackUrl() (string, error) { + setting.slackCondition() + _, err := Db.Get(setting) + + return setting.Value, err +} + +func (setting *Setting) UpdateSlackUrl(url string) (int64, error) { + setting.slackCondition() + setting.Value = url + + return setting.UpdateBean() +} + +func (setting *Setting) slackCondition() { + setting.Code = SlackCode + setting.Key = SlackKey +} + +func (setting *Setting) UpdateBean() (int64, error) { + return Db.Cols("code,key,value").Update(setting) +} \ No newline at end of file diff --git a/models/task.go b/models/task.go index b3fa2e2..31a4974 100644 --- a/models/task.go +++ b/models/task.go @@ -58,7 +58,7 @@ func (task *Task) Create() (insertId int, err error) { } func (task *Task) UpdateBean(id int) (int64, error) { - return Db.ID(id).UseBool("status,multi").Update(task) + return Db.ID(id).Cols("name,spec,protocol,command,timeout,multi,retry_times,host_id,remark,status").Update(task) } // 更新 @@ -91,7 +91,7 @@ func (task *Task) ActiveList() ([]TaskHost, error) { } // 获取某个主机下的所有激活任务 -func (task *Task) ActiveListByHostId(hostId int16) ([]TaskHost, error) { +func (task *Task) ActiveListByHostId(hostId int16) ([]TaskHost, error) { list := make([]TaskHost, 0) fields := "t.*, host.alias,host.name,host.username,host.password,host.port,host.auth_type,host.private_key" err := Db.Alias("t").Join("LEFT", hostTableName(), "t.host_id=host.id").Where("t.status = ? AND t.host_id = ?", Enabled, hostId).Cols(fields).Find(&list) diff --git a/models/task_log.go b/models/task_log.go index 8371e16..e46c9b4 100644 --- a/models/task_log.go +++ b/models/task_log.go @@ -22,7 +22,7 @@ type TaskLog struct { StartTime time.Time `xorm:"datetime created"` // 开始执行时间 EndTime time.Time `xorm:"datetime updated"` // 执行完成(失败)时间 Status Status `xorm:"tinyint notnull default 1"` // 状态 0:执行失败 1:执行中 2:执行完毕 3:任务取消(上次任务未执行完成) - Result string `xorm:"text notnull defalut '' "` // 执行结果 + Result string `xorm:"mediumtext notnull defalut '' "` // 执行结果 TotalTime int `xorm:"-"` // 执行总时长 BaseModel `xorm:"-"` } diff --git a/modules/app/app.go b/modules/app/app.go index a18cb23..d893159 100644 --- a/modules/app/app.go +++ b/modules/app/app.go @@ -8,6 +8,7 @@ import ( "github.com/ouqiang/gocron/modules/setting" "github.com/ouqiang/gocron/modules/logger" "runtime" + "github.com/ouqiang/gocron/modules/notify" ) var ( @@ -36,6 +37,8 @@ func InitEnv() { if Installed { InitDb() InitResource() + settingModel := new(models.Setting) + notify.SlackUrl, _ = settingModel.SlackUrl() } } diff --git a/modules/httpclient/http_client.go b/modules/httpclient/http_client.go new file mode 100644 index 0000000..daf3234 --- /dev/null +++ b/modules/httpclient/http_client.go @@ -0,0 +1,72 @@ +package httpclient + +// http-client + +import ( + "io/ioutil" + "net/http" + "time" + "fmt" + "bytes" +) + +type ResponseWrapper struct { + StatusCode int + Body string + Header http.Header +} + +func Get(url string, timeout int) ResponseWrapper { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return createRequestError(err) + } + + return request(req, timeout) +} + +func PostBody(url string, body string, timeout int) ResponseWrapper { + buf := bytes.NewBufferString(body) + req, err := http.NewRequest("POST", url, buf) + if err != nil { + return createRequestError(err) + } + req.Header.Set("Content-type", "application/json") + + return request(req, timeout) +} + +func request(req *http.Request, timeout int) ResponseWrapper { + wrapper := ResponseWrapper{StatusCode: 0, Body: "", Header: make(http.Header)} + client := &http.Client{} + if timeout > 0 { + client.Timeout = time.Duration(timeout) * time.Second + } + setRequestHeader(req) + resp, err := client.Do(req) + if err != nil { + wrapper.Body = fmt.Sprintf("执行HTTP请求错误-%s", err.Error()) + return wrapper + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + wrapper.Body = fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error()) + return wrapper + } + wrapper.StatusCode = resp.StatusCode + wrapper.Body = string(body) + wrapper.Header = resp.Header + + return wrapper +} + +func setRequestHeader(req *http.Request) { + req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,en;q=0.6") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 golang/gocron") +} + +func createRequestError(err error) ResponseWrapper { + errorMessage := fmt.Sprintf("创建HTTP请求错误-%s", err.Error()) + return ResponseWrapper{0, errorMessage, make(http.Header)} +} \ No newline at end of file diff --git a/modules/notify/notify.go b/modules/notify/notify.go new file mode 100644 index 0000000..b2d0c1a --- /dev/null +++ b/modules/notify/notify.go @@ -0,0 +1,32 @@ +package notify + +import ( + "time" +) + +var SlackUrl string + +type Message map[string]interface{} +type Notifiable interface { + send(msg Message) +} + +var queue chan Message = make(chan Message, 100) + +func init() { + go run() +} + +// 把消息推入队列 +func Push(msg Message) { + queue <- msg +} + +func run() { + slack := new(Slack) + for msg := range queue { + // 根据任务配置执行相应通知 + go slack.Send(msg) + time.Sleep(1 * time.Second) + } +} \ No newline at end of file diff --git a/modules/notify/slack.go b/modules/notify/slack.go new file mode 100644 index 0000000..1368659 --- /dev/null +++ b/modules/notify/slack.go @@ -0,0 +1,53 @@ +package notify +// 发送消息到slack + +import ( + "fmt" + "strings" + "github.com/ouqiang/gocron/modules/httpclient" + "github.com/ouqiang/gocron/modules/logger" + "github.com/ouqiang/gocron/modules/utils" +) + +type Slack struct {} + +func (slack *Slack) Send(msg Message) { + name, nameOk := msg["name"] + statusName, statusOk := msg["status"] + content, contentOk := msg["output"] + if SlackUrl == "" { + logger.Error("slack#webhooks-url为空") + return; + } + if !nameOk || !statusOk || !contentOk { + logger.Error("slack#消息字段不存在") + return + } + body := fmt.Sprintf("============\n============\n============\n任务名称: %s\n状态: %s\n输出:\n %s\n", name, statusName, content) + formatBody := slack.format(body) + timeout := 30 + maxTimes := 3 + i := 0 + for i < maxTimes { + resp := httpclient.PostBody(SlackUrl, formatBody, timeout) + if resp.StatusCode == 200 { + break; + } + i += 1 + if i < maxTimes { + logger.Error("slack#发送消息失败#%s#消息内容-%s", resp.Body, body) + } + } +} + +// 格式化消息内容 +func (slack *Slack) format(content string) string { + content = utils.EscapeJson(content) + specialChars := []string{"&", "<", ">"} + replaceChars := []string{"&", "<", ">"} + for i, v := range specialChars { + content = strings.Replace(content, v, replaceChars[i], 1000) + } + + return fmt.Sprintf(`{"text":"%s","username":"监控"}`, content) +} \ No newline at end of file diff --git a/modules/utils/utils.go b/modules/utils/utils.go index b6292bb..3944164 100644 --- a/modules/utils/utils.go +++ b/modules/utils/utils.go @@ -8,6 +8,7 @@ import ( "time" "runtime" "github.com/Tang-RoseChild/mahonia" + "strings" ) @@ -72,4 +73,15 @@ func GBK2UTF8(s string) (string, bool) { dec := mahonia.NewDecoder("gbk") return dec.ConvertStringOK(s) +} + +// 转义json特殊字符 +func EscapeJson(s string) string { + specialChars := []string{"\\", "\b","\f", "\n", "\r", "\t", "\"",} + replaceChars := []string{ "\\\\", "\\b", "\\f", "\\n", "\\r", "\\t", "\\\"",} + for i, v := range specialChars { + s = strings.Replace(s, v, replaceChars[i], 1000) + } + + return s } \ No newline at end of file diff --git a/routers/install/install.go b/routers/install/install.go index 1017b66..2a91e0d 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -21,9 +21,10 @@ type InstallForm struct { DbUsername string `binding:"Required;MaxSize(50)"` DbPassword string `binding:"Required;MaxSize(30)"` DbName string `binding:"Required;MaxSize(50)"` - DbTablePrefix string `binding:"MinSize(20)"` - AdminUsername string `binding:"Required;MaxSize(3)"` - AdminPassword string `binding:"Required;MaxSize(6)"` + DbTablePrefix string `binding:"MaxSize(20)"` + AdminUsername string `binding:"Required;MinSize(3)"` + AdminPassword string `binding:"Required;MinSize(6)"` + ConfirmAdminPassword string `binding:"Required;MinSize(6)"` AdminEmail string `binding:"Required;Email;MaxSize(50)"` } @@ -46,6 +47,9 @@ func Store(ctx *macaron.Context, form InstallForm) string { if app.Installed { return json.CommonFailure("系统已安装!") } + if form.AdminPassword != form.ConfirmAdminPassword { + return json.CommonFailure("两次输入密码不匹配") + } err := testDbConnection(form) if err != nil { return json.CommonFailure("数据库连接失败", err) diff --git a/routers/routers.go b/routers/routers.go index efe33b2..6f9f60c 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -16,6 +16,7 @@ import ( "github.com/ouqiang/gocron/modules/logger" "github.com/ouqiang/gocron/routers/user" "github.com/go-macaron/gzip" + "github.com/ouqiang/gocron/routers/setting" ) // 静态文件目录 @@ -64,6 +65,14 @@ func Register(m *macaron.Macaron) { m.Post("/remove/:id", host.Remove) }) + // 管理 + m.Group("/admin", func() { + m.Group("/setting/", func() { + m.Get("/slack", setting.EditSlack) + + }) + }, adminAuth) + // 404错误 m.NotFound(func(ctx *macaron.Context) { if isGetRequest(ctx) && !isAjaxRequest(ctx) { @@ -116,7 +125,6 @@ func RegisterMiddleware(m *macaron.Macaron) { setShareData(m) } - // 系统未安装,重定向到安装页面 func checkAppInstall(m *macaron.Macaron) { m.Use(func(ctx *macaron.Context) { @@ -170,6 +178,14 @@ func setShareData(m *macaron.Macaron) { }) } +// 管理员认证 +func adminAuth(ctx *macaron.Context, sess session.Store) { + if !user.IsAdmin(sess) { + ctx.Data["Title"] = "无权限访问此页面" + ctx.HTML(403, "error/no_permission") + } +} + func isAjaxRequest(ctx *macaron.Context) bool { req := ctx.Req.Header.Get("X-Requested-With") if req == "XMLHttpRequest" { diff --git a/routers/setting/setting.go b/routers/setting/setting.go new file mode 100644 index 0000000..c3ab636 --- /dev/null +++ b/routers/setting/setting.go @@ -0,0 +1,31 @@ +package setting + +import ( + "gopkg.in/macaron.v1" + "github.com/ouqiang/gocron/modules/utils" + "github.com/ouqiang/gocron/models" + "github.com/ouqiang/gocron/modules/logger" +) + +func EditSlack(ctx *macaron.Context) { + ctx.Data["Title"] = "slack配置" + settingModel := new(models.Setting) + url, err := settingModel.SlackUrl() + if err != nil { + logger.Error(err) + } + ctx.Data["SlackUrl"] = url + ctx.HTML(200, "setting/slack") +} + +func StoreSlack(ctx *macaron.Context) string { + url := ctx.QueryTrim("url") + settingModel := new(models.Setting) + _, err := settingModel.UpdateSlackUrl(url) + json := utils.JsonResponse{} + if err != nil { + return json.CommonFailure(utils.FailureContent, err) + } + + return json.Success(utils.SuccessContent, nil) +} \ No newline at end of file diff --git a/routers/user/user.go b/routers/user/user.go index cc572a7..9bfd146 100644 --- a/routers/user/user.go +++ b/routers/user/user.go @@ -31,6 +31,7 @@ func ValidateLogin(ctx *macaron.Context, sess session.Store) string { sess.Set("username", userModel.Name) sess.Set("uid", userModel.Id) + sess.Set("isAdmin", userModel.IsAdmin) return json.Success("登录成功", nil) } @@ -70,5 +71,14 @@ func IsLogin(sess session.Store) bool { return true } + return false +} + +func IsAdmin(sess session.Store) bool { + isAdmin, ok := sess.Get("isAdmin").(int8) + if ok && isAdmin > 0 { + return true + } + return false } \ No newline at end of file diff --git a/service/task.go b/service/task.go index 2e53b75..0806ff0 100644 --- a/service/task.go +++ b/service/task.go @@ -2,8 +2,6 @@ package service import ( "github.com/ouqiang/gocron/models" - "io/ioutil" - "net/http" "strconv" "time" "github.com/ouqiang/gocron/modules/logger" @@ -12,12 +10,14 @@ import ( "github.com/ouqiang/gocron/modules/utils" "errors" "fmt" + "github.com/ouqiang/gocron/modules/httpclient" + "github.com/ouqiang/gocron/modules/notify" ) var Cron *cron.Cron var runInstance Instance -// 任务ID作为Key, 不会出现并发读写, 不加锁 +// 任务ID作为Key, 不会出现并发写, 不加锁 type Instance struct { Status map[int]bool } @@ -136,35 +136,13 @@ func (h *LocalCommandHandler) runOnUnix(taskModel models.TaskHost) (string, erro type HTTPHandler struct{} func (h *HTTPHandler) Run(taskModel models.TaskHost) (result string, err error) { - client := &http.Client{} - if taskModel.Timeout > 0 { - client.Timeout = time.Duration(taskModel.Timeout) * time.Second - } - req, err := http.NewRequest("GET", taskModel.Command, nil) - if err != nil { - logger.Error("任务处理#创建HTTP请求错误-", err.Error()) - return - } - req.Header.Set("Content-type", "application/x-www-form-urlencoded") - req.Header.Set("User-Agent", "golang/gocron") - - resp, err := client.Do(req) - if err != nil { - logger.Error("任务处理HTTP请求错误-", err.Error()) - return - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - logger.Error("任务处理#读取HTTP请求返回值失败-", err.Error()) - return - } + resp := httpclient.Get(taskModel.Command, taskModel.Timeout) // 返回状态码非200,均为失败 if resp.StatusCode != 200 { - return string(body), errors.New(fmt.Sprintf("HTTP状态码非200-->%d", resp.StatusCode)) + return resp.Body, errors.New(fmt.Sprintf("HTTP状态码非200-->%d", resp.StatusCode)) } - return string(body), err + return resp.Body, err } // SSH-command任务 @@ -208,7 +186,7 @@ func updateTaskLog(taskLogId int64, taskResult TaskResult) (int64, error) { var status models.Status var result string = taskResult.Result if taskResult.Err != nil { - result = taskResult.Err.Error() + " " + result + result = taskResult.Err.Error() + "\n" + result status = models.Failure } else { status = models.Finish @@ -241,10 +219,27 @@ func createJob(taskModel models.TaskHost) cron.FuncJob { return } taskResult := execJob(handler, taskModel) + if taskResult.Err != nil { + taskResult.Result = taskResult.Err.Error() + "\n" + taskResult.Result + } _, err = updateTaskLog(taskLogId, taskResult) if err != nil { logger.Error("任务结束#更新任务日志失败-", err) } + + var statusName string + if taskResult.Err != nil { + statusName = "失败" + } else { + statusName = "成功" + } + msg := notify.Message{ + "name": taskModel.Task.Name, + "output": taskResult.Result, + "status": statusName, + "taskId": taskModel.Id, + }; + notify.Push(msg) } return taskFunc diff --git a/templates/common/header.html b/templates/common/header.html index 6016baa..16c660d 100644 --- a/templates/common/header.html +++ b/templates/common/header.html @@ -61,7 +61,7 @@ 主机 {{{if gt .LoginUid 0}}} - 管理 + 管理 {{{end}}} diff --git a/templates/error/no_permission.html b/templates/error/no_permission.html new file mode 100644 index 0000000..3fdc4ca --- /dev/null +++ b/templates/error/no_permission.html @@ -0,0 +1,12 @@ +{{{ template "common/header" . }}} + +{{{ template "common/footer" . }}} \ No newline at end of file diff --git a/templates/install/create.html b/templates/install/create.html index 2858aef..425d915 100644 --- a/templates/install/create.html +++ b/templates/install/create.html @@ -48,7 +48,7 @@