V2.7 增加agent执行器

This commit is contained in:
georgehao
2019-07-03 22:31:27 +08:00
parent c3a89e9243
commit 37fb659c4e
48 changed files with 2832 additions and 513 deletions

8
actuator/main.go Normal file
View File

@@ -0,0 +1,8 @@
package main
//任务执行器
//
func main() {
}

25
agent/common/protocol.go Normal file
View File

@@ -0,0 +1,25 @@
/************************************************************
** @Description: common
** @Author: george hao
** @Date: 2018-11-29 11:14
** @Last Modified by: george hao
** @Last Modified time: 2018-11-29 11:14
*************************************************************/
package common
//配置开始 注释查看配置文件
type Conf struct {
Version string
AppMode string
LogLevel string
ServerName string
ServerId int
TcpPort int
TcpIp string
GroupId string
RegisterUrl string
UpdateStatusUrl string
IpType int
}
var ExitChan = make(chan int, 1)

24
agent/config/conf.ini Normal file
View File

@@ -0,0 +1,24 @@
# GOLBAL
# dev prod
AppMode = dev
Version = 1.0.0
# ALL,DEBUG,INFO,NOTICE,WARN,ERROR,FATAL
LogLevel = ALL
# 执行器配置
# auto-自动起名,或者自己起名
ServerName = agent-10.32.40.165-1564
# 启动后回写
ServerId = 7
# 端口,必须配置!!
TcpPort = 1564
# auto-自动获取
TcpIp = 10.32.40.165
# Ip地址是外网还是内网1-外网0-内网若填写TcpIp则本项配置无意义
IpType = 0
# 添加的执行器属于分组Id默认为1
GroupId = 1
# 以下配置必填地址格式http://yourdomain/server/apisave
RegisterUrl = http://localhost:8081/server/apisave
UpdateStatusUrl = http://localhost:8081/server/apistatus

59
agent/main.go Normal file
View File

@@ -0,0 +1,59 @@
package main
import (
"flag"
"github.com/george518/PPGo_Job/agent/server"
"log"
"runtime"
)
//文件配置路径
var configFilePath string
func initArgs() {
//server -c ./configpath
//defaultPath := "/Users/haodaquan/golang/src/github.com/george518/PPGo_Job/agent/config/conf.ini"
defaultPath := "./config/conf.ini"
flag.StringVar(&configFilePath, "c", defaultPath, "config file path request")
flag.Parse()
}
func initEnv() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
func main() {
var err error
//初始化线程
initEnv()
//配置文件路径
initArgs()
//加载配置
if err = server.InitConfig(configFilePath); err != nil {
goto ERR
}
server.NLog("INFO", "配置文件读取完毕...")
//应用关闭监控
server.ListenSignal()
//自动注册
if err = server.Register(); err != nil {
goto ERR
}
server.NLog("INFO", "自动注册完成...")
server.NLog("INFO", "agent is running...")
//监听
if err = server.RpcRun(); err != nil {
goto ERR
}
ERR:
log.Fatal(err.Error())
}

30
agent/run.sh Normal file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
# @Author: haodaquan
# @Date: 2017-06-29 17:44:45
# @Last Modified by: haodaquan
# @Last Modified time: 2019-07-03 17:44:45
case $1 in
start)
nohup ./ppgo_agent 2>&1 >> info_agent.log 2>&1 /dev/null &
echo "服务已启动..."
sleep 1
;;
stop)
killall ppgo_agent
echo "服务已停止..."
sleep 1
;;
restart)
killall ppgo_agent
sleep 1
nohup ./ppgo_agent 2>&1 >> info_agent.log 2>&1 /dev/null &
echo "服务已重启..."
sleep 1
;;
*)
echo "$0 {start|stop|restart}"
exit 4
;;
esac

39
agent/server/config.go Normal file
View File

@@ -0,0 +1,39 @@
/************************************************************
** @Description: server
** @Author: george hao
** @Date: 2018-11-29 11:13
** @Last Modified by: george hao
** @Last Modified time: 2018-11-29 11:13
*************************************************************/
package server
import (
"github.com/george518/PPGo_Job/agent/common"
"github.com/go-ini/ini"
)
var C = new(common.Conf)
var ConfPath string
func InitConfig(path string) error {
Cfg, err := ini.Load(path)
if err != nil {
return err
}
ConfPath = path
err = Cfg.MapTo(C)
return err
}
func SaveConfig(key string, value string) error {
Cfg, err := ini.Load(ConfPath)
if err != nil {
return err
}
Cfg.Section("").Key(key).SetValue(value)
Cfg.SaveTo(ConfPath)
InitConfig(ConfPath)
return nil
}

139
agent/server/job.go Normal file
View File

@@ -0,0 +1,139 @@
/************************************************************
** @Description: job
** @Author: george hao
** @Date: 2019-06-24 15:14
** @Last Modified by: george hao
** @Last Modified time: 2019-06-24 15:14
*************************************************************/
package server
import (
"bytes"
"fmt"
"github.com/astaxie/beego/logs"
. "github.com/george518/PPGo_Job/jobs"
"github.com/george518/PPGo_Job/libs"
"github.com/george518/PPGo_Job/models"
"os/exec"
"runtime"
"runtime/debug"
"sync"
"time"
)
//执行句柄map
var CmdMap sync.Map
func SetCmdMap(key string, cmd *exec.Cmd) {
if _, ok := CmdMap.Load(key); ok {
Counter.Store(key, cmd)
}
}
func GetCmdMap(key string) *exec.Cmd {
if v, ok := CmdMap.Load(key); ok {
return v.(*exec.Cmd)
}
return nil
}
func RestJobFromTask(task *models.Task, serverId int) (*Job, error) {
if task.Id < 1 {
return nil, fmt.Errorf("ToJob: 缺少id")
}
if task.ServerIds == "" {
return nil, fmt.Errorf("任务执行失败,找不到执行的服务器")
}
job := ResetCommandJob(task.Id, serverId, task.TaskName, task.Command)
job.Task = task
job.Concurrent = task.Concurrent == 1
job.ServerId = serverId
job.ServerName = "执行器"
return job, nil
}
func ResetCommandJob(id int, serverId int, name string, command string) *Job {
job := &Job{
Id: id,
Name: name,
}
job.JobKey = libs.JobKey(id, serverId)
job.RunFunc = func(timeout time.Duration) (jobResult *JobResult) {
bufOut := new(bytes.Buffer)
bufErr := new(bytes.Buffer)
//cmd := exec.Command("/bin/bash", "-c", command)
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("CMD", "/C", command)
} else {
cmd = exec.Command("sh", "-c", command)
}
cmd.Stdout = bufOut
cmd.Stderr = bufErr
cmd.Start()
err, isTimeout := runCmdWithTimeout(cmd, timeout)
jobResult = new(JobResult)
jobResult.ErrMsg = libs.GbkAsUtf8(bufErr.String())
jobResult.OutMsg = libs.GbkAsUtf8(bufOut.String())
jobResult.IsOk = true
if err != nil {
jobResult.IsOk = false
}
jobResult.IsTimeout = isTimeout
return
}
return job
}
func runCmdWithTimeout(cmd *exec.Cmd, timeout time.Duration) (error, bool) {
done := make(chan error)
go func() {
done <- cmd.Wait()
}()
var err error
select {
case <-time.After(timeout):
logs.Warn(fmt.Sprintf("任务执行时间超过%d秒进程将被强制杀掉: %d", int(timeout/time.Second), cmd.Process.Pid))
go func() {
<-done // 读出上面的goroutine数据避免阻塞导致无法退出
}()
if err = cmd.Process.Kill(); err != nil {
logs.Error(fmt.Sprintf("进程无法杀掉: %d, 错误信息: %s", cmd.Process.Pid, err))
}
return err, true
case err = <-done:
return err, false
}
}
func Run(j *Job) *JobResult {
defer func() {
if err := recover(); err != nil {
logs.Error(err, "\n", string(debug.Stack()))
}
}()
logs.Debug(fmt.Sprintf("开始执行任务: %d", j.JobKey))
j.Status++
defer func() {
j.Status--
}()
timeout := time.Duration(time.Hour * 24)
if j.Task.Timeout > 0 {
timeout = time.Second * time.Duration(j.Task.Timeout)
}
return j.RunFunc(timeout)
}

41
agent/server/logs.go Normal file
View File

@@ -0,0 +1,41 @@
/************************************************************
** @Description: log
** @Author: haodaquan
** @Date: 2018-08-22 23:00
** @Last Modified by: haodaquan
** @Last Modified time: 2018-08-22 23:00
*************************************************************/
package server
import (
"fmt"
"log"
"net/http"
"strings"
"time"
)
var Env string
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}
//http相关
func WriteLog(r *http.Request, t time.Time, match string, pattern string) {
if C.AppMode != "prod" {
d := time.Now().Sub(t)
l := fmt.Sprintf("[ACCESS] | % -10s | % -40s | % -16s | % -10s | % -40s |",
r.Method, r.URL.Path, d.String(), match, pattern)
log.Println(l)
}
}
//系统运行相关
func NLog(level string, value ...interface{}) {
if strings.Contains(C.LogLevel, level) || C.LogLevel == "ALL" {
log.Println("["+level+"]", value)
return
}
}

104
agent/server/notify.go Normal file
View File

@@ -0,0 +1,104 @@
/************************************************************
** @Description: notify
** @Author: george hao
** @Date: 2019-06-26 15:17
** @Last Modified by: george hao
** @Last Modified time: 2019-06-26 15:17
*************************************************************/
package server
import (
"encoding/json"
"fmt"
"github.com/george518/PPGo_Job/libs"
"strconv"
)
//启动时注册
func Register() error {
//获取本机ip以及端口 todo ip合法性判断
if C.TcpIp == "auto" {
tcpIp := libs.GetHostIp(C.IpType)
if tcpIp == "" {
return fmt.Errorf("无法获取本机IP请手工在配置文件里设置")
}
SaveConfig("TcpIp", tcpIp)
}
param := make(map[string]string, 0)
if C.ServerName == "auto" {
serverName := "agent-" + C.TcpIp + "-" + strconv.Itoa(C.TcpPort)
SaveConfig("ServerName", serverName)
}
param["server_ip"] = C.TcpIp
param["port"] = strconv.Itoa(C.TcpPort)
param["server_name"] = C.ServerName
param["detail"] = "自动注册执行器"
param["connection_type"] = "2"
param["group_id"] = C.GroupId
if C.RegisterUrl == "" {
return fmt.Errorf("自动注册地址配置错误")
}
body, err := libs.HttpGet(C.RegisterUrl, param)
if err != nil {
return err
}
m := make(map[string]interface{})
err = json.Unmarshal([]byte(body), &m)
if err != nil {
return err
}
if _, ok := m["status"]; ok {
if m["status"] == float64(0) {
//回写serverId
serverId := int(m["message"].(float64))
SaveConfig("ServerId", strconv.Itoa(serverId))
return nil
} else {
return fmt.Errorf("自动注册失败:%v", m["message"])
}
}
return fmt.Errorf("自动注册失败")
}
//程序异常退出的通知
func Close() error {
param := make(map[string]string, 0)
param["server_ip"] = C.TcpIp
param["port"] = strconv.Itoa(C.TcpPort)
param["status"] = "1"
if C.UpdateStatusUrl == "" {
return fmt.Errorf("执行器退出通知异常,请到系统中修改状态")
}
body, err := libs.HttpGet(C.UpdateStatusUrl, param)
if err != nil {
return err
}
m := make(map[string]interface{})
err = json.Unmarshal([]byte(body), &m)
if err != nil {
return err
}
if _, ok := m["status"]; ok {
if m["status"] == float64(0) {
return nil
} else {
return fmt.Errorf("执行器退出通知异常:%v", m["message"])
}
}
return fmt.Errorf("执行器退出通知异常:未知原因")
}
//心跳机制
func Heartbeat() error {
return nil
}

38
agent/server/service.go Normal file
View File

@@ -0,0 +1,38 @@
/************************************************************
** @Description: service
** @Author: george hao
** @Date: 2019-06-26 15:27
** @Last Modified by: george hao
** @Last Modified time: 2019-06-26 15:27
*************************************************************/
package server
import (
"net"
"net/rpc"
"net/rpc/jsonrpc"
"strconv"
)
//初始化路由
func init() {
rpc.RegisterName("RpcTask", new(RpcTask))
rpc.RegisterName("HeartBeat", new(RpcTask))
}
func RpcRun() error {
listener, err := net.Listen("tcp", ":"+strconv.Itoa(C.TcpPort))
if err != nil {
return err
}
for {
conn, err := listener.Accept()
if err != nil {
return err
}
//注意ServerCodec是个方法不是接口
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
//go rpc.ServeConn(conn)
}
}

41
agent/server/signal.go Normal file
View File

@@ -0,0 +1,41 @@
/************************************************************
** @Description: server
** @Author: george hao
** @Date: 2018-11-29 11:24
** @Last Modified by: george hao
** @Last Modified time: 2018-11-29 11:24
*************************************************************/
package server
import (
"os"
"os/signal"
"syscall"
)
//监听关闭状态
func ListenSignal() {
//创建监听退出chan
c := make(chan os.Signal)
//监听指定信号 ctrl+c kill
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)
go func() {
for s := range c {
switch s {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2:
NLog("NOTICE", " Ready to quit close type ", s)
//TODO 异常警报,汇报状态
if err := Close(); err != nil {
NLog("ERROR", err.Error())
} else {
NLog("NOTICE", " 执行器安全关闭...")
}
os.Exit(0)
default:
NLog("NOTICE", " close type ", s)
os.Exit(1)
}
}
}()
}

47
agent/server/task.go Normal file
View File

@@ -0,0 +1,47 @@
/************************************************************
** @Description: task
** @Author: george hao
** @Date: 2019-06-24 13:22
** @Last Modified by: george hao
** @Last Modified time: 2019-06-24 13:22
*************************************************************/
package server
import (
"github.com/astaxie/beego/logs"
"github.com/george518/PPGo_Job/jobs"
"github.com/george518/PPGo_Job/models"
)
type RpcTask struct {
}
type RpcResult struct {
Status int
Message string
}
//Execute once
func (r *RpcTask) RunTask(task *models.Task, Result *jobs.JobResult) error {
server_id := C.ServerId
job, err := RestJobFromTask(task, server_id)
if err != nil {
return nil
}
*Result = *(Run(job))
return nil
}
//Kill execution
func (r *RpcTask) KillCommand(task models.Task, reply *RpcResult) error {
reply.Status = 200
reply.Message = "Ok kill " + task.TaskName
return nil
}
func (r *RpcTask) HeartBeat(ping string, reply *RpcResult) error {
reply.Status = 200
reply.Message = ping + " pong"
logs.Info(ping)
return nil
}

28
agent/test/conf/conf.go Normal file
View File

@@ -0,0 +1,28 @@
/************************************************************
** @Description: conf
** @Author: george hao
** @Date: 2019-06-27 09:49
** @Last Modified by: george hao
** @Last Modified time: 2019-06-27 09:49
*************************************************************/
package main
import (
"github.com/astaxie/beego/logs"
"github.com/george518/PPGo_Job/agent/server"
)
func main() {
//获取配置,修改配置
loadconfig()
}
func loadconfig() {
path := "/Users/haodaquan/golang/src/github.com/george518/PPGo_Job/actuator/config/conf.ini"
server.InitConfig(path)
logs.Info(server.C.TcpIp, server.ConfPath)
server.SaveConfig("TcpIp", "10.32.33.22")
logs.Info(server.C.TcpIp, server.ConfPath)
}

18
agent/test/ip/ip.go Normal file
View File

@@ -0,0 +1,18 @@
/************************************************************
** @Description: ip
** @Author: george hao
** @Date: 2019-06-27 09:22
** @Last Modified by: george hao
** @Last Modified time: 2019-06-27 09:22
*************************************************************/
package main
import (
"github.com/astaxie/beego/logs"
"github.com/george518/PPGo_Job/libs"
)
func main() {
logs.Info(libs.PublicIp())
}

13
common/protocol.go Normal file
View File

@@ -0,0 +1,13 @@
/************************************************************
** @Description: protol
** @Author: george hao
** @Date: 2019-06-27 15:33
** @Last Modified by: george hao
** @Last Modified time: 2019-06-27 15:33
*************************************************************/
package common
type RpcResult struct {
Status int
Message string
}

View File

@@ -1,9 +1,9 @@
AppName = PPGo_Job2
HTTPPort = 8080
HTTPPort = 8081
RunMode = dev
SessionOn = true
version= V2.6
version= V2.7
# 允许同时运行的任务数
jobs.pool = 1000
@@ -19,7 +19,7 @@ db.host = 127.0.0.1
db.user = root
db.password = "123456"
db.port = 3306
db.name = ppgo_job2
db.name = cron
db.prefix = pp_
db.timezone = Asia/Shanghai

View File

