From 02f8a9a3706514dd230c9db3d89f6aa5cafb9d6d Mon Sep 17 00:00:00 2001 From: wangyj Date: Tue, 16 Jan 2018 15:33:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=88=86=E5=B8=83=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + atomic64.go | 39 ++++ buffer_pool.go | 40 ++++ cmds.go | 7 + config.go | 5 +- distributed.go | 241 +++++++++++++++++++++ fsm.go | 42 +++- glide.lock | 68 ------ glide.yaml | 2 +- merge_write.go | 152 +++++++++++++ res/css/style.css | 8 + res/index.html | 489 ++++++++++++++++++++++------------------- res/js/index.js | 525 +++++++++++++++++++++++---------------------- res/js/settings.js | 86 ++++---- res/setting.html | 342 +++++++++++++++-------------- web.go | 5 +- 16 files changed, 1286 insertions(+), 767 deletions(-) create mode 100644 atomic64.go create mode 100644 buffer_pool.go create mode 100644 distributed.go delete mode 100644 glide.lock create mode 100644 merge_write.go diff --git a/README.md b/README.md index 16feac3..8eb1946 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ server: password: admin addr: :8083 name: 10.10.99.177 + master: "" notifications: dingtalk: groups: @@ -112,6 +113,7 @@ client: server_url: http://admin:admin@localhost:8083 ``` +master 的支持来自 https://github.com/ihaiker/distributed-gosuv Logs can be found in `$HOME/.gosuv/log/` Edit config file(default located in `$HOME/.gosuv/programs.yml`) and run `gosuv reload` will take effect immediately. diff --git a/atomic64.go b/atomic64.go new file mode 100644 index 0000000..588173a --- /dev/null +++ b/atomic64.go @@ -0,0 +1,39 @@ +package main + +import "sync/atomic" + +type Int64 struct { + v int64 +} + +func (a *Int64) Get() int64 { + return atomic.LoadInt64(&a.v) +} + +func (a *Int64) Set(v int64) { + atomic.StoreInt64(&a.v, v) +} + +func (a *Int64) CompareAndSwap(o, n int64) bool { + return atomic.CompareAndSwapInt64(&a.v, o, n) +} + +func (a *Int64) Swap(v int64) int64 { + return atomic.SwapInt64(&a.v, v) +} + +func (a *Int64) Add(v int64) int64 { + return atomic.AddInt64(&a.v, v) +} + +func (a *Int64) Sub(v int64) int64 { + return a.Add(-v) +} + +func (a *Int64) Incr() int64 { + return a.Add(1) +} + +func (a *Int64) Decr() int64 { + return a.Add(-1) +} diff --git a/buffer_pool.go b/buffer_pool.go new file mode 100644 index 0000000..7fa622c --- /dev/null +++ b/buffer_pool.go @@ -0,0 +1,40 @@ +package main + +import ( + "bytes" +) + +// BufferPool implements a pool of bytes.Buffers in the form of a bounded +// channel. +type BufferPool struct { + c chan *bytes.Buffer +} + +// NewBufferPool creates a new BufferPool bounded to the given size. +func NewBufferPool(size int) (bp *BufferPool) { + return &BufferPool{ + c: make(chan *bytes.Buffer, size), + } +} + +// Get gets a Buffer from the BufferPool, or creates a new one if none are +// available in the pool. +func (bp *BufferPool) Get() (b *bytes.Buffer) { + select { + case b = <-bp.c: + // reuse existing buffer + default: + // create new buffer + b = bytes.NewBuffer([]byte{}) + } + return +} + +// Put returns the given Buffer to the BufferPool. +func (bp *BufferPool) Put(b *bytes.Buffer) { + b.Reset() + select { + case bp.c <- b: + default: // Discard the buffer if the pool is full. + } +} diff --git a/cmds.go b/cmds.go index 6d8f0fb..d3edb34 100644 --- a/cmds.go +++ b/cmds.go @@ -41,6 +41,13 @@ func actionStartServer(c *cli.Context) error { if err != nil { log.Fatal(err) } + + if c.Bool("foreground") { + if err = newDistributed(suv, hdlr); err != nil { + log.Fatal(err) + } + } + auth := cfg.Server.HttpAuth if auth.Enabled { hdlr = httpauth.SimpleBasicAuth(auth.User, auth.Password)(hdlr) diff --git a/config.go b/config.go index 6f0f097..14d03a7 100644 --- a/config.go +++ b/config.go @@ -15,8 +15,9 @@ type Configuration struct { User string `yaml:"username"` Password string `yaml:"password"` } `yaml:"httpauth"` - Addr string `yaml:"addr"` - Name string `yaml:"name"` + Addr string `yaml:"addr"` + Name string `yaml:"name"` + Master string `yaml:"master"` } `yaml:"server,omitempty"` Notifications Notifications `yaml:"notifications,omitempty" json:"-"` diff --git a/distributed.go b/distributed.go new file mode 100644 index 0000000..7ff5fb7 --- /dev/null +++ b/distributed.go @@ -0,0 +1,241 @@ +/* + 分布式实现 + 话外:作者的整体代码结构对于我的分布式修改太合适了 +*/ +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/bluele/gcache" + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + "github.com/qiniu/log" +) + +type Cluster struct { + slaves gcache.Cache + client *http.Client + suv *Supervisor +} + +func (cluster *Cluster) join() { + data := url.Values{"slave": []string{cfg.Server.Addr}} + request, err := http.NewRequest(http.MethodPost, "http://"+cfg.Server.Master+"/distributed/join", strings.NewReader(data.Encode())) + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + request.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) + + if err != nil { + log.Errorf("join cluster %s : %s", cfg.Server.Master, err) + return + } + cluster.auth(request) + resp, err := cluster.client.Do(request) + if err != nil { + log.Errorf("join cluster %s : %s", cfg.Server.Master, err) + return + } + if resp.StatusCode == http.StatusOK { + log.Debugf("join to master %s", cfg.Server.Master) + } else { + log.Debugf("join to master %s error: %d", cfg.Server.Master, resp.StatusCode) + } +} + +func (cluster *Cluster) auth(request *http.Request) { + if cfg.Server.HttpAuth.Enabled { + request.SetBasicAuth(cfg.Server.HttpAuth.User, cfg.Server.HttpAuth.Password) + } +} + +func (cluster *Cluster) dialWebSocket(wsUrl string) (*websocket.Conn, *http.Response, error) { + var dialer *websocket.Dialer + if cfg.Server.HttpAuth.Enabled { + dialer = &websocket.Dialer{Proxy: func(r *http.Request) (*url.URL, error) { + cluster.auth(r) + return websocket.DefaultDialer.Proxy(r) + }} + } else { + dialer = websocket.DefaultDialer + } + return dialer.Dial(wsUrl, nil) +} + +func (cluster *Cluster) requestSlave(url, method string, bodyBuffer *bytes.Buffer) ([]byte, error) { + request, err := http.NewRequest(method, url, nil) + if err != nil { + return nil, err + } + cluster.auth(request) + resp, err := cluster.client.Do(request) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return ioutil.ReadAll(resp.Body) +} + +func (cluster *Cluster) cmdJoinCluster(w http.ResponseWriter, r *http.Request) { + slave := r.PostFormValue("slave") + if slave == "" { + w.WriteHeader(http.StatusForbidden) + return + } + if strings.HasPrefix(slave, ":") { + idx := strings.LastIndex(r.RemoteAddr, ":") + slave = r.RemoteAddr[:idx] + slave + } + log.Debugf("%s join cluster.", slave) + if out, err := cluster.slaves.Get(slave); err != nil || out == nil { + cluster.suv.broadcastEvent("new slave : " + slave) + } + cluster.slaves.Set(slave, slave) + w.WriteHeader(http.StatusOK) +} + +//获取分布式系统下所有的内容 +func (cluster *Cluster) cmdQueryDistributedPrograms(w http.ResponseWriter, r *http.Request) { + jsonOut := "{" + idx := 0 + for _, v := range cluster.slaves.GetALL() { + slave := v.(string) + reqUrl := fmt.Sprintf("http://%s/api/programs", slave) + if body, err := cluster.requestSlave(reqUrl, http.MethodGet, nil); err == nil { + jsonOut += fmt.Sprintf("\"%s\":%s", slave, body) + } + if idx < cluster.slaves.Len()-1 { + jsonOut += "," + } + idx += 1 + } + jsonOut += "}" + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(jsonOut)) +} + +func (cluster *Cluster) cmdSetting(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + slave := mux.Vars(r)["slave"] + cluster.suv.renderHTML(w, "setting", map[string]string{ + "Name": name, + "Slave": slave, + }) +} + +func (cluster *Cluster) cmdWebSocketProxy(w http.ResponseWriter, r *http.Request) { + sock, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Error("upgrade:", err) + return + } + defer sock.Close() + + slave := mux.Vars(r)["slave"] + slaveUri := strings.Replace(r.RequestURI, "/distributed/"+slave, "", 1) + wsUrl := fmt.Sprintf("ws://%s%s", slave, slaveUri) + log.Infof("proxy websocket :%s", wsUrl) + + ws, _, err := cluster.dialWebSocket(wsUrl) + if err != nil { + log.Error("dial:", err) + return + } + defer ws.Close() + + for { + messageType, data, err := ws.ReadMessage() + if err != nil { + log.Error("read message:", err) + return + } + if messageType == websocket.CloseMessage { + log.Infof("close socket") + return + } + w, err := sock.NextWriter(messageType) + if err != nil { + log.Error("write err:", err) + return + } + _, err = w.Write(data) + if err != nil { + log.Error("read:", err) + return + } + } +} + +func (cluster *Cluster) slaveHttpProxy(w http.ResponseWriter, r *http.Request) { + slave := mux.Vars(r)["slave"] + slaveUri := strings.Replace(r.RequestURI, "/distributed/"+slave, "", 1) + requestUrl := fmt.Sprintf("http://%s%s", slave, slaveUri) + log.Infof("proxy :%s %s", r.Method, requestUrl) + + request, err := http.NewRequest(r.Method, requestUrl, r.Body) + for k, v := range r.Header { + request.Header.Set(k, strings.Join(v, ",")) + } + if err != nil { + log.Error(err) + } + cluster.auth(request) + resp, err := cluster.client.Do(request) + if err != nil { + log.Error(err) + } + defer resp.Body.Close() + + if body, err := ioutil.ReadAll(resp.Body); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + } else { + for k, v := range resp.Header { + w.Header().Set(k, strings.Join(v, ",")) + } + w.Write(body) + cluster.suv.broadcastEvent("execute ok : " + slaveUri) + } +} + +func newDistributed(suv *Supervisor, hdlr http.Handler) error { + cluster.suv = suv + + r := hdlr.(*mux.Router) + r.HandleFunc("/distributed/join", cluster.cmdJoinCluster).Methods("POST") + r.HandleFunc("/distributed/api/programs", cluster.cmdQueryDistributedPrograms).Methods("GET") + r.HandleFunc("/distributed/{slave}/settings/{name}", cluster.cmdSetting) + for _, path := range []string{ + "/distributed/{slave}/api/programs", "/distributed/{slave}/api/programs/{name}", + "/distributed/{slave}/api/programs/{name}/start", "/distributed/{slave}/api/programs/{name}/stop", + } { + r.HandleFunc(path, cluster.slaveHttpProxy) + } + r.HandleFunc("/distributed/{slave}/ws/logs/{name}", cluster.cmdWebSocketProxy) + r.HandleFunc("/distributed/{slave}/ws/perfs/{name}", cluster.cmdWebSocketProxy) + + if cfg.Server.Master != "" { + go func() { + t1 := time.NewTimer(time.Second) + for { + select { + case <-t1.C: + cluster.join() + t1.Reset(time.Second) + } + } + }() + } + return nil +} + +var cluster = Cluster{ + slaves: gcache.New(10).LRU().Expiration(time.Second * 3).Build(), + client: new(http.Client), +} diff --git a/fsm.go b/fsm.go index 01580c3..f473767 100644 --- a/fsm.go +++ b/fsm.go @@ -23,16 +23,17 @@ import ( "os/user" "path/filepath" "runtime" + "strconv" "strings" "sync" "syscall" "time" - "github.com/codeskyblue/gosuv/pushover" - "github.com/codeskyblue/kexec" "github.com/kennygrant/sanitize" "github.com/lunny/dingtalk_webhook" "github.com/qiniu/log" + "github.com/soopsio/gosuv/pushover" + "github.com/soopsio/kexec" ) type FSMState string @@ -106,14 +107,15 @@ var ( ) type Program struct { - Name string `yaml:"name" json:"name"` - Command string `yaml:"command" json:"command"` - Environ []string `yaml:"environ" json:"environ"` - Dir string `yaml:"directory" json:"directory"` - StartAuto bool `yaml:"start_auto" json:"startAuto"` - StartRetries int `yaml:"start_retries" json:"startRetries"` - StartSeconds int `yaml:"start_seconds,omitempty" json:"startSeconds"` - StopTimeout int `yaml:"stop_timeout,omitempty" json:"stopTimeout"` + Name string `yaml:"name" json:"name"` + Command string `yaml:"command" json:"command"` + Environ []string `yaml:"environ" json:"environ"` + Dir string `yaml:"directory" json:"directory"` + StartAuto bool `yaml:"start_auto" json:"startAuto"` + StartRetries int `yaml:"start_retries" json:"startRetries"` + StartSeconds int `yaml:"start_seconds,omitempty" json:"startSeconds"` + StopTimeout int `yaml:"stop_timeout,omitempty" json:"stopTimeout"` + retryCount int User string `yaml:"user,omitempty" json:"user"` Notifications Notifications `yaml:"notifications,omitempty" json:"-"` WebHook WebHook `yaml:"webhook,omitempty" json:"-"` @@ -165,9 +167,12 @@ func (p *Program) RunNotification(state FSMState) { } else { host, _ = os.Hostname() } + msg := fmt.Sprintf("[%s] %s: \"%s\" changed: \"%s\"", t, host, p.Name, state) + if state == RetryWait { + msg += " retryCount:" + strconv.Itoa(p.retryCount) + } for _, noti := range notis { po := noti.Pushover - msg := fmt.Sprintf("[%s] %s: %s changed: %s", t, host, p.Name, state) if po.ApiKey != "" && len(po.Users) > 0 { for _, user := range po.Users { err := pushover.Notify(pushover.Params{ @@ -288,16 +293,20 @@ func (p *Process) waitNextRetry() { } func (p *Process) stopCommand() { + fmt.Println("stop 111", p) p.mu.Lock() defer p.mu.Unlock() defer p.SetState(Stopped) if p.cmd == nil { return } + fmt.Println("stop 222", p) + p.SetState(Stopping) if p.cmd.Process != nil { p.cmd.Process.Signal(syscall.SIGTERM) // TODO(ssx): add it to config } + fmt.Println("stop 333", p) select { case <-GoFunc(p.cmd.Wait): p.RunNotification(FSMState("quit normally")) @@ -307,7 +316,9 @@ func (p *Process) stopCommand() { log.Printf("program(%s) terminate all", p.Name) p.cmd.Terminate(syscall.SIGKILL) // cleanup } + fmt.Println("stop 444", p) err := p.cmd.Wait() // This is OK, because Signal KILL will definitely work + fmt.Println("stop 555", p) prefixStr := "\n--- GOSUV LOG " + time.Now().Format("2006-01-02 15:04:05") if err == nil { io.WriteString(p.cmd.Stderr, fmt.Sprintf("%s exit success ---\n\n", prefixStr)) @@ -338,9 +349,13 @@ func (p *Process) startCommand() { p.SetState(Fatal) return } + // 如果是running状态,重置 retryLeft + p.retryLeft = p.StartRetries go func() { + log.Println("开始运行 1111") errC := GoFunc(p.cmd.Wait) startTime := time.Now() + log.Println("开始运行 2222") select { case <-errC: // if p.cmd.Wait() returns, it means program and its sub process all quited. no need to kill again @@ -349,6 +364,7 @@ func (p *Process) startCommand() { if time.Since(startTime) < time.Duration(p.StartSeconds)*time.Second { if p.retryLeft == p.StartRetries { // If first time quit so fast, just set to fatal p.SetState(Fatal) + p.RunNotification(Fatal) log.Printf("program(%s) exit too quick, status -> fatal", p.Name) return } @@ -358,6 +374,7 @@ func (p *Process) startCommand() { log.Println("recv stop command") p.stopCommand() // clean up all process } + log.Println("开始运行 3333") }() } @@ -377,6 +394,9 @@ func NewProcess(pg Program) *Process { pr.Status = string(newStatus) // TODO: status need to filter with config, not hard coded. // if newStatus == Fatal { + if newStatus == RetryWait { + pr.Program.retryCount++ + } go pr.Program.RunNotification(newStatus) // } } diff --git a/glide.lock b/glide.lock deleted file mode 100644 index 6adfdfd..0000000 --- a/glide.lock +++ /dev/null @@ -1,68 +0,0 @@ -hash: b098305412c9c0105d572881e395da369b0acf7738d1e89eb816b1e69f731aca -updated: 2018-01-16T10:18:20.068657958+08:00 -imports: -- name: github.com/codeskyblue/gosuv - version: 6f18d35abfca2d1b82e522ea868771436341dd8e - subpackages: - - pushover -- name: github.com/codeskyblue/kexec - version: 863094f94c7fb7c235764bf8f0f79cccea78c8eb -- name: github.com/equinox-io/equinox - version: f24972fa72facf59d05c91c848b65eac38815915 -- name: github.com/franela/goreq - version: bcd34c9993f899273c74baaa95e15386cd97b6e7 -- name: github.com/glycerine/rbuf - version: 54320fe9f6f340f8fbe3f34306e3c32589d97dc4 -- name: github.com/go-yaml/yaml - version: d670f9405373e636a5a2765eea47fac0c9bc91a4 -- name: github.com/goji/httpauth - version: 2da839ab0f4df05a6db5eb277995589dadbd4fb9 -- name: github.com/gorilla/context - version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 -- name: github.com/gorilla/mux - version: 5bbbb5b2b5729b132181cc7f4aa3b3c973e9a0ed -- name: github.com/gorilla/websocket - version: 292fd08b2560ad524ee37396253d71570339a821 -- name: github.com/kennygrant/sanitize - version: 2e6820834a1f36c626bf19a253b7d3cc060e9b8b -- name: github.com/lunny/dingtalk_webhook - version: e3534c89ef969912856dfa39e56b09e58c5f5daf -- name: github.com/mitchellh/go-ps - version: 4fdf99ab29366514c69ccccddab5dc58b8d84062 -- name: github.com/qiniu/log - version: a304a74568d6982c5b89de1c68ac8fca3add196a -- name: github.com/shirou/gopsutil - version: 6a368fb7cd1221fa6ea90facc9447c9a2234c255 - subpackages: - - process -- name: github.com/shurcooL/httpfs - version: 809beceb23714880abc4a382a00c05f89d13b1cc - subpackages: - - vfsutil -- name: github.com/shurcooL/vfsgen - version: bb654eaf43db9a91d1d3201dbd8c4b0423a96872 -- name: github.com/urfave/cli - version: 75104e932ac2ddb944a6ea19d9f9f26316ff1145 -- name: golang.org/x/net - version: 5ccada7d0a7ba9aeb5d3aca8d3501b4c2a509fec - subpackages: - - html - - html/atom -testImports: -- name: github.com/gopherjs/gopherjs - version: 444abdf920945de5d4a977b572bcc6c674d1e4eb - subpackages: - - js -- name: github.com/jtolds/gls - version: 77f18212c9c7edc9bd6a33d383a7b545ce62f064 -- name: github.com/smartystreets/assertions - version: 0b37b35ec7434b77e77a4bb29b79677cced992ea - subpackages: - - internal/go-render/render - - internal/oglematchers -- name: github.com/smartystreets/goconvey - version: e5b2b7c9111590d019a696c7800593f666e1a7f4 - subpackages: - - convey - - convey/gotest - - convey/reporting diff --git a/glide.yaml b/glide.yaml index cffad80..63905b2 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,6 +1,6 @@ package: github.com/soopsio/gosuv import: -- package: github.com/codeskyblue/kexec +- package: github.com/soopsio/kexec - package: github.com/equinox-io/equinox - package: github.com/franela/goreq - package: github.com/glycerine/rbuf diff --git a/merge_write.go b/merge_write.go new file mode 100644 index 0000000..47387b6 --- /dev/null +++ b/merge_write.go @@ -0,0 +1,152 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "time" + + log "github.com/qiniu/log" +) + +type Bool struct { + c Int64 +} + +func (b *Bool) Get() bool { + return b.c.Get() != 0 +} + +func (b *Bool) toInt64(v bool) int64 { + if v { + return 1 + } else { + return 0 + } +} + +func (b *Bool) Set(v bool) { + b.c.Set(b.toInt64(v)) +} + +func (b *Bool) CompareAndSwap(o, n bool) bool { + return b.c.CompareAndSwap(b.toInt64(o), b.toInt64(n)) +} + +func (b *Bool) Swap(v bool) bool { + return b.c.Swap(b.toInt64(v)) != 0 +} + +var bufferPool *BufferPool + +func init() { + // 4000行日志缓存 + bufferPool = NewBufferPool(4000) +} + +type MergeWriter struct { + lines chan *bytes.Buffer + writer io.Writer + closed Bool +} + +func NewMergeWriter(writer io.Writer) *MergeWriter { + merger := &MergeWriter{ + lines: make(chan *bytes.Buffer, 1000), + writer: writer, + } + merger.closed.Set(false) + merger.drainLines() + return merger +} + +func (m *MergeWriter) Close() { + // log.Printf("Close MergeWriter") + if m.closed.CompareAndSwap(false, true) { + // log.Printf("Close lines chan") + close(m.lines) + } +} + +func (m *MergeWriter) WriteStrLine(line string) { + if m.closed.Get() { + return + } else { + buffer := bufferPool.Get() + buffer.WriteString(line) + m.lines <- buffer + } +} + +func (m *MergeWriter) WriteLine(line *bytes.Buffer) { + if m.closed.Get() { + // 需要回收Buffer + // log.Printf("Write to closed MergeWrite...") + bufferPool.Put(line) + return + } else { + m.lines <- line + } +} + +func (m *MergeWriter) drainLines() { + go func() { + for line := range m.lines { + m.writer.Write(line.Bytes()) + // 回收 + bufferPool.Put(line) + } + }() +} + +// 创建新的BufferWriter +func (m *MergeWriter) NewWriter(index int) io.Writer { + writer := &BufferWriter{ + merge: m, + prefix: fmt.Sprintf(" [P%02d] ", index), + } + + // 分配 + writer.Buffer = bufferPool.Get() + writer.Buffer.WriteString(writer.prefix) + return writer +} + +type BufferWriter struct { + Buffer *bytes.Buffer + prefix string + merge *MergeWriter +} + +func (b *BufferWriter) Write(p []byte) (n int, err error) { + n = len(p) + + for len(p) > 0 { + index := bytes.IndexByte(p, '\n') + if index != -1 { + // 写完完整的一行 + _, err = b.Buffer.Write(p[0 : index+1]) + if err != nil { + log.Error(err, "Writer Buffer failed") + return n, err + } + + // 将buffer转移到merge中 + b.merge.WriteLine(b.Buffer) + + // 分配:写入新数据 + b.Buffer = bufferPool.Get() + b.Buffer.WriteString(time.Now().Format("15:04:05") + b.prefix) + p = p[index+1:] + } else { + // 剩下不足一行,一口气全部写入 + _, err = b.Buffer.Write(p) + if err != nil { + log.Error(err, "Writer Buffer failed") + return n, err + } + break + } + } + return n, nil +} diff --git a/res/css/style.css b/res/css/style.css index 0ec4fe3..0eb9da3 100644 --- a/res/css/style.css +++ b/res/css/style.css @@ -37,3 +37,11 @@ small.user { .color-red { color: red; } + +.panel-body{ + padding: 0px; +} + +.table { + margin: 0px; +} diff --git a/res/index.html b/res/index.html index ca8af59..145efd0 100644 --- a/res/index.html +++ b/res/index.html @@ -2,251 +2,308 @@ - - - GoSUV - - - - + + + Distributed Go Supervisor 1.0 + + + + - +
- +
+
- - - - - - - - - - - - - - - - - -
NameStatusViewCommand
- - - Profiles - - - - - - - -
+
+
+ Master + + + +
+
+ + + + + + + + + + + + + + + + + +
NameStatusViewCommand
+ + + Profiles + + + + + + + +
+
+
+
+
+ {{slave}} + + + +
+
+ + + + + + + + + + + + + + + + + +
NameStatusViewCommand
+ + + Profiles + + + + + + + +
+
+
- +