Merge pull request #69 from shunfei/develop

v0.3.0
pull/98/merge
Doflatango 2018-03-08 14:38:48 +08:00 committed by GitHub
commit 7eba17e1b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 832 additions and 211 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ web/ui/node_modules
web/ui/dist
.vscode
*npm-debug.log
vendor

View File

@ -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:

37
bin/csctl/cmd.go Normal file
View File

@ -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)
}
}

138
bin/csctl/cmd/backup.go Normal file
View File

@ -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
}

142
bin/csctl/cmd/restore.go Normal file
View File

@ -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
}

162
bin/csctl/cmd/upgrade.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
View File

@ -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
}
}

View File

@ -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())
}

View File

@ -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())

View File

@ -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)))

View File

@ -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())
)

View File

@ -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

View File

@ -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');

View File

@ -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>

View File

@ -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>

View File

@ -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){

View File

@ -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 = [];

View File

@ -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>

View File

@ -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');

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -53,4 +53,4 @@ var split = function(str, sep){
return str.split(sep || ',');
}
export {formatDuration, formatTime, formatNumber, split};
export {formatDuration, formatTime, formatNumber, split};

View File

@ -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
});
})

View File

@ -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