Compare commits

...
16 Commits
74 changed files with 983 additions and 1002 deletions
+1
View File
@@ -0,0 +1 @@
.git
+8 -5
View File
@@ -23,16 +23,19 @@ _testmain.go
*.test
*.prof
.DS_Store
.idea
log/*
data/*
conf/*
log
data
conf
profile/*
public/resource/javascript/vue.js
gocron
/gocron
gocron.exe
gocron-node
/gocron-node
gocron-node.exe
/bin
/packages
node_modules
package.json
+1 -1
View File
@@ -1,6 +1,6 @@
language: go
go:
- 1.7.x
- 1.9.x
script: go test `go list ./... | grep -v vendor`
notifications:
+9
View File
@@ -0,0 +1,9 @@
FROM alpine:latest
COPY gocron /usr/local/gocron
WORKDIR /usr/local/gocron
EXPOSE 5920
ENTRYPOINT ["/usr/local/gocron/gocron", "web"]
+16 -11
View File
@@ -25,9 +25,9 @@
* 任务执行结果通知, 支持邮件、Slack
### 截图
![流程图](https://raw.githubusercontent.com/ouqiang/gocron/master/scheduler.png)
![任务](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot_task.png)
![Slack](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot_slack.png)
![流程图](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot/scheduler.png)
![任务](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot/task.png)
![Slack](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot/notification.png)
### 支持平台
> Windows、Linux、Mac OS
@@ -56,12 +56,12 @@
4. 浏览器访问 http://localhost:5920
### 源码安装
1. `go`语言版本1.7+
1. `go`语言版本1.9+
2. `go get -d github.com/ouqiang/gocron`
3. 编译
* 调度器 `go build -tags gocron -o gocron`
* 任务节点 `go build -tags node -o gocron-node`
4. 启动、访问方式同上
3. 编译 `make`
4. 启动
* gocron `./bin/gocron web`
* gocron-node `./bin/gocron-node`
### 命令
@@ -96,7 +96,7 @@
* 定时任务调度 [Cron](https://github.com/robfig/cron)
* ORM [Xorm](https://github.com/go-xorm/xorm)
* UI框架 [Semantic UI](https://semantic-ui.com/)
* 依赖管理(所有依赖包放入vendor目录) [Govendor](https://github.com/kardianos/govendor)
* 依赖管理 [Govendor](https://github.com/kardianos/govendor)
* RPC框架 [gRPC](https://github.com/grpc/grpc)
## 反馈
@@ -104,6 +104,13 @@
## ChangeLog
v1.4
--------
* HTTP任务支持POST请求
* 后台手动停止运行中的shell任务
* 任务执行失败重试间隔时间支持用户自定义
* 修复API接口调用报403错误
v1.3
--------
* 支持多用户登录
@@ -118,8 +125,6 @@ v1.2.2
* 调度器与任务节点支持HTTPS双向认证
* 修复任务列表页总记录数显示错误
v1.1
--------
-121
View File
@@ -1,121 +0,0 @@
#!/usr/bin/env bash
# set -x -u
# 构建应用, 生成压缩包 gocron.zip或gocron.tar.gz
# ./build.sh -p windows -a amd64
# 参数含义
# -p 指定平台(windows|linux|darwin)
# -a 指定体系架构(amd64|386), 默认amd64
TEMP_DIR=`date +%s`-temp-`echo $RANDOM`
# 目标平台 windows,linux,darwin
OS=''
# 目标平台架构
ARCH=''
# 应用名称
APP_NAME='gocron'
# 版本号
VERSION=''
# 可执行文件名
EXEC_NAME=''
# 压缩包名称
COMPRESS_FILE=''
# -p 平台 -a 架构
while getopts "p:a:v:" OPT;
do
case $OPT in
p) OS=$OPTARG
;;
a) ARCH=$OPTARG
;;
v) VERSION=$OPTARG
;;
esac
done
if [[ -z $OS ]];then
echo "平台不能为空"
exit 1
fi
if [[ $OS != 'windows' && $OS != 'linux' && $OS != 'darwin' ]];then
echo '平台错误,支持的平台 windows linux darmin(osx)'
exit 1
fi
if [[ -z $ARCH ]];then
ARCH='amd64'
fi
if [[ $ARCH != '386' && $ARCH != 'amd64' ]];then
echo 'arch错误,仅支持 386 amd64'
exit 1
fi
if [[ -z $VERSION ]];then
echo '版本号不能为空'
exit 1
fi
echo '开始编译调度器'
if [[ $OS = 'windows' ]];then
GOOS=$OS GOARCH=$ARCH go build -tags gocron -ldflags '-w'
else
GOOS=$OS GOARCH=$ARCH go build -tags gocron -ldflags '-w'
fi
if [[ $? != 0 ]];then
exit 1
fi
echo '编译完成'
if [[ $OS = 'windows' ]];then
EXEC_NAME=${APP_NAME}.exe
COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.zip
else
EXEC_NAME=${APP_NAME}
COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.tar.gz
fi
mkdir -p $TEMP_DIR/$APP_NAME
if [[ $? != 0 ]]; then
exit 1
fi
# 需要打包的文件
PACKAGE_FILENAME=(conf log public data templates ${EXEC_NAME})
echo '复制文件到临时目录'
# 复制文件到临时目录
for i in ${PACKAGE_FILENAME[*]}
do
cp -r $i $TEMP_DIR/$APP_NAME
done
# 删除运行时产生的文件
rm -rf $TEMP_DIR/$APP_NAME/conf/*
rm -rf $TEMP_DIR/$APP_NAME/log/*
rm -rf $TEMP_DIR/$APP_NAME/data/sessions/*
rm -rf $TEMP_DIR/$APP_NAME/data/ssh/password/*
rm -rf $TEMP_DIR/$APP_NAME/data/ssh/private_key/*
echo '压缩文件'
# 压缩文件
cd $TEMP_DIR
if [[ $OS = 'windows' ]];then
zip -rq $COMPRESS_FILE *
else
tar czf $COMPRESS_FILE *
fi
mv $COMPRESS_FILE ../
cd ../
rm $EXEC_NAME
rm -rf $TEMP_DIR
echo '打包完成'
echo '生成压缩文件--' $COMPRESS_FILE
-92
View File
@@ -1,92 +0,0 @@
#!/usr/bin/env bash
# set -x -u
# 任务节点打包, 生成压缩包 gocron-node.zip或gocron-node.tar.gz
# ./build-node.sh -p windows -a amd64
# 参数含义
# -p 指定平台(windows|linux|darwin)
# -a 指定体系架构(amd64|386), 默认amd64
# 目标平台 windows,linux,darwin
OS=''
# 目标平台架构
ARCH=''
# 应用名称
APP_NAME='gocron-node'
# 版本号
VERSION=''
# 可执行文件名
EXEC_NAME=''
# 压缩包名称
COMPRESS_FILE=''
# -p 平台 -a 架构
while getopts "p:a:v:" OPT;
do
case $OPT in
p) OS=$OPTARG
;;
a) ARCH=$OPTARG
;;
v) VERSION=$OPTARG
;;
esac
done
if [[ -z $OS ]];then
echo "平台不能为空"
exit 1
fi
if [[ $OS != 'windows' && $OS != 'linux' && $OS != 'darwin' ]];then
echo '平台错误,支持的平台 windows linux darmin(osx)'
exit 1
fi
if [[ -z $ARCH ]];then
ARCH='amd64'
fi
if [[ $ARCH != '386' && $ARCH != 'amd64' ]];then
echo 'arch错误,仅支持 386 amd64'
exit 1
fi
if [[ -z $VERSION ]];then
echo '版本号不能为空'
exit 1
fi
if [[ $OS = 'windows' ]];then
EXEC_NAME=${APP_NAME}.exe
COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.zip
else
EXEC_NAME=${APP_NAME}
COMPRESS_FILE=${APP_NAME}-v${VERSION}-${OS}-${ARCH}.tar.gz
fi
echo '开始编译任务节点'
if [[ $OS = 'windows' ]];then
GOOS=$OS GOARCH=$ARCH go build -tags node -ldflags '-w' -o $EXEC_NAME
else
GOOS=$OS GOARCH=$ARCH go build -tags node -ldflags '-w' -o $EXEC_NAME
fi
if [[ $? != 0 ]];then
exit 1
fi
echo '编译完成'
if [[ $OS = 'windows' ]];then
zip -rq $COMPRESS_FILE $EXEC_NAME
else
tar czf $COMPRESS_FILE $EXEC_NAME
fi
rm $EXEC_NAME
echo '打包完成'
echo '生成压缩文件--' $COMPRESS_FILE
+48 -48
View File
@@ -1,45 +1,63 @@
package cmd
// main gocron
package main
import (
"os"
"os/signal"
"syscall"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
"github.com/ouqiang/gocron/modules/setting"
"github.com/ouqiang/gocron/routers"
"github.com/ouqiang/gocron/service"
"github.com/urfave/cli"
"gopkg.in/macaron.v1"
"os"
"os/signal"
"syscall"
"time"
)
var AppVersion = "1.4"
// web服务器默认端口
const DefaultPort = 5920
var CmdWeb = cli.Command{
Name: "web",
Usage: "run web server",
Action: runWeb,
Flags: []cli.Flag{
cli.StringFlag{
Name: "host",
Value: "0.0.0.0",
Usage: "bind host",
func main() {
cliApp := cli.NewApp()
cliApp.Name = "gocron"
cliApp.Usage = "gocron service"
cliApp.Version = AppVersion
cliApp.Commands = getCommands()
cliApp.Flags = append(cliApp.Flags, []cli.Flag{}...)
cliApp.Run(os.Args)
}
// getCommands
func getCommands() []cli.Command {
command := cli.Command{
Name: "web",
Usage: "run web server",
Action: runWeb,
Flags: []cli.Flag{
cli.StringFlag{
Name: "host",
Value: "0.0.0.0",
Usage: "bind host",
},
cli.IntFlag{
Name: "port,p",
Value: DefaultPort,
Usage: "bind port",
},
cli.StringFlag{
Name: "env,e",
Value: "prod",
Usage: "runtime environment, dev|test|prod",
},
},
cli.IntFlag{
Name: "port,p",
Value: DefaultPort,
Usage: "bind port",
},
cli.StringFlag{
Name: "env,e",
Value: "prod",
Usage: "runtime environment, dev|test|prod",
},
},
}
return []cli.Command{command}
}
func runWeb(ctx *cli.Context) {
@@ -80,13 +98,12 @@ func initModule() {
upgradeIfNeed()
// 初始化定时任务
serviceTask := new(service.Task)
serviceTask.Initialize()
service.ServiceTask.Initialize()
}
// 解析端口
func parsePort(ctx *cli.Context) int {
var port int = DefaultPort
port := DefaultPort
if ctx.IsSet("port") {
port = ctx.Int("port")
}
@@ -106,7 +123,7 @@ func parseHost(ctx *cli.Context) string {
}
func setEnvironment(ctx *cli.Context) {
var env string = "prod"
env := "prod"
if ctx.IsSet("env") {
env = ctx.String("env")
}
@@ -149,26 +166,9 @@ func shutdown() {
return
}
logger.Info("应用准备退出")
serviceTask := new(service.Task)
// 停止所有任务调度
logger.Info("停止定时任务调度")
serviceTask.StopAll()
taskNumInRunning := service.TaskNum.Num()
logger.Infof("正在运行的任务有%d个", taskNumInRunning)
if taskNumInRunning > 0 {
logger.Info("等待所有任务执行完成后退出")
}
for {
if taskNumInRunning <= 0 {
break
}
time.Sleep(3 * time.Second)
taskNumInRunning = service.TaskNum.Num()
}
// 释放gRPC连接池
grpcpool.Pool.ReleaseAll()
service.ServiceTask.WaitAndExit()
}
// 判断应用是否需要升级, 当存在版本号文件且版本小于app.VersionId时升级
+6 -6
View File
@@ -1,20 +1,20 @@
// +build node
// 任务节点
// main gocron-node
package main
import (
"flag"
"fmt"
"github.com/ouqiang/gocron/modules/rpc/auth"
"github.com/ouqiang/gocron/modules/rpc/server"
"github.com/ouqiang/gocron/modules/utils"
"os"
"runtime"
"strings"
"github.com/ouqiang/gocron/modules/rpc/auth"
"github.com/ouqiang/gocron/modules/rpc/server"
"github.com/ouqiang/gocron/modules/utils"
)
const AppVersion = "1.3"
var AppVersion = "1.4"
func main() {
var serverAddr string
View File
-25
View File
@@ -1,25 +0,0 @@
// +build gocron
// 调度中心
package main
import (
"github.com/urfave/cli"
"os"
"github.com/ouqiang/gocron/cmd"
)
const AppVersion = "1.3"
func main() {
app := cli.NewApp()
app.Name = "gocron"
app.Usage = "gocron service"
app.Version = AppVersion
app.Commands = []cli.Command{
cmd.CmdWeb,
}
app.Flags = append(app.Flags, []cli.Flag{}...)
app.Run(os.Args)
}
View File
+18
View File
@@ -0,0 +1,18 @@
all: build
build: gocron node
gocron:
go build -o bin/gocron ./cmd/gocron
node:
go build -o bin/gocron-node ./cmd/node
clean:
rm bin/gocron
rm bin/gocron-node
+33
View File
@@ -0,0 +1,33 @@
LDFLAGS = -w -s -X main.AppVersion=${VERSION}
all: build
build: gocron node
gocron:
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron_darwin_amd64/gocron ./cmd/gocron
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron_linux_amd64/gocron ./cmd/gocron
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron_windows_amd64/gocron.exe ./cmd/gocron
node:
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron-node_darwin_amd64/gocron-node ./cmd/node
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron-node_linux_amd64/gocron-node ./cmd/node
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./gocron-node_windows_amd64/gocron-node.exe ./cmd/node
clean:
rm -rf gocron_darwin_amd64
rm -rf gocron_linux_amd64
rm -rf gocron_windows_amd64
rm -rf gocron-node_darwin_amd64
rm -rf gocron-node_linux_amd64
rm -rf gocron-node_windows_amd64
+27 -4
View File
@@ -3,9 +3,10 @@ package models
import (
"errors"
"fmt"
"strconv"
"github.com/go-xorm/xorm"
"github.com/ouqiang/gocron/modules/logger"
"strconv"
)
type Migration struct{}
@@ -53,11 +54,12 @@ func (migration *Migration) Upgrade(oldVersionId int) {
return
}
versionIds := []int{110, 122, 130}
versionIds := []int{110, 122, 130, 140}
upgradeFuncs := []func(*xorm.Session) error{
migration.upgradeFor110,
migration.upgradeFor122,
migration.upgradeFor130,
migration.upgradeFor140,
}
startIndex := -1
@@ -157,7 +159,7 @@ func (migration *Migration) upgradeFor122(session *xorm.Session) error {
return err
}
// 升级到1.2.3版本
// 升级到v1.3版本
func (migration *Migration) upgradeFor130(session *xorm.Session) error {
logger.Info("开始升级到v1.3")
@@ -168,4 +170,25 @@ func (migration *Migration) upgradeFor130(session *xorm.Session) error {
logger.Info("已升级到v1.3\n")
return err
}
}
// 升级到v1.4版本
func (migration *Migration) upgradeFor140(session *xorm.Session) error {
logger.Info("开始升级到v1.4")
tableName := TablePrefix + "task"
// task表增加字段
// retry_interval 重试间隔时间(秒)
// http_method http请求方法
sql := fmt.Sprintf(
"ALTER TABLE %s ADD COLUMN retry_interval SMALLINT NOT NULL DEFAULT 0,ADD COLUMN http_method TINYINT NOT NULL DEFAULT 1", tableName)
_, err := session.Exec(sql)
if err != nil {
return err
}
logger.Info("已升级到v1.4\n")
return err
}
+5 -4
View File
@@ -2,6 +2,9 @@ package models
import (
"fmt"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
@@ -9,14 +12,12 @@ import (
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/setting"
"gopkg.in/macaron.v1"
"strings"
"time"
)
type Status int8
type CommonMap map[string]interface{}
var TablePrefix string = ""
var TablePrefix = ""
var Db *xorm.Engine
const (
@@ -99,7 +100,7 @@ func CreateTmpDb(setting *setting.Setting) (*xorm.Engine, error) {
// 获取数据库引擎DSN mysql,sqlite
func getDbEngineDSN(setting *setting.Setting) string {
engine := strings.ToLower(setting.Db.Engine)
var dsn string = ""
dsn := ""
switch engine {
case "mysql":
dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s",
+22 -8
View File
@@ -2,9 +2,10 @@ package models
import (
"errors"
"github.com/go-xorm/xorm"
"strings"
"time"
"github.com/go-xorm/xorm"
)
type TaskProtocol int8
@@ -28,21 +29,30 @@ const (
TaskDependencyStatusWeak TaskDependencyStatus = 2 // 弱依赖
)
type TaskHTTPMethod int8
const (
TaskHTTPMethodGet TaskHTTPMethod = 1
TaskHttpMethodPost TaskHTTPMethod = 2
)
// 任务
type Task struct {
Id int `xorm:"int pk autoincr"`
Name string `xorm:"varchar(32) notnull"` // 任务名称
Level TaskLevel `xorm:"smallint notnull index default 1"` // 任务等级 1: 主任务 2: 依赖任务
Level TaskLevel `xorm:"tinyint notnull index default 1"` // 任务等级 1: 主任务 2: 依赖任务
DependencyTaskId string `xorm:"varchar(64) notnull default ''"` // 依赖任务ID,多个ID逗号分隔
DependencyStatus TaskDependencyStatus `xorm:"smallint notnull default 1"` // 依赖关系 1:强依赖 主任务执行成功, 依赖任务才会被执行 2:弱依赖
DependencyStatus TaskDependencyStatus `xorm:"tinyint notnull default 1"` // 依赖关系 1:强依赖 主任务执行成功, 依赖任务才会被执行 2:弱依赖
Spec string `xorm:"varchar(64) notnull"` // crontab
Protocol TaskProtocol `xorm:"tinyint notnull index"` // 协议 1:http 2:系统命令
Command string `xorm:"varchar(256) notnull"` // URL地址或shell命令
HttpMethod TaskHTTPMethod `xorm:"tinyint notnull default 1"` // http请求方法
Timeout int `xorm:"mediumint notnull default 0"` // 任务执行超时时间(单位秒),0不限制
Multi int8 `xorm:"tinyint notnull default 1"` // 是否允许多实例运行
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 重试次数
NotifyStatus int8 `xorm:"smallint notnull default 1"` // 任务执行结束是否通知 0: 不通知 1: 失败通知 2: 执行结束通知
NotifyType int8 `xorm:"smallint notnull default 0"` // 通知类型 1: 邮件 2: slack
RetryInterval int16 `xorm:"smallint notnull default 0"` // 重试间隔时间
NotifyStatus int8 `xorm:"tinyint notnull default 1"` // 任务执行结束是否通知 0: 不通知 1: 失败通知 2: 执行结束通知
NotifyType int8 `xorm:"tinyint notnull default 0"` // 通知类型 1: 邮件 2: slack
NotifyReceiverId string `xorm:"varchar(256) notnull default '' "` // 通知接受者ID, setting表主键ID,多个ID逗号分隔
Tag string `xorm:"varchar(32) notnull default ''"`
Remark string `xorm:"varchar(100) notnull default ''"` // 备注
@@ -83,7 +93,9 @@ func (task *Task) CreateTestTask() {
func (task *Task) UpdateBean(id int) (int64, error) {
return Db.ID(id).
Cols("name,spec,protocol,command,timeout,multi,retry_times,remark,notify_status,notify_type,notify_receiver_id, dependency_task_id, dependency_status, tag").
Cols(`name,spec,protocol,command,timeout,multi,
retry_times,retry_interval,remark,notify_status,
notify_type,notify_receiver_id, dependency_task_id, dependency_status, tag,http_method`).
Update(task)
}
@@ -108,9 +120,11 @@ func (task *Task) Enable(id int) (int64, error) {
}
// 获取所有激活任务
func (task *Task) ActiveList() ([]Task, error) {
func (task *Task) ActiveList(page, pageSize int) ([]Task, error) {
params := CommonMap{"Page": page, "PageSize": pageSize}
task.parsePageAndPageSize(params)
list := make([]Task, 0)
err := Db.Where("status = ? AND level = ?", Enabled, TaskLevelParent).
err := Db.Where("status = ? AND level = ?", Enabled, TaskLevelParent).Limit(task.PageSize, task.pageLimitOffset()).
Find(&list)
if err != nil {
+2 -1
View File
@@ -1,8 +1,9 @@
package models
import (
"github.com/go-xorm/xorm"
"time"
"github.com/go-xorm/xorm"
)
type TaskType int8
+3 -2
View File
@@ -1,8 +1,9 @@
package models
import (
"github.com/ouqiang/gocron/modules/utils"
"time"
"github.com/ouqiang/gocron/modules/utils"
)
const PasswordSaltLength = 6
@@ -78,7 +79,7 @@ func (user *User) Match(username, password string) bool {
}
// 获取用户详情
func (user *User) Find(id int) (error) {
func (user *User) Find(id int) error {
_, err := Db.Id(id).Get(user)
return err
+13 -7
View File
@@ -3,12 +3,14 @@ package app
import (
"os"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/setting"
"github.com/ouqiang/gocron/modules/utils"
"fmt"
"io/ioutil"
"strconv"
"strings"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/setting"
"github.com/ouqiang/gocron/modules/utils"
)
var (
@@ -35,7 +37,7 @@ func InitEnv(versionString string) {
DataDir = AppDir + "/data"
AppConfig = ConfDir + "/app.ini"
VersionFile = ConfDir + "/.version"
checkDirExists(ConfDir, LogDir, DataDir)
createDirIfNotExists(ConfDir, LogDir, DataDir)
Installed = IsInstalled()
VersionId = ToNumberVersion(versionString)
}
@@ -107,10 +109,14 @@ func ToNumberVersion(versionString string) int {
}
// 检测目录是否存在
func checkDirExists(path ...string) {
func createDirIfNotExists(path ...string) {
for _, value := range path {
if !utils.FileExist(value) {
logger.Fatal(value + "目录不存在或无权限访问")
if utils.FileExist(value) {
continue
}
err := os.Mkdir(value, 0755)
if err != nil {
logger.Fatal(fmt.Sprintf("创建目录失败:%s", err.Error()))
}
}
}
+1 -2
View File
@@ -73,8 +73,7 @@ func request(req *http.Request, timeout int) ResponseWrapper {
}
func setRequestHeader(req *http.Request) {
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,en;q=0.6")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 golang/gocron")
req.Header.Set("User-Agent", "golang/gocron")
}
func createRequestError(err error) ResponseWrapper {
+3 -2
View File
@@ -2,10 +2,11 @@ package logger
import (
"fmt"
"github.com/cihub/seelog"
"gopkg.in/macaron.v1"
"os"
"runtime"
"github.com/cihub/seelog"
"gopkg.in/macaron.v1"
)
// 日志库
+4 -3
View File
@@ -1,13 +1,14 @@
package notify
import (
"strconv"
"strings"
"time"
"github.com/go-gomail/gomail"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"strconv"
"strings"
"time"
)
// @author qiang.ou<qingqianludao@gmail.com>
+3 -2
View File
@@ -2,8 +2,9 @@ package notify
import (
"fmt"
"github.com/ouqiang/gocron/modules/logger"
"time"
"github.com/ouqiang/gocron/modules/logger"
)
type Message map[string]interface{}
@@ -12,7 +13,7 @@ type Notifiable interface {
Send(msg Message)
}
var queue chan Message = make(chan Message, 100)
var queue = make(chan Message, 100)
func init() {
go run()
+4 -3
View File
@@ -4,13 +4,14 @@ package notify
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/httpclient"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"strconv"
"strings"
"time"
)
type Slack struct{}
+2 -1
View File
@@ -5,8 +5,9 @@ import (
"crypto/x509"
"errors"
"fmt"
"google.golang.org/grpc/credentials"
"io/ioutil"
"google.golang.org/grpc/credentials"
)
type Certificate struct {
+25 -2
View File
@@ -3,19 +3,38 @@ package client
import (
"errors"
"fmt"
"sync"
"time"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/rpc/grpcpool"
pb "github.com/ouqiang/gocron/modules/rpc/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"time"
)
var (
taskMap sync.Map
)
var (
errUnavailable = errors.New("无法连接远程服务器")
)
func generateTaskUniqueKey(ip string, port int, id int64) string {
return fmt.Sprintf("%s:%d:%d", ip, port, id)
}
func Stop(ip string, port int, id int64) {
key := generateTaskUniqueKey(ip, port, id)
cancel, ok := taskMap.Load(key)
if !ok {
return
}
cancel.(context.CancelFunc)()
}
func Exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
defer func() {
if err := recover(); err != nil {
@@ -39,7 +58,9 @@ func Exec(ip string, port int, taskReq *pb.TaskRequest) (string, error) {
}
timeout := time.Duration(taskReq.Timeout) * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
taskUniqueKey := generateTaskUniqueKey(ip, port, taskReq.Id)
taskMap.Store(taskUniqueKey, cancel)
defer taskMap.Delete(taskUniqueKey)
resp, err := c.Run(ctx, taskReq)
if err != nil {
return parseGRPCError(err, conn, &isConnClosed)
@@ -60,6 +81,8 @@ func parseGRPCError(err error, conn *grpc.ClientConn, connClosed *bool) (string,
return "", errUnavailable
case codes.DeadlineExceeded:
return "", errors.New("执行超时, 强制结束")
case codes.Canceled:
return "", errors.New("手动停止")
}
return "", err
}
+4 -3
View File
@@ -2,13 +2,14 @@ package grpcpool
import (
"errors"
"strings"
"sync"
"time"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/rpc/auth"
"github.com/silenceper/pool"
"google.golang.org/grpc"
"strings"
"sync"
"time"
)
var (
+21 -12
View File
@@ -36,6 +36,7 @@ const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type TaskRequest struct {
Command string `protobuf:"bytes,2,opt,name=command" json:"command,omitempty"`
Timeout int32 `protobuf:"varint,3,opt,name=timeout" json:"timeout,omitempty"`
Id int64 `protobuf:"varint,4,opt,name=id" json:"id,omitempty"`
}
func (m *TaskRequest) Reset() { *m = TaskRequest{} }
@@ -57,6 +58,13 @@ func (m *TaskRequest) GetTimeout() int32 {
return 0
}
func (m *TaskRequest) GetId() int64 {
if m != nil {
return m.Id
}
return 0
}
type TaskResponse struct {
Output string `protobuf:"bytes,1,opt,name=output" json:"output,omitempty"`
Error string `protobuf:"bytes,2,opt,name=error" json:"error,omitempty"`
@@ -161,16 +169,17 @@ var _Task_serviceDesc = grpc.ServiceDesc{
func init() { proto.RegisterFile("task.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 170 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0xb1, 0x0e, 0x82, 0x40,
0x0c, 0x86, 0x45, 0x04, 0x63, 0x75, 0xd0, 0xc6, 0x98, 0x8b, 0x13, 0x61, 0x62, 0x30, 0x0c, 0xea,
0xe8, 0xe2, 0x2b, 0x5c, 0x7c, 0x01, 0xc4, 0x1b, 0x0c, 0x81, 0x9e, 0xbd, 0xde, 0xfb, 0x1b, 0x38,
0x48, 0x18, 0xbf, 0x36, 0xfd, 0xfa, 0xff, 0x00, 0x52, 0xb9, 0xa6, 0xb4, 0x4c, 0x42, 0x18, 0xb3,
0xad, 0xf3, 0x27, 0x6c, 0x5f, 0x95, 0x6b, 0xb4, 0xf9, 0x79, 0xe3, 0x04, 0x15, 0xac, 0x6b, 0x6a,
0xdb, 0xaa, 0xfb, 0xa8, 0x65, 0x16, 0x15, 0x1b, 0x3d, 0x61, 0xbf, 0x91, 0x6f, 0x6b, 0xc8, 0x8b,
0x8a, 0xb3, 0xa8, 0x48, 0xf4, 0x84, 0xf9, 0x03, 0x76, 0x41, 0xe1, 0x2c, 0x75, 0xce, 0xe0, 0x09,
0x52, 0xf2, 0x62, 0xbd, 0xa8, 0x68, 0x50, 0x8c, 0x84, 0x47, 0x48, 0x0c, 0x33, 0xf1, 0x68, 0x0e,
0x70, 0xbd, 0xc3, 0xaa, 0xbf, 0xc6, 0x0b, 0xc4, 0xda, 0x77, 0xb8, 0x2f, 0xd9, 0xd6, 0xe5, 0x2c,
0xd2, 0xf9, 0x30, 0x9b, 0x84, 0x0f, 0xf9, 0xe2, 0x9d, 0x0e, 0x15, 0x6e, 0xff, 0x00, 0x00, 0x00,
0xff, 0xff, 0xef, 0x3c, 0x71, 0x6b, 0xd0, 0x00, 0x00, 0x00,
// 184 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0xcf, 0x8a, 0x83, 0x30,
0x10, 0xc6, 0x37, 0x46, 0x5d, 0x76, 0x76, 0x59, 0xda, 0xa1, 0x94, 0xd0, 0x93, 0x78, 0xf2, 0x50,
0x3c, 0xb4, 0x3d, 0xf6, 0x25, 0x1a, 0xfa, 0x02, 0x56, 0x73, 0x10, 0xd1, 0x49, 0x93, 0xc9, 0xfb,
0x17, 0xff, 0x81, 0xc7, 0xdf, 0x0c, 0xf3, 0xfd, 0xe6, 0x03, 0xe0, 0xca, 0x77, 0xa5, 0x75, 0xc4,
0x84, 0xd2, 0xd9, 0x3a, 0x7f, 0xc0, 0xef, 0xb3, 0xf2, 0x9d, 0x36, 0xef, 0x60, 0x3c, 0xa3, 0x82,
0xef, 0x9a, 0xfa, 0xbe, 0x1a, 0x1a, 0x15, 0x65, 0xa2, 0xf8, 0xd1, 0x2b, 0x8e, 0x1b, 0x6e, 0x7b,
0x43, 0x81, 0x95, 0xcc, 0x44, 0x91, 0xe8, 0x15, 0xf1, 0x1f, 0xa2, 0xb6, 0x51, 0x71, 0x26, 0x0a,
0xa9, 0xa3, 0xb6, 0xc9, 0xef, 0xf0, 0x37, 0x47, 0x7a, 0x4b, 0x83, 0x37, 0x78, 0x84, 0x94, 0x02,
0xdb, 0xc0, 0x4a, 0x4c, 0x91, 0x0b, 0xe1, 0x01, 0x12, 0xe3, 0x1c, 0xb9, 0xc5, 0x34, 0xc3, 0xe5,
0x06, 0xf1, 0x78, 0x8d, 0x67, 0x90, 0x3a, 0x0c, 0xb8, 0x2b, 0x9d, 0xad, 0xcb, 0xcd, 0x8b, 0xa7,
0xfd, 0x66, 0x32, 0x1b, 0xf2, 0xaf, 0x57, 0x3a, 0x55, 0xba, 0x7e, 0x02, 0x00, 0x00, 0xff, 0xff,
0xd7, 0x7f, 0x8a, 0x9d, 0xe0, 0x00, 0x00, 0x00,
}
+1
View File
@@ -9,6 +9,7 @@ service Task {
message TaskRequest {
string command = 2; // 命令
int32 timeout = 3; // 任务执行超时时间
int64 id = 4; // 执行任务唯一ID
}
message TaskResponse {
+2 -7
View File
@@ -1,6 +1,8 @@
package server
import (
"net"
"github.com/ouqiang/gocron/modules/rpc/auth"
pb "github.com/ouqiang/gocron/modules/rpc/proto"
"github.com/ouqiang/gocron/modules/utils"
@@ -8,7 +10,6 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"net"
)
type Server struct{}
@@ -32,12 +33,6 @@ func (s Server) Run(ctx context.Context, req *pb.TaskRequest) (*pb.TaskResponse,
}
func Start(addr string, enableTLS bool, certificate auth.Certificate) {
defer func() {
if err := recover(); err != nil {
grpclog.Println("panic", err)
}
}()
l, err := net.Listen("tcp", addr)
if err != nil {
grpclog.Fatal(err)
+4
View File
@@ -2,6 +2,7 @@ package setting
import (
"errors"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"gopkg.in/ini.v1"
@@ -32,6 +33,8 @@ type Setting struct {
CAFile string
CertFile string
KeyFile string
ConcurrencyQueue int
}
// 读取配置
@@ -60,6 +63,7 @@ func Read(filename string) (*Setting, error) {
s.ApiKey = section.Key("api.key").MustString("")
s.ApiSecret = section.Key("api.secret").MustString("")
s.ApiSignEnable = section.Key("api.sign.enable").MustBool(true)
s.ConcurrencyQueue = section.Key("concurrency.queue").MustInt(500)
s.EnableTLS = section.Key("enable_tls").MustBool(false)
s.CAFile = section.Key("ca_file").MustString("")
-139
View File
@@ -1,139 +0,0 @@
package ssh
import (
"errors"
"fmt"
"golang.org/x/crypto/ssh"
"net"
"time"
)
type HostAuthType int8 // 认证方式
const (
HostPassword = 1 // 密码认证
HostPublicKey = 2 // 公钥认证
)
const SSHConnectTimeout = 10
type SSHConfig struct {
AuthType HostAuthType
User string
Password string
PrivateKey string
Host string
Port int
ExecTimeout int // 执行超时时间
}
type Result struct {
Output string
Err error
}
func parseSSHConfig(sshConfig SSHConfig) (config *ssh.ClientConfig, err error) {
timeout := time.Duration(SSHConnectTimeout) * time.Second
// 密码认证
if sshConfig.AuthType == HostPassword {
config = &ssh.ClientConfig{
User: sshConfig.User,
Auth: []ssh.AuthMethod{
ssh.Password(sshConfig.Password),
},
Timeout: timeout,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
return
}
signer, err := ssh.ParsePrivateKey([]byte(sshConfig.PrivateKey))
if err != nil {
return
}
// 公钥认证
config = &ssh.ClientConfig{
User: sshConfig.User,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
Timeout: timeout,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
return
}
// 执行shell命令
func Exec(sshConfig SSHConfig, cmd string) (output string, err error) {
client, err := getClient(sshConfig)
if err != nil {
return "", err
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return "", err
}
defer session.Close()
// 后台运行
if sshConfig.ExecTimeout < 0 {
go session.CombinedOutput(cmd)
time.Sleep(10 * time.Second)
return "", nil
}
// 不限制超时
if sshConfig.ExecTimeout == 0 {
outputByte, execErr := session.CombinedOutput(cmd)
output = string(outputByte)
err = execErr
return
}
var resultChan chan Result = make(chan Result)
var timeoutChan chan bool = make(chan bool)
go func() {
output, err := session.CombinedOutput(cmd)
resultChan <- Result{string(output), err}
}()
// todo 等待超时后,如何停止远程正在执行的任务, 使用timeout命令,但不具有通用性
go triggerTimeout(timeoutChan, sshConfig.ExecTimeout)
select {
case result := <-resultChan:
output = result.Output
err = result.Err
case <-timeoutChan:
output = ""
err = errors.New("timeout")
}
return
}
func getClient(sshConfig SSHConfig) (*ssh.Client, error) {
config, err := parseSSHConfig(sshConfig)
if err != nil {
return nil, err
}
addr := fmt.Sprintf("%s:%d", sshConfig.Host, sshConfig.Port)
return ssh.Dial("tcp", addr, config)
}
func triggerTimeout(ch chan bool, timeout int) {
// 最长执行时间不能超过24小时
if timeout <= 0 || timeout > 86400 {
timeout = 86400
}
time.Sleep(time.Duration(timeout) * time.Second)
close(ch)
}
+1
View File
@@ -2,6 +2,7 @@ package utils
import (
"encoding/json"
"github.com/ouqiang/gocron/modules/logger"
)
+2 -13
View File
@@ -3,13 +3,12 @@ package utils
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/Tang-RoseChild/mahonia"
"math/rand"
"os"
"runtime"
"strings"
"time"
"github.com/Tang-RoseChild/mahonia"
)
// 生成长度为length的随机字符串
@@ -41,11 +40,6 @@ func RandNumber(max int) int {
return r.Intn(max)
}
// 判断当前系统是否是windows
func IsWindows() bool {
return runtime.GOOS == "windows"
}
// GBK编码转换为UTF8
func GBK2UTF8(s string) (string, bool) {
dec := mahonia.NewDecoder("gbk")
@@ -100,8 +94,3 @@ func FileExist(file string) bool {
return true
}
// 格式化环境变量
func FormatUnixEnv(key, value string) string {
return fmt.Sprintf("export %s=%s; ", key, value)
}
+3 -2
View File
@@ -4,9 +4,10 @@ package utils
import (
"errors"
"golang.org/x/net/context"
"os/exec"
"syscall"
"golang.org/x/net/context"
)
type Result struct {
@@ -20,7 +21,7 @@ func ExecShell(ctx context.Context, command string) (string, error) {
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
var resultChan chan Result = make(chan Result)
resultChan := make(chan Result)
go func() {
output, err := cmd.CombinedOutput()
resultChan <- Result{string(output), err}
+2 -1
View File
@@ -4,10 +4,11 @@ package utils
import (
"errors"
"golang.org/x/net/context"
"os/exec"
"strconv"
"syscall"
"golang.org/x/net/context"
)
type Result struct {
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
#set -x -u
# 构建应用, 生成压缩包 gocron.zip或gocron.tar.gz
# ./package.sh -v 1.4
VERSION=''
GOCRON_APP_NAME='gocron'
GOCRON_NODE_APP_NAME='gocron-node'
PACKAGE_DIR='./packages'
# 用法
usage() {
echo 'usage: ./package.sh -v version'
}
# 初始化
init() {
rm -rf ${PACKAGE_DIR}
mkdir -p ${PACKAGE_DIR}
}
# 构建应用
build() {
make -f makefile.cross-compiles VERSION=${VERSION}
if [[ $? -ne 0 ]];then
echo 'make error'
exit 1
fi
}
# 清理
clean() {
make -f makefile.cross-compiles clean
}
# 打包gocron
package_gocron() {
local OS=$1
local GOCRON_COMPRESS_FILE=''
local PLATFORM_NAME=${GOCRON_APP_NAME}_${OS}_amd64
if [[ ! -d ${PACKAGE_DIR}/${PLATFORM_NAME} ]];then
mkdir ${PACKAGE_DIR}/${PLATFORM_NAME}
fi
for file in public templates LICENSE README.md Dockerfile-release; do
cp -r ${file} ${PLATFORM_NAME}
done
if [[ ${OS} = 'windows' ]];then
GOCRON_COMPRESS_FILE=${GOCRON_APP_NAME}-v${VERSION}-${OS}-amd64.zip
zip -rq ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_COMPRESS_FILE} ${PLATFORM_NAME}
else
GOCRON_COMPRESS_FILE=${GOCRON_APP_NAME}-v${VERSION}-${OS}-amd64.tar.gz
tar czf ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_COMPRESS_FILE} ${PLATFORM_NAME}
fi
}
# 打包gocron-node
package_gocron_node() {
local OS=$1
local GOCRON_NODE_COMPRESS_FILE=''
local PLATFORM_NAME=${GOCRON_NODE_APP_NAME}_${OS}_amd64
if [[ ! -d ${PACKAGE_DIR}/${PLATFORM_NAME} ]];then
mkdir ${PACKAGE_DIR}/${PLATFORM_NAME}
fi
if [[ ${OS} = 'windows' ]];then
GOCRON_NODE_COMPRESS_FILE=${GOCRON_NODE_APP_NAME}-v${VERSION}-${OS}-amd64.zip
zip -rq ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_NODE_COMPRESS_FILE} ${PLATFORM_NAME}
else
GOCRON_NODE_COMPRESS_FILE=${GOCRON_NODE_APP_NAME}-v${VERSION}-${OS}-amd64.tar.gz
tar czf ${PACKAGE_DIR}/${PLATFORM_NAME}/${GOCRON_NODE_COMPRESS_FILE} ${PLATFORM_NAME}
fi
}
package_multi() {
for os in darwin linux windows; do
package_gocron ${os}
package_gocron_node ${os}
done
}
while getopts "v:" OPT; do
case ${OPT} in
v) VERSION=${OPTARG}
;;
esac
done
if [[ -z ${VERSION} ]]; then
usage
exit 1
fi
run() {
init
build
package_multi
clean
}
run
+6 -6
View File
@@ -2,6 +2,10 @@ package host
import (
"fmt"
"html/template"
"strconv"
"strings"
"github.com/Unknwon/paginater"
"github.com/go-macaron/binding"
"github.com/ouqiang/gocron/models"
@@ -13,9 +17,6 @@ import (
"github.com/ouqiang/gocron/routers/base"
"github.com/ouqiang/gocron/service"
"gopkg.in/macaron.v1"
"html/template"
"strconv"
"strings"
)
// Index 主机列表
@@ -127,8 +128,7 @@ func Store(ctx *macaron.Context, form HostForm) string {
if err != nil {
return json.CommonFailure("刷新任务主机信息失败", err)
}
serviceTask := new(service.Task)
serviceTask.BatchAdd(tasks)
service.ServiceTask.BatchAdd(tasks)
}
return json.Success("保存成功", nil)
@@ -190,7 +190,7 @@ func Ping(ctx *macaron.Context) string {
// 解析查询参数
func parseQueryParams(ctx *macaron.Context) models.CommonMap {
var params models.CommonMap = models.CommonMap{}
var params = models.CommonMap{}
params["Id"] = ctx.QueryInt("id")
params["Name"] = ctx.QueryTrim("name")
base.ParsePageAndPageSize(ctx, params)
+4 -3
View File
@@ -2,6 +2,8 @@ package install
import (
"fmt"
"strconv"
"github.com/go-macaron/binding"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/app"
@@ -9,7 +11,6 @@ import (
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/service"
"gopkg.in/macaron.v1"
"strconv"
)
// 系统安装
@@ -97,8 +98,7 @@ func Store(ctx *macaron.Context, form InstallForm) string {
app.Installed = true
// 初始化定时任务
serviceTask := new(service.Task)
serviceTask.Initialize()
service.ServiceTask.Initialize()
return json.Success("安装成功", nil)
}
@@ -121,6 +121,7 @@ func writeConfig(form InstallForm) error {
"api.key", "",
"api.secret", "",
"enable_tls", "false",
"concurrency.queue", "500",
"ca_file", "",
"cert_file", "",
"key_file", "",
+2 -1
View File
@@ -2,12 +2,13 @@ package loginlog
import (
"fmt"
"html/template"
"github.com/Unknwon/paginater"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/routers/base"
"gopkg.in/macaron.v1"
"html/template"
)
func Index(ctx *macaron.Context) {
+1
View File
@@ -2,6 +2,7 @@ package manage
import (
"encoding/json"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
+18 -20
View File
@@ -1,6 +1,12 @@
package routers
import (
"html/template"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-macaron/binding"
"github.com/go-macaron/cache"
"github.com/go-macaron/captcha"
@@ -18,10 +24,6 @@ import (
"github.com/ouqiang/gocron/routers/tasklog"
"github.com/ouqiang/gocron/routers/user"
"gopkg.in/macaron.v1"
"html/template"
"strconv"
"strings"
"time"
)
// 静态文件目录
@@ -65,6 +67,7 @@ func Register(m *macaron.Macaron) {
m.Get("", task.Index)
m.Get("/log", tasklog.Index)
m.Post("/log/clear", tasklog.Clear)
m.Post("/log/stop", tasklog.Stop)
m.Post("/remove/:id", task.Remove)
m.Post("/enable/:id", task.Enable)
m.Post("/disable/:id", task.Disable)
@@ -112,7 +115,7 @@ func Register(m *macaron.Macaron) {
m.NotFound(func(ctx *macaron.Context) {
if isGetRequest(ctx) && !isAjaxRequest(ctx) {
ctx.Data["Title"] = "404 - NOT FOUND"
ctx.HTML(404, "error/404")
ctx.HTML(http.StatusNotFound, "error/404")
} else {
json := utils.JsonResponse{}
ctx.Resp.Write([]byte(json.Failure(utils.NotFound, "您访问的地址不存在")))
@@ -123,7 +126,7 @@ func Register(m *macaron.Macaron) {
logger.Debug("500错误")
if isGetRequest(ctx) && !isAjaxRequest(ctx) {
ctx.Data["Title"] = "500 - INTERNAL SERVER ERROR"
ctx.HTML(500, "error/500")
ctx.HTML(http.StatusInternalServerError, "error/500")
} else {
json := utils.JsonResponse{}
ctx.Resp.Write([]byte(json.Failure(utils.ServerError, "网站暂时无法访问,请稍后再试")))
@@ -140,16 +143,8 @@ func RegisterMiddleware(m *macaron.Macaron) {
}
m.Use(macaron.Static(StaticDir))
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: "templates",
Extensions: []string{".html"},
// 模板语法分隔符,默认为 ["{{", "}}"]
Delims: macaron.Delims{"{{{", "}}}"},
// 追加的 Content-Type 头信息,默认为 "UTF-8"
Charset: "UTF-8",
// 渲染具有缩进格式的 JSON,默认为不缩进
IndentJSON: true,
// 渲染具有缩进格式的 XML,默认为不缩进
IndentXML: true,
Delims: macaron.Delims{"{%", "%}"},
Funcs: []template.FuncMap{map[string]interface{}{
"HostFormat": func(index int) bool {
return (index+1)%3 == 0
@@ -202,7 +197,7 @@ func ipAuth(ctx *macaron.Context) {
allowIps := strings.Split(allowIpsStr, ",")
if !utils.InStringSlice(allowIps, clientIp) {
logger.Warnf("非法IP访问-%s", clientIp)
ctx.Status(403)
ctx.Status(http.StatusForbidden)
}
}
@@ -226,10 +221,15 @@ func userAuth(ctx *macaron.Context, sess session.Store) {
}
// URL权限验证
func urlAuth(ctx *macaron.Context, sess session.Store) {
func urlAuth(ctx *macaron.Context, sess session.Store) {
if user.IsAdmin(sess) {
return
}
uri := strings.TrimSpace(ctx.Req.URL.Path)
uri = strings.TrimRight(uri, "/")
if strings.HasPrefix(uri, "/api") {
return
}
// 普通用户允许访问的URL地址
allowPaths := []string{
"",
@@ -240,15 +240,13 @@ func urlAuth(ctx *macaron.Context, sess session.Store) {
"/user/logout",
"/user/editMyPassword",
}
uri := strings.TrimSpace(ctx.Req.URL.Path)
uri = strings.TrimRight(uri, "/")
for _, path := range allowPaths {
if path == uri {
return
}
}
ctx.Status(403)
ctx.Status(http.StatusUnauthorized)
}
+23 -16
View File
@@ -2,6 +2,10 @@ package task
import (
"fmt"
"html/template"
"strconv"
"strings"
"github.com/Unknwon/paginater"
"github.com/go-macaron/binding"
"github.com/jakecoffman/cron"
@@ -11,9 +15,6 @@ import (
"github.com/ouqiang/gocron/routers/base"
"github.com/ouqiang/gocron/service"
"gopkg.in/macaron.v1"
"html/template"
"strconv"
"strings"
)
type TaskForm struct {
@@ -23,11 +24,13 @@ type TaskForm struct {
DependencyTaskId string
Name string `binding:"Required;MaxSize(32)"`
Spec string
Protocol models.TaskProtocol `binding:"In(1,2)"`
Command string `binding:"Required;MaxSize(256)"`
Timeout int `binding:"Range(0,86400)"`
Multi int8 `binding:"In(1,2)"`
Protocol models.TaskProtocol `binding:"In(1,2)"`
Command string `binding:"Required;MaxSize(256)"`
HttpMethod models.TaskHTTPMethod `binding:"In(1,2)"`
Timeout int `binding:"Range(0,86400)"`
Multi int8 `binding:"In(1,2)"`
RetryTimes int8
RetryInterval int16
HostId string
Tag string
Remark string
@@ -110,11 +113,11 @@ func Edit(ctx *macaron.Context) {
ctx.HTML(200, "task/task_form")
}
// 保存任务
// 保存任务 todo 拆分为多个方法 快变成意大利面条式代码了
func Store(ctx *macaron.Context, form TaskForm) string {
json := utils.JsonResponse{}
taskModel := models.Task{}
var id int = form.Id
var id = form.Id
nameExists, err := taskModel.NameExist(form.Name, form.Id)
if err != nil {
return json.CommonFailure(utils.FailureContent, err)
@@ -129,12 +132,13 @@ func Store(ctx *macaron.Context, form TaskForm) string {
taskModel.Name = form.Name
taskModel.Protocol = form.Protocol
taskModel.Command = form.Command
taskModel.Command = strings.TrimSpace(form.Command)
taskModel.Timeout = form.Timeout
taskModel.Tag = form.Tag
taskModel.Remark = form.Remark
taskModel.Multi = form.Multi
taskModel.RetryTimes = form.RetryTimes
taskModel.RetryInterval = form.RetryInterval
if taskModel.Multi != 1 {
taskModel.Multi = 0
}
@@ -148,6 +152,7 @@ func Store(ctx *macaron.Context, form TaskForm) string {
if taskModel.NotifyStatus > 0 && taskModel.NotifyReceiverId == "" {
return json.CommonFailure("至少选择一个通知接收者")
}
taskModel.HttpMethod = form.HttpMethod
if taskModel.Protocol == models.TaskHTTP {
command := strings.ToLower(taskModel.Command)
if !strings.HasPrefix(command, "http://") && !strings.HasPrefix(command, "https://") {
@@ -162,6 +167,10 @@ func Store(ctx *macaron.Context, form TaskForm) string {
return json.CommonFailure("任务重试次数取值0-10")
}
if taskModel.RetryInterval > 3600 || taskModel.RetryInterval < 0 {
return json.CommonFailure("任务重试间隔时间取值0-3600")
}
if taskModel.DependencyStatus != models.TaskDependencyStatusStrong &&
taskModel.DependencyStatus != models.TaskDependencyStatusWeak {
return json.CommonFailure("请选择依赖关系")
@@ -229,7 +238,7 @@ func Remove(ctx *macaron.Context) string {
taskHostModel := new(models.TaskHost)
taskHostModel.Remove(id)
service.Cron.RemoveJob(strconv.Itoa(id))
service.ServiceTask.Remove(id)
return json.Success(utils.SuccessContent, nil)
}
@@ -255,8 +264,7 @@ func Run(ctx *macaron.Context) string {
}
task.Spec = "手动运行"
serviceTask := new(service.Task)
serviceTask.Run(task)
service.ServiceTask.Run(task)
return json.Success("任务已开始运行, 请到任务日志中查看结果", nil)
}
@@ -276,7 +284,7 @@ func changeStatus(ctx *macaron.Context, status models.Status) string {
if status == models.Enabled {
addTaskToTimer(id)
} else {
service.Cron.RemoveJob(strconv.Itoa(id))
service.ServiceTask.Remove(id)
}
return json.Success(utils.SuccessContent, nil)
@@ -291,8 +299,7 @@ func addTaskToTimer(id int) {
return
}
taskService := service.Task{}
taskService.Add(task)
service.ServiceTask.RemoveAndAdd(task)
}
// 解析查询参数
+27 -1
View File
@@ -4,13 +4,15 @@ package tasklog
import (
"fmt"
"html/template"
"github.com/Unknwon/paginater"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/routers/base"
"github.com/ouqiang/gocron/service"
"gopkg.in/macaron.v1"
"html/template"
)
func Index(ctx *macaron.Context) {
@@ -48,6 +50,30 @@ func Clear(ctx *macaron.Context) string {
return json.Success(utils.SuccessContent, nil)
}
// 停止运行中的任务
func Stop(ctx *macaron.Context) string {
id := ctx.QueryInt64("id")
taskId := ctx.QueryInt("task_id")
taskModel := new(models.Task)
task, err := taskModel.Detail(taskId)
json := utils.JsonResponse{}
if err != nil {
return json.CommonFailure("获取任务信息失败#"+err.Error(), err)
}
if task.Protocol != models.TaskRPC {
return json.CommonFailure("仅支持SHELL任务手动停止")
}
if len(task.Hosts) == 0 {
return json.CommonFailure("任务节点列表为空")
}
for _, host := range task.Hosts {
service.ServiceTask.Stop(host.Name, host.Port, id)
}
return json.Success("已执行停止操作, 请等待任务退出", nil)
}
// 删除N个月前的日志
func Remove(ctx *macaron.Context) string {
month := ctx.ParamsInt(":id")
+17 -17
View File
@@ -1,6 +1,11 @@
package user
import (
"fmt"
"html/template"
"strings"
"github.com/Unknwon/paginater"
"github.com/go-macaron/captcha"
"github.com/go-macaron/session"
"github.com/ouqiang/gocron/models"
@@ -8,10 +13,6 @@ import (
"github.com/ouqiang/gocron/modules/utils"
"github.com/ouqiang/gocron/routers/base"
"gopkg.in/macaron.v1"
"github.com/Unknwon/paginater"
"html/template"
"fmt"
"strings"
)
// @author qiang.ou<qingqianludao@gmail.com>
@@ -19,17 +20,17 @@ import (
// UserForm 用户表单
type UserForm struct {
Id int
Name string `binding:"Required;MaxSize(32)"` // 用户名
Password string // 密码
ConfirmPassword string // 确认密码
Email string `binding:"Required;MaxSize(50)"` // 邮箱
IsAdmin int8 // 是否是管理员 1:管理员 0:普通用户
Status models.Status
Id int
Name string `binding:"Required;MaxSize(32)"` // 用户名
Password string // 密码
ConfirmPassword string // 确认密码
Email string `binding:"Required;MaxSize(50)"` // 邮箱
IsAdmin int8 // 是否是管理员 1:管理员 0:普通用户
Status models.Status
}
// Index 用户列表页
func Index(ctx *macaron.Context) {
func Index(ctx *macaron.Context) {
queryParams := parseQueryParams(ctx)
userModel := new(models.User)
users, err := userModel.List(queryParams)
@@ -59,7 +60,7 @@ func parseQueryParams(ctx *macaron.Context) models.CommonMap {
}
// Create 新增用户页
func Create(ctx *macaron.Context) {
func Create(ctx *macaron.Context) {
userModel := new(models.User)
userModel.Status = models.Enabled
userModel.IsAdmin = 0
@@ -129,9 +130,9 @@ func Store(ctx *macaron.Context, form UserForm) string {
}
} else {
_, err = userModel.Update(form.Id, models.CommonMap{
"name": form.Name,
"email": form.Email,
"status": form.Status,
"name": form.Name,
"email": form.Email,
"status": form.Status,
"is_admin": form.IsAdmin,
})
if err != nil {
@@ -139,7 +140,6 @@ func Store(ctx *macaron.Context, form UserForm) string {
}
}
return json.Success("保存成功", nil)
}

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 295 KiB

+148 -90
View File
@@ -3,81 +3,96 @@ package service
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/jakecoffman/cron"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/app"
"github.com/ouqiang/gocron/modules/httpclient"
"github.com/ouqiang/gocron/modules/logger"
"github.com/ouqiang/gocron/modules/notify"
rpcClient "github.com/ouqiang/gocron/modules/rpc/client"
pb "github.com/ouqiang/gocron/modules/rpc/proto"
"strconv"
"strings"
"sync"
"time"
)
// 定时任务调度管理器
var Cron *cron.Cron
var (
ServiceTask Task
)
// 同一任务是否有实例处于运行中
var runInstance Instance
var (
// 定时任务调度管理器
serviceCron *cron.Cron
// 任务计数-正在运行中的任务
var TaskNum TaskCount
// 同一任务是否有实例处于运行中
runInstance Instance
// 任务计数-正在运行的任务
taskCount TaskCount
// 并发队列, 限制同时运行的任务数量
concurrencyQueue ConcurrencyQueue
)
// 并发队列
type ConcurrencyQueue struct {
queue chan struct{}
}
func (cq *ConcurrencyQueue) Add() {
cq.queue <- struct{}{}
}
func (cq *ConcurrencyQueue) Done() {
<-cq.queue
}
// 任务计数
type TaskCount struct {
num int
sync.RWMutex
wg sync.WaitGroup
exit chan struct{}
}
func (c *TaskCount) Add() {
c.Lock()
defer c.Unlock()
c.num += 1
func (tc *TaskCount) Add() {
tc.wg.Add(1)
}
func (c *TaskCount) Done() {
c.Lock()
defer c.Unlock()
c.num -= 1
func (tc *TaskCount) Done() {
tc.wg.Done()
}
func (c *TaskCount) Num() int {
c.RLock()
defer c.RUnlock()
func (tc *TaskCount) Exit() {
tc.wg.Done()
<-tc.exit
}
return c.num
func (tc *TaskCount) Wait() {
tc.Add()
tc.wg.Wait()
close(tc.exit)
}
// 任务ID作为Key
type Instance struct {
Status map[int]bool
sync.RWMutex
m sync.Map
}
// 是否有任务处于运行中
func (i *Instance) has(key int) bool {
i.RLock()
defer i.RUnlock()
running, ok := i.Status[key]
if ok && running {
return true
}
_, ok := i.m.Load(key)
return false
return ok
}
func (i *Instance) add(key int) {
i.Lock()
defer i.Unlock()
i.Status[key] = true
i.m.Store(key, struct{}{})
}
func (i *Instance) done(key int) {
i.Lock()
defer i.Unlock()
delete(i.Status, key)
i.m.Delete(key)
}
type Task struct{}
@@ -89,34 +104,51 @@ type TaskResult struct {
}
// 初始化任务, 从数据库取出所有任务, 添加到定时任务并运行
func (task *Task) Initialize() {
Cron = cron.New()
Cron.Start()
runInstance = Instance{make(map[int]bool), sync.RWMutex{}}
TaskNum = TaskCount{0, sync.RWMutex{}}
func (task Task) Initialize() {
serviceCron = cron.New()
serviceCron.Start()
concurrencyQueue = ConcurrencyQueue{queue: make(chan struct{}, app.Setting.ConcurrencyQueue)}
taskCount = TaskCount{sync.WaitGroup{}, make(chan struct{})}
go taskCount.Wait()
logger.Info("开始初始化定时任务")
taskModel := new(models.Task)
taskList, err := taskModel.ActiveList()
if err != nil {
logger.Error("定时任务初始化#获取任务列表错误-", err.Error())
return
taskNum := 0
page := 1
pageSize := 1000
maxPage := 1000
for page < maxPage {
taskList, err := taskModel.ActiveList(page, pageSize)
if err != nil {
logger.Fatalf("定时任务初始化#获取任务列表错误-", err.Error())
}
if len(taskList) == 0 {
break
}
for _, item := range taskList {
task.Add(item)
taskNum++
}
page++
}
if len(taskList) == 0 {
logger.Debug("任务列表为空")
return
}
task.BatchAdd(taskList)
logger.Infof("定时任务初始化完成, 共%d个定时任务添加到调度器", taskNum)
}
// 批量添加任务
func (task *Task) BatchAdd(tasks []models.Task) {
func (task Task) BatchAdd(tasks []models.Task) {
for _, item := range tasks {
task.Add(item)
task.RemoveAndAdd(item)
}
}
// 删除任务后添加
func (task Task) RemoveAndAdd(taskModel models.Task) {
task.Remove(taskModel.Id)
task.Add(taskModel)
}
// 添加任务
func (task *Task) Add(taskModel models.Task) {
func (task Task) Add(taskModel models.Task) {
if taskModel.Level == models.TaskLevelChild {
logger.Errorf("添加任务失败#不允许添加子任务到调度器#任务Id-%d", taskModel.Id)
return
@@ -128,26 +160,34 @@ func (task *Task) Add(taskModel models.Task) {
}
cronName := strconv.Itoa(taskModel.Id)
// Cron任务采用数组存储, 删除任务需遍历数组, 并对数组重新赋值, 任务较多时,有性能问题
Cron.RemoveJob(cronName)
err := Cron.AddFunc(taskModel.Spec, taskFunc, cronName)
err := serviceCron.AddFunc(taskModel.Spec, taskFunc, cronName)
if err != nil {
logger.Error("添加任务到调度器失败#", err)
}
}
// 停止所有任务
func (task *Task) StopAll() {
Cron.Stop()
// 停止运行中的任务
func (task Task) Stop(ip string, port int, id int64) {
rpcClient.Stop(ip, port, id)
}
func (task Task) Remove(id int) {
serviceCron.RemoveJob(strconv.Itoa(id))
}
// 等待所有任务结束后退出
func (task Task) WaitAndExit() {
serviceCron.Stop()
taskCount.Exit()
}
// 直接运行任务
func (task *Task) Run(taskModel models.Task) {
func (task Task) Run(taskModel models.Task) {
go createJob(taskModel)()
}
type Handler interface {
Run(taskModel models.Task) (string, error)
Run(taskModel models.Task, taskUniqueId int64) (string, error)
}
// HTTP任务
@@ -156,13 +196,24 @@ type HTTPHandler struct{}
// http任务执行时间不超过300秒
const HttpExecTimeout = 300
func (h *HTTPHandler) Run(taskModel models.Task) (result string, err error) {
func (h *HTTPHandler) Run(taskModel models.Task, taskUniqueId int64) (result string, err error) {
if taskModel.Timeout <= 0 || taskModel.Timeout > HttpExecTimeout {
taskModel.Timeout = HttpExecTimeout
}
resp := httpclient.Get(taskModel.Command, taskModel.Timeout)
var resp httpclient.ResponseWrapper
if taskModel.HttpMethod == models.TaskHTTPMethodGet {
resp = httpclient.Get(taskModel.Command, taskModel.Timeout)
} else {
urlFields := strings.Split(taskModel.Command, "?")
taskModel.Command = urlFields[0]
var params string
if len(urlFields) >= 2 {
params = urlFields[1]
}
resp = httpclient.PostParams(taskModel.Command, params, taskModel.Timeout)
}
// 返回状态码非200,均为失败
if resp.StatusCode != 200 {
if resp.StatusCode != http.StatusOK {
return resp.Body, errors.New(fmt.Sprintf("HTTP状态码非200-->%d", resp.StatusCode))
}
@@ -172,15 +223,16 @@ func (h *HTTPHandler) Run(taskModel models.Task) (result string, err error) {
// RPC调用执行任务
type RPCHandler struct{}
func (h *RPCHandler) Run(taskModel models.Task) (result string, err error) {
func (h *RPCHandler) Run(taskModel models.Task, taskUniqueId int64) (result string, err error) {
taskRequest := new(pb.TaskRequest)
taskRequest.Timeout = int32(taskModel.Timeout)
taskRequest.Command = taskModel.Command
var resultChan chan TaskResult = make(chan TaskResult, len(taskModel.Hosts))
taskRequest.Id = taskUniqueId
resultChan := make(chan TaskResult, len(taskModel.Hosts))
for _, taskHost := range taskModel.Hosts {
go func(th models.TaskHostDetail) {
output, err := rpcClient.Exec(th.Name, th.Port, taskRequest)
var errorMessage string = ""
errorMessage := ""
if err != nil {
errorMessage = err.Error()
}
@@ -192,7 +244,7 @@ func (h *RPCHandler) Run(taskModel models.Task) (result string, err error) {
}
var aggregationErr error = nil
var aggregationResult string = ""
aggregationResult := ""
for i := 0; i < len(taskModel.Hosts); i++ {
taskResult := <-resultChan
aggregationResult += taskResult.Result
@@ -214,7 +266,7 @@ func createTaskLog(taskModel models.Task, status models.Status) (int64, error) {
taskLogModel.Command = taskModel.Command
taskLogModel.Timeout = taskModel.Timeout
if taskModel.Protocol == models.TaskRPC {
var aggregationHost string = ""
aggregationHost := ""
for _, host := range taskModel.Hosts {
aggregationHost += fmt.Sprintf("%s-%s<br>", host.Alias, host.Name)
}
@@ -231,7 +283,7 @@ func createTaskLog(taskModel models.Task, status models.Status) (int64, error) {
func updateTaskLog(taskLogId int64, taskResult TaskResult) (int64, error) {
taskLogModel := new(models.TaskLog)
var status models.Status
var result string = taskResult.Result
result := taskResult.Result
if taskResult.Err != nil {
status = models.Failure
} else {
@@ -246,19 +298,29 @@ func updateTaskLog(taskLogId int64, taskResult TaskResult) (int64, error) {
}
func createJob(taskModel models.Task) cron.FuncJob {
var handler Handler = createHandler(taskModel)
handler := createHandler(taskModel)
if handler == nil {
return nil
}
taskFunc := func() {
TaskNum.Add()
defer TaskNum.Done()
taskCount.Add()
defer taskCount.Done()
taskLogId := beforeExecJob(taskModel)
if taskLogId <= 0 {
return
}
if taskModel.Multi == 0 {
runInstance.add(taskModel.Id)
defer runInstance.done(taskModel.Id)
}
concurrencyQueue.Add()
defer concurrencyQueue.Done()
logger.Infof("开始执行任务#%s#命令-%s", taskModel.Name, taskModel.Command)
taskResult := execJob(handler, taskModel)
taskResult := execJob(handler, taskModel, taskLogId)
logger.Infof("任务完成#%s#命令-%s", taskModel.Name, taskModel.Command)
afterExecJob(taskModel, taskResult, taskLogId)
}
@@ -284,15 +346,11 @@ func beforeExecJob(taskModel models.Task) (taskLogId int64) {
createTaskLog(taskModel, models.Cancel)
return
}
if taskModel.Multi == 0 {
runInstance.add(taskModel.Id)
}
taskLogId, err := createTaskLog(taskModel, models.Running)
if err != nil {
logger.Error("任务开始执行#写入任务日志失败-", err)
return
}
logger.Debugf("任务命令-%s", taskModel.Command)
return taskLogId
@@ -340,10 +398,9 @@ func execDependencyTask(taskModel models.Task, taskResult TaskResult) {
if len(tasks) == 0 {
logger.Errorf("依赖任务列表为空#主任务ID-%d", taskModel.Id)
}
serviceTask := new(Task)
for _, task := range tasks {
task.Spec = fmt.Sprintf("依赖任务(主任务ID-%d)", taskModel.Id)
serviceTask.Run(task)
ServiceTask.Run(task)
}
}
@@ -379,15 +436,12 @@ func SendNotification(taskModel models.Task, taskResult TaskResult) {
}
// 执行具体任务
func execJob(handler Handler, taskModel models.Task) TaskResult {
func execJob(handler Handler, taskModel models.Task, taskUniqueId int64) TaskResult {
defer func() {
if err := recover(); err != nil {
logger.Error("panic#service/task.go:execJob#", err)
}
}()
if taskModel.Multi == 0 {
defer runInstance.done(taskModel.Id)
}
// 默认只运行任务一次
var execTimes int8 = 1
if taskModel.RetryTimes > 0 {
@@ -397,15 +451,19 @@ func execJob(handler Handler, taskModel models.Task) TaskResult {
var output string
var err error
for i < execTimes {
output, err = handler.Run(taskModel)
output, err = handler.Run(taskModel, taskUniqueId)
if err == nil {
return TaskResult{Result: output, Err: err, RetryTimes: i}
}
i++
if i < execTimes {
logger.Warnf("任务执行失败#任务id-%d#重试第%d次#输出-%s#错误-%s", taskModel.Id, i, output, err.Error())
// 重试间隔时间,每次递增1分钟
time.Sleep(time.Duration(i) * time.Minute)
if taskModel.RetryInterval > 0 {
time.Sleep(time.Duration(taskModel.RetryInterval) * time.Second)
} else {
// 默认重试间隔时间,每次递增1分钟
time.Sleep(time.Duration(i) * time.Minute)
}
}
}
+14 -14
View File
@@ -4,7 +4,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="renderer" content="webkit">
<title>{{{.Title}}}</title>
<title>{%.Title%}</title>
<link type="text/css" rel="stylesheet" href="/resource/css/framework.css" />
<link type="text/css" rel="stylesheet" href="/resource/css/main.css" />
<link type="text/css" rel="stylesheet" href="/resource/sweetalert/sweetalert.css" />
@@ -29,29 +29,29 @@
</head>
<body>
<div class="page">
{{{if not .DisableNav}}}
{%if not .DisableNav%}
<!--header begin-->
<header>
<div class="bigcontainer">
<div id="logo">
{{{.AppName}}}
{%.AppName%}
</div>
<div class="user">
<div class="ui inline labeled icon top right pointing dropdown">
{{{if not .LoginUsername}}}
{%if not .LoginUsername%}
你好
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="/user/login"><i class="sign in icon"></i>登录</a>
</div>
{{{else}}}
你好{{{.LoginUsername}}}
{%else%}
你好{%.LoginUsername%}
<i class="dropdown icon"></i>
<div class="menu">
<a href="/user/editMyPassword" class="item"><i class="edit icon"></i>修改密码</a>
<a class="item" href="/user/logout"><i class="sign out icon"></i>退出</a>
</div>
{{{end}}}
{%end%}
</div>
</div>
</div>
@@ -60,13 +60,13 @@
<div class="ui teal inverted menu">
<div class="bigcontainer">
<div class="right menu">
<a class="item {{{if or (eq .Controller "task") (eq .Controller "delaytask")}}}active{{{end}}}" href="/task"><i class="tasks icon"></i>任务</a>
<a class="item {{{if eq .Controller "host"}}}active{{{end}}}" href="/host"><i class="linux icon"></i>任务节点</a>
{{{if .IsAdmin}}}
<a class="item {{{if eq .Controller "user"}}}active{{{end}}}" href="/user"><i class="user icon"></i>用户管理</a>
<a class="item {{{if eq .Controller "manage"}}}active{{{end}}}" href="/manage/slack/edit"><i class="settings icon"></i>系统管理</a>
{{{end}}}
<a class="item {%if or (eq .Controller "task") (eq .Controller "delaytask")%}active{%end%}" href="/task"><i class="tasks icon"></i>任务</a>
<a class="item {%if eq .Controller "host"%}active{%end%}" href="/host"><i class="linux icon"></i>任务节点</a>
{%if .IsAdmin%}
<a class="item {%if eq .Controller "user"%}active{%end%}" href="/user"><i class="user icon"></i>用户管理</a>
<a class="item {%if eq .Controller "manage"%}active{%end%}" href="/manage/slack/edit"><i class="settings icon"></i>系统管理</a>
{%end%}
</div>
</div>
</div>
{{{end}}}
{%end%}
+22 -22
View File
@@ -1,41 +1,41 @@
{{{if gt .Pagination.Total 0}}}
{%if gt .Pagination.Total 0%}
<div class="ui pagination menu">
{{{if not .Pagination.IsFirst}}}
<a class="item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page=1">
{%if not .Pagination.IsFirst%}
<a class="item" href="{%.URI%}?{% .Params.PageParams%}&page=1">
首页
</a>
{{{end}}}
{%end%}
{{{if .Pagination.HasPrevious}}}
<a class="icon item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page={{{.Pagination.Previous}}}">
{%if .Pagination.HasPrevious%}
<a class="icon item" href="{%.URI%}?{% .Params.PageParams%}&page={%.Pagination.Previous%}">
<i class="icon left arrow"></i>
</a>
{{{end}}}
{%end%}
{{{range .Pagination.Pages}}}
{{{if eq .Num -1}}}
{%range .Pagination.Pages%}
{%if eq .Num -1%}
<div class="disabled item">
...
</div>
{{{else}}}
<a class="icon {{{if .IsCurrent}}}active{{{end}}} item" href="{{{$.URI}}}?{{{ $.Params.PageParams}}}&page={{{.Num}}}">
{{{.Num}}}
{%else%}
<a class="icon {%if .IsCurrent%}active{%end%} item" href="{%$.URI%}?{% $.Params.PageParams%}&page={%.Num%}">
{%.Num%}
</a>
{{{end}}}
{{{end}}}
{%end%}
{%end%}
{{{if .Pagination.HasNext}}}
<a class="icon item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page={{{.Pagination.Next}}}">
{%if .Pagination.HasNext%}
<a class="icon item" href="{%.URI%}?{% .Params.PageParams%}&page={%.Pagination.Next%}">
<i class="icon right arrow"></i>
</a>
{{{end}}}
{{{if not .Pagination.IsLast}}}
<a class="icon item" href="{{{.URI}}}?{{{ .Params.PageParams}}}&page={{{.Pagination.TotalPages}}}">
{%end%}
{%if not .Pagination.IsLast%}
<a class="icon item" href="{%.URI%}?{% .Params.PageParams%}&page={%.Pagination.TotalPages%}">
尾页
</a>
{{{end}}}
<div class="item">{{{.Pagination.Total}}}/{{{.Pagination.TotalPages}}}</div>
{%end%}
<div class="item">{%.Pagination.Total%}/{%.Pagination.TotalPages%}</div>
</div>
{{{end}}}
{%end%}
+2 -2
View File
@@ -1,4 +1,4 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<script>
swal({
title: "404 - NOT FOUND",
@@ -9,4 +9,4 @@
location.href = "/"
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+2 -2
View File
@@ -1,4 +1,4 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<script>
swal({
title: "500 - INTERNAL SERVER ERROR",
@@ -8,4 +8,4 @@
confirmButtonText: "确定"
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+2 -2
View File
@@ -1,3 +1,3 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+8 -8
View File
@@ -1,7 +1,7 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{ template "host/menu" . }}}
{% template "host/menu" . %}
<div class="twelve wide column">
<div class="pageHeader">
@@ -14,24 +14,24 @@
</div>
</div>
<form class="ui form fluid vertical segment">
<input type="hidden" name="id" value="{{{.Host.Id}}}">
<input type="hidden" name="id" value="{%.Host.Id%}">
<div class="four fields">
<div class="field">
<label>主机名</label>
<div class="ui small input">
<input type="text" name="name" value="{{{.Host.Name}}}" placeholder="127.0.0.1">
<input type="text" name="name" value="{%.Host.Name%}" placeholder="127.0.0.1">
</div>
</div>
<div class="field">
<label>端口</label>
<div class="ui small input">
<input type="text" name="port" value="{{{.Host.Port}}}" placeholder="5921">
<input type="text" name="port" value="{%.Host.Port%}" placeholder="5921">
</div>
</div>
<div class="field">
<label>节点名称</label>
<div class="ui small input">
<input type="text" name="alias" value="{{{.Host.Alias}}}"
<input type="text" name="alias" value="{%.Host.Alias%}"
placeholder="节点名称如web">
</div>
</div>
@@ -40,7 +40,7 @@
<div class="field">
<label>备注</label>
<div class="ui small input">
<textarea rows="7" name="remark" >{{{.Host.Remark}}}</textarea>
<textarea rows="7" name="remark" >{%.Host.Remark%}</textarea>
</div>
</div>
</div>
@@ -113,4 +113,4 @@
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+23 -23
View File
@@ -1,10 +1,10 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{ template "host/menu" . }}}
{% template "host/menu" . %}
<div class="twelve wide column">
{{{if .IsAdmin}}}
{%if .IsAdmin%}
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
@@ -17,14 +17,14 @@
</h3>
</div>
</div>
{{{end}}}
{%end%}
<form class="ui form">
<div class="three fields">
<div class="field">
<input type="text" placeholder="ID" name="id" value="{{{if gt .Params.Id 0}}}{{{.Params.Id}}}{{{end}}}">
<input type="text" placeholder="ID" name="id" value="{%if gt .Params.Id 0%}{%.Params.Id%}{%end%}">
</div>
<div class="field">
<input type="text" placeholder="主机名" name="name" value="{{{.Params.Name}}}">
<input type="text" placeholder="主机名" name="name" value="{%.Params.Name%}">
</div>
<div class="field">
<button class="ui linkedin submit button">搜索</button>
@@ -43,30 +43,30 @@
</tr>
</thead>
<tbody>
{{{range $i, $v := .Hosts}}}
{%range $i, $v := .Hosts%}
<tr>
<td>{{{.Id}}}</td>
<td>{{{.Name}}}</td>
<td>{{{.Alias}}}</td>
<td>{{{.Port}}}</td>
<td>{{{.Remark}}}</td>
<td>{%.Id%}</td>
<td>{%.Name%}</td>
<td>{%.Alias%}</td>
<td>{%.Port%}</td>
<td>{%.Remark%}</td>
<td class="operation">
{{{if $.IsAdmin}}}
<a class="ui purple button" href="/host/edit/{{{.Id}}}">编辑</a>
<button class="ui positive button" onclick="util.removeConfirm('/host/remove/{{{.Id}}}')">删除</button><br>
{{{end}}}
{%if $.IsAdmin%}
<a class="ui purple button" href="/host/edit/{%.Id%}">编辑</a>
<button class="ui positive button" onclick="util.removeConfirm('/host/remove/{%.Id%}')">删除</button><br>
{%end%}
<div style="margin-top: 5px;">
<a class="ui twitter button" href="/task?host_id={{{.Id}}}">查看任务</a>
{{{if $.IsAdmin}}}
<button class="ui blue button" @click="ping({{{.Id}}})">连接测试</button>
{{{end}}}
<a class="ui twitter button" href="/task?host_id={%.Id%}">查看任务</a>
{%if $.IsAdmin%}
<button class="ui blue button" @click="ping({%.Id%})">连接测试</button>
{%end%}
</div>
</td>
</tr>
{{{end}}}
{%end%}
</tbody>
</table>
{{{ template "common/pagination" .}}}
{% template "common/pagination" .%}
</div>
</div>
@@ -83,4 +83,4 @@
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="four wide column">
<div class="verticalMenu">
<div class="ui vertical pointing menu fluid">
<a class="{{{if eq .URI "/host"}}}active teal{{{end}}} item" href="/host">
<a class="{%if eq .URI "/host"%}active teal{%end%} item" href="/host">
<i class="linux icon"></i> 节点列表
</a>
</div>
+3 -3
View File
@@ -1,7 +1,7 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{ template "install/menu" . }}}
{% template "install/menu" . %}
<div class="ten wide column">
<form class="ui form">
<div class="ui blue center aligned segment">
@@ -186,4 +186,4 @@
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+9 -9
View File
@@ -1,7 +1,7 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{ template "manage/menu" . }}}
{% template "manage/menu" . %}
<div class="twelve wide column">
<div class="pageHeader">
@@ -22,18 +22,18 @@
</tr>
</thead>
<tbody>
{{{range $i, $v := .LoginLogs}}}
{%range $i, $v := .LoginLogs%}
<tr>
<td>{{{.Username}}}</td>
<td>{{{.Ip}}}</td>
<td>{{{.Created}}}</td>
<td>{%.Username%}</td>
<td>{%.Ip%}</td>
<td>{%.Created%}</td>
</tr>
{{{end}}}
{%end%}
</tbody>
</table>
{{{ template "common/pagination" .}}}
{% template "common/pagination" .%}
</div>
</div>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+13 -13
View File
@@ -1,12 +1,12 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{template "manage/menu" .}}}
{%template "manage/menu" .%}
<div class="twelve wide column">
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<div class="content">
{{{.Title}}}
{%.Title%}
</div>
</h3>
</div>
@@ -19,7 +19,7 @@
SMTP服务器
</label>
<div class="ui small input">
<input type="text" name="host" value="{{{.Mail.Host}}}">
<input type="text" name="host" value="{%.Mail.Host%}">
</div>
</div>
<div class="field">
@@ -27,7 +27,7 @@
端口
</label>
<div class="ui small input">
<input type="text" name="port" value="{{{if gt .Mail.Port 0}}}{{{.Mail.Port}}}{{{end}}}">
<input type="text" name="port" value="{%if gt .Mail.Port 0%}{%.Mail.Port%}{%end%}">
</div>
</div>
<div class="field">
@@ -35,7 +35,7 @@
用户名
</label>
<div class="ui small input">
<input type="text" name="user" value="{{{.Mail.User}}}">
<input type="text" name="user" value="{%.Mail.User%}">
</div>
</div>
<div class="field">
@@ -43,25 +43,25 @@
密码
</label>
<div class="ui small input">
<input type="text" name="password" value="{{{.Mail.Password}}}">
<input type="text" name="password" value="{%.Mail.Password%}">
</div>
</div>
</div>
<button class="ui primary button">保存</button>
{{{if .Mail.Host}}}
{%if .Mail.Host%}
<a class="ui green button" onclick="clearMailServer()">删除</a>
{{{end}}}
{%end%}
<br><br><br>
<div>
<div class="content">邮箱用户</div><p></p>
<div class="fields">
{{{range $i, $v := .Mail.MailUsers}}}
{%range $i, $v := .Mail.MailUsers%}
<div class="field">
<div class="ui segment">
{{{.Username}}}-{{{.Email}}}&nbsp;&nbsp;&nbsp;<div class="ui blue button" onclick="removeMailUser({{{.Id}}})">删除</div>
{%.Username%}-{%.Email%}&nbsp;&nbsp;&nbsp;<div class="ui blue button" onclick="removeMailUser({%.Id%})">删除</div>
</div>
</div>
{{{end}}}
{%end%}
</div>
</div>
</form>
@@ -198,4 +198,4 @@
});
}
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+3 -3
View File
@@ -1,13 +1,13 @@
<div class="three wide column">
<div class="verticalMenu">
<div class="ui vertical pointing menu fluid">
<a class="{{{if eq .URI "/manage/slack/edit"}}}active teal{{{end}}} item" href="/manage/slack/edit">
<a class="{%if eq .URI "/manage/slack/edit"%}active teal{%end%} item" href="/manage/slack/edit">
<i class="slack icon"></i> Slack配置
</a>
<a class="{{{if eq .URI "/manage/mail/edit"}}}active teal{{{end}}} item" href="/manage/mail/edit">
<a class="{%if eq .URI "/manage/mail/edit"%}active teal{%end%} item" href="/manage/mail/edit">
<i class="slack icon"></i> 邮件配置
</a>
<a class="{{{if eq .URI "/manage/login-log"}}}active teal{{{end}}} item" href="/manage/login-log">
<a class="{%if eq .URI "/manage/login-log"%}active teal{%end%} item" href="/manage/login-log">
<i class="slack icon"></i> 登录日志
</a>
</div>
+8 -8
View File
@@ -1,12 +1,12 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{template "manage/menu" .}}}
{%template "manage/menu" .%}
<div class="twelve wide column">
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<div class="content">
{{{.Title}}}
{%.Title%}
</div>
</h3>
</div>
@@ -18,7 +18,7 @@
<div class="content">Slack WebHook URL</div>
</label>
<div class="ui small input">
<input type="text" id="url" value="{{{.Slack.Url}}}">
<input type="text" id="url" value="{%.Slack.Url%}">
</div>
</div>
<div class="ui primary button" @click="updateUrl">保存</div>
@@ -27,13 +27,13 @@
<div>
<div class="content">Slack Channel(配置任务通知时可选择多Channel)</div><p></p>
<div class="fields">
{{{range $i, $v := .Slack.Channels}}}
{%range $i, $v := .Slack.Channels%}
<div class="field">
<div class="ui segment">
{{{.Name}}}&nbsp;&nbsp;&nbsp;<div class="ui blue button" @click="removeChannel({{{.Id}}})">删除</div>
{%.Name%}&nbsp;&nbsp;&nbsp;<div class="ui blue button" @click="removeChannel({%.Id%})">删除</div>
</div>
</div>
{{{end}}}
{%end%}
</div>
</div>
<div class="ui facebook button" @click="createChannel">新增Channel</div>
@@ -85,4 +85,4 @@
}
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+59 -54
View File
@@ -1,8 +1,8 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{template "task/menu" .}}}
{%template "task/menu" .%}
<div class="twelve wide column">
{{{if .IsAdmin}}}
{%if .IsAdmin%}
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
@@ -15,40 +15,40 @@
</h3>
</div>
</div>
{{{end}}}
{%end%}
<form class="ui form">
<div class="six fields search">
<div class="one wide field">
<input type="text" placeholder="任务ID" name="id" value="{{{if gt .Params.Id 0}}}{{{.Params.Id}}}{{{end}}}">
<input type="text" placeholder="任务ID" name="id" value="{%if gt .Params.Id 0%}{%.Params.Id%}{%end%}">
</div>
<div class="field">
<input type="text" placeholder="任务名称" name="name" value="{{{.Params.Name}}}">
<input type="text" placeholder="任务名称" name="name" value="{%.Params.Name%}">
</div>
<div class="field">
<input type="text" placeholder="标签名称" name="tag" value="{{{.Params.Tag}}}">
<input type="text" placeholder="标签名称" name="tag" value="{%.Params.Tag%}">
</div>
</div>
<div class="six fields search">
<div class="field">
<select name="host_id" id="hostId">
<option value="">选择节点</option>
{{{range $i, $v := .Hosts}}}
<option value="{{{.Id}}}" {{{if eq $.Params.HostId .Id }}} selected {{{end}}} >{{{.Alias}}}-{{{.Name}}}</option>
{{{end}}}
{%range $i, $v := .Hosts%}
<option value="{%.Id%}" {%if eq $.Params.HostId .Id %} selected {%end%} >{%.Alias%}-{%.Name%}</option>
{%end%}
</select>
</div>
<div class="field">
<select name="protocol" id="protocol">
<option value="0">执行方式</option>
<option value="2" {{{if eq .Params.Protocol 2}}}selected{{{end}}} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
<option value="1" {{{if eq .Params.Protocol 1}}}selected{{{end}}}>HTTP</option>
<option value="2" {%if eq .Params.Protocol 2%}selected{%end%} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
<option value="1" {%if eq .Params.Protocol 1%}selected{%end%}>HTTP</option>
</select>
</div>
<div class="field">
<select name="status">
<option value="0">状态</option>
<option value="1" {{{if eq .Params.Status 0}}}selected{{{end}}} >停止</option>
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>激活</option>
<option value="1" {%if eq .Params.Status 0%}selected{%end%} >停止</option>
<option value="2" {%if eq .Params.Status 1%}selected{%end%}>激活</option>
</select>
</div>
<div class="field">
@@ -56,7 +56,7 @@
</div>
</div>
</form>
{{{if .IsAdmin}}}
{%if .IsAdmin%}
<div class="field">
<select id="batch-operation">
<option value="0">批量操作</option>
@@ -65,16 +65,16 @@
<option value="3">删除</option>
</select>
</div>
{{{end}}}
{%end%}
<br>
<table class="ui celled table task-list">
<thead>
<tr>
{{{if .IsAdmin}}}
{%if .IsAdmin%}
<th>
<input type="checkbox" onclick="checkAll(this)" style="width:25px;height: 25px;">
</th>
{{{end}}}
{%end%}
<th>任务ID</th>
<th>任务名称</th>
<th>任务类型</th>
@@ -90,58 +90,58 @@
</tr>
</thead>
<tbody>
{{{range $i, $v := .Tasks}}}
{%range $i, $v := .Tasks%}
<tr>
{{{if $.IsAdmin}}}
{%if $.IsAdmin%}
<td>
<input type="checkbox"
class="sub-check"
data-id="{{{.Id}}}"
data-id="{%.Id%}"
style="width:25px;height: 25px;">
</td>
{{{end}}}
<td>{{{.Id}}}</td>
<td>{{{.Name}}}</td>
<td>{{{if eq .Level 1}}}主任务{{{else}}}子任务{{{end}}}</td>
<td>{{{.Tag}}}</td>
<td>{{{.Spec}}}</td>
<td>{{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SHELL {{{end}}}</td>
<td>{{{if eq .Timeout -1}}}后台运行{{{else if gt .Timeout 0}}}{{{.Timeout}}}{{{else}}}不限制{{{end}}}</td>
<td>{{{.RetryTimes}}}</td>
<td>{{{if gt .Multi 0}}}{{{else}}}{{{end}}}</td>
{%end%}
<td>{%.Id%}</td>
<td>{%.Name%}</td>
<td>{%if eq .Level 1%}主任务{%else%}子任务{%end%}</td>
<td>{%.Tag%}</td>
<td>{%.Spec%}</td>
<td>{%if eq .Protocol 1%} HTTP {%else if eq .Protocol 2%} SHELL {%end%}</td>
<td>{%if eq .Timeout -1%}后台运行{%else if gt .Timeout 0%}{%.Timeout%}{%else%}不限制{%end%}</td>
<td>{%.RetryTimes%}</td>
<td>{%if gt .Multi 0%}{%else%}{%end%}</td>
<td>
{{{range $k, $h := .Hosts}}}
{{{$h.Alias}}}<br>
{{{end}}}
{%range $k, $h := .Hosts%}
{%$h.Alias%}<br>
{%end%}
</td>
<td>
{{{if eq .Level 1}}}
{{{if eq .Status 1}}}<span><i class="checkmark big icon"></i></span>{{{else}}}<span><i class="minus big icon"></i><span>{{{end}}}
{{{end}}}
{%if eq .Level 1%}
{%if eq .Status 1%}<span><i class="checkmark big icon"></i></span>{%else%}<span><i class="minus big icon"></i><span>{%end%}
{%end%}
</td>
<td>
<div class="ui buttons operation">
{{{if $.IsAdmin}}}
<a href="/task/edit/{{{.Id}}}" ><i class="edit big icon" title="编辑"></i></a>&nbsp;&nbsp;&nbsp;&nbsp;
{{{if eq .Level 1}}}
{{{if eq .Status 1}}}
<a href="javascript:void(0);" @click="changeStatus({{{.Id}}},{{{.Status}}})"><i class="pause circle big icon" title="停止"></i></a>&nbsp;&nbsp;
{{{else}}}
<a href="javascript:void(0);" @click="changeStatus({{{.Id}}},{{{.Status}}})"><i class="play big icon" title="激活"></i></a>&nbsp;&nbsp;
{{{end}}}
{{{end}}}
<a href="javascript:void(0);" @click="remove({{{.Id}}})"><i class="remove big icon" title="删除"></i></a>
<a href="javascript:void(0);" @click="run({{{.Id}}})"><i class="rocket big icon" title="手动执行"></i></a>&nbsp;&nbsp;
{{{end}}}
{%if $.IsAdmin%}
<a href="/task/edit/{%.Id%}" ><i class="edit big icon" title="编辑"></i></a>&nbsp;&nbsp;&nbsp;&nbsp;
{%if eq .Level 1%}
{%if eq .Status 1%}
<a href="javascript:void(0);" @click="changeStatus({%.Id%},{%.Status%})"><i class="pause circle big icon" title="停止"></i></a>&nbsp;&nbsp;
{%else%}
<a href="javascript:void(0);" @click="changeStatus({%.Id%},{%.Status%})"><i class="play big icon" title="激活"></i></a>&nbsp;&nbsp;
{%end%}
{%end%}
<a href="javascript:void(0);" @click="remove({%.Id%})"><i class="remove big icon" title="删除"></i></a>
<a href="javascript:void(0);" @click="run({%.Id%})"><i class="rocket big icon" title="手动执行"></i></a>&nbsp;&nbsp;
{%end%}
<a href="/task/log?task_id={{{.Id}}}"><i class="bar chart icon big" title="查看日志"></i></a>
<a href="/task/log?task_id={%.Id%}"><i class="bar chart icon big" title="查看日志"></i></a>
</div>
</td>
</tr>
{{{end}}}
{%end%}
</tbody>
</table>
{{{ template "common/pagination" .}}}
{% template "common/pagination" .%}
</div>
</div>
@@ -211,7 +211,12 @@
}
function remove(id, stopReload) {
util.post('/task/remove/' + id, {}, function () {
var url = '/task/remove/' + id;
if (stopReload === undefined) {
util.removeConfirm(url);
return;
}
util.post(url, {}, function () {
if (stopReload === undefined) {
location.reload();
}
@@ -234,4 +239,4 @@
}
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+51 -36
View File
@@ -1,4 +1,4 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<style type="text/css">
pre {
white-space: pre-wrap;
@@ -10,10 +10,10 @@
</style>
<div class="ui grid">
<!--the vertical menu-->
{{{ template "task/menu" . }}}
{% template "task/menu" . %}
<div class="twelve wide column">
{{{if .IsAdmin}}}
{%if .IsAdmin%}
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
@@ -23,26 +23,26 @@
</h3>
</div>
</div>
{{{end}}}
{%end%}
<form class="ui form">
<div class="six fields search">
<div class="field">
<input type="text" placeholder="任务ID" name="task_id" value="{{{if gt .Params.TaskId 0}}}{{{.Params.TaskId}}}{{{end}}}">
<input type="text" placeholder="任务ID" name="task_id" value="{%if gt .Params.TaskId 0%}{%.Params.TaskId%}{%end%}">
</div>
<div class="field">
<select name="protocol" id="protocol">
<option value="0">执行方式</option>
<option value="2" {{{if eq .Params.Protocol 2}}}selected{{{end}}} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
<option value="1" {{{if eq .Params.Protocol 1}}}selected{{{end}}}>HTTP</option>
<option value="2" {%if eq .Params.Protocol 2%}selected{%end%} data-match="host_id" data-validate-type="selectProtocol">SHELL</option>
<option value="1" {%if eq .Params.Protocol 1%}selected{%end%}>HTTP</option>
</select>
</div>
<div class="field">
<select name="status">
<option value="0">状态</option>
<option value="1" {{{if eq .Params.Status 0}}}selected{{{end}}} >失败</option>
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>执行中</option>
<option value="3" {{{if eq .Params.Status 2}}}selected{{{end}}}>成功</option>
<option value="4" {{{if eq .Params.Status 3}}}selected{{{end}}}>取消</option>
<option value="1" {%if eq .Params.Status 0%}selected{%end%} >失败</option>
<option value="2" {%if eq .Params.Status 1%}selected{%end%}>执行中</option>
<option value="3" {%if eq .Params.Status 2%}selected{%end%}>成功</option>
<option value="4" {%if eq .Params.Status 3%}selected{%end%}>取消</option>
</select>
</div>
<div class="field">
@@ -65,47 +65,54 @@
</tr>
</thead>
<tbody>
{{{range $i, $v := .Logs}}}
{%range $i, $v := .Logs%}
<tr>
<td><a href="/task?id={{{.TaskId}}}">{{{.TaskId}}}</a></td>
<td>{{{.Name}}}</td>
<td>{{{.Spec}}}</td>
<td>{{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SHELL {{{end}}}</td>
<td>{{{.RetryTimes}}}</td>
<td>{{{unescape .Hostname}}}</td>
<td><a href="/task?id={%.TaskId%}">{%.TaskId%}</a></td>
<td>{%.Name%}</td>
<td>{%.Spec%}</td>
<td>{%if eq .Protocol 1%} HTTP {%else if eq .Protocol 2%} SHELL {%end%}</td>
<td>{%.RetryTimes%}</td>
<td>{%unescape .Hostname%}</td>
<td>
{{{if and (ne .Status 3) (ne .Status 4)}}}
{{{if gt .TotalTime 0}}}{{{.TotalTime}}}{{{else}}}1{{{end}}}<br>
开始时间: {{{.StartTime.Format "2006-01-02 15:04:05" }}}<br>
{{{if ne .Status 1}}}
结束时间: {{{.EndTime.Format "2006-01-02 15:04:05" }}}
{{{end}}}
{{{end}}}
{%if and (ne .Status 3) (ne .Status 4)%}
{%if gt .TotalTime 0%}{%.TotalTime%}{%else%}1{%end%}<br>
开始时间: {%.StartTime.Format "2006-01-02 15:04:05" %}<br>
{%if ne .Status 1%}
结束时间: {%.EndTime.Format "2006-01-02 15:04:05" %}
{%end%}
{%end%}
</td>
<td>
{{{if eq .Status 2}}}
{%if eq .Status 2%}
成功
{{{else if eq .Status 1}}}
{%else if eq .Status 1%}
<span style="color:green">执行中</span>
{{{else if eq .Status 0}}}
{%else if eq .Status 0%}
<span style="color:red">失败</span>
{{{else if eq .Status 3}}}
{%else if eq .Status 3%}
<span style="color:#4499EE">取消</span>
{{{end}}}
{%end%}
</td>
<td>
{{{if or (eq .Status 2) (eq .Status 0)}}}
{%if or (eq .Status 2) (eq .Status 0)%}
<button class="ui small primary button"
onclick="showResult('{{{.Name}}}', '{{{.Command}}}', '{{{.Result}}}')"
onclick="showResult('{%.Name%}', '{%.Command%}', '{%.Result%}')"
>查看结果
</button>
{{{end}}}
{%end%}
{%if $.IsAdmin%}
{%if and (eq .Status 1) (eq .Protocol 2) %}
<button class="ui small blue button" onclick="stopTask({%.Id%}, {%.TaskId%})">停止任务</button>
{%end%}
{%end%}
</td>
</tr>
{{{end}}}
{%end%}
</tbody>
</table>
{{{ template "common/pagination" .}}}
{% template "common/pagination" .%}
</div>
</div>
@@ -148,6 +155,14 @@
}).modal('refresh').modal('show');
}
function stopTask(id, taskId) {
util.confirm("确定要停止任务吗", function () {
util.post("/task/log/stop/", {id: id, task_id:taskId}, function (code, message) {
swal('提示', message, 'info');
});
});
}
function clearLog() {
util.confirm("确定要删除所有日志吗?", function() {
util.post("/task/log/clear",{}, function() {
@@ -156,4 +171,4 @@
});
}
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+2 -2
View File
@@ -1,10 +1,10 @@
<div class="three wide column">
<div class="verticalMenu">
<div class="ui vertical pointing menu fluid">
<a class="{{{if eq .URI "/task"}}}active teal{{{end}}} item" href="/task">
<a class="{%if eq .URI "/task"%}active teal{%end%} item" href="/task">
<i class="tasks icon"></i> 定时任务列表
</a>
<a class="item {{{if eq .URI "/task/log"}}}active teal{{{end}}} " href="/task/log">
<a class="item {%if eq .URI "/task/log"%}active teal{%end%} " href="/task/log">
<i class="bar chart icon"></i> 定时任务日志
</a>
</div>
+67 -39
View File
@@ -1,25 +1,25 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{template "task/menu" .}}}
{%template "task/menu" .%}
<div class="twelve wide column">
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<div class="content">
{{{.Title}}}
{%.Title%}
</div>
</h3>
</div>
</div>
<form class="ui form fluid vertical segment">
<input type="hidden" name="id" value="{{{.Task.Id}}}">
<input type="hidden" name="id" value="{%.Task.Id%}">
<div class="two fields">
<div class="field">
<label>
<div class="content">任务名称</div>
</label>
<div class="ui small input">
<input type="text" name="name" placeholder="任务名称" value="{{{.Task.Name}}}">
<input type="text" name="name" placeholder="任务名称" value="{%.Task.Name%}">
</div>
</div>
@@ -28,7 +28,7 @@
<div class="content">标签名称</div>
</label>
<div class="ui small input">
<input type="text" name="tag" placeholder="标签用于任务分类" value="{{{.Task.Tag}}}">
<input type="text" name="tag" placeholder="标签用于任务分类" value="{%.Task.Tag%}">
</div>
</div>
</div>
@@ -41,9 +41,9 @@
任务类型新增后不能变更
</div>
</label>
<select name="level" id="level" {{{if .Task}}}disabled="disabled"{{{end}}}>
<option value="1" {{{if .Task}}} {{{if eq .Task.Level 1}}}selected{{{end}}} {{{end}}}>主任务</option>
<option value="2" {{{if .Task}}} {{{if eq .Task.Level 2}}}selected{{{end}}} {{{end}}}>子任务</option>
<select name="level" id="level" {%if .Task%}disabled="disabled"{%end%}>
<option value="1" {%if .Task%} {%if eq .Task.Level 1%}selected{%end%} {%end%}>主任务</option>
<option value="2" {%if .Task%} {%if eq .Task.Level 2%}selected{%end%} {%end%}>子任务</option>
</select>
</div>
</div>
@@ -58,8 +58,8 @@
</div>
</label>
<select name="dependency_status" id="dependency_status">
<option value="1" {{{if .Task}}} {{{if eq .Task.DependencyStatus 1}}}selected{{{end}}} {{{end}}}>强依赖</option>
<option value="2" {{{if .Task}}} {{{if eq .Task.DependencyStatus 2}}}selected{{{end}}} {{{end}}}>弱依赖</option>
<option value="1" {%if .Task%} {%if eq .Task.DependencyStatus 1%}selected{%end%} {%end%}>强依赖</option>
<option value="2" {%if .Task%} {%if eq .Task.DependencyStatus 2%}selected{%end%} {%end%}>弱依赖</option>
</select>
</div>
<div class="field">
@@ -71,7 +71,7 @@
</div>
</label>
<div class="ui small input">
<input type="text" name="dependency_task_id" placeholder="可选" value="{{{.Task.DependencyTaskId}}}">
<input type="text" name="dependency_task_id" placeholder="可选" value="{%.Task.DependencyTaskId%}">
</div>
</div>
</div>
@@ -83,7 +83,7 @@
</div>
</label>
<div class="ui small input">
<input type="text" name="spec" value="{{{.Task.Spec}}}" placeholder="秒 分 时 天 月 周"/>
<input type="text" name="spec" value="{%.Task.Spec%}" placeholder="秒 分 时 天 月 周"/>
</div>
</div>
</div>
@@ -92,9 +92,9 @@
<div class="field">
<label>执行方式</label>
<select name="protocol" id="protocol">
<option value="2" {{{if .Task}}} {{{if eq .Task.Protocol 2}}}selected{{{end}}} {{{end}}}
<option value="2" {%if .Task%} {%if eq .Task.Protocol 2%}selected{%end%} {%end%}
data-validate-type="selectProtocol">SHELL</option>
<option value="1" {{{if .Task}}} {{{if eq .Task.Protocol 1}}}selected{{{end}}} {{{end}}}>HTTP</option>
<option value="1" {%if .Task%} {%if eq .Task.Protocol 1%}selected{%end%} {%end%}>HTTP</option>
</select>
</div>
</div>
@@ -102,47 +102,64 @@
<div class="field">
<label>选择任务节点</label>
<div id="hostId">
{{{range $i, $v := .Hosts}}}
{%range $i, $v := .Hosts%}
<label>
<input type="checkbox" value="{{{.Id}}}" {{{if $.Task}}}{{{if $v.Selected}}} checked {{{end}}}{{{end}}} style="width:25px;height: 25px;">{{{.Alias}}}-{{{.Name}}}
{{{if (HostFormat $i) }}}<br>{{{end}}}
<input type="checkbox" value="{%.Id%}" {%if $.Task%}{%if $v.Selected%} checked {%end%}{%end%} style="width:25px;height: 25px;">{%.Alias%}-{%.Name%}
{%if (HostFormat $i) %}<br>{%end%}
</label>
{{{end}}}
{%end%}
</div> &nbsp; <br> <a class="ui blue button" href="/host/create" target="_blank">添加节点</a>
</div>
</div>
<div class="two fields" id="http-method" style="display: none">
<div class="field">
<label>请求方法</label>
<select name="http_method">
<option value="1" {%if .Task%} {%if eq .Task.HttpMethod 1%}selected{%end%} {%end%}>GET</option>
<option value="2" {%if .Task%} {%if eq .Task.HttpMethod 2%}selected{%end%} {%end%}
data-validate-type="selectProtocol">POST</option>
</select>
</div>
</div>
<div class="two fields">
<div class="field">
<label>命令</label>
<textarea rows="5" name="command" placeholder="请输入系统命令" id="command">{{{.Task.Command}}}</textarea>
<textarea rows="5" name="command" placeholder="请输入系统命令" id="command">{%.Task.Command%}</textarea>
</div>
</div>
<div class="three fields">
<div class="two fields">
<div class="field">
<label>任务超时时间(, 0-86400)</label>
<input type="text" name="timeout" placeholder="默认0, 不限制" value="{{{if .Task}}} {{{.Task.Timeout}}} {{{else}}}0{{{end}}}">
</div>
<div class="field">
<label>任务失败重试次数 (0-10)</label>
<input type="text" name="retry_times" placeholder="默认0, 不重试" value="{{{if .Task}}} {{{.Task.RetryTimes}}} {{{else}}}0{{{end}}}">
<input type="text" name="timeout" placeholder="默认0, 不限制" value="{%if .Task%} {%.Task.Timeout%} {%else%}0{%end%}">
</div>
<div class="field">
<label>允许多实例同时运行</label>
<select name="multi">
<option value="2" {{{if .Task}}} {{{if eq .Task.Multi 0}}}selected{{{end}}} {{{end}}}></option>
<option value="1"{{{if .Task}}} {{{if eq .Task.Multi 1}}}selected{{{end}}} {{{end}}}></option>
<option value="2" {%if .Task%} {%if eq .Task.Multi 0%}selected{%end%} {%end%}></option>
<option value="1"{%if .Task%} {%if eq .Task.Multi 1%}selected{%end%} {%end%}></option>
</select>
</div>
</div>
<div class="two fields">
<div class="field">
<label>任务失败重试次数 (0-10)</label>
<input type="text" name="retry_times" placeholder="默认0, 不重试" value="{%if .Task%} {%.Task.RetryTimes%} {%else%}0{%end%}">
</div>
<div class="field">
<label>任务失败重试间隔时间 (, 0-3600)</label>
<input type="text" name="retry_interval" placeholder="默认0, 执行默认重试策略" value="{%if .Task%} {%.Task.RetryInterval%} {%else%}0{%end%}">
</div>
</div>
<div class="three fields">
<div class="field">
<label>任务通知</label>
<select name="notify_status" id="task-status">
<option value="1"{{{if .Task}}} {{{if eq .Task.NotifyStatus 0}}}selected{{{end}}} {{{end}}}>不通知</option>
<option value="2" {{{if .Task}}} {{{if eq .Task.NotifyStatus 1}}}selected{{{end}}} {{{end}}}>失败通知</option>
<option value="3" {{{if .Task}}} {{{if eq .Task.NotifyStatus 2}}}selected{{{end}}} {{{end}}}>执行结束通知</option>
<option value="1"{%if .Task%} {%if eq .Task.NotifyStatus 0%}selected{%end%} {%end%}>不通知</option>
<option value="2" {%if .Task%} {%if eq .Task.NotifyStatus 1%}selected{%end%} {%end%}>失败通知</option>
<option value="3" {%if .Task%} {%if eq .Task.NotifyStatus 2%}selected{%end%} {%end%}>执行结束通知</option>
</select>
</div>
</div>
@@ -150,9 +167,9 @@
<div class="field" >
<label>通知类型</label>
<select name="notify_type">
<option value="1"{{{if .Task}}} {{{if eq .Task.NotifyType 0}}}selected{{{end}}} {{{end}}}>请选择</option>
<option value="2" {{{if .Task}}} {{{if eq .Task.NotifyType 1}}}selected{{{end}}} {{{end}}}>邮件</option>
<option value="3" {{{if .Task}}} {{{if eq .Task.NotifyType 2}}}selected{{{end}}} {{{end}}}>Slack</option>
<option value="1"{%if .Task%} {%if eq .Task.NotifyType 0%}selected{%end%} {%end%}>请选择</option>
<option value="2" {%if .Task%} {%if eq .Task.NotifyType 1%}selected{%end%} {%end%}>邮件</option>
<option value="3" {%if .Task%} {%if eq .Task.NotifyType 2%}selected{%end%} {%end%}>Slack</option>
</select>
</div>
</div>
@@ -160,7 +177,7 @@
<div class="two fields">
<div class="field">
<label>备注</label>
<textarea rows="5" name="remark" placeholder="任务备注信息">{{{.Task.Remark}}}</textarea>
<textarea rows="5" name="remark" placeholder="任务备注信息">{%.Task.Remark%}</textarea>
</div>
</div>
<div class="ui primary submit button">保存</div> <a class="ui button" onclick="location.href='/task';">取消</a>
@@ -239,11 +256,11 @@
}
function showNotify() {
var notifyStatus = {{{.Task.NotifyStatus}}};
var notifyStatus = {%.Task.NotifyStatus%};
if (notifyStatus > 0) {
$('#task-notify-type').show();
}
var notifyReceiverIds = '{{{.Task.NotifyReceiverId}}}'.split(',');
var notifyReceiverIds = '{%.Task.NotifyReceiverId%}'.split(',');
changeNotify(notifyReceiverIds);
}
@@ -292,10 +309,12 @@
var protocol = $('#protocol').val();
if (protocol == 2) {
$('#hostField').show();
$('#http-method').hide();
return;
}
$('#hostField').hide();
$('#http-method').show();
}
$('.ui.checkbox')
@@ -423,6 +442,15 @@
}
]
},
retryInterval: {
identifier : 'retry_interval',
rules: [
{
type : 'integer[0..3600]',
prompt : '重试间隔时间0-3600'
}
]
},
remark: {
identifier : 'remark',
rules: [
@@ -437,4 +465,4 @@
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+2 -2
View File
@@ -1,4 +1,4 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui small modal">
<div class="header">修改密码</div>
<div class="content">
@@ -70,4 +70,4 @@
inline : true
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+3 -3
View File
@@ -1,4 +1,4 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui small modal">
<div class="header">修改密码</div>
<div class="content">
@@ -25,7 +25,7 @@
$('.ui.form').form(
{
onSuccess: function(event, fields) {
util.post('/user/editPassword/{{{.Id}}}', fields, function(code, message) {
util.post('/user/editPassword/{%.Id%}', fields, function(code, message) {
swal("操作成功", '修改成功', 'success');
location.href = "/user"
});
@@ -55,4 +55,4 @@
inline : true
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+19 -19
View File
@@ -1,7 +1,7 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{ template "user/menu" . }}}
{% template "user/menu" . %}
<div class="twelve wide column">
<div class="pageHeader">
@@ -28,30 +28,30 @@
</tr>
</thead>
<tbody>
{{{range $i, $v := .Users}}}
{%range $i, $v := .Users%}
<tr>
<td>{{{.Id}}}</td>
<td>{{{.Name}}}</td>
<td>{{{.Email}}}</td>
<td>{{{if .IsAdmin}}}管理员{{{else}}}普通用户{{{end}}}</td>
<td>{{{if .Status}}}启用{{{else}}}禁用{{{end}}}</td>
<td>{%.Id%}</td>
<td>{%.Name%}</td>
<td>{%.Email%}</td>
<td>{%if .IsAdmin%}管理员{%else%}普通用户{%end%}</td>
<td>{%if .Status%}启用{%else%}禁用{%end%}</td>
<td class="operation">
<a class="ui purple button" href="/user/edit/{{{.Id}}}">编辑</a>
{{{if eq .Status 1}}}
<button class="ui button red" onclick="changeStatus({{{.Id}}},{{{.Status}}})">禁用</button>&nbsp;&nbsp;
{{{else}}}
<button class="ui button twitter" onclick="changeStatus({{{.Id}}},{{{.Status}}})">启用</button>&nbsp;&nbsp;
{{{end}}}
<a href="/user/editPassword/{{{.Id}}}">
<a class="ui purple button" href="/user/edit/{%.Id%}">编辑</a>
{%if eq .Status 1%}
<button class="ui button red" onclick="changeStatus({%.Id%},{%.Status%})">禁用</button>&nbsp;&nbsp;
{%else%}
<button class="ui button twitter" onclick="changeStatus({%.Id%},{%.Status%})">启用</button>&nbsp;&nbsp;
{%end%}
<a href="/user/editPassword/{%.Id%}">
<button class="ui button facebook">修改密码</button>
</a>
<button class="ui positive button" onclick="util.removeConfirm('/user/remove/{{{.Id}}}')">删除</button>
<button class="ui positive button" onclick="util.removeConfirm('/user/remove/{%.Id%}')">删除</button>
</td>
</tr>
{{{end}}}
{%end%}
</tbody>
</table>
{{{ template "common/pagination" .}}}
{% template "common/pagination" .%}
</div>
</div>
@@ -70,4 +70,4 @@
}
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+3 -3
View File
@@ -1,4 +1,4 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui small modal">
<div class="header">用户登录</div>
<div class="content">
@@ -15,7 +15,7 @@
</div>
<div class="two fields">
<div class="field">
{{{.Captcha.CreateHtml}}}
{%.Captcha.CreateHtml%}
<input type="text" name="captcha" placeholder="验证码">
</div>
</div>
@@ -71,4 +71,4 @@
inline : true
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="four wide column">
<div class="verticalMenu">
<div class="ui vertical pointing menu fluid">
<a class="{{{if eq .URI "/user"}}}active teal{{{end}}} item" href="/user">
<a class="{%if eq .URI "/user"%}active teal{%end%} item" href="/user">
<i class="user icon"></i> 用户列表
</a>
</div>
+12 -12
View File
@@ -1,7 +1,7 @@
{{{ template "common/header" . }}}
{% template "common/header" . %}
<div class="ui grid">
{{{ template "user/menu" . }}}
{% template "user/menu" . %}
<div class="twelve wide column">
<div class="pageHeader">
@@ -14,22 +14,22 @@
</div>
</div>
<form class="ui form fluid vertical segment">
<input type="hidden" name="id" value="{{{.User.Id}}}">
<input type="hidden" name="id" value="{%.User.Id%}">
<div class="three fields">
<div class="field">
<label>用户名</label>
<div class="ui small input">
<input type="text" name="name" value="{{{.User.Name}}}" placeholder="请输入用户名">
<input type="text" name="name" value="{%.User.Name%}" placeholder="请输入用户名">
</div>
</div>
<div class="field">
<label>邮箱</label>
<div class="ui small input">
<input type="text" name="email" value="{{{.User.Email}}}" placeholder="请输入邮箱">
<input type="text" name="email" value="{%.User.Email%}" placeholder="请输入邮箱">
</div>
</div>
</div>
{{{if eq .User.Id 0}}}
{%if eq .User.Id 0%}
<div class="three fields">
<div class="field">
<label>密码</label>
@@ -41,20 +41,20 @@
<input type="password" name="confirm_password" placeholder="请再次输入密码">
</div>
</div>
{{{end}}}
{%end%}
<div class="three fields">
<div class="field">
<label>角色</label>
<input type="radio" name="is_admin" value="0" {{{if eq .User.IsAdmin 0}}}checked{{{end}}}> 普通用户
<input type="radio" name="is_admin" value="1" {{{if eq .User.IsAdmin 1}}}checked{{{end}}}> 管理员
<input type="radio" name="is_admin" value="0" {%if eq .User.IsAdmin 0%}checked{%end%}> 普通用户
<input type="radio" name="is_admin" value="1" {%if eq .User.IsAdmin 1%}checked{%end%}> 管理员
</div>
<div class="field">
<label>状态</label>
<input type="radio" name="status" value="1" {{{if eq .User.Status 1}}}checked{{{end}}}> 启用
<input type="radio" name="status" value="1" {%if eq .User.Status 1%}checked{%end%}> 启用
<input type="radio" name="status" value="0"
{{{if eq .User.Status 0}}}checked{{{end}}}
{%if eq .User.Status 0%}checked{%end%}
> 禁用
</div>
</div>
@@ -139,4 +139,4 @@
});
</script>
{{{ template "common/footer" . }}}
{% template "common/footer" . %}
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# set -x -u
# 上传二进制包到七牛
if [[ -z $QINIU_ACCESS_KEY || -z $QINIU_SECRET_KEY || -z $QINIU_URL ]];then
echo 'QINIU_ACCESS_KEY | QINIU_SECRET_KEY | QINIU_URL is need'
exit 1
fi
# 打包
for i in linux darwin windows
do
./build.sh -p $i
if [[ $? != 0 ]];then
break
fi
./build_node.sh -p $i
if [[ $? != 0 ]];then
break
fi
done
# 身份认证
qrsctl login $QINIU_ACCESS_KEY $QINIU_SECRET_KEY
# 上传
for i in `ls gocron*.gz gocron*.zip`
do
# 上传文件 qrsctl put bucket key srcFile
KEY=gocron/$i
qrsctl put github $KEY $i
if [[ $? != 0 ]];then
break
fi
echo "刷新七牛CDN-" $QINIU_URL/$KEY
qrsctl cdn/refresh $QINIU_URL/$KEY
rm $i
done
echo '打包并上传成功'