use machine id instead of ip

pull/69/head
Doflatango 2018-03-06 14:11:14 +08:00
parent 2f94f1dd9c
commit 9bbd94698b
26 changed files with 307 additions and 175 deletions

1
.gitignore vendored
View File

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

View File

@ -33,38 +33,39 @@ var UpgradeCmd = &cobra.Command{
if prever < "0.3.0" {
fmt.Println("upgrading data to version 0.3.0")
ipMapper := getIPMapper(ea)
if to_0_3_0(ea, ipMapper) {
nodesById := getIPMapper(ea)
if to_0_3_0(ea, nodesById) {
return
}
}
},
}
func getIPMapper(ea *ExitAction) map[string]string {
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]string, len(nodes))
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.ID
ipMapper[n.IP] = n
}
return ipMapper
}
func to_0_3_0(ea *ExitAction, ipMapper map[string]string) (shouldStop bool) {
// 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 machineID, ok := ipMapper[list[i]]; ok {
list[i] = machineID
if node, ok := nodesById[list[i]]; ok {
list[i] = node.ID
}
}
}
@ -129,8 +130,8 @@ func to_0_3_0(ea *ExitAction, ipMapper map[string]string) (shouldStop bool) {
// upgrade logs
cronsun.GetDb().WithC(cronsun.Coll_JobLog, func(c *mgo.Collection) error {
for ip, mid := range ipMapper {
_, err = c.UpdateAll(bson.M{"node": ip}, bson.M{"$set": bson.M{"node": mid}})
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())
@ -142,5 +143,20 @@ func to_0_3_0(ea *ExitAction, ipMapper map[string]string) (shouldStop bool) {
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

@ -38,6 +38,7 @@ func main() {
log.Errorf(err.Error())
return
}
web.EnsureJobLogIndex()
l, err := net.Listen("tcp", conf.Config.Web.BindAddr)
if err != nil {

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
}
@ -605,10 +606,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
}
}
@ -625,10 +632,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

@ -7,6 +7,7 @@ import (
"time"
client "github.com/coreos/etcd/clientv3"
"github.com/denisbrodbeck/machineid"
"github.com/shunfei/cronsun"
"github.com/shunfei/cronsun/conf"
@ -35,16 +36,29 @@ type Node struct {
}
func NewNode(cfg *conf.Conf) (n *Node, err error) {
mid, err := machineid.ProtectedID("cronsun")
if err != nil {
return
}
ip, err := utils.LocalIP()
if err != nil {
return
}
hostname, err := os.Hostname()
if err != nil {
hostname = mid
err = nil
}
n = &Node{
Client: cronsun.DefalutClient,
Node: &cronsun.Node{
ID: ip.String(),
PID: strconv.Itoa(os.Getpid()),
ID: mid,
PID: strconv.Itoa(os.Getpid()),
IP: ip.String(),
Hostname: hostname,
},
Cron: cron.New(),
@ -62,6 +76,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 +150,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 +159,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
}
@ -314,7 +332,7 @@ func (n *Node) groupAddNode(g *cronsun.Group) {
n.link.delGroupJob(g.ID, jid)
continue
}
job.Init(n.ID)
job.Init(n.ID, n.Hostname)
}
cmds := job.Cmds(n.ID, n.groups)
@ -370,7 +388,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)
@ -379,7 +397,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

@ -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}">
@ -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.substr(0, 16) + "\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

@ -22,6 +22,7 @@
<script>
import Dropdown from './basic/Dropdown.vue';
import {nodeDropdownData} from '../libraries/functions';
export default {
name: 'node_group_edit',
@ -57,11 +58,7 @@ export default {
}
this.$rest.GET('nodes').onsucceed(200, (resp)=>{
var allNodes = [];
for (var i in resp) {
allNodes.push(resp[i].id);
}
vm.allNodes = allNodes;
vm.allNodes = nodeDropdownData(resp);
}).do();
},
@ -100,4 +97,4 @@ export default {
Dropdown
}
}
</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,13 @@ var split = function(str, sep){
return str.split(sep || ',');
}
export {formatDuration, formatTime, formatNumber, split};
var nodeDropdownData = function(nodeList){
var data = [];
nodeList.forEach(n => {
data.push({value: n.id, name: n.hostname == '' ? n.id : n.hostname});
});
return data;
}
export {formatDuration, formatTime, formatNumber, split, nodeDropdownData};

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