mirror of https://github.com/shunfei/cronsun
commit
7eba17e1b5
|
@ -8,3 +8,4 @@ web/ui/node_modules
|
|||
web/ui/dist
|
||||
.vscode
|
||||
*npm-debug.log
|
||||
vendor
|
||||
|
|
|
@ -11,6 +11,9 @@ install:
|
|||
- go get github.com/cockroachdb/cmux
|
||||
- go get github.com/gorilla/mux
|
||||
- go get github.com/smartystreets/goconvey/convey
|
||||
- go get github.com/spf13/cobra
|
||||
- go get github.com/satori/go.uuid
|
||||
|
||||
before_script:
|
||||
- go vet -x ./...
|
||||
script:
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/shunfei/cronsun"
|
||||
subcmd "github.com/shunfei/cronsun/bin/csctl/cmd"
|
||||
)
|
||||
|
||||
var confFile string
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "csctl",
|
||||
Short: "cronsun command tools for data manage",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
if err := cronsun.Init(confFile, false); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&confFile, "conf", "c", "", "base.json file path.")
|
||||
rootCmd.AddCommand(subcmd.BackupCmd, subcmd.RestoreCmd, subcmd.UpgradeCmd)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/shunfei/cronsun"
|
||||
"github.com/shunfei/cronsun/conf"
|
||||
)
|
||||
|
||||
var (
|
||||
backupDir string
|
||||
backupFile string
|
||||
)
|
||||
|
||||
var BackupCmd = &cobra.Command{
|
||||
Use: "backup",
|
||||
Short: "backup job & group data",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var ea = NewExitAction()
|
||||
|
||||
backupDir = strings.TrimSpace(backupDir)
|
||||
if len(backupDir) > 0 {
|
||||
err = os.MkdirAll(backupDir, os.ModeDir)
|
||||
if err != nil {
|
||||
ea.Exit("failed to make directory %s, err: %s", backupDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
backupFile = strings.TrimSpace(backupFile)
|
||||
if len(backupFile) == 0 {
|
||||
backupFile = time.Now().Format("20060102_150405")
|
||||
}
|
||||
backupFile += ".zip"
|
||||
|
||||
name := path.Join(backupDir, backupFile)
|
||||
f, err := os.OpenFile(name, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
ea.ExitOnErr(err)
|
||||
ea.Defer = func() {
|
||||
f.Close()
|
||||
if err != nil {
|
||||
os.Remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
var waitForStore = [][]string{
|
||||
// [file name in ZIP archive, key prefix in etcd]
|
||||
[]string{"job", conf.Config.Cmd},
|
||||
[]string{"node_group", conf.Config.Group},
|
||||
}
|
||||
zw := zip.NewWriter(f)
|
||||
|
||||
for i := range waitForStore {
|
||||
zf, err := zw.Create(waitForStore[i][0])
|
||||
ea.ExitOnErr(err)
|
||||
storeKvs(zf, waitForStore[i][1])
|
||||
}
|
||||
|
||||
ea.ExitOnErr(zw.Close())
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
BackupCmd.Flags().StringVarP(&backupDir, "dir", "d", "", "the directory to store backup file")
|
||||
BackupCmd.Flags().StringVarP(&backupFile, "file", "f", "", "the backup file name")
|
||||
}
|
||||
|
||||
type ExitAction struct {
|
||||
Defer func()
|
||||
}
|
||||
|
||||
func NewExitAction() *ExitAction {
|
||||
return &ExitAction{}
|
||||
}
|
||||
|
||||
func (ea *ExitAction) ExitOnErr(err error) {
|
||||
if err != nil {
|
||||
_, f, l, _ := runtime.Caller(1)
|
||||
ea.Exit("%s line %d: %s", f, l, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (ea *ExitAction) Exit(format string, v ...interface{}) {
|
||||
if ea.Defer != nil {
|
||||
ea.Defer()
|
||||
}
|
||||
|
||||
fmt.Printf(format+"\n", v...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var (
|
||||
sizeBuf = make([]byte, 2+4) // key length(uint16) + value length(uint32)
|
||||
)
|
||||
|
||||
func storeKvs(w io.Writer, keyPrefix string) error {
|
||||
gresp, err := cronsun.DefalutClient.Get(keyPrefix, clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch %s from etcd: %s", keyPrefix, err)
|
||||
}
|
||||
|
||||
var prefixLen = len(keyPrefix)
|
||||
|
||||
for i := range gresp.Kvs {
|
||||
key := gresp.Kvs[i].Key[prefixLen:]
|
||||
binary.LittleEndian.PutUint16(sizeBuf[:2], uint16(len(key)))
|
||||
binary.LittleEndian.PutUint32(sizeBuf[2:], uint32(len(gresp.Kvs[i].Value)))
|
||||
|
||||
// length of key
|
||||
if _, err = w.Write(sizeBuf[:2]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = w.Write(key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// lenght of value
|
||||
if _, err = w.Write(sizeBuf[2:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = w.Write(gresp.Kvs[i].Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/shunfei/cronsun"
|
||||
"github.com/shunfei/cronsun/conf"
|
||||
)
|
||||
|
||||
var restoreFile string
|
||||
|
||||
func init() {
|
||||
RestoreCmd.Flags().StringVarP(&restoreFile, "file", "f", "", "the backup zip file")
|
||||
}
|
||||
|
||||
var RestoreCmd = &cobra.Command{
|
||||
Use: "restore",
|
||||
Short: "restore job & group data",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
var ea = NewExitAction()
|
||||
|
||||
restoreFile = strings.TrimSpace(restoreFile)
|
||||
if len(restoreFile) == 0 {
|
||||
ea.Exit("backup file is required")
|
||||
}
|
||||
|
||||
r, err := zip.OpenReader(restoreFile)
|
||||
ea.ExitOnErr(err)
|
||||
ea.Defer = func() {
|
||||
r.Close()
|
||||
}
|
||||
|
||||
restoreChan, wg := startRestoreProcess()
|
||||
for _, f := range r.File {
|
||||
var keyPrefix string
|
||||
switch f.Name {
|
||||
case "job":
|
||||
keyPrefix = conf.Config.Cmd
|
||||
case "node_group":
|
||||
keyPrefix = conf.Config.Group
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
ea.ExitOnErr(err)
|
||||
|
||||
ea.ExitOnErr(restoreKvs(rc, keyPrefix, restoreChan, wg))
|
||||
rc.Close()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(restoreChan)
|
||||
},
|
||||
}
|
||||
|
||||
type kv struct {
|
||||
k, v string
|
||||
}
|
||||
|
||||
var (
|
||||
keyLenBuf = make([]byte, 2)
|
||||
valLenBuf = make([]byte, 4)
|
||||
keyBuf = make([]byte, 256)
|
||||
valBuf = make([]byte, 1024)
|
||||
)
|
||||
|
||||
func restoreKvs(r io.Reader, keyPrefix string, storeChan chan *kv, wg *sync.WaitGroup) error {
|
||||
for {
|
||||
// read length of key
|
||||
n, err := r.Read(keyLenBuf)
|
||||
if err == io.EOF && n != 0 {
|
||||
return fmt.Errorf("unexcepted data, the file may broken")
|
||||
} else if err == io.EOF && n == 0 {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
keylen := binary.LittleEndian.Uint16(keyLenBuf)
|
||||
|
||||
// read key
|
||||
if n, err = r.Read(keyBuf[:keylen]); err != nil {
|
||||
return err
|
||||
}
|
||||
key := keyBuf[:keylen]
|
||||
|
||||
// read length of value
|
||||
if n, err = r.Read(valLenBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
vallen := binary.LittleEndian.Uint32(valLenBuf)
|
||||
|
||||
// read value
|
||||
if len(valBuf) < int(vallen) {
|
||||
valBuf = make([]byte, vallen*2)
|
||||
}
|
||||
if n, err = r.Read(valBuf[:vallen]); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
value := valBuf[:vallen]
|
||||
|
||||
wg.Add(1)
|
||||
storeChan <- &kv{
|
||||
k: keyPrefix + string(key),
|
||||
v: string(value),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startRestoreProcess() (chan *kv, *sync.WaitGroup) {
|
||||
c := make(chan *kv, 0)
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
const maxResries = 3
|
||||
go func() {
|
||||
for _kv := range c {
|
||||
for retries := 1; retries <= maxResries; retries++ {
|
||||
_, err := cronsun.DefalutClient.Put(_kv.k, _kv.v)
|
||||
if err != nil {
|
||||
if retries == maxResries {
|
||||
fmt.Println("[Error] restore err:", err)
|
||||
fmt.Println("\tKey: ", string(_kv.k))
|
||||
fmt.Println("\tValue: ", string(_kv.v))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
return c, wg
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/spf13/cobra"
|
||||
mgo "gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
|
||||
"github.com/shunfei/cronsun"
|
||||
"github.com/shunfei/cronsun/conf"
|
||||
)
|
||||
|
||||
var prever string
|
||||
|
||||
func init() {
|
||||
UpgradeCmd.Flags().StringVarP(&prever, "prever", "p", "", "previous version of cronsun you are used")
|
||||
}
|
||||
|
||||
var UpgradeCmd = &cobra.Command{
|
||||
Use: "upgrade",
|
||||
Short: "upgrade will upgrade data to the current version(" + cronsun.VersionNumber + ")",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var ea = NewExitAction()
|
||||
|
||||
prever = strings.TrimLeft(strings.TrimSpace(prever), "v")
|
||||
if len(prever) < 5 {
|
||||
ea.Exit("invalid version number")
|
||||
}
|
||||
|
||||
if prever < "0.3.0" {
|
||||
fmt.Println("upgrading data to version 0.3.0")
|
||||
nodesById := getIPMapper(ea)
|
||||
if to_0_3_0(ea, nodesById) {
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func getIPMapper(ea *ExitAction) map[string]*cronsun.Node {
|
||||
nodes, err := cronsun.GetNodes()
|
||||
if err != nil {
|
||||
ea.Exit("failed to fetch nodes from MongoDB: %s", err.Error())
|
||||
}
|
||||
|
||||
var ipMapper = make(map[string]*cronsun.Node, len(nodes))
|
||||
for _, n := range nodes {
|
||||
n.IP = strings.TrimSpace(n.IP)
|
||||
if n.IP == "" || n.ID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ipMapper[n.IP] = n
|
||||
}
|
||||
|
||||
return ipMapper
|
||||
}
|
||||
|
||||
// to_0_3_0 can be run many times
|
||||
func to_0_3_0(ea *ExitAction, nodesById map[string]*cronsun.Node) (shouldStop bool) {
|
||||
var replaceIDs = func(list []string) {
|
||||
for i := range list {
|
||||
if node, ok := nodesById[list[i]]; ok {
|
||||
list[i] = node.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update job data
|
||||
gresp, err := cronsun.DefalutClient.Get(conf.Config.Cmd, clientv3.WithPrefix())
|
||||
ea.ExitOnErr(err)
|
||||
|
||||
total := len(gresp.Kvs)
|
||||
upgraded := 0
|
||||
for i := range gresp.Kvs {
|
||||
job := cronsun.Job{}
|
||||
err = json.Unmarshal(gresp.Kvs[i].Value, &job)
|
||||
if err != nil {
|
||||
fmt.Printf("[Error] failed to decode job(%s) data: %s\n", string(gresp.Kvs[i].Key), err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
for _, rule := range job.Rules {
|
||||
replaceIDs(rule.ExcludeNodeIDs)
|
||||
replaceIDs(rule.NodeIDs)
|
||||
}
|
||||
|
||||
d, err := json.Marshal(&job)
|
||||
if err != nil {
|
||||
fmt.Printf("[Error] failed to encode job(%s) data: %s\n", string(gresp.Kvs[i].Key), err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = cronsun.DefalutClient.Put(job.Key(), string(d))
|
||||
if err != nil {
|
||||
fmt.Printf("[Warn] failed to restore job(%s) data: %s\n", string(gresp.Kvs[i].Key), err.Error())
|
||||
continue
|
||||
}
|
||||
upgraded++
|
||||
}
|
||||
if total != upgraded {
|
||||
shouldStop = true
|
||||
}
|
||||
fmt.Printf("%d of %d jobs has been upgraded.\n", upgraded, total)
|
||||
|
||||
// migrate node group data
|
||||
nodeGroups, err := cronsun.GetNodeGroups()
|
||||
if err != nil {
|
||||
ea.Exit("[Error] failed to get node group datas: ", err.Error())
|
||||
}
|
||||
|
||||
total = len(nodeGroups)
|
||||
upgraded = 0
|
||||
for i := range nodeGroups {
|
||||
replaceIDs(nodeGroups[i].NodeIDs)
|
||||
if _, err = nodeGroups[i].Put(0); err != nil {
|
||||
fmt.Printf("[Warn] failed to restore node group(id: %s, name: %s) data: %s\n", nodeGroups[i].ID, nodeGroups[i].Name, err.Error())
|
||||
continue
|
||||
}
|
||||
upgraded++
|
||||
}
|
||||
if total != upgraded {
|
||||
shouldStop = true
|
||||
}
|
||||
fmt.Printf("%d of %d node group has been upgraded.\n", upgraded, total)
|
||||
|
||||
// upgrade logs
|
||||
cronsun.GetDb().WithC(cronsun.Coll_JobLog, func(c *mgo.Collection) error {
|
||||
for ip, node := range nodesById {
|
||||
_, err = c.UpdateAll(bson.M{"node": ip}, bson.M{"$set": bson.M{"node": node.ID, "hostname": node.Hostname}})
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
fmt.Println("failed to upgrade job logs: ", err.Error())
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
shouldStop = true
|
||||
return err
|
||||
})
|
||||
|
||||
// upgrade logs
|
||||
cronsun.GetDb().WithC(cronsun.Coll_JobLatestLog, func(c *mgo.Collection) error {
|
||||
for ip, node := range nodesById {
|
||||
_, err = c.UpdateAll(bson.M{"node": ip}, bson.M{"$set": bson.M{"node": node.ID, "hostname": node.Hostname}})
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
fmt.Println("failed to upgrade job latest logs: ", err.Error())
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
shouldStop = true
|
||||
return err
|
||||
})
|
||||
|
||||
return
|
||||
}
|
|
@ -17,7 +17,8 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
level = flag.Int("l", 0, "log level, -1:debug, 0:info, 1:warn, 2:error")
|
||||
level = flag.Int("l", 0, "log level, -1:debug, 0:info, 1:warn, 2:error")
|
||||
confFile = flag.String("conf", "conf/files/base.json", "config file path")
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -32,7 +33,7 @@ func main() {
|
|||
}
|
||||
log.SetLogger(logger.Sugar())
|
||||
|
||||
if err := cronsun.Init(); err != nil {
|
||||
if err = cronsun.Init(*confFile, true); err != nil {
|
||||
log.Errorf(err.Error())
|
||||
return
|
||||
}
|
||||
|
|
|
@ -18,7 +18,8 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
level = flag.Int("l", 0, "log level, -1:debug, 0:info, 1:warn, 2:error")
|
||||
level = flag.Int("l", 0, "log level, -1:debug, 0:info, 1:warn, 2:error")
|
||||
confFile = flag.String("conf", "conf/files/base.json", "config file path")
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -33,10 +34,11 @@ func main() {
|
|||
}
|
||||
log.SetLogger(logger.Sugar())
|
||||
|
||||
if err := cronsun.Init(); err != nil {
|
||||
if err = cronsun.Init(*confFile, true); err != nil {
|
||||
log.Errorf(err.Error())
|
||||
return
|
||||
}
|
||||
web.EnsureJobLogIndex()
|
||||
|
||||
l, err := net.Listen("tcp", conf.Config.Web.BindAddr)
|
||||
if err != nil {
|
||||
|
|
|
@ -14,7 +14,7 @@ var (
|
|||
_Uid int
|
||||
)
|
||||
|
||||
func Init() (err error) {
|
||||
func Init(baseConfFile string, watchConfiFile bool) (err error) {
|
||||
if initialized {
|
||||
return
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ func Init() (err error) {
|
|||
}
|
||||
|
||||
// init config
|
||||
if err = conf.Init(); err != nil {
|
||||
if err = conf.Init(baseConfFile, watchConfiFile); err != nil {
|
||||
return fmt.Errorf("Init Config failed: %s", err)
|
||||
}
|
||||
|
||||
|
|
71
conf/conf.go
71
conf/conf.go
|
@ -1,13 +1,15 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
client "github.com/coreos/etcd/clientv3"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/go-gomail/gomail"
|
||||
"github.com/satori/go.uuid"
|
||||
|
||||
"github.com/shunfei/cronsun/db"
|
||||
"github.com/shunfei/cronsun/event"
|
||||
|
@ -16,9 +18,6 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
confFile = flag.String("conf",
|
||||
"conf/files/base.json", "config file path")
|
||||
|
||||
Config = new(Conf)
|
||||
initialized bool
|
||||
|
||||
|
@ -26,23 +25,27 @@ var (
|
|||
exitChan = make(chan struct{})
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
func Init(confFile string, watchConfiFile bool) error {
|
||||
if initialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
if err := Config.parse(); err != nil {
|
||||
if err := Config.parse(confFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := Config.watch(); err != nil {
|
||||
return err
|
||||
|
||||
if watchConfiFile {
|
||||
if err := Config.watch(confFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type Conf struct {
|
||||
dir string
|
||||
Node string // node 进程路径
|
||||
Proc string // 当前执行任务路径
|
||||
Cmd string // cmd 路径
|
||||
|
@ -137,12 +140,46 @@ func cleanKeyPrefix(p string) string {
|
|||
return p
|
||||
}
|
||||
|
||||
func (c *Conf) parse() error {
|
||||
err := utils.LoadExtendConf(*confFile, c)
|
||||
const UUID_FILE = "CRONSUN_UUID"
|
||||
|
||||
func (c *Conf) UUID() (string, error) {
|
||||
b, err := ioutil.ReadFile(path.Join(c.dir, UUID_FILE))
|
||||
if err == nil {
|
||||
if len(b) == 0 {
|
||||
return c.genUUID()
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return c.genUUID()
|
||||
}
|
||||
|
||||
func (c *Conf) genUUID() (string, error) {
|
||||
u, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(path.Join(c.dir, UUID_FILE), []byte(u.String()), 0600)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func (c *Conf) parse(confFile string) error {
|
||||
err := utils.LoadExtendConf(confFile, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.dir = path.Dir(confFile)
|
||||
|
||||
if c.Etcd.DialTimeout > 0 {
|
||||
c.Etcd.conf.DialTimeout = time.Duration(c.Etcd.DialTimeout) * time.Second
|
||||
}
|
||||
|
@ -185,7 +222,7 @@ func (c *Conf) parse() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Conf) watch() error {
|
||||
func (c *Conf) watch(confFile string) error {
|
||||
var err error
|
||||
watcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
|
@ -207,7 +244,7 @@ func (c *Conf) watch() error {
|
|||
timer.Reset(duration)
|
||||
case <-timer.C:
|
||||
if update {
|
||||
c.reload()
|
||||
c.reload(confFile)
|
||||
event.Emit(event.WAIT, nil)
|
||||
update = false
|
||||
}
|
||||
|
@ -218,7 +255,7 @@ func (c *Conf) watch() error {
|
|||
}
|
||||
}()
|
||||
|
||||
return watcher.Add(*confFile)
|
||||
return watcher.Add(confFile)
|
||||
}
|
||||
|
||||
// 重新加载配置项
|
||||
|
@ -226,9 +263,9 @@ func (c *Conf) watch() error {
|
|||
// Etcd
|
||||
// Mgo
|
||||
// Web
|
||||
func (c *Conf) reload() {
|
||||
func (c *Conf) reload(confFile string) {
|
||||
cf := new(Conf)
|
||||
if err := cf.parse(); err != nil {
|
||||
if err := cf.parse(confFile); err != nil {
|
||||
log.Warnf("config file reload err: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
@ -237,7 +274,7 @@ func (c *Conf) reload() {
|
|||
cf.Node, cf.Proc, cf.Cmd, cf.Once, cf.Lock, cf.Group, cf.Noticer = c.Node, c.Proc, c.Cmd, c.Once, c.Lock, c.Group, c.Noticer
|
||||
|
||||
*c = *cf
|
||||
log.Infof("config file[%s] reload success", *confFile)
|
||||
log.Infof("config file[%s] reload success", confFile)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -79,3 +79,9 @@ func (self *Mdb) FindOne(collection string, query interface{}, result interface{
|
|||
return c.Find(query).One(result)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Mdb) RemoveId(collection string, id interface{}) error {
|
||||
return self.WithC(collection, func(c *mgo.Collection) error {
|
||||
return c.RemoveId(id)
|
||||
})
|
||||
}
|
||||
|
|
38
job.go
38
job.go
|
@ -67,7 +67,8 @@ type Job struct {
|
|||
LogExpiration int `json:"log_expiration"`
|
||||
|
||||
// 执行任务的结点,用于记录 job log
|
||||
runOn string
|
||||
runOn string
|
||||
hostname string
|
||||
// 用于存储分隔后的任务
|
||||
cmd []string
|
||||
// 控制同时执行任务数
|
||||
|
@ -185,9 +186,9 @@ func (j *Job) unlimit() {
|
|||
atomic.AddInt64(j.Count, -1)
|
||||
}
|
||||
|
||||
func (j *Job) Init(n string) {
|
||||
func (j *Job) Init(nodeID, hostname string) {
|
||||
var c int64
|
||||
j.Count, j.runOn = &c, n
|
||||
j.Count, j.runOn, j.hostname = &c, nodeID, hostname
|
||||
}
|
||||
|
||||
func (c *Cmd) lockTtl() int64 {
|
||||
|
@ -270,14 +271,14 @@ func (c *Cmd) lock() *locker {
|
|||
}
|
||||
|
||||
// 优先取结点里的值,更新 group 时可用 gid 判断是否对 job 进行处理
|
||||
func (j *JobRule) included(nid string, gs map[string]*Group) bool {
|
||||
for i, count := 0, len(j.NodeIDs); i < count; i++ {
|
||||
if nid == j.NodeIDs[i] {
|
||||
func (rule *JobRule) included(nid string, gs map[string]*Group) bool {
|
||||
for i, count := 0, len(rule.NodeIDs); i < count; i++ {
|
||||
if nid == rule.NodeIDs[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, gid := range j.GroupIDs {
|
||||
for _, gid := range rule.GroupIDs {
|
||||
if g, ok := gs[gid]; ok && g.Included(nid) {
|
||||
return true
|
||||
}
|
||||
|
@ -287,22 +288,22 @@ func (j *JobRule) included(nid string, gs map[string]*Group) bool {
|
|||
}
|
||||
|
||||
// 验证 timer 字段
|
||||
func (j *JobRule) Valid() error {
|
||||
func (rule *JobRule) Valid() error {
|
||||
// 注意 interface nil 的比较
|
||||
if j.Schedule != nil {
|
||||
if rule.Schedule != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(j.Timer) == 0 {
|
||||
if len(rule.Timer) == 0 {
|
||||
return ErrNilRule
|
||||
}
|
||||
|
||||
sch, err := cron.Parse(j.Timer)
|
||||
sch, err := cron.Parse(rule.Timer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid JobRule[%s], parse err: %s", j.Timer, err.Error())
|
||||
return fmt.Errorf("invalid JobRule[%s], parse err: %s", rule.Timer, err.Error())
|
||||
}
|
||||
|
||||
j.Schedule = sch
|
||||
rule.Schedule = sch
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -606,10 +607,16 @@ func (j *Job) Cmds(nid string, gs map[string]*Group) (cmds map[string]*Cmd) {
|
|||
return
|
||||
}
|
||||
|
||||
LOOP_TIMER_CMD:
|
||||
for _, r := range j.Rules {
|
||||
for _, id := range r.ExcludeNodeIDs {
|
||||
if nid == id {
|
||||
continue
|
||||
// 在当前定时器规则中,任务不会在该节点执行(节点被排除)
|
||||
// 但是任务可以在其它定时器中,在该节点被执行
|
||||
// 比如,一个定时器设置在凌晨 1 点执行,但是此时不想在这个节点执行,然后,
|
||||
// 同时又设置一个定时器在凌晨 2 点执行,这次这个任务由于某些原因,必须在当前节点执行
|
||||
// 下面的 LOOP_TIMER 标签,原因同上
|
||||
continue LOOP_TIMER_CMD
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -626,10 +633,11 @@ func (j *Job) Cmds(nid string, gs map[string]*Group) (cmds map[string]*Cmd) {
|
|||
}
|
||||
|
||||
func (j Job) IsRunOn(nid string, gs map[string]*Group) bool {
|
||||
LOOP_TIMER:
|
||||
for _, r := range j.Rules {
|
||||
for _, id := range r.ExcludeNodeIDs {
|
||||
if nid == id {
|
||||
continue
|
||||
continue LOOP_TIMER
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,8 @@ type JobLog struct {
|
|||
JobGroup string `bson:"jobGroup" json:"jobGroup"` // 任务分组,配合 Id 跳转用
|
||||
User string `bson:"user" json:"user"` // 执行此次任务的用户
|
||||
Name string `bson:"name" json:"name"` // 任务名称
|
||||
Node string `bson:"node" json:"node"` // 运行此次任务的节点 ip,索引
|
||||
Node string `bson:"node" json:"node"` // 运行此次任务的节点 id,索引
|
||||
Hostname string `bson:"hostname" json:"hostname"` // 运行此次任务的节点主机名称,索引
|
||||
Command string `bson:"command" json:"command,omitempty"` // 执行的命令,包括参数
|
||||
Output string `bson:"output" json:"output,omitempty"` // 任务输出的所有内容
|
||||
Success bool `bson:"success" json:"success"` // 是否执行成功
|
||||
|
@ -95,7 +96,8 @@ func CreateJobLog(j *Job, t time.Time, rs string, success bool) {
|
|||
Name: j.Name,
|
||||
User: j.User,
|
||||
|
||||
Node: j.runOn,
|
||||
Node: j.runOn,
|
||||
Hostname: j.hostname,
|
||||
|
||||
Command: j.Command,
|
||||
Output: rs,
|
||||
|
@ -124,7 +126,7 @@ func CreateJobLog(j *Job, t time.Time, rs string, success bool) {
|
|||
JobLog: jl,
|
||||
}
|
||||
latestLog.Id = ""
|
||||
if err := mgoDB.Upsert(Coll_JobLatestLog, bson.M{"node": jl.Node, "jobId": jl.JobId, "jobGroup": jl.JobGroup}, latestLog); err != nil {
|
||||
if err := mgoDB.Upsert(Coll_JobLatestLog, bson.M{"node": jl.Node, "hostname": jl.Hostname, "jobId": jl.JobId, "jobGroup": jl.JobGroup}, latestLog); err != nil {
|
||||
log.Errorf(err.Error())
|
||||
}
|
||||
|
||||
|
|
9
node.go
9
node.go
|
@ -23,8 +23,10 @@ const (
|
|||
// 执行 cron cmd 的进程
|
||||
// 注册到 /cronsun/node/<id>
|
||||
type Node struct {
|
||||
ID string `bson:"_id" json:"id"` // ip
|
||||
PID string `bson:"pid" json:"pid"` // 进程 pid
|
||||
ID string `bson:"_id" json:"id"` // machine id
|
||||
PID string `bson:"pid" json:"pid"` // 进程 pid
|
||||
IP string `bson:"ip" json:"ip"` // node ip
|
||||
Hostname string `bson:"hostname" json:"hostname"`
|
||||
|
||||
Version string `bson:"version" json:"version"`
|
||||
UpTime time.Time `bson:"up" json:"up"` // 启动时间
|
||||
|
@ -134,6 +136,9 @@ func WatchNode() client.WatchChan {
|
|||
|
||||
// On 结点实例启动后,在 mongoDB 中记录存活信息
|
||||
func (n *Node) On() {
|
||||
// remove old version(< 0.3.0) node info
|
||||
mgoDB.RemoveId(Coll_Node, n.IP)
|
||||
|
||||
n.Alived, n.Version, n.UpTime = true, Version, time.Now()
|
||||
if err := mgoDB.Upsert(Coll_Node, bson.M{"_id": n.ID}, n); err != nil {
|
||||
log.Errorf(err.Error())
|
||||
|
|
29
node/node.go
29
node/node.go
|
@ -35,16 +35,29 @@ type Node struct {
|
|||
}
|
||||
|
||||
func NewNode(cfg *conf.Conf) (n *Node, err error) {
|
||||
uuid, err := cfg.UUID()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ip, err := utils.LocalIP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostname = uuid
|
||||
err = nil
|
||||
}
|
||||
|
||||
n = &Node{
|
||||
Client: cronsun.DefalutClient,
|
||||
Node: &cronsun.Node{
|
||||
ID: ip.String(),
|
||||
PID: strconv.Itoa(os.Getpid()),
|
||||
ID: uuid,
|
||||
PID: strconv.Itoa(os.Getpid()),
|
||||
IP: ip.String(),
|
||||
Hostname: hostname,
|
||||
},
|
||||
Cron: cron.New(),
|
||||
|
||||
|
@ -62,6 +75,9 @@ func NewNode(cfg *conf.Conf) (n *Node, err error) {
|
|||
|
||||
// 注册到 /cronsun/node/xx
|
||||
func (n *Node) Register() (err error) {
|
||||
// remove old version(< 0.3.0) node info
|
||||
cronsun.DefalutClient.Delete(conf.Config.Node + n.IP)
|
||||
|
||||
pid, err := n.Node.Exist()
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -133,7 +149,7 @@ func (n *Node) loadJobs() (err error) {
|
|||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
job.Init(n.ID)
|
||||
job.Init(n.ID, n.Hostname)
|
||||
n.addJob(job, false)
|
||||
}
|
||||
|
||||
|
@ -142,6 +158,7 @@ func (n *Node) loadJobs() (err error) {
|
|||
|
||||
func (n *Node) addJob(job *cronsun.Job, notice bool) {
|
||||
n.link.addJob(job)
|
||||
|
||||
if job.IsRunOn(n.ID, n.groups) {
|
||||
n.jobs[job.ID] = job
|
||||
}
|
||||
|
@ -321,7 +338,7 @@ func (n *Node) groupAddNode(g *cronsun.Group) {
|
|||
continue
|
||||
}
|
||||
|
||||
job.Init(n.ID)
|
||||
job.Init(n.ID, n.Hostname)
|
||||
}
|
||||
|
||||
cmds := job.Cmds(n.ID, n.groups)
|
||||
|
@ -377,7 +394,7 @@ func (n *Node) watchJobs() {
|
|||
continue
|
||||
}
|
||||
|
||||
job.Init(n.ID)
|
||||
job.Init(n.ID, n.Hostname)
|
||||
n.addJob(job, true)
|
||||
case ev.IsModify():
|
||||
job, err := cronsun.GetJobFromKv(ev.Kv.Key, ev.Kv.Value)
|
||||
|
@ -386,7 +403,7 @@ func (n *Node) watchJobs() {
|
|||
continue
|
||||
}
|
||||
|
||||
job.Init(n.ID)
|
||||
job.Init(n.ID, n.Hostname)
|
||||
n.modJob(job)
|
||||
case ev.Type == client.EventTypeDelete:
|
||||
n.delJob(cronsun.GetIDFromKey(string(ev.Kv.Key)))
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"runtime"
|
||||
)
|
||||
|
||||
const Binary = "v0.2.3"
|
||||
const VersionNumber = "0.3.0"
|
||||
|
||||
var (
|
||||
Version = fmt.Sprintf("%s (build %s)", Binary, runtime.Version())
|
||||
Version = fmt.Sprintf("v%s (build %s)", VersionNumber, runtime.Version())
|
||||
)
|
||||
|
|
|
@ -13,6 +13,14 @@ import (
|
|||
"github.com/shunfei/cronsun"
|
||||
)
|
||||
|
||||
func EnsureJobLogIndex() {
|
||||
cronsun.GetDb().WithC(cronsun.Coll_JobLog, func(c *mgo.Collection) error {
|
||||
return c.EnsureIndex(mgo.Index{
|
||||
Key: []string{"beginTime"},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type JobLog struct{}
|
||||
|
||||
func (jl *JobLog) GetDetail(ctx *Context) {
|
||||
|
@ -43,7 +51,7 @@ func (jl *JobLog) GetDetail(ctx *Context) {
|
|||
}
|
||||
|
||||
func (jl *JobLog) GetList(ctx *Context) {
|
||||
nodes := getStringArrayFromQuery("nodes", ",", ctx.R)
|
||||
hostnames := getStringArrayFromQuery("hostnames", ",", ctx.R)
|
||||
names := getStringArrayFromQuery("names", ",", ctx.R)
|
||||
ids := getStringArrayFromQuery("ids", ",", ctx.R)
|
||||
begin := getTime(ctx.R.FormValue("begin"))
|
||||
|
@ -51,14 +59,11 @@ func (jl *JobLog) GetList(ctx *Context) {
|
|||
page := getPage(ctx.R.FormValue("page"))
|
||||
failedOnly := ctx.R.FormValue("failedOnly") == "true"
|
||||
pageSize := getPageSize(ctx.R.FormValue("pageSize"))
|
||||
sort := "-beginTime"
|
||||
if ctx.R.FormValue("sort") == "1" {
|
||||
sort = "beginTime"
|
||||
}
|
||||
orderBy := "-beginTime"
|
||||
|
||||
query := bson.M{}
|
||||
if len(nodes) > 0 {
|
||||
query["node"] = bson.M{"$in": nodes}
|
||||
if len(hostnames) > 0 {
|
||||
query["hostname"] = bson.M{"$in": hostnames}
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
|
@ -95,13 +100,13 @@ func (jl *JobLog) GetList(ctx *Context) {
|
|||
var err error
|
||||
if ctx.R.FormValue("latest") == "true" {
|
||||
var latestLogList []*cronsun.JobLatestLog
|
||||
latestLogList, pager.Total, err = cronsun.GetJobLatestLogList(query, page, pageSize, sort)
|
||||
latestLogList, pager.Total, err = cronsun.GetJobLatestLogList(query, page, pageSize, orderBy)
|
||||
for i := range latestLogList {
|
||||
latestLogList[i].JobLog.Id = bson.ObjectIdHex(latestLogList[i].RefLogId)
|
||||
pager.List = append(pager.List, &latestLogList[i].JobLog)
|
||||
}
|
||||
} else {
|
||||
pager.List, pager.Total, err = cronsun.GetJobLogList(query, page, pageSize, sort)
|
||||
pager.List, pager.Total, err = cronsun.GetJobLogList(query, page, pageSize, orderBy)
|
||||
}
|
||||
if err != nil {
|
||||
outJSONWithCode(ctx.W, http.StatusInternalServerError, err.Error())
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -40,31 +40,6 @@ export default {
|
|||
store,
|
||||
|
||||
mounted: function(){
|
||||
var vm = this;
|
||||
|
||||
this.$rest.GET('session?check=1').
|
||||
onsucceed(200, (resp) => {
|
||||
vm.$store.commit('enabledAuth', resp.enabledAuth);
|
||||
vm.$store.commit('setEmail', resp.email);
|
||||
vm.$store.commit('setRole', resp.role);
|
||||
|
||||
vm.$loadConfiguration();
|
||||
}).onfailed((data, xhr) => {
|
||||
if (xhr.status !== 401) {
|
||||
vm.$bus.$emit('error', data);
|
||||
} else {
|
||||
vm.$store.commit('enabledAuth', true);
|
||||
}
|
||||
vm.$router.push('/login');
|
||||
}).
|
||||
do();
|
||||
|
||||
this.$bus.$on('goLogin', () => {
|
||||
vm.$store.commit('setEmail', '');
|
||||
vm.$store.commit('setRole', 0);
|
||||
vm.$router.push('/login');
|
||||
});
|
||||
|
||||
$(this.$refs.langSelection).dropdown({
|
||||
onChange: function(value, text){
|
||||
var old = window.$.cookie('locale');
|
||||
|
|
|
@ -130,28 +130,26 @@ export default {
|
|||
chart.update();
|
||||
}
|
||||
|
||||
var renderNodeInfo = function(resp){
|
||||
vm.totalNodes = resp ? resp.length : 0;
|
||||
var online = 0;
|
||||
var offline = 0;
|
||||
var damaged = 0;
|
||||
for (var i in resp) {
|
||||
if (resp[i].alived && resp[i].connected) {
|
||||
online++;
|
||||
} else if (resp[i].alived && !resp[i].connected) {
|
||||
damaged++;
|
||||
} else if(!resp[i].alived) {
|
||||
offline++;
|
||||
}
|
||||
var nodes = this.$store.getters.nodes;
|
||||
this.totalNodes = nodes.length;
|
||||
var online = 0;
|
||||
var offline = 0;
|
||||
var damaged = 0;
|
||||
for (var id in nodes) {
|
||||
if (nodes[id].alived && nodes[id].connected) {
|
||||
online++;
|
||||
} else if (nodes[id].alived && !nodes[id].connected) {
|
||||
damaged++;
|
||||
} else if(!nodes[id].alived) {
|
||||
offline++;
|
||||
}
|
||||
|
||||
vm.totalOnlineNodes = online;
|
||||
vm.totalOfflineNodes = offline;
|
||||
vm.totalDamagedNodes = damaged;
|
||||
}
|
||||
|
||||
this.totalOnlineNodes = online;
|
||||
this.totalOfflineNodes = offline;
|
||||
this.totalDamagedNodes = damaged;
|
||||
|
||||
this.$rest.GET('/info/overview').onsucceed(200, renderJobInfo).do();
|
||||
this.$rest.GET('nodes').onsucceed(200, renderNodeInfo).do();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<i class="close icon"></i>
|
||||
<div class="header">{{$L('executing job: {job}', jobName)}}</div>
|
||||
<div class="content">
|
||||
<Dropdown :title="$L('node')" :items="nodes" v-on:change="changeNode"></Dropdown>
|
||||
<Dropdown :title="$L('node')" :items="nodes" v-on:change="changeNode" style="width:100%"></Dropdown>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="ui deny button">{{$L('cancel')}}</div>
|
||||
|
@ -51,8 +51,11 @@ export default {
|
|||
this.loading = true;
|
||||
this.$rest.GET('job/'+this.jobGroup+'-'+this.jobId+'/nodes').
|
||||
onsucceed(200, (resp)=>{
|
||||
resp.unshift('全部节点');
|
||||
vm.nodes = resp;
|
||||
var nodes = [{value: 'all nodes', name: vm.$L('all nodes')}];
|
||||
for (var i in resp) {
|
||||
nodes.push({value: resp[i], name: vm.$store.getters.getHostnameByID(resp[i])})
|
||||
}
|
||||
vm.nodes = nodes;
|
||||
}).
|
||||
onfailed((msg)=>{
|
||||
vm.$bus.$emit('error', msg);
|
||||
|
@ -65,7 +68,7 @@ export default {
|
|||
submit(){
|
||||
var vm = this;
|
||||
this.loading = true;
|
||||
var node = this.selectedNode === '全部节点' ? '' : this.selectedNode;
|
||||
var node = this.selectedNode === 'all nodes' ? '' : this.selectedNode;
|
||||
this.$rest.PUT('/job/'+this.jobGroup+'-'+this.jobId+'/execute?node='+node).
|
||||
onsucceed(204, ()=>{
|
||||
vm.$bus.$emit('success', '执行命令已发送,注意查看任务日志');
|
||||
|
@ -86,4 +89,4 @@ export default {
|
|||
Dropdown
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
</td>
|
||||
<td class="center aligned"><i class="icon" v-bind:class="{pause: job.pause, play: !job.pause, green: !job.pause}"></i></td>
|
||||
<td>{{job.group}}</td>
|
||||
<td>{{job.user}}</td>
|
||||
<td>{{job.user && job.user.length > 0 ? job.user : '-'}}</td>
|
||||
<td><router-link :to="'/job/edit/'+job.group+'/'+job.id">{{job.name}}</router-link></td>
|
||||
<td>
|
||||
<span v-if="!job.latestStatus">-</span>
|
||||
|
@ -110,12 +110,9 @@ export default {
|
|||
this.fetchList(this.buildQuery());
|
||||
}).do();
|
||||
|
||||
this.$rest.GET('nodes').onsucceed(200, (resp)=>{
|
||||
vm.nodes.push({name: vm.$L('all nodes'), value: ''});
|
||||
for (var i in resp) {
|
||||
vm.nodes.push(resp[i].id);
|
||||
}
|
||||
}).do();
|
||||
var nodes = Array.from(this.$store.getters.dropdownNodes);
|
||||
nodes.unshift({value: '', name: this.$L('all nodes')});
|
||||
vm.nodes = nodes;
|
||||
|
||||
$('.ui.checkbox').checkbox();
|
||||
},
|
||||
|
@ -186,7 +183,7 @@ export default {
|
|||
},
|
||||
|
||||
formatLatest: function(latest){
|
||||
return this.$L('on {node} took {times}, {begin ~ end}', latest.node, formatDuration(latest.beginTime, latest.endTime), formatTime(latest.beginTime, latest.endTime));
|
||||
return this.$L('on {node} took {times}, {begin ~ end}', latest.hostname, formatDuration(latest.beginTime, latest.endTime), formatTime(latest.beginTime, latest.endTime));
|
||||
},
|
||||
|
||||
showExecuteJobModal: function(jobName, jobGroup, jobId){
|
||||
|
|
|
@ -38,12 +38,7 @@ export default {
|
|||
|
||||
mounted: function(){
|
||||
var vm = this;
|
||||
this.$rest.GET('nodes').onsucceed(200, (resp)=>{
|
||||
for (var i in resp) {
|
||||
vm.activityNodes.push(resp[i].id);
|
||||
}
|
||||
}).do();
|
||||
|
||||
this.activityNodes = this.$store.getters.dropdownNodes;
|
||||
|
||||
this.$rest.GET('node/groups').onsucceed(200, (resp)=>{
|
||||
var groups = [];
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
<input type="text" ref="ids" v-model:value="ids" :placeholder="$L('multiple IDs can separated by commas')"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{$L('select group')}}</label>
|
||||
<Dropdown :title="$L('select group')" v-bind:items="prefetchs.groups" v-on:change="changeGroup" :selected="groups" :multiple="true"/>
|
||||
<label>{{$L('select groups')}}</label>
|
||||
<Dropdown :title="$L('select groups')" v-bind:items="prefetchs.groups" v-on:change="changeGroup" :selected="groups" :multiple="true"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{$L('select nodes')}}</label>
|
||||
|
@ -137,4 +137,4 @@ export default {
|
|||
Dropdown
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</div>
|
||||
<div class="field">
|
||||
<label>{{$L('node')}}</label>
|
||||
<input type="text" v-model="nodes" :placeholder="$L('multiple IPs can separated by commas')">
|
||||
<input type="text" v-model="hostnames" :placeholder="$L('multiple Hostnames can separated by commas')">
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
|
@ -56,7 +56,7 @@
|
|||
<tbody>
|
||||
<tr v-for="log in list">
|
||||
<td><router-link class="item" :to="'/job/edit/'+log.jobGroup+'/'+log.jobId">{{log.name}}</router-link></td>
|
||||
<td>{{log.node}}</td>
|
||||
<td :title="log.node">{{$store.getters.getHostnameByID(log.node)}}</td>
|
||||
<td>{{log.user}}</td>
|
||||
<td :class="{warning: durationAttention(log.beginTime, log.endTime)}"><i class="attention icon" v-if="durationAttention(log.beginTime, log.endTime)"></i> {{formatTime(log)}}</td>
|
||||
<td :class="{error: !log.success}">
|
||||
|
@ -66,7 +66,7 @@
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Pager v-if="list && list.length>0" :total="total" :length="5"/>
|
||||
<Pager v-if="list && list.length>0" :total="total" :maxBtn="5"/>
|
||||
<ExecuteJob ref="executeJobModal"/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -83,7 +83,7 @@ export default {
|
|||
loading: false,
|
||||
names: '',
|
||||
ids: '',
|
||||
nodes: '',
|
||||
hostnames: '',
|
||||
begin: '',
|
||||
end: '',
|
||||
latest: false,
|
||||
|
@ -114,7 +114,7 @@ export default {
|
|||
fillParams(){
|
||||
this.names = this.$route.query.names || '';
|
||||
this.ids = this.$route.query.ids || '';
|
||||
this.nodes = this.$route.query.nodes || '';
|
||||
this.hostnames = this.$route.query.hostnames || '';
|
||||
this.begin = this.$route.query.begin || '';
|
||||
this.end = this.$route.query.end || '';
|
||||
this.page = this.$route.query.page || 1;
|
||||
|
@ -139,7 +139,7 @@ export default {
|
|||
var params = [];
|
||||
if (this.names) params.push('names='+this.names);
|
||||
if (this.ids) params.push('ids='+this.ids);
|
||||
if (this.nodes) params.push('nodes='+this.nodes);
|
||||
if (this.hostnames) params.push('hostnames='+this.hostnames);
|
||||
if (this.begin) params.push('begin='+this.begin);
|
||||
if (this.end) params.push('end='+this.end);
|
||||
if (this.failedOnly) params.push('failedOnly=true');
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</div>
|
||||
<div class="ui segment">
|
||||
<p>
|
||||
<span class="title">{{$L('node')}}</span> {{log.node}}
|
||||
<span class="title">{{$L('node')}}</span> {{node.hostname}} [{{node.ip}}]
|
||||
</p>
|
||||
</div>
|
||||
<div class="ui segment">
|
||||
|
@ -65,6 +65,7 @@ export default {
|
|||
beginTime: new Date(),
|
||||
endTime: new Date()
|
||||
},
|
||||
node: {},
|
||||
error: ''
|
||||
}
|
||||
},
|
||||
|
@ -78,8 +79,17 @@ export default {
|
|||
mounted: function(){
|
||||
var vm = this;
|
||||
this.$rest.GET('log/'+this.$route.params.id).
|
||||
onsucceed(200, (resp)=>{vm.log = resp}).
|
||||
onfailed((data)=>{vm.error = data}).
|
||||
onsucceed(200, (resp)=>{
|
||||
vm.log = resp;
|
||||
vm.node = vm.$store.getters.getNodeByID(resp.node)
|
||||
}).
|
||||
onfailed((data, xhr) => {
|
||||
if (xhr.status === 404) {
|
||||
vm.error = vm.$L('log has been deleted')
|
||||
} else {
|
||||
vm.error = data
|
||||
}
|
||||
}).
|
||||
do();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<style scoped>
|
||||
.node {
|
||||
width: 140px;
|
||||
padding: 0 13px;
|
||||
border-radius: 3px;
|
||||
margin: 3px;
|
||||
display: inline-block;
|
||||
background: #e8e8e8;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
line-height: 1.9em;
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,6 @@
|
|||
<div>
|
||||
<div class="clearfix">
|
||||
<router-link class="ui right floated primary button" to="/node/group"><i class="cubes icon"></i> {{$L('group manager')}}</router-link>
|
||||
<div class="ui label"
|
||||
<div class="ui label" v-for="group in groups" v-bind:title="$L(group.title)">
|
||||
<i class="cube icon" v-bind:class="group.css"></i> {{group.nodes.length}} {{$L(group.name)}}
|
||||
</div>
|
||||
|
@ -42,7 +41,7 @@
|
|||
<div v-for="(node, nodeIndex) in group.nodes" class="node" v-bind:title="node.title">
|
||||
<router-link class="item" :to="'/job?node='+node.id">
|
||||
<i class="red icon fork" v-if="node.version !== version" :title="$L('version inconsistent, node: {version}', node.version)"></i>
|
||||
{{node.id}}
|
||||
{{node.hostname || node.id+"(need to upgrade)"}}
|
||||
</router-link>
|
||||
<i v-if="groupIndex != 2" v-on:click="removeConfirm(groupIndex, nodeIndex, node.id)" class="icon remove"></i>
|
||||
</div>
|
||||
|
@ -70,30 +69,20 @@ export default {
|
|||
this.$rest.GET('version').onsucceed(200, (resp)=>{
|
||||
vm.version = resp;
|
||||
}).do();
|
||||
this.$rest.GET('nodes').onsucceed(200, (resp)=>{
|
||||
resp.sort(function(a, b){
|
||||
var aid = a.id.split('.');
|
||||
var bid = b.id.split('.');
|
||||
var ai = 0, bi = 0;
|
||||
for (var i in aid) {
|
||||
ai += (+aid[i])*Math.pow(255,3-i);
|
||||
bi += (+bid[i])*Math.pow(255,3-i);
|
||||
}
|
||||
return ai - bi;
|
||||
});
|
||||
for (var i in resp) {
|
||||
var n = resp[i];
|
||||
n.title = n.version + "\nstarted at: " + n.up
|
||||
if (n.alived && n.connected) {
|
||||
vm.groups[2].nodes.push(n);
|
||||
} else if (n.alived && !n.connected) {
|
||||
vm.groups[0].nodes.push(n);
|
||||
} else {
|
||||
vm.groups[1].nodes.push(n);
|
||||
}
|
||||
|
||||
var nodes = this.$store.getters.nodes;
|
||||
for (var id in nodes) {
|
||||
var n = nodes[id];
|
||||
n.title = n.ip + "\n" + n.id + "\n" + n.version + "\nstarted at: " + n.up
|
||||
if (n.alived && n.connected) {
|
||||
vm.groups[2].nodes.push(n);
|
||||
} else if (n.alived && !n.connected) {
|
||||
vm.groups[0].nodes.push(n);
|
||||
} else {
|
||||
vm.groups[1].nodes.push(n);
|
||||
}
|
||||
vm.count = resp.length || 0;
|
||||
}).do();
|
||||
}
|
||||
vm.count = nodes.length || 0;
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -101,7 +90,7 @@ export default {
|
|||
if (!confirm(this.$L('are you sure to remove the node {nodeId}?', nodeId))) return;
|
||||
|
||||
var vm = this;
|
||||
this.$rest.DELETE('node/'+nodeId).onsucceed(204, (resp)=>{
|
||||
this.$rest.DELETE('node/'+nodeId).onsucceed(204, (resp) => {
|
||||
vm.groups[groupIndex].nodes.splice(nodeIndex, 1);
|
||||
}).do();
|
||||
}
|
||||
|
|
|
@ -30,7 +30,13 @@
|
|||
<router-link class="header" :to="'/node/group/'+g.id">{{g.name}}</router-link>
|
||||
<div class="description">
|
||||
<div class="ui middle large aligned divided list">
|
||||
<div class="item" v-for="n in g.nids">{{n}}</div>
|
||||
<div class="item" v-for="nodeID in g.nids">
|
||||
<span v-if="nodes[nodeID]">{{nodes[nodeID].hostname || nodes[nodeID].id}}
|
||||
<i class="arrow circle up icon red" v-if="nodes[nodeID].hostname == ''"></i>
|
||||
<i v-if="nodes[nodeID].hostname == ''">(need to upgrade)</i>
|
||||
</span>
|
||||
<span v-else :title="$L('node not found, was it removed?')">{{nodeID}} <i class="question circle icon red"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -63,6 +69,12 @@ export default {
|
|||
onend(()=>{vm.loading = false}).
|
||||
do();
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
nodes: function () {
|
||||
return this.$store.getters.nodes;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
<div class="field">
|
||||
<label>{{$L('include nodes')}}</label>
|
||||
<Dropdown :title="$L('select nodes')" multiple="true" v-bind:items="allNodes" v-bind:selected="group.nids" v-on:change="changeGroup"/>
|
||||
<Dropdown :title="$L('select nodes')" multiple="true" v-bind:items="$store.getters.dropdownNodes" v-bind:selected="group.nids" v-on:change="changeGroup"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="fluid blue ui button" type="button" v-on:click="submit"><i class="upload icon"></i> {{$L('save group')}}</button>
|
||||
|
@ -30,7 +30,6 @@ export default {
|
|||
error: '',
|
||||
loading: false,
|
||||
action: '',
|
||||
allNodes: [],
|
||||
group: {
|
||||
id: '',
|
||||
name: '',
|
||||
|
@ -55,14 +54,6 @@ export default {
|
|||
onend(()=>{vm.loading = false}).
|
||||
do();
|
||||
}
|
||||
|
||||
this.$rest.GET('nodes').onsucceed(200, (resp)=>{
|
||||
var allNodes = [];
|
||||
for (var i in resp) {
|
||||
allNodes.push(resp[i].id);
|
||||
}
|
||||
vm.allNodes = allNodes;
|
||||
}).do();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -100,4 +91,4 @@ export default {
|
|||
Dropdown
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div style="text-align: center; margin-bottom: 1em;">
|
||||
<div class="ui icon buttons">
|
||||
<router-link :to="pageURL(_startPage-1)" class="ui button" :class="{disabled: _startPage<=1}"><i class="angle left icon"></i></router-link>
|
||||
<router-link :to="pageURL(_startPage + n - 1)" v-for="n in _pageBtnNum" class="ui button" :class="{blue: _startPage+n-1 == _current}">{{_startPage + n-1}}</router-link>
|
||||
<router-link :to="pageURL(startPage-1)" class="ui button" :class="{disabled: startPage<=1}"><i class="angle left icon"></i></router-link>
|
||||
<router-link :to="pageURL(startPage + n - 1)" v-for="n in pageBtnNum" class="ui button" :class="{blue: startPage+n-1 == _current}">{{startPage + n-1}}</router-link>
|
||||
<a class="ui button disabled">{{_current}}/{{total}}</a>
|
||||
<router-link :to="pageURL(_startPage+length)" class="ui button" :class="{disabled: _startPage+length>total}"><i class="angle right icon"></i></router-link>
|
||||
<router-link :to="pageURL(startPage+maxBtn)" class="ui button" :class="{disabled: startPage+maxBtn>total}"><i class="angle right icon"></i></router-link>
|
||||
</div>
|
||||
<div class="ui action input">
|
||||
<input type="text" ref="gopage" style="width: 70px;">
|
||||
|
@ -20,22 +20,19 @@ export default {
|
|||
name: 'pager',
|
||||
props: {
|
||||
total: {type: Number, default: 1, required: true},
|
||||
length: {type: Number, default: 5}
|
||||
maxBtn: {type: Number, default: 5}
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
_pagevar: '',
|
||||
_pagevar: 'page',
|
||||
_current: 1,
|
||||
_startPage: 1,
|
||||
_pageBtnNUm: 5
|
||||
startPage: 1
|
||||
}
|
||||
},
|
||||
|
||||
created: function(){
|
||||
this._pagevar = this.pageVar || 'page';
|
||||
this._current = this.$route.query[this._pagevar] || 1;
|
||||
this._startPage = Math.floor((this._current-1)/this.length) * this.length + 1;
|
||||
this._pageBtnNum = this.total - this._startPage - this.length <= 0 ? this.total - this._startPage + 1 : this.length;
|
||||
this._current = parseInt(this.$route.query[this._pagevar]) || 1;
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -52,9 +49,8 @@ export default {
|
|||
|
||||
watch: {
|
||||
'$route': function(){
|
||||
this._current = this.$route.query[this._pagevar] || 1;
|
||||
this._startPage = Math.floor((this._current-1)/this.length) * this.length + 1;
|
||||
this._pageBtnNum = this.total - this._startPage - this.length <= 0 ? this.total - this._startPage + 1 : this.length;
|
||||
this._current = parseInt(this.$route.query[this._pagevar]) || 1;
|
||||
this.startPage = Math.floor((this._current-1)/this.maxBtn) * this.maxBtn + 1;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -67,7 +63,11 @@ export default {
|
|||
}
|
||||
|
||||
return this.$route.path+'?'+query.join('&') + '&';
|
||||
},
|
||||
|
||||
pageBtnNum: function(){
|
||||
return this.total - this.startPage - this.maxBtn <= 0 ? this.total - this.startPage + 1 : this.maxBtn;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -30,7 +30,7 @@ var language = {
|
|||
'multiple names can separated by commas': 'Multiple names can separated by commas',
|
||||
'job ID': 'Job ID',
|
||||
'multiple IDs can separated by commas': 'Multiple IDs can separated by commas',
|
||||
'multiple IPs can separated by commas': 'Multiple IPs can separated by commas',
|
||||
'multiple Hostnames can separated by commas': 'Multiple Hostnames can separated by commas',
|
||||
'starting date': 'Starting date',
|
||||
'end date': 'End date',
|
||||
'failure only': 'Failure only',
|
||||
|
@ -75,6 +75,7 @@ var language = {
|
|||
'spend time': 'Spend time',
|
||||
'result': 'Result',
|
||||
'loading configurations': 'Loading configurations',
|
||||
'log has been deleted': 'Log has been deleted',
|
||||
|
||||
'job type': 'Job type',
|
||||
'common job': 'Common',
|
||||
|
@ -126,7 +127,8 @@ var language = {
|
|||
'select nodes': 'Select nodes',
|
||||
'select groups': 'Select groups',
|
||||
'are you sure to delete the group {name}?': 'Are you sure to delete the group {0}?',
|
||||
'are you sure to remove the node {nodeId}?': 'Are you sure to remove the node {0}?'
|
||||
'are you sure to remove the node {nodeId}?': 'Are you sure to remove the node {0}?',
|
||||
'node not found, was it removed?': 'Node not found, was it removed?'
|
||||
}
|
||||
|
||||
export default language;
|
||||
|
|
|
@ -30,7 +30,7 @@ var language = {
|
|||
'multiple names can separated by commas': '多个名称用英文逗号分隔',
|
||||
'job ID': '任务 ID',
|
||||
'multiple IDs can separated by commas': '多个 ID 用英文逗号分隔',
|
||||
'multiple IPs can separated by commas': '多个 IP 用英文逗号分隔',
|
||||
'multiple Hostnames can separated by commas': '多个主机名称用英文逗号分隔',
|
||||
'starting date': '起始日期',
|
||||
'end date': '截至日期',
|
||||
'failure only': '只看失败的任务',
|
||||
|
@ -77,6 +77,7 @@ var language = {
|
|||
'spend time': '耗时',
|
||||
'result': '结果',
|
||||
'loading configurations': '正在加载配置',
|
||||
'log has been deleted': '日志已经被删除',
|
||||
|
||||
'job type': '任务类型',
|
||||
'common job': '普通任务',
|
||||
|
@ -128,7 +129,8 @@ var language = {
|
|||
'select nodes': '选择节点',
|
||||
'select groups': '选择分组',
|
||||
'are you sure to delete the group {name}?': '确定删除分组 {0}?',
|
||||
'are you sure to remove the node {nodeId}?': '确定删除节点 {0}?'
|
||||
'are you sure to remove the node {nodeId}?': '确定删除节点 {0}?',
|
||||
'node not found, was it removed?': '不存在的节点,被删除了吗?'
|
||||
}
|
||||
|
||||
export default language;
|
||||
|
|
|
@ -53,4 +53,4 @@ var split = function(str, sep){
|
|||
return str.split(sep || ',');
|
||||
}
|
||||
|
||||
export {formatDuration, formatTime, formatNumber, split};
|
||||
export {formatDuration, formatTime, formatNumber, split};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
window.$ = window.jQuery = require('jquery');
|
||||
require('semantic');
|
||||
require('semantic-ui/dist/semantic.min.css');
|
||||
import store from './vuex/store';
|
||||
|
||||
import Vue from 'vue';
|
||||
Vue.config.debug = true;
|
||||
|
@ -110,8 +111,55 @@ var router = new VueRouter({
|
|||
routes: routes
|
||||
});
|
||||
|
||||
var app = new Vue({
|
||||
el: '#app',
|
||||
render: h => h(App),
|
||||
router: router
|
||||
bus.$on('goLogin', () => {
|
||||
store.commit('setEmail', '');
|
||||
store.commit('setRole', 0);
|
||||
router.push('/login');
|
||||
});
|
||||
|
||||
var initConf = new Promise((resolve) => {
|
||||
restApi.GET('session?check=1').
|
||||
onsucceed(200, (resp) => {
|
||||
store.commit('enabledAuth', resp.enabledAuth);
|
||||
store.commit('setEmail', resp.email);
|
||||
store.commit('setRole', resp.role);
|
||||
|
||||
restApi.GET('configurations').
|
||||
onsucceed(200, (resp) => {
|
||||
Vue.use((Vue) => Vue.prototype.$appConfig = resp);
|
||||
bus.$emit('conf_loaded', resp);
|
||||
|
||||
restApi.GET('nodes').onsucceed(200, (resp)=>{
|
||||
var nodes = {};
|
||||
for (var i in resp) {
|
||||
nodes[resp[i].id] = resp[i];
|
||||
}
|
||||
store.commit('setNodes', nodes);
|
||||
resolve();
|
||||
}).do();
|
||||
}).onfailed((data, xhr) => {
|
||||
bus.$emit('error', data ? data : xhr.status + ' ' + xhr.statusText);
|
||||
resolve();
|
||||
}).do();
|
||||
}).onfailed((data, xhr) => {
|
||||
if (xhr.status !== 401) {
|
||||
bus.$emit('error', data);
|
||||
} else {
|
||||
store.commit('enabledAuth', true);
|
||||
}
|
||||
router.push('/login');
|
||||
resolve()
|
||||
}).
|
||||
do();
|
||||
})
|
||||
|
||||
initConf.then(() => {
|
||||
new Vue({
|
||||
el: '#app',
|
||||
render: h => h(App),
|
||||
router: router
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -3,13 +3,15 @@ import Vuex from 'vuex';
|
|||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
enabledAuth: false,
|
||||
user: {
|
||||
email: '',
|
||||
role: 0
|
||||
}
|
||||
},
|
||||
nodes: {},
|
||||
dropdownNodes: []
|
||||
},
|
||||
|
||||
getters: {
|
||||
|
@ -23,6 +25,26 @@ export default new Vuex.Store({
|
|||
|
||||
enabledAuth: function (state) {
|
||||
return state.enabledAuth;
|
||||
},
|
||||
|
||||
nodes: function (state) {
|
||||
return state.nodes;
|
||||
},
|
||||
|
||||
getHostnameByID: function (state) {
|
||||
return (id) => {
|
||||
return state.nodes[id] ? state.nodes[id].hostname : id;
|
||||
}
|
||||
},
|
||||
|
||||
getNodeByID: function (state) {
|
||||
return (id) => {
|
||||
return state.nodes[id]
|
||||
}
|
||||
},
|
||||
|
||||
dropdownNodes: function (state) {
|
||||
return state.dropdownNodes;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -37,6 +59,17 @@ export default new Vuex.Store({
|
|||
|
||||
enabledAuth: function (state, enabledAuth) {
|
||||
state.enabledAuth = enabledAuth;
|
||||
},
|
||||
|
||||
setNodes: function (state, nodes) {
|
||||
state.nodes = nodes;
|
||||
var dn = []
|
||||
for (var i in nodes) {
|
||||
dn.push({value: nodes[i].id, name: nodes[i].hostname || nodes[i].id + '(need to upgrade)'})
|
||||
}
|
||||
state.dropdownNodes = dn;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default store
|
||||
|
|
Loading…
Reference in New Issue