@@ -9,7 +9,6 @@ package controllers
import (
"github.com/astaxie/beego"
"github.com/axgle/mahonia"
"github.com/george518/PPGo_Job/libs"
"github.com/george518/PPGo_Job/models"
"strconv"
@@ -100,7 +99,10 @@ func (self *BaseController) Auth() {
self.actionName != "loginin" &&
self.actionName != "apistart" &&
self.actionName != "apitask" &&
self.actionName != "apipause") {
self.actionName != "apipause" &&
self.actionName != "apisave" &&
self.actionName != "apistatus" &&
self.actionName != "apiget") {
self.redirect(beego.URLFor("LoginController.Login"))
}
}
@@ -336,7 +338,7 @@ type serverList struct {
func serverLists(authStr string, adminId int) (sls []serverList) {
serverGroup := serverGroupLists(authStr, adminId)
Filters := make([]interface{}, 0)
Filters = append(Filters, "status", 0)
Filters = append(Filters, "status__in", []int{0, 1})
Result, _ := models.TaskServerGetList(1, 1000, Filters...)
for k, v := range serverGroup {
@@ -354,16 +356,3 @@ func serverLists(authStr string, adminId int) (sls []serverList) {
}
return sls
}
func gbkAsUtf8(str string) string {
srcDecoder := mahonia.NewDecoder("gbk")
desDecoder := mahonia.NewDecoder("utf-8")
resStr := srcDecoder.ConvertString(str)
_, resBytes, _ := desDecoder.Translate([]byte(resStr), true)
return string(resBytes)
}
//任务识别码
func jobKey(taskId, serverId int) int {
return taskId*10000 + serverId
}

View File

@@ -8,7 +8,6 @@
package controllers
import (
"fmt"
"strconv"
"time"
@@ -43,7 +42,6 @@ func (self *LoginController) LoginIn() {
password := strings.TrimSpace(self.GetString("password"))
if username != "" && password != "" {
user, err := models.AdminGetByName(username)
fmt.Println(user)
if err != nil || user.Password != libs.Md5([]byte(password+user.Salt)) {
self.ajaxMsg("帐号或密码错误", MSG_ERR)
} else if user.Status == -1 {

View File

@@ -8,16 +8,12 @@
package controllers
import (
"fmt"
"github.com/astaxie/beego/logs"
"github.com/george518/PPGo_Job/libs"
"github.com/george518/PPGo_Job/models"
"golang.org/x/crypto/ssh"
"io/ioutil"
"net"
"strconv"
"strings"
"time"
"github.com/linxiaozhi/go-telnet"
"github.com/pkg/errors"
)
type ServerController struct {
@@ -25,13 +21,13 @@ type ServerController struct {
}
func (self *ServerController) List() {
self.Data["pageTitle"] = "资源管理"
self.Data["pageTitle"] = "执行资源管理"
self.Data["serverGroup"] = serverGroupLists(self.serverGroups, self.userId)
self.display()
}
func (self *ServerController) Add() {
self.Data["pageTitle"] = "新增服务器资源"
self.Data["pageTitle"] = "新增执行资源"
self.Data["serverGroup"] = serverGroupLists(self.serverGroups, self.userId)
self.display()
}
@@ -92,7 +88,7 @@ func (self *ServerController) GetServerByGroupId() {
}
func (self *ServerController) Edit() {
self.Data["pageTitle"] = "编辑服务器资源"
self.Data["pageTitle"] = "编辑执行资源"
id, _ := self.GetInt("id", 0)
server, _ := models.TaskServerGetById(id)
@@ -136,12 +132,12 @@ func (self *ServerController) AjaxTestServer() {
if server.ConnectionType == 0 {
if server.Type == 0 {
//密码登录
err = RemoteCommandByPassword(server)
err = libs.RemoteCommandByPassword(server)
}
if server.Type == 1 {
//密钥登录
err = RemoteCommandByKey(server)
err = libs.RemoteCommandByKey(server)
}
if err != nil {
@@ -151,7 +147,7 @@ func (self *ServerController) AjaxTestServer() {
} else if server.ConnectionType == 1 {
if server.Type == 0 {
//密码登录
err = RemoteCommandByTelnetPassword(server)
err = libs.RemoteCommandByTelnetPassword(server)
} else {
self.ajaxMsg("Telnet方式暂不支持密钥登陆", MSG_ERR)
}
@@ -160,114 +156,19 @@ func (self *ServerController) AjaxTestServer() {
self.ajaxMsg(err.Error(), MSG_ERR)
}
self.ajaxMsg("Success", MSG_OK)
} else if server.ConnectionType == 2 {
if err := libs.RemoteAgent(server); err != nil {
self.ajaxMsg(err.Error(), MSG_ERR)
} else {
self.ajaxMsg("Success", MSG_OK)
}
}
self.ajaxMsg("未知连接方式", MSG_ERR)
}
func RemoteCommandByTelnetPassword(servers *models.TaskServer) error {
addr := fmt.Sprintf("%s:%d", servers.ServerIp, servers.Port)
conn, err := gote.DialTimeout("tcp", addr, time.Second*10)
if err != nil {
return err
}
defer conn.Close()
buf := make([]byte, 4096)
_, err = conn.Read(buf)
if err != nil {
return err
}
_, err = conn.Write([]byte(servers.ServerAccount + "\r\n"))
if err != nil {
return err
}
_, err = conn.Read(buf)
if err != nil {
return err
}
_, err = conn.Write([]byte(servers.Password + "\r\n"))
if err != nil {
return err
}
_, err = conn.Read(buf)
if err != nil {
return err
}
str := gbkAsUtf8(string(buf[:]))
if strings.Contains(str, ">") {
return nil
}
return errors.Errorf("连接失败!")
}
func RemoteCommandByPassword(servers *models.TaskServer) error {
var (
auth []ssh.AuthMethod
addr string
clientConfig *ssh.ClientConfig
)
auth = make([]ssh.AuthMethod, 0)
auth = append(auth, ssh.Password(servers.Password))
clientConfig = &ssh.ClientConfig{
User: servers.ServerAccount,
Auth: auth,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: 5 * time.Second,
}
addr = fmt.Sprintf("%s:%d", servers.ServerIp, servers.Port)
client, err := ssh.Dial("tcp", addr, clientConfig)
if err == nil {
defer client.Close()
}
return err
}
func RemoteCommandByKey(servers *models.TaskServer) error {
key, err := ioutil.ReadFile(servers.PrivateKeySrc)
if err != nil {
return err
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return err
}
addr := fmt.Sprintf("%s:%d", servers.ServerIp, servers.Port)
config := &ssh.ClientConfig{
User: servers.ServerAccount,
Auth: []ssh.AuthMethod{
// Use the PublicKeys method for remote authentication.
ssh.PublicKeys(signer),
},
//HostKeyCallback: ssh.FixedHostKey(hostKey),
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: 5 * time.Second,
}
client, err := ssh.Dial("tcp", addr, config)
if err == nil {
client.Close()
}
return err
}
func (self *ServerController) Copy() {
self.Data["pageTitle"] = "复制服务器资源"
@@ -347,9 +248,14 @@ func (self *ServerController) AjaxSave() {
func (self *ServerController) AjaxDel() {
id, _ := self.GetInt("id")
if id == 1 {
self.ajaxMsg("默认分组id=1禁止删除", MSG_ERR)
}
server, _ := models.TaskServerGetById(id)
server.UpdateTime = time.Now().Unix()
server.Status = 1
server.Status = 2
server.Id = id
//TODO 查询服务器是否用于定时任务
@@ -374,18 +280,19 @@ func (self *ServerController) Table() {
serverName := strings.TrimSpace(self.GetString("serverName"))
StatusText := []string{
"正常",
"<font color='red'>禁用</font>",
"<i class='fa fa-refresh' style='color:#5FB878'></i>",
"<i class='fa fa-ban' style='color:#FF5722'></i>",
}
//
//loginType := [2]string{
// "密码",
// "密钥",
//}
loginType := [2]string{
"密码",
"密钥",
}
connectionType := [2]string{
connectionType := [3]string{
"SSH",
"Telnet",
"Agent",
}
serverGroup := serverGroupLists(self.serverGroups, self.userId)
@@ -393,7 +300,8 @@ func (self *ServerController) Table() {
self.pageSize = limit
//查询条件
filters := make([]interface{}, 0)
filters = append(filters, "status", 0)
ids := []int{0, 1}
filters = append(filters, "status__in", ids)
groupsIds := make([]int, 0)
if self.userId != 1 {
@@ -425,17 +333,128 @@ func (self *ServerController) Table() {
row := make(map[string]interface{})
row["id"] = v.Id
row["connection_type"] = connectionType[v.ConnectionType]
row["server_name"] = v.ServerName
row["server_name"] = StatusText[v.Status] + " " + v.ServerName
row["detail"] = v.Detail
if serverGroup[v.GroupId] == "" {
v.GroupId = 0
}
row["ip_port"] = v.ServerIp + ":" + strconv.Itoa(v.Port)
row["group_name"] = serverGroup[v.GroupId]
row["type"] = loginType[v.Type]
//row["type"] = loginType[v.Type]
row["status"] = v.Status
row["status_text"] = StatusText[v.Status]
list[k] = row
}
self.ajaxList("成功", MSG_OK, count, list)
}
//以下函数为执行器接口
//注册
func (self *ServerController) ApiSave() {
//唯一确定值 ip+port
serverIp := strings.TrimSpace(self.GetString("server_ip"))
port, _ := self.GetInt("port")
if serverIp == "" || port == 0 {
self.ajaxMsg("执行器和端口号必填", MSG_ERR)
}
defaultActName := "agent-" + serverIp + "-" + strconv.Itoa(port)
id := models.TaskServerForActuator(serverIp, port)
if id == 0 {
//新增
server := new(models.TaskServer)
server.ConnectionType, _ = self.GetInt("connection_type", 3)
server.ServerName = strings.TrimSpace(self.GetString("server_name", defaultActName))
server.ServerAccount = strings.TrimSpace(self.GetString("server_account", "agent"))
server.ServerOuterIp = strings.TrimSpace(self.GetString("server_outer_ip", ""))
server.ServerIp = strings.TrimSpace(self.GetString("server_ip"))
server.PrivateKeySrc = strings.TrimSpace(self.GetString("private_key_src", ""))
server.PublicKeySrc = strings.TrimSpace(self.GetString("public_key_src", ""))
server.Password = strings.TrimSpace(self.GetString("password", "agent"))
server.Detail = strings.TrimSpace(self.GetString("detail", ""))
server.Type, _ = self.GetInt("type", 0)
server.Port, _ = self.GetInt("port")
server.GroupId, _ = self.GetInt("group_id", 0)
server.Status = 0
server.CreateTime = time.Now().Unix()
server.UpdateTime = time.Now().Unix()
server.Status = 0
serverId, err := models.TaskServerAdd(server)
if err != nil {
self.ajaxMsg(err.Error(), MSG_ERR)
}
self.ajaxMsg(serverId, MSG_OK)
} else {
//修改状态
server, _ := models.TaskServerGetById(id)
server.UpdateTime = time.Now().Unix()
server.Status, _ = self.GetInt("status", 0)
if err := server.Update(); err != nil {
self.ajaxMsg(err.Error(), MSG_ERR)
}
self.ajaxMsg(id, MSG_OK)
}
}
//检测0-正常1-异常2-删除
func (self *ServerController) ApiStatus() {
//唯一确定值 ip+port
serverId := strings.TrimSpace(self.GetString("server_ip"))
port, _ := self.GetInt("port")
status, _ := self.GetInt("status", 0)
if serverId == "" || port == 0 {
self.ajaxMsg("执行器和端口号必填", MSG_ERR)
}
id := models.TaskServerForActuator(serverId, port)
if id == 0 {
self.ajaxMsg("执行器不存在", MSG_ERR)
}
if status != 0 && status != 1 {
status = 0
}
server, _ := models.TaskServerGetById(id)
server.UpdateTime = time.Now().Unix()
server.Status = status
server.Id = id
logs.Info(server)
//TODO 查询执行器是否正在使用中
if err := server.Update(); err != nil {
self.ajaxMsg(err.Error(), MSG_ERR)
}
self.ajaxMsg(id, MSG_OK)
}
//获取 不检测执行器状态
func (self *ServerController) ApiGet() {
//唯一确定值 ip+port
serverId := strings.TrimSpace(self.GetString("server_ip"))
port, _ := self.GetInt("port")
if serverId == "" || port == 0 {
self.ajaxMsg("执行器和端口号必填", MSG_ERR)
}
id := models.TaskServerForActuator(serverId, port)
if id == 0 {
self.ajaxMsg("执行器不存在", MSG_ERR)
}
server, err := models.TaskServerGetById(id)
if err != nil {
self.ajaxMsg(err.Error(), MSG_ERR)
} else {
self.ajaxMsg(server, MSG_OK)
}
}

View File

@@ -11,8 +11,6 @@ import (
"strings"
"time"
"fmt"
"strconv"
"github.com/astaxie/beego"
@@ -55,7 +53,6 @@ func (self *ServerGroupController) AjaxSave() {
servergroup_id, _ := self.GetInt("id")
fmt.Println(servergroup_id)
if servergroup_id == 0 {
//新增
servergroup.CreateTime = time.Now().Unix()

View File

@@ -9,6 +9,7 @@ package controllers
import (
"fmt"
"github.com/george518/PPGo_Job/libs"
"strconv"
"strings"
"time"
@@ -115,11 +116,18 @@ func (self *TaskController) Copy() {
if err != nil {
self.ajaxMsg(err.Error(), MSG_ERR)
}
if task.Status == 1 {
self.ajaxMsg("运行状态无法编辑任务,请先暂停任务", MSG_ERR)
}
self.Data["task"] = task
self.Data["adminInfo"] = AllAdminInfo("")
// 分组列表
self.Data["taskGroup"] = taskGroupLists(self.taskGroups, self.userId)
self.Data["serverGroup"] = serverLists(self.serverGroups, self.userId)
self.Data["isAdmin"] = self.userId
var notifyUserIds []int
if task.NotifyUserIds != "0" {
notifyUserIdsStr := strings.Split(task.NotifyUserIds, ",")
@@ -128,7 +136,33 @@ func (self *TaskController) Copy() {
notifyUserIds = append(notifyUserIds, i)
}
}
self.Data["notify_user_ids"] = notifyUserIds
server_ids := strings.Split(task.ServerIds, ",")
var server_ids_arr []int
for _, sv := range server_ids {
i, _ := strconv.Atoi(sv)
server_ids_arr = append(server_ids_arr, i)
}
self.Data["service_ids"] = server_ids_arr
notifyTplList, _, err := models.NotifyTplGetByTplTypeList(task.NotifyType)
tplList := make([]map[string]interface{}, len(notifyTplList))
if err == nil {
for k, v := range notifyTplList {
row := make(map[string]interface{})
row["id"] = v.Id
row["tpl_name"] = v.TplName
row["tpl_type"] = v.TplType
tplList[k] = row
}
}
self.Data["notifyTpl"] = tplList
self.display()
}
@@ -157,12 +191,12 @@ func (self *TaskController) Detail() {
serverName := ""
if task.ServerIds == "0" {
serverName = "本地服务器"
serverName = "本地服务器 <br>"
} else {
serverIdSli := strings.Split(task.ServerIds, ",")
for _, v := range serverIdSli {
if v == "0" {
serverName = "本地服务器 "
serverName = "本地服务器 <br>"
}
}
servers, n := models.TaskServerGetByIds(task.ServerIds)
@@ -170,9 +204,9 @@ func (self *TaskController) Detail() {
for _, server := range servers {
fmt.Println(server.Status)
if server.Status != 0 {
serverName += server.ServerName + "【无效】 "
serverName += server.ServerName + " <i class='fa fa-ban' style='color:#FF5722'></i> <br/> "
} else {
serverName += server.ServerName + " "
serverName += server.ServerName + " <br/> "
}
}
} else {
@@ -180,6 +214,12 @@ func (self *TaskController) Detail() {
}
}
//执行策略
self.Data["ServerType"] = "同时执行"
if task.ServerType == 1 {
self.Data["ServerType"] = "轮询执行"
}
//任务分组
groupName := "默认分组"
if task.GroupId > 0 {
@@ -242,6 +282,8 @@ func (self *TaskController) AjaxSave() {
task.Command = strings.TrimSpace(self.GetString("command"))
task.Timeout, _ = self.GetInt("timeout")
task.IsNotify, _ = self.GetInt("is_notify")
task.ServerType, _ = self.GetInt("server_type")
task.NotifyType, _ = self.GetInt("notify_type")
task.NotifyTplId, _ = self.GetInt("notify_tpl_id")
task.NotifyUserIds = strings.TrimSpace(self.GetString("notify_user_ids"))
@@ -287,6 +329,7 @@ func (self *TaskController) AjaxSave() {
task.CronSpec = strings.TrimSpace(self.GetString("cron_spec"))
task.Command = strings.TrimSpace(self.GetString("command"))
task.Timeout, _ = self.GetInt("timeout")
task.ServerType, _ = self.GetInt("server_type")
task.IsNotify, _ = self.GetInt("is_notify")
task.NotifyType, _ = self.GetInt("notify_type")
task.NotifyTplId, _ = self.GetInt("notify_tpl_id")
@@ -402,7 +445,7 @@ func (self *TaskController) AjaxPause() {
for _, server_id := range TaskServerIdsArr {
server_id_int, _ := strconv.Atoi(server_id)
jobKey := jobKey(task.Id, server_id_int)
jobKey := libs.JobKey(task.Id, server_id_int)
jobs.RemoveJob(jobKey)
}
@@ -477,7 +520,7 @@ func (self *TaskController) AjaxBatchPause() {
for _, server_id := range TaskServerIdsArr {
server_id_int, _ := strconv.Atoi(server_id)
jobKey := jobKey(task.Id, server_id_int)
jobKey := libs.JobKey(task.Id, server_id_int)
jobs.RemoveJob(jobKey)
}
if err == nil {
@@ -507,7 +550,7 @@ func (self *TaskController) AjaxBatchDel() {
for _, server_id := range TaskServerIdsArr {
server_id_int, _ := strconv.Atoi(server_id)
jobKey := jobKey(task.Id, server_id_int)
jobKey := libs.JobKey(task.Id, server_id_int)
jobs.RemoveJob(jobKey)
}
models.TaskDel(id)
@@ -688,8 +731,16 @@ func (self *TaskController) Table() {
row["status"] = v.Status
row["pre_time"] = beego.Date(time.Unix(v.PrevTime, 0), "Y-m-d H:i:s")
row["execute_times"] = v.ExecuteTimes
row["cron_spec"] = v.CronSpec
TaskServerIdsArr := strings.Split(v.ServerIds, ",")
serverId := 0
if len(TaskServerIdsArr) > 1 {
serverId, _ = strconv.Atoi(TaskServerIdsArr[0])
}
jobskey := libs.JobKey(v.Id, serverId)
e := jobs.GetEntryById(jobskey)
e := jobs.GetEntryById(v.Id)
if e != nil {
row["next_time"] = beego.Date(e.Next, "Y-m-d H:i:s")
row["prev_time"] = "-"
@@ -850,7 +901,7 @@ func (self *TaskController) ApiPause() {
for _, server_id := range TaskServerIdsArr {
server_id_int, _ := strconv.Atoi(server_id)
jobKey := jobKey(task.Id, server_id_int)
jobKey := libs.JobKey(task.Id, server_id_int)
jobs.RemoveJob(jobKey)
}

View File

@@ -75,7 +75,7 @@ func (self *TaskLogController) Table() {
for k, v := range result {
row := make(map[string]interface{})
row["id"] = v.Id
row["task_id"] = jobKey(v.TaskId, v.ServerId)
row["task_id"] = libs.JobKey(v.TaskId, v.ServerId)
row["start_time"] = beego.Date(time.Unix(v.CreateTime, 0), "Y-m-d H:i:s")
row["process_time"] = float64(v.ProcessTime) / 1000

View File

@@ -32,7 +32,7 @@ func AddJob(spec string, job *Job) bool {
lock.Lock()
defer lock.Unlock()
if GetEntryById(job.jobKey) != nil {
if GetEntryById(job.JobKey) != nil {
return false
}
err := mainCron.AddJob(spec, job)
@@ -47,7 +47,7 @@ func AddJob(spec string, job *Job) bool {
func RemoveJob(jobKey int) {
mainCron.RemoveJob(func(e *cron.Entry) bool {
if v, ok := e.Job.(*Job); ok {
if v.jobKey == jobKey {
if v.JobKey == jobKey {
return true
}
}
@@ -59,7 +59,7 @@ func GetEntryById(jobKey int) *cron.Entry {
entries := mainCron.Entries()
for _, e := range entries {
if v, ok := e.Job.(*Job); ok {
if v.jobKey == jobKey {
if v.JobKey == jobKey {
return e
}
}

View File

@@ -9,11 +9,18 @@ package jobs
import (
"bytes"
"errors"
"fmt"
"github.com/astaxie/beego/logs"
"github.com/george518/PPGo_Job/libs"
"github.com/george518/PPGo_Job/models"
"io/ioutil"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"os/exec"
"runtime/debug"
"sync"
"time"
"runtime"
@@ -22,25 +29,52 @@ import (
"encoding/json"
"github.com/astaxie/beego"
"github.com/axgle/mahonia"
"github.com/george518/PPGo_Job/models"
"github.com/george518/PPGo_Job/notify"
"github.com/linxiaozhi/go-telnet"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
)
type Job struct {
jobKey int // jobId = id*10000+serverId
id int // taskID
logId int64 // 日志记录ID
serverId int //服务器信息
serverName string //服务器名称
name string // 任务名称
task *models.Task // 任务对象
runFunc func(time.Duration) (string, string, error, bool) // 执行函数
status int // 任务状态大于0表示正在执行中
Concurrent bool // 同一个任务是否允许并行执行
JobKey int // jobId = id*10000+serverId
Id int // taskID
LogId int64 // 日志记录ID
ServerId int // 执行器信息
ServerName string // 执行器名称
ServerType int // 执行器类型2-agent 1-telnet 0-ssh
Name string // 任务名称
Task *models.Task // 任务对象
RunFunc func(time.Duration) *JobResult // 执行函数
Status int // 任务状态大于0表示正在执行
Concurrent bool // 同一个任务是否允许并行执行
}
type JobResult struct {
OutMsg string
ErrMsg string
IsOk bool
IsTimeout bool
}
//调度计数器
var Counter sync.Map
func GetCounter(key string) int {
if v, ok := Counter.LoadOrStore(key, 0); ok {
n := v.(int)
return n
}
return 0
}
func SetCounter(key string) {
if v, ok := Counter.Load(key); ok {
n := v.(int)
m := n + 1
if n > 1000 {
m = 0
}
Counter.Store(key, m)
}
}
func NewJobFromTask(task *models.Task) ([]*Job, error) {
@@ -53,24 +87,26 @@ func NewJobFromTask(task *models.Task) ([]*Job, error) {
}
TaskServerIdsArr := strings.Split(task.ServerIds, ",")
jobArr := make([]*Job, 0)
for _, server_id := range TaskServerIdsArr {
if server_id == "0" {
//本地执行
job := NewCommandJob(task.Id, 0, task.TaskName, task.Command)
job.task = task
job.Concurrent = task.Concurrent == 1
job.serverId = 0
job.serverName = "本地服务器"
job.Task = task
job.Concurrent = false
if task.Concurrent == 1 {
job.Concurrent = true
}
//job.Concurrent = task.Concurrent == 1
job.ServerId = 0
job.ServerName = "本地服务器"
jobArr = append(jobArr, job)
} else {
server_id_int, _ := strconv.Atoi(server_id)
//远程执行
server, _ := models.TaskServerGetById(server_id_int)
if server.Status == 1 {
if server.Status == 2 {
fmt.Println("服务器已禁用")
continue
}
@@ -79,29 +115,54 @@ func NewJobFromTask(task *models.Task) ([]*Job, error) {
if server.Type == 0 {
//密码验证登录服务器
job := RemoteCommandJobByPassword(task.Id, server_id_int, task.TaskName, task.Command, server)
job.task = task
job.Concurrent = task.Concurrent == 1
job.serverId = server_id_int
job.serverName = server.ServerName
job.Task = task
job.Concurrent = false
if task.Concurrent == 1 {
job.Concurrent = true
}
//job.Concurrent = task.Concurrent == 1
job.ServerId = server_id_int
job.ServerName = server.ServerName
jobArr = append(jobArr, job)
} else {
job := RemoteCommandJob(task.Id, server_id_int, task.TaskName, task.Command, server)
job.task = task
job.Concurrent = task.Concurrent == 1
job.serverId = server_id_int
job.serverName = server.ServerName
job.Task = task
job.Concurrent = false
if task.Concurrent == 1 {
job.Concurrent = true
}
//job.Concurrent = task.Concurrent == 1
job.ServerId = server_id_int
job.ServerName = server.ServerName
jobArr = append(jobArr, job)
}
} else if server.ConnectionType == 1 {
if server.Type == 0 {
//密码验证登录服务器
job := RemoteCommandJobByTelnetPassword(task.Id, server_id_int, task.TaskName, task.Command, server)
job.task = task
job.Concurrent = task.Concurrent == 1
job.serverId = server_id_int
job.serverName = server.ServerName
job.Task = task
job.Concurrent = false
if task.Concurrent == 1 {
job.Concurrent = true
}
//job.Concurrent = task.Concurrent == 1
job.ServerId = server_id_int
job.ServerName = server.ServerName
jobArr = append(jobArr, job)
}
} else if server.ConnectionType == 2 {
//密码验证登录服务器
job := RemoteCommandJobByAgentPassword(task.Id, server_id_int, task.TaskName, task.Command, server)
job.Task = task
job.Concurrent = false
if task.Concurrent == 1 {
job.Concurrent = true
}
//job.Concurrent = task.Concurrent == 1
job.ServerId = server_id_int
job.ServerName = server.ServerName
jobArr = append(jobArr, job)
}
}
}
@@ -111,12 +172,12 @@ func NewJobFromTask(task *models.Task) ([]*Job, error) {
func NewCommandJob(id int, serverId int, name string, command string) *Job {
job := &Job{
id: id,
name: name,
Id: id,
Name: name,
}
job.jobKey = jobKey(id, serverId)
job.runFunc = func(timeout time.Duration) (string, string, error, bool) {
job.JobKey = libs.JobKey(id, serverId)
job.RunFunc = func(timeout time.Duration) (jobresult *JobResult) {
bufOut := new(bytes.Buffer)
bufErr := new(bytes.Buffer)
//cmd := exec.Command("/bin/bash", "-c", command)
@@ -130,8 +191,18 @@ func NewCommandJob(id int, serverId int, name string, command string) *Job {
cmd.Stderr = bufErr
cmd.Start()
err, isTimeout := runCmdWithTimeout(cmd, timeout)
jobresult = new(JobResult)
jobresult.OutMsg = libs.GbkAsUtf8(bufOut.String())
jobresult.ErrMsg = libs.GbkAsUtf8(bufErr.String())
return gbkAsUtf8(bufOut.String()), gbkAsUtf8(bufErr.String()), err, isTimeout
jobresult.IsOk = true
if err != nil {
jobresult.IsOk = false
}
jobresult.IsTimeout = isTimeout
return jobresult
}
return job
}
@@ -139,23 +210,29 @@ func NewCommandJob(id int, serverId int, name string, command string) *Job {
//远程执行任务 密钥验证
func RemoteCommandJob(id int, serverId int, name string, command string, servers *models.TaskServer) *Job {
job := &Job{
id: id,
name: name,
serverId: serverId,
Id: id,
Name: name,
ServerId: serverId,
}
job.jobKey = jobKey(id, serverId)
job.JobKey = libs.JobKey(id, serverId)
job.runFunc = func(timeout time.Duration) (string, string, error, bool) {
job.RunFunc = func(timeout time.Duration) (jobresult *JobResult) {
jobresult = new(JobResult)
jobresult.OutMsg = ""
jobresult.ErrMsg = ""
jobresult.IsTimeout = false
key, err := ioutil.ReadFile(servers.PrivateKeySrc)
if err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
// Create the Signer for this private key.
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
addr := fmt.Sprintf("%s:%d", servers.ServerIp, servers.Port)
config := &ssh.ClientConfig{
@@ -172,14 +249,16 @@ func RemoteCommandJob(id int, serverId int, name string, command string, servers
// Connect to the remote server and perform the SSH handshake.47.93.220.5
client, err := ssh.Dial("tcp", addr, config)
if err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
defer session.Close()
@@ -193,10 +272,14 @@ func RemoteCommandJob(id int, serverId int, name string, command string, servers
//session.Output(command)
if err := session.Run(command); err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
isTimeout := false
return b.String(), c.String(), err, isTimeout
jobresult.OutMsg = b.String()
jobresult.ErrMsg = c.String()
jobresult.IsOk = true
jobresult.IsTimeout = false
return
}
return job
}
@@ -212,12 +295,18 @@ func RemoteCommandJobByPassword(id int, serverId int, name string, command strin
)
job := &Job{
id: id,
name: name,
serverId: serverId,
Id: id,
Name: name,
ServerId: serverId,
ServerType: servers.ConnectionType,
}
job.jobKey = jobKey(id, serverId)
job.runFunc = func(timeout time.Duration) (string, string, error, bool) {
job.JobKey = libs.JobKey(id, serverId)
job.RunFunc = func(timeout time.Duration) (jobresult *JobResult) {
jobresult = new(JobResult)
jobresult.OutMsg = ""
jobresult.ErrMsg = ""
jobresult.IsTimeout = false
// get auth method
auth = make([]ssh.AuthMethod, 0)
auth = append(auth, ssh.Password(servers.Password))
@@ -235,14 +324,16 @@ func RemoteCommandJobByPassword(id int, serverId int, name string, command strin
addr = fmt.Sprintf("%s:%d", servers.ServerIp, servers.Port)
if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
defer client.Close()
// create session
if session, err = client.NewSession(); err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
var b bytes.Buffer
@@ -251,10 +342,14 @@ func RemoteCommandJobByPassword(id int, serverId int, name string, command strin
session.Stderr = &c
//session.Output(command)
if err := session.Run(command); err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
isTimeout := false
return b.String(), c.String(), err, isTimeout
jobresult.OutMsg = b.String()
jobresult.ErrMsg = c.String()
jobresult.IsOk = true
jobresult.IsTimeout = false
return
}
return job
@@ -263,18 +358,23 @@ func RemoteCommandJobByPassword(id int, serverId int, name string, command strin
func RemoteCommandJobByTelnetPassword(id int, serverId int, name string, command string, servers *models.TaskServer) *Job {
job := &Job{
id: id,
name: name,
serverId: serverId,
Id: id,
Name: name,
ServerId: serverId,
}
job.jobKey = jobKey(id, serverId)
job.runFunc = func(timeout time.Duration) (string, string, error, bool) {
job.JobKey = libs.JobKey(id, serverId)
job.RunFunc = func(timeout time.Duration) (jobresult *JobResult) {
jobresult = new(JobResult)
jobresult.OutMsg = ""
jobresult.ErrMsg = ""
jobresult.IsTimeout = false
addr := fmt.Sprintf("%s:%d", servers.ServerIp, servers.Port)
conn, err := gote.DialTimeout("tcp", addr, timeout)
if err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
defer conn.Close()
@@ -282,28 +382,35 @@ func RemoteCommandJobByTelnetPassword(id int, serverId int, name string, command
buf := make([]byte, 4096)
if _, err = conn.Read(buf); err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
if _, err = conn.Write([]byte(servers.ServerAccount + "\r\n")); err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
if _, err = conn.Read(buf); err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
if _, err = conn.Write([]byte(servers.Password + "\r\n")); err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
if _, err = conn.Read(buf); err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
loginStr := gbkAsUtf8(string(buf[:]))
loginStr := libs.GbkAsUtf8(string(buf[:]))
if !strings.Contains(loginStr, ">") {
return "", "", errors.Errorf("Login failed!"), false
jobresult.ErrMsg = jobresult.ErrMsg + "Login failed!"
jobresult.IsOk = false
return
}
commandArr := strings.Split(command, "\n")
@@ -312,44 +419,178 @@ func RemoteCommandJobByTelnetPassword(id int, serverId int, name string, command
for _, c := range commandArr {
_, err = conn.Write([]byte(c + "\r\n"))
if err != nil {
return "", "", err, false
jobresult.IsOk = false
return
}
n, err = conn.Read(buf)
out = out + gbkAsUtf8(string(buf[0:n]))
out = out + libs.GbkAsUtf8(string(buf[0:n]))
if err != nil ||
strings.Contains(out, "'"+c+"' is not recognized as an internal or external command") ||
strings.Contains(out, "'"+c+"' 不是内部或外部命令,也不是可运行的程序") {
return out, "", fmt.Errorf(gbkAsUtf8(string(buf[0:n]))), false
jobresult.ErrMsg = jobresult.ErrMsg + " " + libs.GbkAsUtf8(string(buf[0:n]))
jobresult.IsOk = false
jobresult.OutMsg = out
return
}
}
return out, "", nil, false
jobresult.IsOk = true
jobresult.OutMsg = out
return
}
return job
}
func (j *Job) Status() int {
return j.status
func RemoteCommandJobByAgentPassword(id int, serverId int, name string, command string, servers *models.TaskServer) *Job {
job := &Job{
Id: id,
Name: name,
ServerType: servers.ConnectionType,
}
job.JobKey = libs.JobKey(id, serverId)
job.RunFunc = func(timeout time.Duration) *JobResult {
return new(JobResult)
}
return job
}
func (j *Job) GetStatus() int {
return j.Status
}
func (j *Job) GetName() string {
return j.name
return j.Name
}
func (j *Job) GetId() int {
return j.id
return j.Id
}
func (j *Job) GetLogId() int64 {
return j.logId
return j.LogId
}
type RpcResult struct {
Status int
Message string
}
func (j *Job) agentRun() (reply *JobResult) {
server, _ := models.TaskServerGetById(j.ServerId)
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", server.ServerIp, server.Port))
reply = new(JobResult)
if err != nil {
logs.Error("Net error:", err)
reply.IsOk = false
reply.ErrMsg = "Net error:" + err.Error()
reply.IsTimeout = false
reply.OutMsg = ""
return reply
}
defer conn.Close()
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
defer client.Close()
reply = new(JobResult)
task := j.Task
err = client.Call("RpcTask.RunTask", task, &reply)
if err != nil {
reply.IsOk = false
reply.ErrMsg = "Net error:" + err.Error()
reply.IsTimeout = false
reply.OutMsg = ""
return reply
}
return
}
func TestServer(server *models.TaskServer) error {
if server.ConnectionType == 0 {
switch server.Type {
case 0:
//密码登录
return libs.RemoteCommandByPassword(server)
case 1:
//密钥登录
return libs.RemoteCommandByKey(server)
default:
return errors.New("未知的登录方式")
}
} else if server.ConnectionType == 1 {
if server.Type == 0 {
//密码登录]
return libs.RemoteCommandByTelnetPassword(server)
} else {
return errors.New("Telnet方式暂不支持密钥登陆")
}
} else if server.ConnectionType == 2 {
return libs.RemoteAgent(server)
}
return errors.New("未知错误")
}
func PollServer(j *Job) bool {
//判断是否是当前执行器执行
TaskServerIdsArr := strings.Split(j.Task.ServerIds, ",")
num := len(TaskServerIdsArr)
if num == 0 {
return false
}
count := GetCounter(strconv.Itoa(j.Task.Id))
index := count % num
pollServerId, _ := strconv.Atoi(TaskServerIdsArr[index])
if j.ServerId != pollServerId {
return false
}
//本地服务器
if pollServerId == 0 {
return true
}
//判断执行器或者服务器是否存活
server, _ := models.TaskServerGetById(pollServerId)
if server.Status != 0 {
return false
}
if err := TestServer(server); err != nil {
server.Status = 1
server.Update()
return false
}
return true
}
func (j *Job) Run() {
if !j.Concurrent && j.status > 0 {
beego.Warn(fmt.Sprintf("任务[%d]上一次执行尚未结束,本次被忽略。", j.jobKey))
//执行策略 轮询
if j.Task.ServerType == 1 {
if !PollServer(j) {
return
} else {
SetCounter(strconv.Itoa(j.Task.Id))
}
}
if !j.Concurrent && j.Status > 0 {
beego.Warn(fmt.Sprintf("任务[%d]上一次执行尚未结束,本次被忽略。", j.JobKey))
return
}
@@ -366,42 +607,50 @@ func (j *Job) Run() {
}()
}
beego.Debug(fmt.Sprintf("开始执行任务: %d", j.jobKey))
beego.Debug(fmt.Sprintf("开始执行任务: %d", j.JobKey))
j.status++
j.Status++
defer func() {
j.status--
j.Status--
}()
t := time.Now()
timeout := time.Duration(time.Hour * 24)
if j.task.Timeout > 0 {
timeout = time.Second * time.Duration(j.task.Timeout)
if j.Task.Timeout > 0 {
timeout = time.Second * time.Duration(j.Task.Timeout)
}
cmdOut, cmdErr, err, isTimeout := j.runFunc(timeout)
var jobResult = new(JobResult)
//anget
if j.ServerType == 2 {
jobResult = j.agentRun()
} else {
jobResult = j.RunFunc(timeout)
}
ut := time.Now().Sub(t) / time.Millisecond
// 插入日志
log := new(models.TaskLog)
log.TaskId = j.id
log.ServerId = j.serverId
log.ServerName = j.serverName
log.Output = cmdOut
log.Error = cmdErr
log.TaskId = j.Id
log.ServerId = j.ServerId
log.ServerName = j.ServerName
log.Output = jobResult.OutMsg
log.Error = jobResult.ErrMsg
log.ProcessTime = int(ut)
log.CreateTime = t.Unix()
if isTimeout {
if jobResult.IsTimeout {
log.Status = models.TASK_TIMEOUT
log.Error = fmt.Sprintf("任务执行超过 %d 秒\n----------------------\n%s\n", int(timeout/time.Second), cmdErr)
} else if err != nil {
log.Error = fmt.Sprintf("任务执行超过 %d 秒\n----------------------\n%s\n", int(timeout/time.Second), jobResult.ErrMsg)
} else if !jobResult.IsOk {
log.Status = models.TASK_ERROR
log.Error = err.Error() + ":" + cmdErr
log.Error = "ERROR:" + jobResult.ErrMsg
}
if log.Status < 0 && j.task.IsNotify == 1 {
if j.task.NotifyUserIds != "0" && j.task.NotifyUserIds != "" {
adminInfo := AllAdminInfo(j.task.NotifyUserIds)
if log.Status < 0 && j.Task.IsNotify == 1 {
if j.Task.NotifyUserIds != "0" && j.Task.NotifyUserIds != "" {
adminInfo := AllAdminInfo(j.Task.NotifyUserIds)
phone := make(map[string]string, 0)
dingtalk := make(map[string]string, 0)
wechat := make(map[string]string, 0)
@@ -432,9 +681,9 @@ func (j *Job) Run() {
title, content, taskOutput, errOutput := "", "", "", ""
notifyTpl, err := models.NotifyTplGetById(j.task.NotifyTplId)
notifyTpl, err := models.NotifyTplGetById(j.Task.NotifyTplId)
if err != nil {
notifyTpl, err := models.NotifyTplGetByTplType(j.task.NotifyType, models.NotifyTplTypeSystem)
notifyTpl, err := models.NotifyTplGetByTplType(j.Task.NotifyType, models.NotifyTplTypeSystem)
if err == nil {
title = notifyTpl.Title
content = notifyTpl.Content
@@ -450,10 +699,10 @@ func (j *Job) Run() {
errOutput = strings.Replace(errOutput, "\"", "\\\"", -1)
if title != "" {
title = strings.Replace(title, "{{TaskId}}", strconv.Itoa(j.task.Id), -1)
title = strings.Replace(title, "{{ServerId}}", strconv.Itoa(j.serverId), -1)
title = strings.Replace(title, "{{TaskName}}", j.task.TaskName, -1)
title = strings.Replace(title, "{{ExecuteCommand}}", j.task.Command, -1)
title = strings.Replace(title, "{{TaskId}}", strconv.Itoa(j.Task.Id), -1)
title = strings.Replace(title, "{{ServerId}}", strconv.Itoa(j.ServerId), -1)
title = strings.Replace(title, "{{TaskName}}", j.Task.TaskName, -1)
title = strings.Replace(title, "{{ExecuteCommand}}", j.Task.Command, -1)
title = strings.Replace(title, "{{ExecuteTime}}", beego.Date(time.Unix(log.CreateTime, 0), "Y-m-d H:i:s"), -1)
title = strings.Replace(title, "{{ProcessTime}}", strconv.FormatFloat(float64(log.ProcessTime)/1000, 'f', 6, 64), -1)
title = strings.Replace(title, "{{ExecuteStatus}}", TextStatus[status], -1)
@@ -462,10 +711,10 @@ func (j *Job) Run() {
}
if content != "" {
content = strings.Replace(content, "{{TaskId}}", strconv.Itoa(j.task.Id), -1)
content = strings.Replace(content, "{{ServerId}}", strconv.Itoa(j.serverId), -1)
content = strings.Replace(content, "{{TaskName}}", j.task.TaskName, -1)
content = strings.Replace(content, "{{ExecuteCommand}}", j.task.Command, -1)
content = strings.Replace(content, "{{TaskId}}", strconv.Itoa(j.Task.Id), -1)
content = strings.Replace(content, "{{ServerId}}", strconv.Itoa(j.ServerId), -1)
content = strings.Replace(content, "{{TaskName}}", j.Task.TaskName, -1)
content = strings.Replace(content, "{{ExecuteCommand}}", j.Task.Command, -1)
content = strings.Replace(content, "{{ExecuteTime}}", beego.Date(time.Unix(log.CreateTime, 0), "Y-m-d H:i:s"), -1)
content = strings.Replace(content, "{{ProcessTime}}", strconv.FormatFloat(float64(log.ProcessTime)/1000, 'f', 6, 64), -1)
content = strings.Replace(content, "{{ExecuteStatus}}", TextStatus[status], -1)
@@ -473,14 +722,14 @@ func (j *Job) Run() {
content = strings.Replace(content, "{{ErrorOutput}}", errOutput, -1)
}
if j.task.NotifyType == 0 && toEmail != "" {
if j.Task.NotifyType == 0 && toEmail != "" {
//邮件
mailtype := "html"
ok := notify.SendToChan(toEmail, title, content, mailtype)
if !ok {
fmt.Println("发送邮件错误", toEmail)
}
} else if j.task.NotifyType == 1 && len(phone) > 0 {
} else if j.Task.NotifyType == 1 && len(phone) > 0 {
//信息
param := make(map[string]string)
err := json.Unmarshal([]byte(content), &param)
@@ -493,7 +742,7 @@ func (j *Job) Run() {
if !ok {
fmt.Println("发送信息错误", phone)
}
} else if j.task.NotifyType == 2 && len(dingtalk) > 0 {
} else if j.Task.NotifyType == 2 && len(dingtalk) > 0 {
//钉钉
param := make(map[string]interface{})
@@ -507,7 +756,7 @@ func (j *Job) Run() {
if !ok {
fmt.Println("发送钉钉错误", dingtalk)
}
} else if j.task.NotifyType == 3 && len(wechat) > 0 {
} else if j.Task.NotifyType == 3 && len(wechat) > 0 {
//微信
param := make(map[string]string)
err := json.Unmarshal([]byte(content), &param)
@@ -524,12 +773,12 @@ func (j *Job) Run() {
}
}
j.logId, _ = models.TaskLogAdd(log)
j.LogId, _ = models.TaskLogAdd(log)
// 更新上次执行时间
j.task.PrevTime = t.Unix()
j.task.ExecuteTimes++
j.task.Update("PrevTime", "ExecuteTimes")
j.Task.PrevTime = t.Unix()
j.Task.ExecuteTimes++
j.Task.Update("PrevTime", "ExecuteTimes")
}
//冗余代码
@@ -572,16 +821,3 @@ func AllAdminInfo(adminIds string) []*adminInfo {
return adminInfos
}
func gbkAsUtf8(str string) string {
srcDecoder := mahonia.NewDecoder("gbk")
desDecoder := mahonia.NewDecoder("utf-8")
resStr := srcDecoder.ConvertString(str)
_, resBytes, _ := desDecoder.Translate([]byte(resStr), true)
return string(resBytes)
}
//任务识别码
func jobKey(taskId, serverId int) int {
return taskId*10000 + serverId
}

15
libs/convert.go Normal file
View File

@@ -0,0 +1,15 @@
/************************************************************
** @Description: convert
** @Author: george hao
** @Date: 2019-06-28 09:34
** @Last Modified by: george hao
** @Last Modified time: 2019-06-28 09:34
*************************************************************/
package libs
import "fmt"
//查看数据类型
func DataType(i interface{}) string {
return fmt.Sprintf("%T", i)
}

View File

@@ -9,10 +9,10 @@ package libs
import (
"github.com/pkg/errors"
"io/ioutil"
"strings"
"net/http"
"io"
"io/ioutil"
"net/http"
"strings"
)
func HttpGet(url string, param map[string]string) (string, error) {

52
libs/ip.go Normal file
View File

@@ -0,0 +1,52 @@
/************************************************************
** @Description: ip
** @Author: george hao
** @Date: 2019-06-27 09:20
** @Last Modified by: george hao
** @Last Modified time: 2019-06-27 09:20
*************************************************************/
package libs
import (
"io/ioutil"
"net"
"net/http"
)
func GetHostIp(IpType int) string {
if IpType == 0 {
return LocalIp()
} else {
return PublicIp()
}
}
func LocalIp() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
}
for _, address := range addrs {
// 检查ip地址判断是否回环地址
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
return ""
}
func PublicIp() string {
resp, err := http.Get("http://myexternalip.com/raw")
if err != nil {
return ""
}
defer resp.Body.Close()
content, _ := ioutil.ReadAll(resp.Body)
//buf := new(bytes.Buffer)
//buf.ReadFrom(resp.Body)
//s := buf.String()
return string(content)
}

150
libs/server.go Normal file
View File

@@ -0,0 +1,150 @@
/************************************************************
** @Description: server
** @Author: george hao
** @Date: 2019-07-02 11:16
** @Last Modified by: george hao
** @Last Modified time: 2019-07-02 11:16
*************************************************************/
package libs
import (
"fmt"
"github.com/george518/PPGo_Job/common"
"github.com/george518/PPGo_Job/models"
"github.com/linxiaozhi/go-telnet"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
"io/ioutil"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"strconv"
"strings"
"time"
)
func RemoteCommandByTelnetPassword(servers *models.TaskServer) error {
addr := fmt.Sprintf("%s:%d", servers.ServerIp, servers.Port)
conn, err := gote.DialTimeout("tcp", addr, time.Second*10)
if err != nil {
return err
}
defer conn.Close()
buf := make([]byte, 4096)
_, err = conn.Read(buf)
if err != nil {
return err
}
_, err = conn.Write([]byte(servers.ServerAccount + "\r\n"))
if err != nil {
return err
}
_, err = conn.Read(buf)
if err != nil {
return err
}
_, err = conn.Write([]byte(servers.Password + "\r\n"))
if err != nil {
return err
}
_, err = conn.Read(buf)
if err != nil {
return err
}
str := GbkAsUtf8(string(buf[:]))
if strings.Contains(str, ">") {
return nil
}
return errors.Errorf("连接失败!")
}
func RemoteCommandByPassword(servers *models.TaskServer) error {
var (
auth []ssh.AuthMethod
addr string
clientConfig *ssh.ClientConfig
)
auth = make([]ssh.AuthMethod, 0)
auth = append(auth, ssh.Password(servers.Password))
clientConfig = &ssh.ClientConfig{
User: servers.ServerAccount,
Auth: auth,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: 5 * time.Second,
}
addr = fmt.Sprintf("%s:%d", servers.ServerIp, servers.Port)
client, err := ssh.Dial("tcp", addr, clientConfig)
if err == nil {
defer client.Close()
}
return err
}
func RemoteCommandByKey(servers *models.TaskServer) error {
key, err := ioutil.ReadFile(servers.PrivateKeySrc)
if err != nil {
return err
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return err
}
addr := fmt.Sprintf("%s:%d", servers.ServerIp, servers.Port)
config := &ssh.ClientConfig{
User: servers.ServerAccount,
Auth: []ssh.AuthMethod{
// Use the PublicKeys method for remote authentication.
ssh.PublicKeys(signer),
},
//HostKeyCallback: ssh.FixedHostKey(hostKey),
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: 5 * time.Second,
}
client, err := ssh.Dial("tcp", addr, config)
if err == nil {
client.Close()
}
return err
}
func RemoteAgent(servers *models.TaskServer) error {
conn, err := net.Dial("tcp", servers.ServerIp+":"+strconv.Itoa(servers.Port))
if err != nil {
return err
}
defer conn.Close()
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
var reply *common.RpcResult
defer client.Close()
ping := "ping"
err = client.Call("RpcTask.HeartBeat", ping, &reply)
if err != nil {
return err
}
if reply.Status == 200 {
return nil
} else {
return fmt.Errorf("链接错误:%v", reply.Message)
}
}

View File

@@ -11,6 +11,7 @@ package libs
import (
"crypto/md5"
"fmt"
"github.com/axgle/mahonia"
"math/rand"
"regexp"
"time"
@@ -67,3 +68,16 @@ func GetRandomString(lens int) string {
}
return string(result)
}
func GbkAsUtf8(str string) string {
srcDecoder := mahonia.NewDecoder("gbk")
desDecoder := mahonia.NewDecoder("utf-8")
resStr := srcDecoder.ConvertString(str)
_, resBytes, _ := desDecoder.Translate([]byte(resStr), true)
return string(resBytes)
}
//任务识别码
func JobKey(taskId, serverId int) int {
return taskId*10000000 + serverId
}

View File

@@ -16,10 +16,12 @@ import (
)
func init() {
//初始化数据模型
var StartTime = time.Now().Unix()
models.Init(StartTime)
jobs.InitJobs()
}
func main() {

View File

@@ -10,8 +10,6 @@ package models
import (
"net/url"
"fmt"
"github.com/astaxie/beego"
"github.com/astaxie/beego/orm"
_ "github.com/go-sql-driver/mysql"
@@ -31,7 +29,6 @@ func Init(startTime int64) {
dbport = "3306"
}
dsn := dbuser + ":" + dbpassword + "@tcp(" + dbhost + ":" + dbport + ")/" + dbname + "?charset=utf8"
fmt.Println(dsn)
if timezone != "" {
dsn = dsn + "&loc=" + url.QueryEscape(timezone)
}

View File

@@ -16,22 +16,22 @@ import (
)
type TaskServer struct {
Id int
GroupId int
Id int
GroupId int
ConnectionType int
ServerName string
ServerAccount string
ServerOuterIp string
ServerIp string
Port int
Password string
PrivateKeySrc string
PublicKeySrc string
Type int
Detail string
CreateTime int64
UpdateTime int64
Status int
ServerName string
ServerAccount string
ServerOuterIp string
ServerIp string
Port int
Password string
PrivateKeySrc string
PublicKeySrc string
Type int
Detail string
CreateTime int64
UpdateTime int64
Status int
}
func (t *TaskServer) TableName() string {
@@ -97,6 +97,20 @@ func TaskServerGetById(id int) (*TaskServer, error) {
return obj, nil
}
func TaskServerForActuator(serverIp string, port int) int {
serverFilters := make([]interface{}, 0)
serverFilters = append(serverFilters, "status__in", []int{0, 1})
serverFilters = append(serverFilters, "server_ip", serverIp)
serverFilters = append(serverFilters, "port", port)
server, _ := TaskServerGetList(1, 1, serverFilters...)
if len(server) == 1 {
return server[0].Id
}
return 0
}
//
func TaskServerGetByIds(ids string) ([]*TaskServer, int64) {

View File

@@ -24,6 +24,7 @@ type Task struct {
Id int
GroupId int
ServerIds string
ServerType int
TaskName string
Description string
CronSpec string

View File

@@ -332,11 +332,14 @@ COMMIT;
BEGIN;
ALTER TABLE `ppgo_job2`.`pp_task` CHANGE COLUMN `server_id` `server_ids` varchar(200) NOT NULL DEFAULT '0' COMMENT '服务器id字符串英文都好隔开';
BEGIN;
COMMIT;
BEGIN;
ALTER TABLE `ppgo_job2`.`pp_task_log` ADD COLUMN `server_id` int(11) NOT NULL DEFAULT '-1' COMMENT '服务器ID-1异常' AFTER `task_id`, CHANGE COLUMN `output` `output` mediumtext NOT NULL COMMENT '任务输出' AFTER `server_id`, CHANGE COLUMN `error` `error` text NOT NULL COMMENT '错误信息' AFTER `output`, CHANGE COLUMN `status` `status` tinyint(4) NOT NULL COMMENT '状态' AFTER `error`, CHANGE COLUMN `process_time` `process_time` int(11) NOT NULL DEFAULT '0' COMMENT '消耗时间/毫秒' AFTER `status`, CHANGE COLUMN `create_time` `create_time` int(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间' AFTER `process_time`;
ALTER TABLE `ppgo_job2`.`pp_task_log` ADD COLUMN `server_name` varchar(60) NOT NULL DEFAULT '\"\"' COMMENT '服务器名称' AFTER `server_id`, CHANGE COLUMN `output` `output` mediumtext NOT NULL COMMENT '任务输出' AFTER `server_name`, CHANGE COLUMN `error` `error` text NOT NULL COMMENT '错误信息' AFTER `output`, CHANGE COLUMN `status` `status` tinyint(4) NOT NULL COMMENT '状态' AFTER `error`, CHANGE COLUMN `process_time` `process_time` int(11) NOT NULL DEFAULT '0' COMMENT '消耗时间/毫秒' AFTER `status`, CHANGE COLUMN `create_time` `create_time` int(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间' AFTER `process_time`;
BEGIN;
COMMIT;
BEGIN;
ALTER TABLE `ppgo_job2`.`pp_task` CHANGE COLUMN `is_notify` ` is_notify` tinyint(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '0-不通知1-通知', ADD COLUMN `server_type` tinyint(1) UNSIGNED NOT NULL DEFAULT '1' COMMENT '执行策略0-同时执行1-轮询执行' AFTER `update_id`;
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,946 @@
'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
/**
* name: formSelects
* 基于Layui Select多选
* version: 3.0.9.0607
* https://faysunshine.com/layui/template/formSelects-v3/formSelects-v3.js
*/
(function (layui, window, factory) {
if ((typeof exports === 'undefined' ? 'undefined' : _typeof(exports)) === 'object') {
// 支持 CommonJS
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
// 支持 AMD
define(factory);
} else if (window.layui && layui.define) {
//layui加载
layui.define(['jquery', 'form'], function (exports) {
exports('formSelects', factory());
});
} else {
window.formSelects = factory();
}
})(layui, window, function () {
//针对IE的一些处理
if (window.Map == undefined) {
var _Map = function _Map() {
this.value = {};
};
_Map.prototype.set = function (key, val) {
this.value[key] = val;
};
_Map.prototype.get = function (key) {
return this.value[key];
};
_Map.prototype.has = function (key) {
return this.value.hasOwnProperty(key);
};
_Map.prototype.delete = function (key) {
delete this.value[key];
};
window.Map = _Map;
}
var $ = layui.jquery || $,
form = layui.form,
select3 = {
value: function value(name) {
var type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'all';
var vals = arguments[2];
if (Array.isArray(type)) {
vals = type;
type = 'all';
}
if (name && vals && Array.isArray(vals)) {
var options = commons.data.confs.get(name);
if (options) {
var dl = commons.methods.getDiv(options).find('dl');
if (!options.repeat) {
vals = new Set(vals);
}
var on = options.on;
options.on = null;
commons.methods.removeAll(options);
options.delete = false;
vals.forEach(function (val) {
dl.find('dd:not(.layui-disabled)[lay-value=\'' + val + '\']').click();
});
options.on = on;
}
return;
}
var arr = commons.data.values.get(name);
if (!arr) {
return vals;
}
if (type == 'val') {
return arr.map(function (val) {
return val.val;
});
}
if (type == 'valStr') {
return arr.map(function (val) {
return val.val;
}).join(',');
}
if (type == 'name') {
return arr.map(function (val) {
return val.name;
});
}
if (type == 'nameStr') {
return arr.map(function (val) {
return val.name;
}).join(',');
}
return arr;
},
render: function render(options) {
if (options) {
if (commons.data.confs.get(options.name)) {
options = commons.methods.cloneOptions(options, commons.data.confs.get(options.name));
commons.methods.init(options);
} else {
var dom = commons.methods.getDom(options, true);
if (dom.length) {
var hisOptions = commons.methods.cloneOptions(commons.methods.getOptions(dom), commons.data.DEFAULT_OPTIONS);
options = commons.methods.cloneOptions(options, hisOptions);
commons.methods.init(options);
}
}
} else {
commons.methods.autoInit();
}
},
delete: function _delete(name, abs) {
if (name && commons.data.confs.get(name)) {
var dom = commons.methods.getDom({
name: name
});
if (dom.parent().hasClass(commons.data.pclass)) {
if (abs) {
dom.removeAttr(commons.data.name);
}
dom.removeAttr('style');
dom.parent()[0].outerHTML = dom[0].outerHTML;
}
commons.data.confs.delete(name);
for (var item in commons.data.temps) {
commons.data.temps[item].delete(name);
}
commons.data.values.delete(name);
}
},
style: function style(name, colors) {
if (name) {
if (!colors) {
commons.methods.loadCss(name, null);
} else if (Array.isArray(colors)) {
commons.methods.loadCss(name, colors);
} else {
var arr = [colors.labelBgColor, colors.labelColor, colors.labelIconBgColor, colors.labelIconColor, colors.labelLabelBorderColor, colors.thisBgColor, colors.thisColor];
commons.methods.loadCss(name, arr);
}
}
}
},
commons = {
data: {
name: 'xm-select',
pclass: 'xm-select-parent',
vclass: 'xm-select-validate',
DEFAULT_OPTIONS: {
name: null, //xm-select="xxx"
type: 1, //显示模式, 1:layui-this, 2:checkbox, 3:icon
icon: {
class: 'layui-icon-ok',
text: '&#xe618;'
},
max: null,
maxTips: null,
init: null, //初始化的选择值,
on: null, //select值发生变化
data: null
},
DEFAULT_RENDER: {
arr: null,
name: 'name',
val: 'val',
selected: 'selected',
disabled: 'disabled'
},
confs: new Map(),
temps: {
dom: new Map(),
div: new Map()
},
resize: new Map(),
values: new Map(),
times: new Map()
},
methods: {
init: function init(options, clone) {
if (!clone) {
options = commons.methods.cloneOptions(options);
}
//原始dom添加一个filter
var _ref = ['xm-' + options.name, commons.methods.getDom(options, true)],
filter = _ref[0],
dom = _ref[1];
if (dom.next().hasClass('layui-form-select')) {
dom.next().remove();
}
if (options.data && options.data.arr) {
var os = $.extend({}, commons.data.DEFAULT_RENDER, options.data);
var html = '<option value=""></option>';
for (var i in os.arr) {
var db = os.arr[i];
if (db.arr && Array.isArray(db.arr)) {
html += '<optgroup label="' + db.name + '">';
for (var j in db.arr) {
var gdb = db.arr[j];
html += '<option value="' + gdb[os.val] + '" ' + (gdb[os.selected] ? 'selected="selected"' : '') + ' ' + (gdb[os.disabled] ? 'disabled="disabled"' : '') + '>' + gdb[os.name] + '</option>';
}
html += '</optgroup>';
} else {
html += '<option value="' + db[os.val] + '" ' + (db[os.selected] ? 'selected="selected"' : '') + ' ' + (db[os.disabled] ? 'disabled="disabled"' : '') + '>' + db[os.name] + '</option>';
}
}
dom.html(html);
}
//判断dom中是否包含了空的option, 如果不包含, 添加
if (!dom.find('option[value=""]').length) {
$('<option value=""></option>').insertBefore(dom.find('option:first'));
}
if (dom.parent().hasClass(commons.data.pclass)) {
dom.parent().attr('lay-filter', filter).addClass('layui-form');
} else {
dom.wrap('<div class="layui-form ' + commons.data.pclass + '" lay-filter="' + filter + '"></div>');
}
dom.attr('lay-filter', filter);
options.type ? dom.attr('xm-select-type', options.type) : dom.removeAttr('xm-select-type');
options.max ? dom.attr('xm-select-max', options.max) : dom.removeAttr('xm-select-max');
commons.methods.formRender('select', filter, true);
//1.去掉layui的原始渲染
commons.methods.getDom(options).next().addClass(commons.data.vclass);
//2.
commons.data.confs.set(options.name, options);
for (var item in commons.data.temps) {
commons.data.temps[item].delete(options.name);
}
commons.data.values.delete(options.name);
//3.渲染input
commons.methods.overrideInput(options);
commons.methods.typeInit(options, filter);
//4.初始化init
var vals = commons.methods.getInitVal(options);
vals.forEach(function (val) {
commons.methods.valHandler(options, val, true);
});
commons.methods.showPlaceholder(options);
commons.methods.retop(options);
commons.methods.removeDefaultClass(options);
commons.methods.on(options, filter);
},
addLabel: function addLabel(options, vals) {
var ipt = commons.methods.getIpt(options);
ipt.find('.xm-select-empty').remove();
vals.forEach(function (val) {
var tips = 'fsw="' + options.name + '"';
var _ref2 = [$('<span ' + tips + ' value=\'' + val.val + '\'><font ' + tips + '>' + val.name + '</font></span>'), $('<i ' + tips + ' class="layui-icon">&#x1006;</i>')],
$label = _ref2[0],
$close = _ref2[1];
$label.append($close);
ipt.append($label);
});
},
delLabel: function delLabel(options, vals) {
var ipt = commons.methods.getIpt(options);
vals.forEach(function (val) {
ipt.find('span[value=\'' + val.val + '\']:first').remove();
});
},
showPlaceholder: function showPlaceholder(options) {
var vals = commons.methods.getValues(options);
if (!vals.length) {
var _ref3 = ['fsw="' + options.name + '"', commons.methods.getIpt(options)],
tips = _ref3[0],
ipt = _ref3[1];
if (!ipt.find('.xm-select-empty').length) {
var _tips = options.tips ? options.tips : ipt.prev().attr('placeholder');
var cls = 'fsw="' + options.name + '"';
var span = $('<span ' + cls + ' class="xm-select-empty">' + _tips + '</span>');
ipt.append(span);
}
}
},
valHandler: function valHandler(options, val, isAdd, isShow) {
var vals = commons.methods.getValues(options);
var dd = commons.methods.getDiv(options).find('dl dd[lay-value=\'' + val.val + '\']');
if (isAdd) {
if (!options.max || options.max && vals.length < options.max) {
vals.push(val);
commons.methods.addLabel(options, [val]);
commons.methods.typeHandler(options, dd, isAdd);
} else {
commons.methods.maxTips(options, val);
commons.methods.typeHandler(options, dd, false);
}
} else {
if (!dd.hasClass('layui-disabled')) {
commons.methods.remove(vals, val);
commons.methods.delLabel(options, [val]);
if (commons.methods.indexOf(vals, val) == -1) {
commons.methods.typeHandler(options, dd, isAdd);
}
}
}
commons.methods.retop(options);
},
typeInit: function typeInit(options, filter) {
var div = commons.methods.getDiv(options);
if (options.type == 2) {
//checkbox
div.find('dl dd:not(.layui-select-tips)').each(function (index, target) {
var $target = $(target);
var text = $target.text();
var dis = $target.hasClass('layui-disabled') ? 'disabled' : '';
$target.text('');
$target.append('\n\t\t\t\t\t\t\t\t<span lay-filter="' + filter + '">\n\t\t\t\t\t\t\t\t\t<input type="checkbox" title="' + text + '" lay-skin="primary" ' + dis + '> \t\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t');
});
form.render('checkbox', filter);
div.find('dl dd .layui-form-checkbox').css('margin-top', '1px');
} else {
div.find('dl dd:not(.layui-select-tips)').each(function (index, target) {
$(target).css({
margin: '1px 0'
});
});
}
},
typeHandler: function typeHandler(options, dd, isAdd) {
if (options.type == 3) {
//对勾
if (isAdd) {
var span = $('\n\t\t\t\t\t\t\t\t<span><i class="layui-icon ' + options.icon.class + '">' + options.icon.text + '</i></span>\t\n\t\t\t\t\t\t\t');
dd.append(span);
} else {
dd.find('span').remove();
}
} else if (options.type == 2) {
//checkbox
if (isAdd) {
dd.find('.layui-form-checkbox').addClass('layui-form-checked');
} else {
dd.find('.layui-form-checkbox').removeClass('layui-form-checked');
}
}
isAdd ? dd.addClass(commons.data.name + '-this') : dd.removeClass(commons.data.name + '-this');
dd.removeClass('layui-this');
commons.methods.showPlaceholder(options);
},
on: function on(options, filter) {
form.on('select(' + filter + ')', function (data) {
if (options.radio) {
commons.methods.getDiv(options).find('dl').removeClass('xm-select-show').addClass('xm-select-hidn').css('display', 'none');
var selected = void 0,
val = void 0;
if (data.value) {
val = {
name: commons.methods.getDom(options).find('option[value=\'' + data.value + '\']').text(),
val: data.value
};
selected = commons.methods.indexOf(commons.methods.getValues(options), val) == -1;
if (selected) {
var hisVal = select3.value(options.name)[0];
if (hisVal) {
commons.methods.valHandler(options, hisVal, false, false);
commons.methods.setValues(options, []);
}
}
commons.methods.valHandler(options, val, selected, false);
} else {
selected = false;
val = select3.value(options.name)[0];
if (val) {
commons.methods.valHandler(options, val, selected, false);
commons.methods.setValues(options, []);
}
}
if (val && options.on && options.on instanceof Function) {
options.on(data, select3.value(options.name), val, selected);
}
return;
}
if (commons.methods.getDiv(options).hasClass('layui-form-selectup') || commons.methods.getDiv(options).find('dl').css('top').indexOf('-') == 0) {
setTimeout(function () {
commons.methods.getDiv(options).addClass('layui-form-selectup');
}, 50);
}
if (options.repeat) {
if (data.value) {
var ipt = commons.methods.getIpt(options);
if (options.delete) {
var arr = select3.value(options.name);
var _val = arr[commons.methods.indexOf(arr, data.value)];
if (_val) {
commons.methods.valHandler(options, _val, false);
ipt.parent().next().find('dd[lay-value=\'' + data.value + '\'] .layui-form-checkbox > i').html('&#xe605;');
if (options.on && options.on instanceof Function) {
options.on(data, select3.value(options.name), _val, false);
}
}
options.delete = false;
return;
} else {
var vals = commons.methods.getValues(options);
var _val2 = {
name: commons.methods.getDom(options).find('option[value=\'' + data.value + '\']').text(),
val: data.value
};
commons.methods.valHandler(options, _val2, true);
if (options.on && options.on instanceof Function) {
options.on(data, select3.value(options.name), _val2, true);
}
}
setTimeout(function () {
commons.methods.getDiv(options).addClass('layui-form-selected').find('dl dd.layui-this').removeClass('layui-this');
}, 3);
return;
}
}
if (data.value) {
var _val3 = {
name: commons.methods.getDom(options).find('option[value=\'' + data.value + '\']').text(),
val: data.value
};
var _selected = commons.methods.indexOf(commons.methods.getValues(options), _val3) == -1;
commons.methods.valHandler(options, _val3, _selected, true);
if (options.on && options.on instanceof Function) {
options.on(data, select3.value(options.name), _val3, _selected);
}
setTimeout(function () {
commons.methods.getDiv(options).addClass('layui-form-selected').find('dl dd.layui-this').removeClass('layui-this');
}, 3);
} else {
if (commons.methods.getDom(options).attr('xm-select-search') == undefined) {
commons.methods.removeAll(options);
setTimeout(function () {
commons.methods.getDiv(options).addClass('layui-form-selected');
}, 3);
}
}
});
},
maxTips: function maxTips(options, val) {
if (options.maxTips && options.maxTips instanceof Function) {
options.maxTips(select3.value(options.name), val, options.max);
return;
}
var ipt = commons.methods.getIpt(options);
if (ipt.parents('.layui-form-item[pane]').length) {
ipt = ipt.parents('.layui-form-item[pane]');
}
ipt.css('border-color', 'red');
setTimeout(function () {
ipt.css('border-color', '#e6e6e6');
}, 300);
},
indexOf: function indexOf(arr, val) {
for (var i = 0; i < arr.length; i++) {
if (arr[i].val == val || arr[i].val == (val ? val.val : val) || arr[i] == val || JSON.stringify(arr[i]) == JSON.stringify(val)) {
return i;
}
}
return -1;
},
remove: function remove(arr, val) {
var index = commons.methods.indexOf(arr, val ? val.val : val);
if (index > -1) {
arr.splice(index, 1);
return true;
}
return false;
},
removeAll: function removeAll(options) {
var div = commons.methods.getDiv(options);
var vals = div.find('.xm-select > span');
vals.each(function (index, item) {
options.delete = true;
div.find('dl dd.xm-select-this:not(.layui-disabled)[lay-value=\'' + item.getAttribute('value') + '\']').click();
});
return vals.length;
},
removeDefaultClass: function removeDefaultClass(options) {
var _ref4 = [commons.methods.getDom(options), commons.methods.getDiv(options)],
dom = _ref4[0],
div = _ref4[1];
div.find('dl').addClass('xm-select-hidn');
div.find('dl dd.layui-this').removeClass('layui-this');
div.find('dl').append('<dt style="display:none;">x<dt/>');
var text = options.radio ? '请选择' : '清空已选择' + (options.max ? '. \u5F53\u524D\u914D\u7F6E: \u6700\u591A\u9009\u62E9 ' + options.max + ' \u4E2A' : '');
var html = '<span>' + text + '</span>';
var that = div.find('dl dd.layui-select-tips');
if (dom.attr('xm-select-search') != undefined) {
var icon = '&#xe640;';
html = '<input class="layui-input xm-select-input-search" placeholder="\u5173\u952E\u5B57\u641C\u7D22"><span><i title="' + text + '" xm-select-clear="' + options.name + '" class="layui-icon">' + icon + '</i></span>';
that.addClass('xm-select-dd-search');
that.off('click');
}
commons.methods.handlerDataTable(dom, '38px');
that.html(html);
},
handlerDataTable: function handlerDataTable(dom, height) {
if (dom.length == undefined) {
dom = $(dom);
}
if (dom.parents('div.layui-table-cell').length) {
var index = dom.parents('tr').attr('data-index');
dom.parents('.layui-table-box').find('tbody tr[data-index=\'' + index + '\'] div.layui-table-cell').css({
height: height,
lineHeight: height,
overflow: 'unset'
});
}
},
overrideInput: function overrideInput(options) {
var _ref5 = [commons.methods.getDiv(options), commons.methods.getDom(options)],
_div = _ref5[0],
_dom = _ref5[1];
var _ref6 = [_div.find('.layui-select-title input:first'), $('<div class="layui-input ' + commons.data.name + '"></div>')],
$input = _ref6[0],
$orinput = _ref6[1];
$orinput.insertAfter($input);
if ($input.parents('.layui-form-pane').length && $input.parents('.layui-form-item[pane]').length) {
$orinput.css('border', 'none');
}
if (options.height) {
$orinput.addClass('.xm-select-height').css('height', options.height);
}
if (_dom.attr('disabled') != undefined) {
$orinput.append($('<div style="\n\t\t\t\t\t\t\t width: 100%;\n\t\t\t\t\t\t\t height: 100%;\n\t\t\t\t\t\t\t display: block;\n\t\t\t\t\t\t\t position: absolute;\n\t\t\t\t\t\t\t left: 0;\n\t\t\t\t\t\t\t top: 0;\n\t\t\t\t\t\t\t"></div>').addClass('layui-disabled'));
}
},
retop: function retop(options) {
var div = commons.methods.getIpt(options);
if (div.length) {
var dl = div.parent('.layui-select-title').next();
var up = dl.parent().hasClass('layui-form-selectup') || dl.css('top').indexOf('-') == 0;
var time = commons.data.times.get(options.name);
if (time) {
if (Date.now() - time.time < 100 || options.delete) {
up = time.up;
options.delete = false;
} else {
time.up = up;
}
time.time = Date.now();
} else {
commons.data.times.set(options.name, {
time: Date.now(),
up: up
});
}
if (up) {
div.parent('.layui-select-title').next().css({
top: 'auto',
bottom: div[0].offsetTop + div.height() + 14 + 'px'
});
} else {
div.parent('.layui-select-title').next().css({
top: div[0].offsetTop + div.height() + 14 + 'px',
bottom: 'auto'
});
}
}
},
getElementTop: function getElementTop(element) {
var actualTop = element.offsetTop;
var current = element.offsetParent;
while (current !== null) {
actualTop += current.offsetTop;
current = current.offsetParent;
}
return actualTop;
},
getDom: function getDom(options, isNew) {
var _ref7 = [commons.data.temps.dom, options.name],
_dom = _ref7[0],
_name = _ref7[1];
if (isNew) {
_dom.set(_name, $('select[' + commons.data.name + '=\'' + _name + '\']'));
}
if (!_dom.has(_name)) {
_dom.set(_name, $('select[' + commons.data.name + '=\'' + _name + '\']'));
}
return _dom.get(_name);
},
getDiv: function getDiv(options) {
var _ref8 = [commons.data.temps.div, options.name],
_div = _ref8[0],
_name = _ref8[1];
if (!_div.has(_name)) {
_div.set(_name, $('select[' + commons.data.name + '=\'' + _name + '\']').next());
}
return _div.get(_name);
},
getIpt: function getIpt(options) {
return commons.methods.getDiv(options).find('div .' + commons.data.name);
},
getInitVal: function getInitVal(options) {
var _dom = commons.methods.getDom(options);
var vals = options.init ? options.init : _dom.find('option[selected]').map(function (index, target) {
return $(target).attr('value');
}).toArray();
var result = void 0;
if (options.radio) {
var one = vals.map(function (val) {
var opt = _dom.find('option[value=\'' + val + '\']');
return {
name: opt.attr('disabled') != undefined ? null : opt.text(),
val: val + ""
};
}).filter(function (val) {
return val.name && val.val;
})[0];
result = one ? [one] : [];
} else {
result = vals.map(function (val) {
return {
name: _dom.find('option[value=\'' + val + '\']').text(),
val: val + ""
};
}).filter(function (val) {
return val.name && val.val;
});
}
return result;
},
getValues: function getValues(options) {
var _ref9 = [commons.data.values, options.name],
_arr = _ref9[0],
_name = _ref9[1];
if (!_arr.has(_name)) {
_arr.set(_name, []);
}
return _arr.get(_name);
},
setValues: function setValues(options, vals) {
var _ref10 = [commons.data.values, options.name],
_arr = _ref10[0],
_name = _ref10[1];
_arr.set(_name, vals);
},
getOptions: function getOptions(sel) {
return {
name: sel.attr('' + commons.data.name),
type: sel.attr(commons.data.name + '-type'),
max: sel.attr(commons.data.name + '-max'),
icon: sel.attr(commons.data.name + '-icon'),
tips: sel.attr(commons.data.name + '-placeholder'),
height: sel.attr(commons.data.name + '-height'),
radio: sel.attr(commons.data.name + '-radio') != undefined,
repeat: sel.attr(commons.data.name + '-repeat') != undefined
};
},
cloneOptions: function cloneOptions(options, hisOptions) {
if (!hisOptions) {
hisOptions = commons.data.DEFAULT_OPTIONS;
}
if (options.icon && typeof options.icon == 'string') {
var icon = options.icon;
if (icon.indexOf('layui') == 0) {
options.icon = {
class: icon,
text: ''
};
} else {
options.icon = {
class: '',
text: icon
};
}
} else {
var v = layui.v.split('.');
if (v[0] < 2 || v[1] <= 2) {
options.icon = {
class: '',
text: hisOptions.icon.text
};
} else {
options.icon = {
class: hisOptions.icon.class,
text: ''
};
}
}
return $.extend({}, hisOptions, options);
},
autoInit: function autoInit(repeat) {
$('select[' + commons.data.name + ']').each(function (index, target) {
var sel = $(target);
sel.css('display', 'none');
var options = commons.methods.getOptions(sel);
if (!repeat) {
var hisOptions = commons.data.confs.get(options.name);
options.init = select3.value(options.name, 'val');
commons.methods.init(commons.methods.cloneOptions(options, hisOptions), true);
} else {
commons.methods.init(options);
}
});
},
listenCloseHandler: function listenCloseHandler(e, that, block) {
if (block.length) {
var _ref11 = [block.find('.layui-form-select'), block.find('dl')],
div = _ref11[0],
dl = _ref11[1];
var name = block.find('select').attr('xm-select');
if (that.hasClass('layui-disabled')) {
return;
}
if (that.attr('xm-select-clear') != undefined) {
if (that.is('i')) {
var options = commons.data.confs.get($(e.target).attr('xm-select-clear'));
if (options) {
var length = commons.methods.removeAll(options);
if (options.radio && length) {
return;
}
}
div.addClass('layui-form-selected');
dl.find('.xm-select-input-search').focus();
return;
}
}
if (that.hasClass('xm-select-dd-search') || that.hasClass('xm-select-input-search')) {
div.addClass('layui-form-selected');
dl.find('.xm-select-input-search').focus();
return;
}
$('.xm-select-parent > select:not([xm-select=\'' + name + '\']) + div > dl').removeClass('xm-select-show').addClass('xm-select-hidn').css('display', 'none');
if (that.attr('fsw') != undefined) {
if (that.is('i')) {
var _ref12 = [$(e.target).parents('.layui-form-select').prev(), $(e.target).parent()],
sel = _ref12[0],
span = _ref12[1];
var val = {
name: span.find('font').text(),
val: span.attr('value')
};
var _options = commons.methods.getOptions(sel);
_options = commons.data.confs.get(_options.name);
_options.delete = true;
commons.methods.getDiv(_options).find('dl dd[lay-value=\'' + val.val + '\']').click();
setTimeout(function () {
if (dl.hasClass('xm-select-show')) {
div.addClass('layui-form-selected');
dl.find('.xm-select-input-search').focus();
} else {
div.removeClass('layui-form-selected');
}
}, 3);
return;
}
}
if (block.find('select').attr('xm-select-radio') != undefined) {
setTimeout(function () {
if (dl.hasClass('xm-select-show')) {
div.removeClass('layui-form-selected');
dl.removeClass('xm-select-show').addClass('xm-select-hidn').css('display', 'none');
} else {
div.addClass('layui-form-selected');
dl.removeClass('xm-select-hidn').addClass('xm-select-show').css('display', 'block');
dl.find('.xm-select-input-search').focus();
}
}, 3);
return;
}
if (dl.hasClass('xm-select-show')) {
if (that.hasClass('xm-select') || that.hasClass('xm-select-empty')) {
dl.removeClass('xm-select-show').addClass('xm-select-hidn').css('display', 'none');
div.removeClass('layui-form-selected');
} else {
div.addClass('layui-form-selected');
dl.find('.xm-select-input-search').focus();
}
return;
}
dl.removeClass('xm-select-hidn').addClass('xm-select-show').css('display', 'block');
div.addClass('layui-form-selected');
dl.find('.xm-select-input-search').focus();
} else {
$('.xm-select-parent > .layui-form-select > dl.xm-select-show').removeClass('xm-select-show').addClass('xm-select-hidn').css('display', 'none');
}
},
listenClose: function listenClose() {
$(document).on('click', function (e) {
var that = $(e.target);
var block = that.parents('.xm-select-parent');
setTimeout(function () {
commons.methods.listenCloseHandler(e, that, block);
commons.methods.retop({ name: block.find('select[xm-select]').attr('xm-select') });
$('.xm-select-parent input.xm-select-input-search').each(function (index, item) {
var that = $(item);
if (that.parents('dl').hasClass('xm-select-hidn')) {
that.val('');
that.parents('dl').find('.layui-hide').removeClass('layui-hide');
that.parents('dl').find('p').remove();
}
});
}, 10);
});
$(document).on({
'input propertychange': function inputPropertychange(e) {
var that = $(e.target);
var dl = that.parents('dl');
that.parents('dl').find('p').remove();
dl.find('.layui-hide').removeClass('layui-hide');
dl.find('dd:not(.layui-select-tips):not(:contains(\'' + that.val() + '\'))').addClass('layui-hide');
if (!dl.find('dd:not(.layui-select-tips):not(.layui-hide)').length) {
dl.append('<p class="xm-select-search-empty">\u65E0\u5339\u914D\u9879</p>');
}
dl.find('dt').each(function (index, item) {
if (!$(item).nextUntil('dt', ':not(.layui-hide)').length) {
$(item).addClass('layui-hide');
}
});
}
}, '.xm-select-parent input.xm-select-input-search');
},
formRender: null,
rewriteRender: function rewriteRender() {
commons.methods.formRender = form.render;
form.render = function (type, filter, repeat) {
commons.methods.formRender(type, filter);
if (filter) {
var sel = $('[lay-filter=' + filter + ']').find('select[' + commons.data.name + ']');
if (sel.length) {
if (repeat) {
commons.methods.init(commons.methods.getOptions(sel));
}
}
return;
}
commons.methods.autoInit(repeat);
};
},
loadCss: function loadCss(skin, cs) {
if (skin) {
if (skin == 'default') {
return;
}
var sn = void 0;
if ($('style[skin]').length) {
sn = $('style[skin]');
} else {
sn = $('<style ' + skin + ' type="text/css"></style>');
}
var df = styles.colors[skin] ? styles.colors[skin] : styles.colors.default;
var colors = $.extend([], df, cs);
sn.html(styles.cssColor(skin, colors[0], colors[1], colors[2], colors[3], colors[4], colors[5], colors[6]));
sn.insertAfter($('head style:last'));
} else {
$('<style ' + commons.data.name + ' type="text/css">' + styles.css() + '</style>').insertBefore($('head *:first'));
var html = '';
for (var name in styles.colors) {
var _colors = styles.colors[name];
if (_colors) {
_colors = $.extend([], styles.colors.default, _colors);
html += '<style ' + name + ' type="text/css">' + styles.cssColor(name, _colors[0], _colors[1], _colors[2], _colors[3], _colors[4], _colors[5], _colors[6]) + '</style>';
}
}
if (html) {
$(html).insertAfter($('head style[' + commons.data.name + ']'));
}
}
},
loadLastStyle: function loadLastStyle() {
$('head').append('<style>' + styles.renderCss() + '</style>');
},
resize: function resize(selector, fun) {
var id = commons.data.resize.get(selector);
if (id != undefined) {
clearInterval(id);
}
if (fun && fun instanceof Function) {
var hisize = new Map();
id = setInterval(function (e) {
$(selector).each(function (index, item) {
var thisize = [item.clientHeight, item.clientWidth];
if (hisize.get(item)) {
var his = hisize.get(item);
if (his[0] != thisize[0] || his[1] != thisize[1]) {
fun(item);
}
}
hisize.set(item, thisize);
});
}, 250);
commons.data.resize.set(selector, id);
}
},
run: function run() {
commons.methods.rewriteRender();
commons.methods.listenClose();
commons.methods.loadCss();
commons.methods.loadLastStyle();
commons.methods.autoInit();
commons.methods.resize('.xm-select:not(.xm-select-height)', function (dom) {
commons.methods.handlerDataTable(dom, dom.clientHeight + 'px');
});
}
}
},
styles = {
colors: { //name, spanBgColor, spanColor, iBgColor, iColor, borderColor, tBgColor, tColor
default: ['#F0F2F5', '#909399', '#C0C4CC', '#FFFFFF', '#F0F2F5', '#5FB878', '#FFFFFF'],
primary: ['#009688', '#FFFFFF', '#009688', '#FFFFFF', '#009688', '#009688', '#FFFFFF'],
normal: ['#1E9FFF', '#FFFFFF', '#1E9FFF', '#FFFFFF', '#1E9FFF', '#1E9FFF', '#FFFFFF'],
warm: ['#FFB800', '#FFFFFF', '#FFB800', '#FFFFFF', '#FFB800', '#FFB800', '#FFFFFF'],
danger: ['#FF5722', '#FFFFFF', '#FF5722', '#FFFFFF', '#FF5722', '#FF5722', '#FFFFFF']
},
css: function css() {
return '\n\t\t\t\t\tselect[xm-select] + div .layui-select-title > input{display: none!important;}\n\t\t\t\t\tselect[xm-select] + div .layui-select-title > div.xm-select{\n\t\t\t\t\t\tline-height: normal;\n\t\t\t\t\t\theight: auto;\n\t\t\t\t\t\tpadding: 4px 10px;\n\t\t\t\t\t\toverflow: hidden;\n\t\t\t\t\t\tmin-height: 38px;\n\t\t\t\t\t\tleft: 0px;\n\t\t\t\t\t\tz-index: 99;\n\t\t\t\t\t\tposition: relative;\n\t\t\t\t\t\tbackground: none;\n\t\t\t\t\t\tpadding-right: 20px;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select] + div .layui-select-title > div.xm-select > span:not(.xm-select-empty){\n\t\t\t\t\t\tpadding: 2px 5px;\n\t\t\t\t\t\tbackground: #f0f2f5;\n\t\t\t\t\t\tborder-radius: 2px;\n\t\t\t\t\t\tcolor: #909399;\n\t\t\t\t\t\tdisplay: block;\n\t\t\t\t\t\tline-height: 18px;\n\t\t\t\t\t\theight: 18px;\n\t\t\t\t\t\tmargin: 2px 5px 2px 0px;\n\t\t\t\t\t\tfloat: left;\n\t\t\t\t\t\tcursor: initial;\n\t\t\t\t\t\tuser-select: none;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select] + div .layui-select-title > div.xm-select > span:not(.xm-select-empty) i{\n\t\t\t\t\t\tbackground-color: #c0c4cc;\n\t\t\t\t\t\tcolor: #ffffff;\n\t\t\t\t\t\tmargin-left: 8px;\n\t\t\t\t\t\tborder-radius: 20px;\n\t\t\t\t\t\tfont-size: 12px;\n\t\t\t\t\t\tcursor: initial;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select] + div .layui-select-title > div.xm-select > span:not(.xm-select-empty) i:hover{\n\t\t\t\t\t\tbackground-color: #909399;\n\t\t\t\t\t\tcursor: pointer;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select] + div .layui-select-title > div.xm-select > span.xm-select-empty{\n\t\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\t\theight: 28px;\n\t\t\t\t\t\tline-height: 28px;\n\t\t\t\t\t\tcolor: #999999\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select] + div dl dd.xm-select-dd-search{\n\t\t\t\t\t\tbackground-color: #FFFFFF;\n\t\t\t\t\t\tpadding-bottom: 5px;\n\t\t\t\t\t\tmargin-right: 30px;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select] + div dl dd.xm-select-dd-search > input{\n\t\t\t\t\t\tcursor: text;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select] + div dl dd.xm-select-dd-search > span{\n\t\t\t\t\t\tposition: absolute;\n\t\t\t\t\t right: 8px;\n\t\t\t\t\t top: 5px;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select] + div dl dd.xm-select-dd-search > span > i{\n\t\t\t\t\t\tfont-size: 22px;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select] + div dl dd.xm-select-dd-search > span > i:hover{\n\t\t\t\t\t\tcolor: red;\n\t\t\t\t\t\topacity:.8;\n\t\t\t\t\t\tfilter:alpha(opacity=80);\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select] + div dl p.xm-select-search-empty{\n\t\t\t\t\t\tmargin: 10px 0;\n\t\t\t\t\t text-align: center;\n\t\t\t\t\t color: #999;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-type=\'1\'] + div dl dd.xm-select-this,select[xm-select]:not([xm-select-type]) + div dl dd.xm-select-this{\n\t\t\t\t\t\tbackground-color: #5FB878;\n\t\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-type=\'1\'] + div dl dd.layui-disabled,select[xm-select]:not([xm-select-type]) + div dl dd.layui-disabled{\n\t\t\t\t\t\tbackground-color: #FFFFFF;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-type=\'2\'] + div dl dd.layui-disabled i{\n\t\t\t\t\t\tborder-color: #C2C2C2;\n\t\t\t\t\t\tcolor: #FFFFFF;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-type=\'2\'] + div dl dd.xm-select-this.layui-disabled i{\n\t\t\t\t\t\tbackground-color: #D2D2D2;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-type=\'2\'] + div dl dd.xm-select-this:not(.layui-disabled) i{\n\t\t\t\t\t\tbackground-color: #5FB878;\n\t\t\t\t\t\tborder-color: #5FB878;\n\t\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-type=\'3\'] + div dl dd.xm-select-this:not(.layui-disabled){\n\t\t\t\t\t\tcolor: #5FB878;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-type=\'3\'] + div dl dd.xm-select-this i{\n\t\t\t\t\t\tposition: absolute;\n\t\t\t\t\t\tright: 10px;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t';
},
cssColor: function cssColor(name, spanBgColor, spanColor, iBgColor, iColor, borderColor, tBgColor, tColor) {
return '\n\t\t\t\t\tselect[xm-select][xm-select-skin=\'' + name + '\'] + div .layui-select-title > div.xm-select > span:not(.xm-select-empty){\n\t\t\t\t\t\tbackground: ' + spanBgColor + ';\n\t\t\t\t\t\tcolor: ' + spanColor + ';\n\t\t\t\t\t\tborder: 1px solid ' + borderColor + '\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-skin=\'' + name + '\'] + div .layui-select-title > div.xm-select > span:not(.xm-select-empty) i{\n\t\t\t\t\t\tbackground: ' + iBgColor + ';\n\t\t\t\t\t\tcolor: ' + iColor + ';\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-skin=\'' + name + '\'] + div .layui-select-title > div.xm-select > span:not(.xm-select-empty) i:hover{\n\t\t\t\t\t\topacity:.8;\n\t\t\t\t\t\tfilter:alpha(opacity=80);\n\t\t\t\t\t\tcursor: pointer;\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-skin=\'' + name + '\'][xm-select-type=\'1\'] + div dl dd.xm-select-this:not(.layui-disabled),select[xm-select][xm-select-skin=\'' + name + '\']:not([xm-select-type]) + div dl dd.xm-select-this:not(.layui-disabled){\n\t\t\t\t\t\tbackground-color: ' + tBgColor + ';\n\t\t\t\t\t\tcolor: ' + tColor + ';\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-skin=\'' + name + '\'][xm-select-type=\'2\'] + div dl dd.xm-select-this:not(.layui-disabled) i{\n\t\t\t\t\t\tbackground-color: ' + tBgColor + ';\n\t\t\t\t\t\tborder-color: ' + tBgColor + ';\n\t\t\t\t\t\tcolor: ' + tColor + ';\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-skin=\'' + name + '\'][xm-select-type=\'3\'] + div dl dd.xm-select-this:not(.layui-disabled){\n\t\t\t\t\t\tcolor: ' + tBgColor + ';\n\t\t\t\t\t}\n\t\t\t\t\tselect[xm-select][xm-select-type=\'3\'] + div dl dd.xm-select-this i{\n\t\t\t\t\t\tposition: absolute;\n\t\t\t\t\t\tright: 10px;\n\t\t\t\t\t}\n\t\t\t\t';
},
renderCss: function renderCss() {
return '\n\t\t\t\t\tselect[xm-select] + div .layui-select-title > div.xm-select > span:not(.xm-select-empty){\n\t\t\t\t\t\tbox-sizing: content-box;\n\t\t\t\t\t}\n\t\t\t\t\t.xm-select-parent .layui-form-select dl dd.layui-this{\n\t\t\t\t\t background-color: inherit;\n\t\t\t\t\t color: inherit;\n\t\t\t\t\t}\n\t\t\t\t\t.xm-select-parent .layui-form-select dl dd.layui-this.layui-select-tips {\n\t\t\t\t\t\tbackground-color: #FFFFFF;\n\t\t\t\t\t color: #999999;\n\t\t\t\t\t}\n\t\t\t\t\t.xm-select-parent .layui-form-selected dl {\n\t\t\t\t\t display: none;\n\t\t\t\t\t}\n\t\t\t\t\t.xm-select-show:{\n\t\t\t\t\t\tdisplay: block;\n\t\t\t\t\t}\n\t\t\t\t\t.xm-select-hidn:{\n\t\t\t\t\t\tdisplay: none;\n\t\t\t\t\t}\n\t\t\t\t';
}
};
commons.methods.run();
return select3;
});

View File

@@ -74,7 +74,7 @@
title:"新增禁用命令",
content: '/ban/add',
area:['500px','250px'],
cancel:function(){
cancel:function(index){
window.location.reload();
layer.close(index);
}
@@ -92,7 +92,7 @@
title:"编辑禁用命令",
content: '/ban/edit?id='+data.id,
area:['500px','250px'],
cancel:function(){
cancel:function(index){
window.location.reload();
layer.close(index);
}

View File

@@ -84,7 +84,7 @@
<div class="layui-footer" style="border-top: 2px solid #e4e4e4">
<!-- 底部固定区域 -->
© Power by <a href="http://www.haodaquan.com/" target="_blank">PPGo_Job V2.6 </a>
© Power by <a href="http://www.haodaquan.com/" target="_blank">PPGo_Job {{.version}}</a>
</div>
</div>
<script src="/static/layui/layui.js"></script>

View File

@@ -1,7 +1,7 @@
<div class="layui-layout layui-layout-admin" style="padding-left: 40px;margin-top: 20px;">
<div style="margin: 10px 0px">
<blockquote class="layui-elem-quote">
说明提交之前请先测试服务器资源是否可以连通
说明提交之前请先测试执行器资源是否可以连通
</blockquote>
</div>
<form class="layui-form" action="" method="post" >

View File

@@ -23,12 +23,13 @@
<div class="layui-input-inline mw400">
<input type="radio" name="connection_type" lay-verify="type" value="0" title="SSH" {{if eq .server.connection_type 0}}checked{{end}}>
<input type="radio" name="connection_type" lay-verify="type" value="1" title="Telnet" {{if eq .server.connection_type 1}}checked{{end}}>
<input type="radio" name="connection_type" lay-verify="type" value="2" title="Agent" {{if eq .server.connection_type 2}}checked{{end}}>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200">服务器名称</label>
<label class="layui-form-label mw200">器名称</label>
<div class="layui-input-inline mw400">
<input type="text" name="server_name" id="server_name" lay-verify="required" autocomplete="off" placeholder="服务器名称" class="layui-input" value="{{.server.server_name}}">
</div>

View File

@@ -23,6 +23,7 @@
<div class="layui-input-inline mw400">
<input type="radio" name="connection_type" lay-verify="type" value="0" title="SSH" {{if eq .server.connection_type 0}}checked{{end}}>
<input type="radio" name="connection_type" lay-verify="type" value="1" title="Telnet" {{if eq .server.connection_type 1}}checked{{end}}>
<input type="radio" name="connection_type" lay-verify="type" value="2" title="Agent" {{if eq .server.connection_type 2}}checked{{end}}>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>

View File

@@ -7,7 +7,7 @@
<form class="layui-form" action="" onsubmit="javascript:return false;">
<div class="demoTable">
<div class="layui-inline" style="width: 40%">
<input class="layui-input" name="serverName" id="serverName" autocomplete="off" placeholder="资源名称" >
<input class="layui-input" name="serverName" id="serverName" autocomplete="off" placeholder="执行资源名称" >
</div>
<div class="layui-inline" style="width: 20%;text-align: left;">
<select name="serverGroupId" id="serverGroupId">
@@ -53,13 +53,12 @@
,url: '/server/table'
,cols: [[
// {checkbox: true, fixed: true},
{field:'id', title: 'ID', align:'center',sort: true, width:150}
,{field:'server_name',title: '资源名称'}
,{field:'group_name',title: '分组名称'}
,{field:'connection_type', title: '连接类型'}
,{field:'type', title: '登录类型'}
{field:'id', title: 'ID', align:'center',sort: true, width:80}
,{field:'server_name',title: '执行资源名称',width:350}
,{field:'connection_type', title: '类型',width:100}
,{field:'ip_port', title: 'IP端口'}
,{field:'group_name',title: '分组名称',width:200}
,{field:'detail', title: '备注'}
// ,{field:'status_text', title: '状态'}
,{fixed: 'right', width:160, align:'center', toolbar: '#bar'}
]]
,id: 'listReload'

View File

@@ -3,6 +3,7 @@
display: none;
}
</style>
<div class="layui-layout layui-layout-admin" style="padding-left: 40px;margin-top: 20px;">
<form class="layui-form" action="" method="post">
<div class="layui-form-item">
@@ -14,7 +15,7 @@
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200">所属分组</label>
<label class="layui-form-label mw200">任务分组</label>
<div class="layui-input-inline">
<select name="group_id" lay-verify="required">
{{range $k, $v := .taskGroup}}
@@ -25,28 +26,35 @@
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200">服务器资源</label>
<div class="layui-block">
<div class="layui-col-md3"></div>
<div class="layui-col-md9">
<div style="margin-bottom: 20px;">
<input type="checkbox" name="server_id" lay-skin="primary" title="本地服务器" value="0">
</div>
<div class="layui-row layui-col-space10">
{{range $k, $v := .serverGroup}}
<div class="layui-col-md12" ><i class="layui-icon">&#xe65b;</i> {{$v.GroupName}}</div>
<label class="layui-form-label mw200">执行资源</label>
<div class="layui-input-inline mw400">
<select name="server_id" lay-verify="required" lay-search xm-select="select_server" >
<option value="">请选择</option>
<option value="0">本机执行</option>
{{range $k, $v := .serverGroup}}
<optgroup label="{{$v.GroupName}}">
{{range $kk, $vv := $v.Servers}}
<div class="layui-col-md4">
<input type="checkbox" name="server_id" lay-skin="primary" title="{{$vv}}" value="{{$kk}}">
</div>
<option value="{{$kk}}">{{$vv}}</option>
{{end}}
{{end}}
</div>
</div>
</optgroup>
{{end}}
</select>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200">执行策略</label>
<div class="layui-input-inline mw400">
<input type="radio" name="server_type" value="0" lay-filter="server_type" title="同时执行" checked>
<input type="radio" name="server_type" value="1" lay-filter="server_type" title="轮询执行">
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200">任务说明</label>
<div class="layui-input-inline mw400">
@@ -71,7 +79,7 @@
<label class="layui-form-label mw200">时间表达式</label>
<div class="layui-input-inline mw400">
<input type="text" name="cron_spec" id="cron_spec" lay-verify="required" autocomplete="off"
placeholder="时间表达式" class="layui-input" value="">
placeholder="*/40 * * * * " class="layui-input" value="">
</div>
<div class="layui-form-mid layui-word-aux"><a href="/help" target="_blank"><i class="fa fa-question-circle"
aria-hidden="true"></i></a>
@@ -81,7 +89,7 @@
<div class="layui-form-item">
<label class="layui-form-label mw200">命令脚本</label>
<div class="layui-input-inline mw400">
<textarea name="command" id="command" rows="5" placeholder="请输入命令内容" class="layui-textarea"></textarea>
<textarea name="command" id="command" rows="5" placeholder="echo hello world && ls -a" class="layui-textarea"></textarea>
</div>
</div>
@@ -125,14 +133,16 @@
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item notify">
<label class="layui-form-label mw200">通知用户</label>
<div class="layui-input-inline mw400 ">
{{range $k, $v := .adminInfo}}
<input type="checkbox" name="notify_user" lay-filter="notify_user" title="{{$v.RealName}}"
value="{{$v.Id}}" lay-skin="primary">
{{end}}
<div class="layui-form-item notify">
<label class="layui-form-label mw200">通知用户</label>
<div class="layui-input-inline mw400">
<select name="notify_user" lay-search xm-select="select_notify" >
<option value="">请选择</option>
{{range $k, $v := .adminInfo}}
<option value="{{$v.Id}}">{{$v.RealName}}</option>
{{end}}
</select>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
@@ -148,9 +158,25 @@
</form>
</div>
<script>
layui.use(['form', 'element', 'layer', 'jquery'], function () {
layui.config({
base:'/static/admin/js/'
}).extend({
formSelects: 'formSelects-v3'
});
layui.use(['form', 'element', 'layer', 'jquery','formSelects'], function () {
var form = layui.form; //只有执行了这一步,部分表单元素才会自动修饰成功
var $ = layui.jquery;
var formSelects = layui.formSelects;
formSelects.render({
name : 'select_server',
});
formSelects.render({
name : 'select_notify',
});
var error_info = "{{.flash.error}}";
if (error_info) {
layer.msg(error_info, {icon: 2, shade: 0.3}, function () {
@@ -165,20 +191,6 @@
});
var notify_user_ids = [];
form.on('checkbox(notify_user)', function (data) {
if (data.elem.checked == true) {
notify_user_ids.push(data.value)
} else {
$.each(notify_user_ids, function (index, item) {
// index是索引值即下标 item是每次遍历得到的值
if (item == data.value) {
notify_user_ids.splice(index, 1);
}
});
}
$("#notify_user_ids").val(notify_user_ids.join(","));
});
form.on('radio(is_notify)', function (data) {
if (data.value == 1) {
@@ -191,10 +203,18 @@
form.on('submit(sub)', function (data) {
var form_data = data.field;
//选择执行器
var ids = formSelects.value('select_server'); //获取选中的
if (ids.length<1){
layer.msg("请选择执行资源");
return false;
}
var server_arr = new Array();
$("input:checkbox[name=server_id]:checked").each(function(){
server_arr.push($(this).val());
});
$.each(ids,function (k,v) {
server_arr.push(v.val);
})
form_data.server_ids = server_arr.join(",");
@@ -203,17 +223,28 @@
return false;
}
var notify_user_arr = new Array();
$("input:checkbox[name=notify_user]:checked").each(function(){
notify_user_arr.push($(this).val());
});
form_data.notify_user_ids = notify_user_arr.join(",");
if (form_data.is_notify==1 && form_data.notify_user_ids==="") {
layer.msg("请选择通知用户");
return false;
//选择通知人
if (form_data.is_notify==1 ) {
var ids = formSelects.value('select_notify'); //获取选中的
if (ids.length<1){
layer.msg("请选择通知人");
return false;
}
var notify_arr = new Array();
$.each(ids,function (k,v) {
notify_arr.push(v.val);
})
form_data.notify_user_ids = notify_arr.join(",");
if (form_data.notify_user_ids==="" || form_data.notify_user_ids===null){
layer.msg("请选择通知人");
return false;
}
}
$.post('{{urlfor "TaskController.AjaxSave"}}', form_data, function (out) {
if (out.status == 0) {
layer.msg("操作成功", {icon: 1, shade: 0.3, time: 1000}, function () {

View File

@@ -53,11 +53,11 @@
,url: '/task/table?status=2'
,cols: [[
{checkbox: true, fixed: true},
{field:'id', title: 'ID', align:'center',sort: true, width:50}
{field:'id', title: 'ID', align:'center',sort: true, width:100}
,{field:'task_name',title: '任务名称'}
,{field:'description',title: '任务说明'}
,{field:'cron_spec',title: '执行时间', width:150}
,{field:'pre_time', width:170,title: '上次执行时间'}
,{field:'execute_times', width:70,title: '次数'}
,{field:'execute_times', width:100,title: '次数'}
,{fixed:'right', width:150, align:'center', title:'操作', toolbar: '#bar'}
]]
,id: 'listReload'

View File

@@ -4,7 +4,7 @@
}
</style>
<div class="layui-layout layui-layout-admin" style="padding-left: 40px;margin-top: 20px;">
<form class="layui-form" action="" method="post" >
<form class="layui-form" action="javascript:void(0)" method="post" >
<div class="layui-form-item">
<label class="layui-form-label mw200">任务名称</label>
<div class="layui-input-inline mw400">
@@ -16,31 +16,40 @@
<label class="layui-form-label mw200">所属分组</label>
<div class="layui-input-inline">
<select name="group_id" lay-verify="required">
{{range $k, $v := .taskGroup}}
<option value="{{$k}}" {{if eq $k $.task.GroupId}}selected{{end}}>{{$v}}</option>
{{end}}
{{range $k, $v := .taskGroup}}
<option value="{{$k}}" {{if eq $k $.task.GroupId}}selected{{end}}>{{$v}}</option>
{{end}}
</select>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200">执行资源</label>
<div class="layui-input-inline mw400">
<select name="server_id" xm-select="select_server" >
<option value="">请选择</option>
<option value="0">本机执行</option>
{{range $k, $v := .serverGroup}}
<optgroup label="{{$v.GroupName}}">
{{range $kk, $vv := $v.Servers}}
<option value="{{$kk}}">{{$vv}}</option>
{{end}}
</optgroup>
{{end}}
</select>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200">服务器资源</label>
<div class="layui-inline">
<div class="layui-input-inline">
<select name="server_id">
<option value="0">本地服务器</option>
{{range $k, $v := .serverGroup}}
<optgroup label="{{$v.GroupName}}">
{{range $kk, $vv := $v.Servers}}
<option value="{{$kk}}" {{if eq $kk $.task.ServerId}}selected{{end}} >{{$vv}}</option>
{{end}}
</optgroup>
{{end}}
</select>
</div>
<label class="layui-form-label mw200">执行策略</label>
<div class="layui-input-inline mw400">
<input type="radio" name="server_type" value="0" lay-filter="server_type" title="同时执行" {{if eq .task.ServerType 0}}checked{{end}}>
<input type="radio" name="server_type" value="1" lay-filter="server_type" title="轮询执行" {{if eq .task.ServerType 1}}checked{{end}}>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
@@ -95,28 +104,42 @@
</div>
<div class="layui-form-item notify">
<hr>
<label class="layui-form-label mw200">通知类型</label>
<div class="layui-input-inline mw400">
<input type="radio" name="notify_type" lay-verify="required" value="0" title="邮件" {{if eq .task.NotifyType 0}}checked{{end}}>
<input type="radio" name="notify_type" lay-verify="required" value="1" title="短信" {{if eq .task.NotifyType 1}}checked{{end}}>
<input type="radio" name="notify_type" lay-verify="required" value="2" title="钉钉" {{if eq .task.NotifyType 2}}checked{{end}}>
<input type="radio" name="notify_type" lay-verify="required" value="3" title="微信" {{if eq .task.NotifyType 3}}checked{{end}}>
<input type="radio" name="notify_type" value="0" lay-filter="notify_type" title="邮件" {{if eq .task.NotifyType 0}}checked{{end}}>
<input type="radio" name="notify_type" value="1" lay-filter="notify_type" title="短信" {{if eq .task.NotifyType 1}}checked{{end}}>
<input type="radio" name="notify_type" value="2" lay-filter="notify_type" title="钉钉" {{if eq .task.NotifyType 2}}checked{{end}}>
<input type="radio" name="notify_type" value="3" lay-filter="notify_type" title="微信" {{if eq .task.NotifyType 3}}checked{{end}}>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item notify">
<label class="layui-form-label mw200">通知用户</label>
<div class="layui-input-inline mw400 ">
{{range $k, $v := .adminInfo}}
<input type="checkbox" name="notify_user" lay-filter="notify_user" title="{{$v.RealName}}" value="{{$v.Id}}" lay-skin="primary" {{range $ks,$vs:=$.notify_user_ids}} {{if eq $v.Id $vs}}checked{{end}}{{end}}>
{{end}}
<input type="hidden" name="notify_user_ids" id="notify_user_ids" value="{{.task.NotifyUserIds}}">
<div class="layui-form-item notify">
<label class="layui-form-label mw200">通知模板</label>
<div class="layui-input-inline">
<select name="notify_tpl_id" lay-filter="notify_tpl_id">
<option value="0">请选择</option>
{{range $k, $v := .notifyTpl}}
<option value="{{$v.id}}" {{if eq $.task.NotifyTplId $v.id}} selected {{end}}>{{$v.tpl_name}}</option>
{{end}}
</select>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
<input type="hidden" name="id" id="id" value="0">
<div class="layui-form-item notify">
<label class="layui-form-label mw200">通知用户</label>
<div class="layui-input-inline mw400">
<select name="notify_user" lay-search xm-select="select_notify" >
<option value="">请选择</option>
{{range $k, $v := .adminInfo}}
<option value="{{$v.Id}}">{{$v.RealName}}</option>
{{end}}
</select>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200"></label>
<div class="layui-input-inline mw400">
@@ -127,16 +150,38 @@
</form>
</div>
<script>
layui.use(['form','element','layer','jquery'],function(){
layui.config({
base:'/static/admin/js/'
}).extend({
formSelects: 'formSelects-v3'
}).use(['form','element','layer','jquery','formSelects'],function(){
var form = layui.form; //只有执行了这一步,部分表单元素才会自动修饰成功
var $ = layui.jquery;
var error_info = "{{.flash.error}}";
if(error_info){
layer.msg(error_info,{icon: 2,shade:0.3},function () {
window.history.go(-1)
})
return;
}
var formSelects = layui.formSelects;
var server_id = {{$.service_ids}};
formSelects.render({
init : server_id,
name : 'select_server',
});
var select_notify = {{$.notify_user_ids}};
console.log(select_notify);
formSelects.render({
init : select_notify,
name : 'select_notify',
});
var error_info = "{{.flash.error}}";
if(error_info){
layer.msg(error_info,{icon: 2,shade:0.3},function () {
self.location=document.referrer;
})
return;
}
$("#des").on('click',function () {
layer.tips('设为的话如果该任务在上一个时间点还没执行完则略过不执行', '#des', {
tips: [1, '#0FA6D8'] //还可配置颜色
@@ -154,34 +199,91 @@
}
});
var notify_user_ids = [];
form.on('checkbox(notify_user)', function(data){
if(data.elem.checked==true){
notify_user_ids.push(data.value)
}else{
$.each(notify_user_ids,function(index,item){
// index是索引值即下标 item是每次遍历得到的值
if(item==data.value){
notify_user_ids.splice(index,1);
}
});
form.on('submit(sub)', function(data){
var isAdmin = "{{.isAdmin}}";
var msg = "编辑任务需要重新审核,是否确认需要编辑?";
var okmsg = "操作成功,可去【任务审核】中查看该任务";
if (isAdmin==1){
msg = "是否确认需要编辑?";
okmsg = "操作成功";
}
$("#notify_user_ids").val(notify_user_ids.join(","));
layer.confirm(msg, {icon: 3, title:'提示'}, function(index){
var form_data = data.field;
//选择执行器
var ids = formSelects.value('select_server'); //获取选中的
if (ids.length<1){
layer.msg("请选择执行资源");
return false;
}
var server_arr = new Array();
$.each(ids,function (k,v) {
server_arr.push(v.val);
})
form_data.server_ids = server_arr.join(",");
if (form_data.server_ids==="" || form_data.server_ids===null){
layer.msg("请选择服务器资源");
return false;
}
//选择通知人
if (form_data.is_notify==1 ) {
var ids = formSelects.value('select_notify'); //获取选中的
if (ids.length<1){
layer.msg("请选择通知人");
return false;
}
var notify_arr = new Array();
$.each(ids,function (k,v) {
notify_arr.push(v.val);
});
form_data.notify_user_ids = notify_arr.join(",");
if (form_data.notify_user_ids==="" || form_data.notify_user_ids===null){
layer.msg("请选择通知人");
return false;
}
}
$.post('{{urlfor "TaskController.AjaxSave"}}', form_data, function (out) {
if (out.status == 0) {
layer.msg(okmsg,{icon: 1,shade:0.3,time:1000},function () {
// self.location=document.referrer;
window.parent.deleteCurrentTab();
})
} else {
layer.msg(out.message);
return false;
}
}, "json");
setTimeout(function(){
layer.closeAll('loading');
}, 2000);
layer.close(index);
});
});
form.on('submit(sub)', function(data){
var form_data = data.field;
$.post('{{urlfor "TaskController.AjaxSave"}}', form_data, function (out) {
if (out.status == 0) {
layer.msg("复制成功!,请到列表中查看",{icon: 1,shade:0.3,time:1000},function () {
//self.location=document.referrer;
window.parent.deleteCurrentTab();
})
form.on('radio(notify_type)', function (data) {
$.post('{{urlfor "TaskController.AjaxNotifyType"}}', {notify_type: data.value}, function (out) {
if (out.code == 0) {
$('select[name="notify_tpl_id"]').empty();
$('<option value="0" selected>请选择</option>').appendTo('select[name="notify_tpl_id"]');
for (var i = 0; i < out.data.length; i++) {
$('<option value="' + out.data[i].id + '">' + out.data[i].tpl_name + '</option>').appendTo('select[name="notify_tpl_id"]');
}
form.render();
} else {
layer.msg(out.message)
}
}, "json");
return false;
});
//但是如果你的HTML是动态生成的自动渲染就会失效

View File

@@ -64,8 +64,14 @@
<td></td>
</tr>
<tr>
<td>服务资源</td>
<td> {{.serverName}}</td>
<td>执行资源</td>
<td> {{str2html .serverName}}</td>
<td></td>
</tr>
<tr>
<td>执行策略</td>
<td> {{.ServerType}}</td>
<td></td>
</tr>
<tr>
@@ -114,7 +120,7 @@
<td>通知用户</td>
<td>
{{range $k, $v := .adminInfo}}
{{$v.RealName}} &nbsp;&nbsp;&nbsp;
{{$v.RealName}} &nbsp;<br>
{{end}}
</td>
<td></td>

View File

@@ -4,7 +4,7 @@
}
</style>
<div class="layui-layout layui-layout-admin" style="padding-left: 40px;margin-top: 20px;">
<form class="layui-form" action="javascript:return false;" method="post" >
<form class="layui-form" action="javascript:void(0)" method="post" >
<div class="layui-form-item">
<label class="layui-form-label mw200">任务名称</label>
<div class="layui-input-inline mw400">
@@ -24,29 +24,35 @@
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200">服务器资源</label>
<div class="layui-block">
<div class="layui-col-md3"></div>
<div class="layui-col-md9">
<div style="margin-bottom: 20px;">
<input type="checkbox" name="server_id" lay-skin="primary" title="本地服务器" value="0" {{range $ks,$vs:=$.service_ids}} {{if eq 0 $vs}}checked{{end}}{{end}}>
</div>
<div class="layui-row layui-col-space10">
{{range $k, $v := .serverGroup}}
<div class="layui-col-md12" ><i class="layui-icon">&#xe65b;</i> {{$v.GroupName}}</div>
{{range $kk, $vv := $v.Servers}}
<div class="layui-col-md4">
<input type="checkbox" name="server_id" lay-skin="primary" title="{{$vv}}" value="{{$kk}}" {{range $ks,$vs:=$.service_ids}} {{if eq $kk $vs}}checked{{end}}{{end}}>
</div>
{{end}}
{{end}}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200">执行资源</label>
<div class="layui-input-inline mw400">
<select name="server_id" xm-select="select_server" >
<option value="">请选择</option>
<option value="0">本机执行</option>
{{range $k, $v := .serverGroup}}
<optgroup label="{{$v.GroupName}}">
{{range $kk, $vv := $v.Servers}}
<option value="{{$kk}}">{{$vv}}</option>
{{end}}
</optgroup>
{{end}}
</select>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200">执行策略</label>
<div class="layui-input-inline mw400">
<input type="radio" name="server_type" value="0" lay-filter="server_type" title="同时执行" {{if eq .task.ServerType 0}}checked{{end}}>
<input type="radio" name="server_type" value="1" lay-filter="server_type" title="轮询执行" {{if eq .task.ServerType 1}}checked{{end}}>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mw200">任务说明</label>
<div class="layui-input-inline mw400">
@@ -121,16 +127,20 @@
<div class="layui-form-mid layui-word-aux"></div>
</div>
<div class="layui-form-item notify">
<div class="layui-form-item notify">
<label class="layui-form-label mw200">通知用户</label>
<div class="layui-input-inline mw400 ">
{{range $k, $v := .adminInfo}}
<input type="checkbox" name="notify_user" lay-filter="notify_user" title="{{$v.RealName}}" value="{{$v.Id}}" lay-skin="primary" {{range $ks,$vs:=$.notify_user_ids}} {{if eq $v.Id $vs}}checked{{end}}{{end}}>
{{end}}
<input type="hidden" name="notify_user_ids" id="notify_user_ids" value="{{.task.NotifyUserIds}}">
<div class="layui-input-inline mw400">
<select name="notify_user" lay-search xm-select="select_notify" >
<option value="">请选择</option>
{{range $k, $v := .adminInfo}}
<option value="{{$v.Id}}">{{$v.RealName}}</option>
{{end}}
</select>
</div>
<div class="layui-form-mid layui-word-aux"></div>
</div>
<input type="hidden" name="id" id="id" value="{{.task.Id}}">
<div class="layui-form-item">
<label class="layui-form-label mw200"></label>
@@ -142,10 +152,30 @@
</form>
</div>
<script>
layui.use(['form','element','layer','jquery'],function(){
layui.config({
base:'/static/admin/js/'
}).extend({
formSelects: 'formSelects-v3'
}).use(['form','element','layer','jquery','formSelects'],function(){
var form = layui.form; //只有执行了这一步,部分表单元素才会自动修饰成功
var $ = layui.jquery;
var formSelects = layui.formSelects;
var server_id = {{$.service_ids}};
formSelects.render({
init : server_id,
name : 'select_server',
});
var select_notify = {{$.notify_user_ids}};
console.log(select_notify);
formSelects.render({
init : select_notify,
name : 'select_notify',
});
var error_info = "{{.flash.error}}";
if(error_info){
layer.msg(error_info,{icon: 2,shade:0.3},function () {
@@ -171,22 +201,6 @@
}
});
var notify_user_ids = [];
form.on('checkbox(notify_user)', function(data){
if(data.elem.checked==true){
notify_user_ids.push(data.value)
}else{
$.each(notify_user_ids,function(index,item){
// index是索引值即下标 item是每次遍历得到的值
if(item==data.value){
notify_user_ids.splice(index,1);
}
});
}
$("#notify_user_ids").val(notify_user_ids.join(","));
});
form.on('submit(sub)', function(data){
var isAdmin = "{{.isAdmin}}";
var msg = "编辑任务需要重新审核,是否确认需要编辑?";
@@ -198,19 +212,46 @@
}
layer.confirm(msg, {icon: 3, title:'提示'}, function(index){
var form_data = data.field;
var ids = [];
$("input[name=server_id][type=checkbox]").each(function() {
if ($(this).prop("checked")) {
ids.push($(this).val());
}
});
//选择执行器
var ids = formSelects.value('select_server'); //获取选中的
if (ids.length<1){
layer.msg("请选择服务资源");
layer.msg("请选择执行资源");
return false;
}
form_data.server_ids = ids.join(",");
var server_arr = new Array();
$.each(ids,function (k,v) {
server_arr.push(v.val);
})
form_data.server_ids = server_arr.join(",");
if (form_data.server_ids==="" || form_data.server_ids===null){
layer.msg("请选择服务器资源");
return false;
}
//选择通知人
if (form_data.is_notify==1 ) {
var ids = formSelects.value('select_notify'); //获取选中的
if (ids.length<1){
layer.msg("请选择通知人");
return false;
}
var notify_arr = new Array();
$.each(ids,function (k,v) {
notify_arr.push(v.val);
});
form_data.notify_user_ids = notify_arr.join(",");
if (form_data.notify_user_ids==="" || form_data.notify_user_ids===null){
layer.msg("请选择通知人");
return false;
}
}
$.post('{{urlfor "TaskController.AjaxSave"}}', form_data, function (out) {
if (out.status == 0) {
@@ -219,8 +260,8 @@
window.parent.deleteCurrentTab();
})
} else {
layer.msg(out.message)
return
layer.msg(out.message);
return false;
}
}, "json");

View File

@@ -58,12 +58,12 @@
,url: '/task/table'
,cols: [[
{checkbox: true, fixed: true},
{field:'id', title: 'ID', align:'center', width:50,sort: true}
,{field:'task_name',width:300, title: '任务名称'}
,{field:'description',title: '任务说明'}
{field:'id', title: 'ID', align:'center', width:100,sort: true}
,{field:'task_name', title: '任务名称'}
,{field:'cron_spec',title: '时间表达式', width:150}
,{field:'next_time', width:170,title: '下次执行时间'}
,{field:'pre_time', title: '上次执行时间'}
,{field:'execute_times', title: '次数'}
,{field:'pre_time', title: '上次执行时间',width:170,}
,{field:'execute_times', title: '次数',width:100}
,{width:120, align:'center', title:'操作', toolbar: '#bar'}
]]
,id: 'listReload'

View File

@@ -48,7 +48,7 @@
{checkbox: true, fixed: true}
// ,{field:'id', title: 'ID', align:'center',sort: true, width:150}
,{field:'task_id', title: '任务ID', align:'center',width:100}
,{field:'server_name', title: '服务器', width:150}
,{field:'server_name', title: '服务器', width:250}
,{field:'start_time',title: '开始时间'}
,{field:'process_time',width:100, title: '执行时间'}
,{field:'output_size',title: '输出'}