Merge pull request #45 from shunfei/develop

v0.2.2
pull/62/head v0.2.2
Doflatango 7 years ago committed by GitHub
commit 3a48caf81b

@ -21,6 +21,7 @@ import (
"github.com/shunfei/cronsun/conf"
"github.com/shunfei/cronsun/log"
"github.com/shunfei/cronsun/node/cron"
"github.com/shunfei/cronsun/utils"
)
const (
@ -387,7 +388,15 @@ func (j *Job) alone() {
}
func (j *Job) splitCmd() {
j.cmd = strings.Split(j.Command, " ")
ps := strings.SplitN(j.Command, " ", 2)
if len(ps) == 1 {
j.cmd = ps
return
}
j.cmd = make([]string, 0, 2)
j.cmd = append(j.cmd, ps[0])
j.cmd = append(j.cmd, utils.ParseCmdArguments(ps[1])...)
}
func (j *Job) String() string {

@ -90,7 +90,14 @@ func GetNodesBy(query interface{}) (nodes []*Node, err error) {
return
}
func ISNodeFault(id string) (bool, error) {
func RemoveNode(query interface{}) error {
return mgoDB.WithC(Coll_Node, func(c *mgo.Collection) error {
return c.Remove(query)
})
}
func ISNodeAlive(id string) (bool, error) {
n := 0
err := mgoDB.WithC(Coll_Node, func(c *mgo.Collection) error {
var e error

@ -182,7 +182,7 @@ func monitorNodes(n Noticer) {
switch {
case ev.Type == client.EventTypeDelete:
id = GetIDFromKey(string(ev.Kv.Key))
ok, err = ISNodeFault(id)
ok, err = ISNodeAlive(id)
if err != nil {
log.Warnf("query node[%s] err: %s", id, err.Error())
continue
@ -190,7 +190,8 @@ func monitorNodes(n Noticer) {
if ok {
n.Send(&Message{
Subject: "node[" + id + "] fault at time[" + time.Now().Format(time.RFC3339) + "]",
Subject: "Node[" + id + "] break away cluster, this happed at " + time.Now().Format(time.RFC3339),
Body: "Node breaked away cluster, this might happed when node crash or network problems.",
To: conf.Config.Mail.To,
})
}

@ -0,0 +1,144 @@
package utils
import (
"errors"
)
type fmsState int
const (
stateArgumentOutside fmsState = iota
stateArgumentStart
stateArgumentEnd
)
var errEndOfLine = errors.New("End of line")
type cmdArgumentParser struct {
s string
i int
length int
state fmsState
startToken byte
shouldEscape bool
currArgument []byte
err error
}
func newCmdArgumentParser(s string) *cmdArgumentParser {
return &cmdArgumentParser{
s: s,
i: -1,
length: len(s),
currArgument: make([]byte, 0, 16),
}
}
func (cap *cmdArgumentParser) parse() (arguments []string) {
for {
cap.next()
if cap.err != nil {
if cap.shouldEscape {
cap.currArgument = append(cap.currArgument, '\\')
}
if len(cap.currArgument) > 0 {
arguments = append(arguments, string(cap.currArgument))
}
return
}
switch cap.state {
case stateArgumentOutside:
cap.detectStartToken()
case stateArgumentStart:
if !cap.detectEnd() {
cap.detectContent()
}
case stateArgumentEnd:
cap.state = stateArgumentOutside
arguments = append(arguments, string(cap.currArgument))
cap.currArgument = cap.currArgument[:0]
}
}
}
func (cap *cmdArgumentParser) previous() {
if cap.i >= 0 {
cap.i--
}
}
func (cap *cmdArgumentParser) next() {
if cap.length-cap.i == 1 {
cap.err = errEndOfLine
return
}
cap.i++
}
func (cap *cmdArgumentParser) detectStartToken() {
c := cap.s[cap.i]
if c == ' ' {
return
}
switch c {
case '\\':
cap.startToken = 0
cap.shouldEscape = true
case '"', '\'':
cap.startToken = c
default:
cap.startToken = 0
cap.previous()
}
cap.state = stateArgumentStart
}
func (cap *cmdArgumentParser) detectContent() {
c := cap.s[cap.i]
if cap.shouldEscape {
switch c {
case ' ', '\\', cap.startToken:
cap.currArgument = append(cap.currArgument, c)
default:
cap.currArgument = append(cap.currArgument, '\\', c)
}
cap.shouldEscape = false
return
}
if c == '\\' {
cap.shouldEscape = true
} else {
cap.currArgument = append(cap.currArgument, c)
}
}
func (cap *cmdArgumentParser) detectEnd() (detected bool) {
c := cap.s[cap.i]
if cap.startToken == 0 {
if c == ' ' && !cap.shouldEscape {
cap.state = stateArgumentEnd
cap.previous()
return true
}
return false
}
if c == cap.startToken && !cap.shouldEscape {
cap.state = stateArgumentEnd
return true
}
return false
}
func ParseCmdArguments(s string) (arguments []string) {
return newCmdArgumentParser(s).parse()
}

@ -0,0 +1,78 @@
package utils
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestCmdArgumentParser(t *testing.T) {
var args []string
var str string
Convey("Parse Cmd Arguments ["+str+"]", t, func() {
args = ParseCmdArguments(str)
So(len(args), ShouldEqual, 0)
})
str = " "
Convey("Parse Cmd Arguments ["+str+"]", t, func() {
args = ParseCmdArguments(str)
So(len(args), ShouldEqual, 0)
})
str = "aa bbb ccc "
Convey("Parse Cmd Arguments ["+str+"]", t, func() {
args = ParseCmdArguments(str)
So(len(args), ShouldEqual, 3)
So(args[0], ShouldEqual, "aa")
So(args[1], ShouldEqual, "bbb")
So(args[2], ShouldEqual, "ccc")
})
str = "' \\\""
Convey("Parse Cmd Arguments ["+str+"]", t, func() {
args = ParseCmdArguments(str)
So(len(args), ShouldEqual, 1)
So(args[0], ShouldEqual, " \\\"")
})
str = `a "b c"` // a "b c"
Convey("Parse Cmd Arguments ["+str+"]", t, func() {
args = ParseCmdArguments(str)
So(len(args), ShouldEqual, 2)
So(args[0], ShouldEqual, "a")
So(args[1], ShouldEqual, "b c")
})
str = `a '\''"`
Convey("Parse Cmd Arguments ["+str+"]", t, func() {
args = ParseCmdArguments(str)
So(len(args), ShouldEqual, 2)
So(args[0], ShouldEqual, "a")
So(args[1], ShouldEqual, "'")
})
str = ` \\a 'b c' c\ d\ `
Convey("Parse Cmd Arguments ["+str+"]", t, func() {
args = ParseCmdArguments(str)
So(len(args), ShouldEqual, 3)
So(args[0], ShouldEqual, "\\a")
So(args[1], ShouldEqual, "b c")
So(args[2], ShouldEqual, "c d ")
})
str = `\`
Convey("Parse Cmd Arguments ["+str+"]", t, func() {
args = ParseCmdArguments(str)
So(len(args), ShouldEqual, 1)
So(args[0], ShouldEqual, "\\")
})
str = ` \ ` // \SPACE
Convey("Parse Cmd Arguments ["+str+"]", t, func() {
args = ParseCmdArguments(str)
So(len(args), ShouldEqual, 1)
So(args[0], ShouldEqual, " ")
})
}

@ -8,6 +8,7 @@ import (
v3 "github.com/coreos/etcd/clientv3"
"github.com/gorilla/mux"
"gopkg.in/mgo.v2/bson"
"github.com/shunfei/cronsun"
"github.com/shunfei/cronsun/conf"
@ -163,3 +164,62 @@ func (n *Node) GetNodes(ctx *Context) {
outJSONWithCode(ctx.W, http.StatusOK, nodes)
}
// DeleteNode force remove node (by ip) which state in offline or damaged.
func (n *Node) DeleteNode(ctx *Context) {
vars := mux.Vars(ctx.R)
ip := strings.TrimSpace(vars["ip"])
if len(ip) == 0 {
outJSONWithCode(ctx.W, http.StatusBadRequest, "node ip is required.")
return
}
resp, err := cronsun.DefalutClient.Get(conf.Config.Node + ip)
if err != nil {
outJSONWithCode(ctx.W, http.StatusInternalServerError, err.Error())
return
}
if len(resp.Kvs) > 0 {
outJSONWithCode(ctx.W, http.StatusBadRequest, "can not remove a running node.")
return
}
err = cronsun.RemoveNode(bson.M{"_id": ip})
if err != nil {
outJSONWithCode(ctx.W, http.StatusInternalServerError, err.Error())
return
}
// remove node from group
var errmsg = "failed to remove node %s from groups, please remove it manually: %s"
resp, err = cronsun.DefalutClient.Get(conf.Config.Group, v3.WithPrefix())
if err != nil {
outJSONWithCode(ctx.W, http.StatusInternalServerError, fmt.Sprintf(errmsg, ip, err.Error()))
return
}
for i := range resp.Kvs {
g := cronsun.Group{}
err = json.Unmarshal(resp.Kvs[i].Value, &g)
if err != nil {
outJSONWithCode(ctx.W, http.StatusInternalServerError, fmt.Sprintf(errmsg, ip, err.Error()))
return
}
var nids = make([]string, 0, len(g.NodeIDs))
for _, nid := range g.NodeIDs {
if nid != ip {
nids = append(nids, nid)
}
}
g.NodeIDs = nids
if _, err = g.Put(resp.Kvs[i].ModRevision); err != nil {
outJSONWithCode(ctx.W, http.StatusInternalServerError, fmt.Sprintf(errmsg, ip, err.Error()))
return
}
}
outJSONWithCode(ctx.W, http.StatusNoContent, nil)
}

@ -82,6 +82,8 @@ func initRouters() (s *http.Server, err error) {
h = NewAuthHandler(nodeHandler.GetNodes)
subrouter.Handle("/nodes", h).Methods("GET")
h = NewAuthHandler(nodeHandler.DeleteNode)
subrouter.Handle("/node/{ip}", h).Methods("DELETE")
// get node group list
h = NewAuthHandler(nodeHandler.GetGroups)
subrouter.Handle("/node/groups", h).Methods("GET")

File diff suppressed because one or more lines are too long

@ -169,7 +169,7 @@ export default {
},
formatLatest: function(latest){
return this.$L('{begin ~ end}, on {node} took {times}', formatTime(latest.beginTime, latest.endTime), latest.node, formatDuration(latest.beginTime, latest.endTime))
return this.$L('on {node} took {times}, {begin ~ end}', latest.node, formatDuration(latest.beginTime, latest.endTime), formatTime(latest.beginTime, latest.endTime));
},
showExecuteJobModal: function(jobName, jobGroup, jobId){
@ -182,4 +182,4 @@ export default {
ExecuteJob
}
}
</script>
</script>

@ -165,7 +165,7 @@ export default {
},
formatTime: function(log){
return this.$L('{begin ~ end}, took {times}', formatTime(log.beginTime, log.endTime), formatDuration(log.beginTime, log.endTime));
return this.$L('took {times}, {begin ~ end}', formatDuration(log.beginTime, log.endTime), formatTime(log.beginTime, log.endTime));
},
showExecuteJobModal: function(jobName, jobGroup, jobId){
@ -177,4 +177,4 @@ export default {
ExecuteJob
}
}
</script>
</script>

@ -1,32 +1,50 @@
<style scoped>
.node {
width: 130px;
width: 140px;
border-radius: 3px;
padding: 4px 0;
margin: 3px;
display: inline-block;
background: #e8e8e8;
text-align: center;
position: relative;
overflow: hidden;
line-height: 1.9em;
}
.node > i.icon.remove {
position: absolute;
top: 0;
right: 0;
background: #2185D0;
bottom: 0;
display: none;
color: white;
margin: 0;
height: auto;
width: 100%;
cursor: pointer;
}
.node:hover > i.icon.remove {display: block;}
</style>
<template>
<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="item in items" v-bind:title="$L(item.title)">
<i class="cube icon" v-bind:class="item.css"></i> {{item.nodes.length}} {{$L(item.name)}}
<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>
{{$L('(total {n} nodes)', count)}}
<div class="ui label" :title="$L('currently version')"> {{version}} </div>
</div>
<div class="ui relaxed list" v-for="item in items">
<h4 v-if="item.nodes.length > 0" class="ui horizontal divider header"><i class="cube icon" v-bind:class="item.css"></i> {{$L(item.name)}} {{item.nodes.length}}</h4>
<div v-for="node in item.nodes" class="node" v-bind:title="node.title">
<div class="ui relaxed list" v-for="(group, groupIndex) in groups">
<h4 v-if="group.nodes.length > 0" class="ui horizontal divider header"><i class="cube icon" v-bind:class="group.css"></i> {{$L(group.name)}} {{group.nodes.length}}</h4>
<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}}
</router-link>
<i v-if="groupIndex != 2" v-on:click="removeConfirm(groupIndex, nodeIndex, node.id)" class="icon remove"></i>
</div>
</div>
</div>
@ -37,10 +55,10 @@ export default {
name: 'node',
data: function(){
return {
items: [
{nodes:[],name:'node damaged',title:'node can not be deceted due to itself or network etc.',css:'red'},
{nodes:[],name:'node offline',title:'node is in maintenance or is shutdown manually',css:''},
{nodes:[],name:'node normaly',title:'node is running',css:'green'}
groups: [
{nodes: [], name: 'node damaged', title: 'node can not be deceted due to itself or network etc.', css:'red'},
{nodes: [], name: 'node offline', title: 'node is in maintenance or is shutdown manually', css:''},
{nodes: [], name: 'node normaly', title: 'node is running', css:'green'}
],
count: 0,
version: ''
@ -67,15 +85,26 @@ export default {
var n = resp[i];
n.title = n.version + "\nstarted at: " + n.up
if (n.alived && n.connected) {
vm.items[2].nodes.push(n);
vm.groups[2].nodes.push(n);
} else if (n.alived && !n.connected) {
vm.items[0].nodes.push(n);
vm.groups[0].nodes.push(n);
} else {
vm.items[1].nodes.push(n);
vm.groups[1].nodes.push(n);
}
}
vm.count = resp.length || 0;
}).do();
},
methods: {
removeConfirm: function(groupIndex, nodeIndex, nodeId){
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)=>{
vm.groups[groupIndex].nodes.splice(nodeIndex, 1);
}).do();
}
}
}
</script>
</script>

@ -34,11 +34,11 @@ export default {
});
setTimeout(()=>{
$(vm.$el).dropdown('set exactly', vm.selected).dropdown('refresh');
}, 200);
}, 300);
},
updated: function(){
$(this.$el).dropdown('set exactly', this.selected);
}
}
</script>
</script>

@ -49,7 +49,7 @@ var language = {
'successed': 'Successed',
'failed': 'Failed',
'click to select a node and re-execute job': 'Click to select a node and re-execute job',
'{begin ~ end}, took {times}': '{0}, took {1}',
'took {times}, {begin ~ end}': 'Took {0}, {1}',
'executing job: {job}': 'executing job: "{0}"',
'cancel': 'Cancel',
'execute now': 'Execute now!',
@ -73,7 +73,7 @@ var language = {
'delete': 'Delete',
'all groups': 'All groups',
'all nodes': 'All nodes',
'{begin ~ end}, on {node} took {times}': '{0}, on {1} took {2}',
'on {node} took {times}, {begin ~ end}': 'On {0} took {1}, {2}',
'create job': 'Create job',
'update job': 'Update job',
'output': 'Output',
@ -129,7 +129,8 @@ var language = {
'include nodes': 'Include nodes',
'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 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}?'
}
export default language;

@ -50,7 +50,7 @@ var language = {
'successed': '成功',
'failed': '失败',
'click to select a node and re-execute job': '点此选择节点重新执行任务',
'{begin ~ end}, took {times}': '{0},耗时 {1}',
'took {times}, {begin ~ end}': '耗时 {0}, {1}',
'executing job: {job}': '执行任务:“{0}”',
'cancel': '取消',
'execute now': '立刻执行任务',
@ -75,7 +75,7 @@ var language = {
'delete': '删除',
'all groups': '所有分组',
'all nodes': '所有节点',
'{begin ~ end}, on {node} took {times}': '{0}, 于 {1} 耗时 {2}',
'on {node} took {times}, {begin ~ end}': '于 {0} 耗时 {1}, {2}',
'create job': '新建任务',
'update job': '更新任务',
'output': '输出',
@ -131,7 +131,8 @@ var language = {
'include nodes': '包含的节点',
'select nodes': '选择节点',
'select groups': '选择分组',
'are you sure to delete the group {name}?': '确定删除分组 {0}?'
'are you sure to delete the group {name}?': '确定删除分组 {0}?',
'are you sure to remove the node {nodeId}?': '确定删除节点 {0}?'
}
export default language;

Loading…
Cancel
Save