diff --git a/go.mod b/go.mod index 1391d1b7..0b75b354 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,8 @@ require ( github.com/influxdata/influxdb v1.8.0 github.com/mattn/go-isatty v0.0.12 github.com/mattn/go-sqlite3 v1.14.0 // indirect + github.com/onsi/ginkgo v1.7.0 // indirect + github.com/onsi/gomega v1.4.3 // indirect github.com/open-falcon/rrdlite v0.0.0-20200214140804-bf5829f786ad github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect diff --git a/src/modules/job/http/router_task.go b/src/modules/job/http/router_task.go index 6f15ca33..b5d3a50b 100644 --- a/src/modules/job/http/router_task.go +++ b/src/modules/job/http/router_task.go @@ -9,10 +9,12 @@ import ( "github.com/gin-gonic/gin" "github.com/toolkits/pkg/logger" + "github.com/toolkits/pkg/net/httplib" "github.com/toolkits/pkg/slice" "github.com/didi/nightingale/src/models" "github.com/didi/nightingale/src/modules/job/config" + "github.com/didi/nightingale/src/common/address" ) type taskForm struct { @@ -563,7 +565,10 @@ func taskCallback(c *gin.Context) { // 这个数据结构是tt回调的时候使用的通用数据结构,里边既有工单基本信息,也有结构化数据,job这里只需要从中解析出结构化数据 type ttForm struct { - + Id int64 `json:"id" binding:"required"` + RunUser string `json:"runUser" binding:"required"` + Form map[string]interface{} `json:"form" binding:"required"` + Approval int `json:"approval"` } // /api/job-ce/run/:id?hosts=10.3.4.5,10.4.5.6 @@ -571,6 +576,11 @@ func taskRunForTT(c *gin.Context) { var f ttForm bind(c, &f) + action := c.Request.Host + c.Request.URL.Path + if f.Approval == 2 { + renderMessage(c, "该任务未通过审批") + return + } tpl := TaskTpl(urlParamInt64(c, "id")) arr, err := tpl.Hosts() dangerous(err) @@ -587,13 +597,14 @@ func taskRunForTT(c *gin.Context) { arr = tmp } } else { - // TODO 从结构化数据中取hosts - hosts = "xyz" - hosts = strings.ReplaceAll(hosts, "\r", ",") - hosts = strings.ReplaceAll(hosts, "\n", ",") - tmp := cleanHosts(strings.Split(hosts, ",")) - if len(tmp) > 0 { - arr = tmp + if v, ok := f.Form["hosts"]; ok { + hosts = v.(string) + hosts = strings.ReplaceAll(hosts, "\r", ",") + hosts = strings.ReplaceAll(hosts, "\n", ",") + tmp := cleanHosts(strings.Split(hosts, ",")) + if len(tmp) > 0 { + arr = tmp + } } } @@ -616,9 +627,144 @@ func taskRunForTT(c *gin.Context) { Creator: user.Username, } - // TODO 把结构化数据转换为脚本命令行参数 task.Args = "" + for k, v := range f.Form { + switch v.(type) { + case string: + if k == "hosts" { + tmp := v.(string) + tmp = strings.ReplaceAll(tmp, "\r", ",") + tmp = strings.ReplaceAll(tmp, "\n", ",") + tmpArray := cleanHosts(strings.Split(hosts, ",")) + if len(tmpArray) > 0 { + v = strings.Join(tmpArray, ",") + } + + } + if len(v.(string)) < 1600 { + task.Args += fmt.Sprintf("--%s=%s,,", k, v.(string)) + } + case int: + task.Args += fmt.Sprintf("--%s=%d,,", k, v.(int)) + case int64: + task.Args += fmt.Sprintf("--%s=%d,,", k, v.(int64)) + case float64: + //TODO 暂时不支持传非整型 + task.Args += fmt.Sprintf("--%s=%d,,", k, int64(v.(float64))) + } + } + + task.Args = strings.TrimSuffix(task.Args, ",,") dangerous(task.Save(arr, "start")) - renderData(c, task.Id, nil) + go func() { + for { + var ( + restHosts []string + ) + for _, h := range arr { + th, err := models.TaskHostGet(task.Id, h) + if err == nil { + if th.Status == "killed" { + reply := fmt.Sprintf("### Job通知推送\n* Job平台任务(ID:%d)在机器%s中执行失败,"+ + "原因为task被kill掉\n* 执行action接口地址为: %s\n* 标准输出: %s\n* 错误输出: %s\n", + task.Id, h, action, th.Stdout, th.Stderr) + err = TicketSender(f.Id, action, "task has been killed", reply, -1, + nil) + if err != nil { + logger.Errorf("send callback to ticket, err: %v", err) + } + } else if th.Status == "failed" { + reply := fmt.Sprintf("### Job通知推送\n* Job平台任务(ID:%d)在机器%s中执行失败,"+ + "详情见错误输出\n* 执行action接口地址为: %s\n* 标准输出: %s\n* 错误输出: %s\n", + task.Id, h, action, th.Stdout, th.Stderr) + err = TicketSender(f.Id, action, "run task failed", reply, -1, + nil) + if err != nil { + logger.Errorf("send callback to ticket, err: %v", err) + } + } else if th.Status == "timeout" { + reply := fmt.Sprintf("### Job通知推送\n* Job平台任务(ID:%d)在机器%s中执行超时"+ + "\n* 执行action接口地址为: %s\n* 标准输出: %s\n* 错误输出: %s\n", + task.Id, h, action, th.Stdout, th.Stderr) + err = TicketSender(f.Id, action, "run task failed", reply, -1, + nil) + if err != nil { + logger.Errorf("send callback to ticket, err: %v", err) + } + } else if th.Status == "success" { + reply := fmt.Sprintf("### Job通知推送\n* Job平台任务(ID:%d)在机器%s中执行成功"+ + "\n* 执行action接口地址为: %s\n* 标准输出: %s\n* 错误输出: %s\n", + task.Id, h, action, th.Stdout, th.Stderr) + err = TicketSender(f.Id, action, "task ", reply, 1, + nil) + if err != nil { + logger.Errorf("send callback to ticket, err: %v", err) + } + } else { + restHosts = append(restHosts, h) + } + } else { + logger.Errorf("get task_host err: %v", err) + } + } + arr = restHosts + time.Sleep(time.Second) + } + }() + + go func() { + time.Sleep(time.Second) + reply := fmt.Sprintf("[任务详情请关注Job平台任务(ID:%d)详情页地址](%s)", task.Id, fmt.Sprintf("/job/tasks/%d/result", task.Id)) + err = TicketSender(f.Id, action, "", reply, -1, + nil) + if err != nil { + logger.Errorf("send callback to ticket, err: %v", err) + } + }() + + renderData(c, gin.H{"taskID": task.Id, "detailPage": fmt.Sprintf("/job/tasks/%d/result", task.Id)}, nil) +} + +type ticketCallBackForm struct { + TicketId int64 `json:"ticketId" binding:"required"` + ActionApi string `json:"actionApi" binding:"required"` + SystemName string `json:"systemName" binding:"required"` + Success int `json:"success" binding:"required"` + Reason string `json:"reason"` + Info interface{} `json:"info"` + AutoReply string `json:"autoReply"` +} + +func TicketSender(id int64, action, reason, reply string, result int, info interface{}) error { + addr := address.GetHTTPListen("ticket") + + data := ticketCallBackForm{ + TicketId: id, + ActionApi: action, + Success: result, + Reason: reason, + Info: info, + AutoReply: reply, + } + + url := fmt.Sprintf("%s/v1/ticket/callback?systemName=job", addr) + if !(strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")) { + url = "http://" + url + } + + res, code, err := httplib.PostJSON(url, time.Second*5, data, map[string]string{"x-srv-token": "ticket-builtin-token"}) + if err != nil { + logger.Errorf("call sender api failed, server: %v, data: %+v, err: %v, resp:%v, status code:%d", url, data, err, string(res), code) + return err + } + + if code != 200 { + logger.Errorf("call sender api failed, server: %v, data: %+v, resp:%v, code:%d", url, data, string(res), code) + return err + } + + logger.Debugf("ticket response %s", string(res)) + + return nil }