├── webui ├── src │ ├── assets │ │ ├── .gitkeep │ │ ├── scss │ │ │ ├── _variables.scss │ │ │ ├── _ie-fix.scss │ │ │ ├── vendors │ │ │ │ ├── _variables.scss │ │ │ │ └── chart.js │ │ │ │ │ └── chart.scss │ │ │ ├── style.scss │ │ │ └── _custom.scss │ │ ├── images │ │ │ ├── logo.png │ │ │ └── logo2.png │ │ └── toast.css │ ├── tools │ │ ├── plugins.js │ │ ├── utils.js │ │ ├── http.js │ │ └── mixins.js │ ├── containers │ │ └── AppAside.vue │ ├── views │ │ ├── logs │ │ │ └── Logs.vue │ │ ├── program │ │ │ ├── CPU.vue │ │ │ ├── args.vue │ │ │ ├── RAM.vue │ │ │ ├── ProgramLogger.vue │ │ │ ├── PCommand.vue │ │ │ └── tags.vue │ │ ├── system │ │ │ ├── Systems.vue │ │ │ └── webhook.vue │ │ ├── nodes │ │ │ ├── ModifyTag.vue │ │ │ ├── TokenForm.vue │ │ │ └── Nodes.vue │ │ ├── dashboard │ │ │ └── SocialBoxChart.vue │ │ ├── tags │ │ │ ├── Tags.vue │ │ │ └── TagForm.vue │ │ └── Login.vue │ ├── plugins │ │ ├── vTitle.vue │ │ ├── delete.vue │ │ ├── XPage.vue │ │ └── loadings.vue │ ├── App.vue │ ├── main.js │ └── router.js ├── .env.production ├── .env ├── dist │ ├── static │ │ ├── css │ │ │ ├── chunk-00548b71.6dc57fe0.css │ │ │ ├── chunk-5cdb028c.864801b0.css │ │ │ ├── chunk-d2642d9c.864801b0.css │ │ │ └── chunk-8d988810.fe8a04b4.css │ │ ├── img │ │ │ ├── logo.e7019282.png │ │ │ └── logo2.66d14f79.png │ │ ├── fonts │ │ │ ├── Simple-Line-Icons.d2285965.ttf │ │ │ ├── Simple-Line-Icons.f33df365.eot │ │ │ ├── Simple-Line-Icons.0cb0b9c5.woff2 │ │ │ ├── Simple-Line-Icons.78f07e2c.woff │ │ │ ├── fontawesome-webfont.674f50d2.eot │ │ │ ├── fontawesome-webfont.b06871f2.ttf │ │ │ ├── fontawesome-webfont.fee66e71.woff │ │ │ ├── fontawesome-webfont.af7ae505.woff2 │ │ │ ├── CoreUI-Icons-Linear-Free.0087dce4.woff │ │ │ ├── CoreUI-Icons-Linear-Free.089ab3c1.eot │ │ │ └── CoreUI-Icons-Linear-Free.1bc37648.ttf │ │ └── js │ │ │ ├── chunk-9a6bc50a.801e34ca.js │ │ │ ├── chunk-f106c87c.4fd2d7d6.js │ │ │ ├── chunk-2a30e9d6.88f246a6.js │ │ │ └── chunk-8d988810.32b266aa.js │ ├── favicon.ico │ └── index.html ├── .postcssrc.js ├── babel.config.js ├── public │ ├── favicon.ico │ └── index.html ├── Makefile ├── .editorconfig ├── .gitignore ├── vue.config.js ├── .eslintrc.js └── package.json ├── docs ├── dd.png ├── qq.png ├── views │ ├── tags.png │ ├── nodes.png │ ├── dashboard.png │ └── programs.png ├── dingding.template.tpl ├── email.template.tpl ├── CONTRIBUTING.md ├── TODOLIST.md ├── webhook.default.json ├── CHANGELOG.md ├── INSTALL.md └── NOTIFY.md ├── nodes ├── dao │ ├── page.go │ ├── notify.go │ ├── json.go │ ├── tags.go │ ├── user.go │ ├── database.go │ ├── logger.go │ └── node.go ├── http │ ├── auth │ │ ├── service.go │ │ └── basic.go │ ├── http_static_file.go │ ├── http_static_bindata.go │ ├── ctl.tag.go │ ├── ctl.user.go │ ├── ctl.node.go │ ├── ctl.notify.go │ ├── ctl.websocket.go │ ├── ctl.program.go │ ├── ctl.dashboard.go │ ├── ctl.routers.go │ └── http_server.go ├── notify │ ├── webhook │ │ ├── webhook.go │ │ └── send.go │ ├── mail │ │ ├── mail_test.go │ │ ├── mail.go │ │ ├── message.go │ │ └── send.go │ └── server.go ├── join │ ├── client.go │ └── manager.go └── manager │ └── join_server.go ├── .gitignore ├── generator.go ├── daemon ├── process_info_test.go ├── utils_test.go ├── utils.go ├── process_logger_test.go ├── program_test.go ├── manager_local_test.go ├── process_info.go ├── fsm.go ├── interface.go ├── process_list.go ├── process_test.go ├── process_logger.go └── program.go ├── cmds ├── console │ ├── start.go │ ├── stop.go │ ├── status.go │ ├── detail.go │ ├── delete.go │ ├── tag.go │ ├── add.go │ ├── join.go │ ├── modify.go │ ├── list.go │ ├── tail.go │ └── console.go ├── initd │ ├── cmd.go │ ├── windows.go │ └── linux.go └── node │ ├── cmd.go │ └── node.go ├── Dockerfile ├── libs ├── config │ ├── sudis.example.yaml │ └── config.go ├── errors │ └── errors.go └── ipapi │ └── api.go ├── Makefile ├── README_ZH.md ├── README.md ├── go.mod └── sudis.go /webui/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webui/.env.production: -------------------------------------------------------------------------------- 1 | VUE_APP_URL= 2 | VUE_APP_WS= 3 | -------------------------------------------------------------------------------- /webui/src/assets/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Variable overrides 2 | -------------------------------------------------------------------------------- /docs/dd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/docs/dd.png -------------------------------------------------------------------------------- /docs/qq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/docs/qq.png -------------------------------------------------------------------------------- /webui/.env: -------------------------------------------------------------------------------- 1 | VUE_APP_URL=http://127.0.0.1:5984 2 | VUE_APP_WS=ws://127.0.0.1:5984 3 | -------------------------------------------------------------------------------- /webui/dist/static/css/chunk-00548b71.6dc57fe0.css: -------------------------------------------------------------------------------- 1 | .echarts{width:600px;height:400px} -------------------------------------------------------------------------------- /docs/views/tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/docs/views/tags.png -------------------------------------------------------------------------------- /docs/views/nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/docs/views/nodes.png -------------------------------------------------------------------------------- /webui/.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } -------------------------------------------------------------------------------- /webui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } -------------------------------------------------------------------------------- /webui/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/favicon.ico -------------------------------------------------------------------------------- /docs/views/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/docs/views/dashboard.png -------------------------------------------------------------------------------- /docs/views/programs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/docs/views/programs.png -------------------------------------------------------------------------------- /webui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/public/favicon.ico -------------------------------------------------------------------------------- /webui/src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/src/assets/images/logo.png -------------------------------------------------------------------------------- /webui/src/assets/images/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/src/assets/images/logo2.png -------------------------------------------------------------------------------- /webui/dist/static/img/logo.e7019282.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/img/logo.e7019282.png -------------------------------------------------------------------------------- /webui/dist/static/img/logo2.66d14f79.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/img/logo2.66d14f79.png -------------------------------------------------------------------------------- /webui/dist/static/fonts/Simple-Line-Icons.d2285965.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/fonts/Simple-Line-Icons.d2285965.ttf -------------------------------------------------------------------------------- /webui/dist/static/fonts/Simple-Line-Icons.f33df365.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/fonts/Simple-Line-Icons.f33df365.eot -------------------------------------------------------------------------------- /webui/dist/static/fonts/Simple-Line-Icons.0cb0b9c5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/fonts/Simple-Line-Icons.0cb0b9c5.woff2 -------------------------------------------------------------------------------- /webui/dist/static/fonts/Simple-Line-Icons.78f07e2c.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/fonts/Simple-Line-Icons.78f07e2c.woff -------------------------------------------------------------------------------- /webui/dist/static/fonts/fontawesome-webfont.674f50d2.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/fonts/fontawesome-webfont.674f50d2.eot -------------------------------------------------------------------------------- /webui/dist/static/fonts/fontawesome-webfont.b06871f2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/fonts/fontawesome-webfont.b06871f2.ttf -------------------------------------------------------------------------------- /webui/dist/static/fonts/fontawesome-webfont.fee66e71.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/fonts/fontawesome-webfont.fee66e71.woff -------------------------------------------------------------------------------- /webui/dist/static/fonts/fontawesome-webfont.af7ae505.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/fonts/fontawesome-webfont.af7ae505.woff2 -------------------------------------------------------------------------------- /webui/dist/static/fonts/CoreUI-Icons-Linear-Free.0087dce4.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/fonts/CoreUI-Icons-Linear-Free.0087dce4.woff -------------------------------------------------------------------------------- /webui/dist/static/fonts/CoreUI-Icons-Linear-Free.089ab3c1.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/fonts/CoreUI-Icons-Linear-Free.089ab3c1.eot -------------------------------------------------------------------------------- /webui/dist/static/fonts/CoreUI-Icons-Linear-Free.1bc37648.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihaiker/sudis/HEAD/webui/dist/static/fonts/CoreUI-Icons-Linear-Free.1bc37648.ttf -------------------------------------------------------------------------------- /webui/src/assets/scss/_ie-fix.scss: -------------------------------------------------------------------------------- 1 | //IE fix for sticky footer 2 | @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { 3 | .app { 4 | height: 100%; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /webui/src/assets/scss/vendors/_variables.scss: -------------------------------------------------------------------------------- 1 | // Override Boostrap variables 2 | @import "../variables"; 3 | @import "~bootstrap/scss/mixins"; 4 | @import "~@coreui/coreui/scss/variables"; 5 | -------------------------------------------------------------------------------- /nodes/dao/page.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | type Page struct { 4 | Total int64 `json:"total"` 5 | Page int `json:"page"` 6 | Limit int `json:"limit"` 7 | Data interface{} `json:"data"` 8 | } 9 | -------------------------------------------------------------------------------- /webui/Makefile: -------------------------------------------------------------------------------- 1 | npm=$(shell command -v cnpm) 2 | 3 | ifeq ("${npm}","") 4 | npm=npm 5 | endif 6 | 7 | build: dep 8 | $(npm) run build 9 | 10 | clean: 11 | rm -rf dist 12 | 13 | dep: 14 | $(npm) i 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.test 7 | *.out 8 | 9 | /bin 10 | /vendor 11 | .idea 12 | 13 | /conf 14 | /etc 15 | /.env 16 | /nodes/http/http_static_bindata_assets.go 17 | *.iml 18 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go get -u github.com/go-bindata/go-bindata/... 4 | 5 | //go:generate go-bindata -nomemcopy -prefix "webui/dist" -fs -pkg http -o nodes/http/http_static_bindata_assets.go webui/dist/... 6 | -------------------------------------------------------------------------------- /daemon/process_info_test.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestProcInfo(t *testing.T) { 8 | cup, rss, err := GetProcessInfo(362) 9 | t.Log(cup) 10 | t.Log(rss) 11 | t.Log(err) 12 | } 13 | -------------------------------------------------------------------------------- /webui/src/tools/plugins.js: -------------------------------------------------------------------------------- 1 | //全局组件 2 | // import Vue from 'vue' 3 | // import StackModal from '@innologica/vue-stackable-modal' 4 | export default { 5 | install() { 6 | // Vue.component('StackModal', StackModal) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /daemon/utils_test.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestAsync(t *testing.T) { 9 | err := Async(func() error { 10 | time.Sleep(time.Second) 11 | return nil 12 | }) 13 | 14 | select { 15 | case e := <-err: 16 | t.Log(e) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webui/src/assets/scss/style.scss: -------------------------------------------------------------------------------- 1 | // If you want to override variables do it here 2 | @import "variables"; 3 | 4 | // Import styles 5 | @import "~@coreui/coreui/scss/coreui"; 6 | 7 | // If you want to add something do it here 8 | @import "custom"; 9 | 10 | // ie fixes 11 | @import "ie-fix"; 12 | -------------------------------------------------------------------------------- /webui/src/containers/AppAside.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /webui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /webui/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | package-lock.json 4 | /coverage 5 | 6 | selenium-debug.log 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw* 25 | -------------------------------------------------------------------------------- /webui/src/views/logs/Logs.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /webui/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | outputDir: "dist", assetsDir: "static", 3 | lintOnSave: false, runtimeCompiler: true, 4 | devServer: { 5 | disableHostCheck: true, 6 | }, 7 | configureWebpack: { 8 | externals: { 9 | vue: "Vue", 10 | 'vue-router': 'VueRouter', 11 | 'bootstrap-vue': "BootstrapVue", 12 | }, 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /nodes/http/auth/service.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/ihaiker/sudis/nodes/dao" 5 | "github.com/kataras/iris/v12" 6 | ) 7 | 8 | type ( 9 | LoginToken struct { 10 | Token string `json:"token"` 11 | } 12 | 13 | Service interface { 14 | Login(data *dao.JSON) *LoginToken 15 | Check(ctx iris.Context) 16 | } 17 | ) 18 | 19 | func NewService() Service { 20 | return &basicService{} 21 | } 22 | -------------------------------------------------------------------------------- /docs/dingding.template.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "msgtype": "markdown", 3 | "markdown": { 4 | "title":"Sudis通知", 5 | {{ if eq .Type "process" }} 6 | "text": "#### 程序状态通知 \n> 节点:{{.Process.Node}},程序:{{.Process.Name}} \n > {{.FromStatus}} => {{.ToStatus}} \n\n >###### 此通知由sudis自动发送" 7 | {{else}} 8 | "text": "#### 节点状态通知 \n> {{.Node}} 状态变更为:{{.Status}} \n\n > ###### 此通知由sudis自动发送" 9 | {{end}} 10 | } 11 | } -------------------------------------------------------------------------------- /cmds/console/start.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var startCmd = &cobra.Command{ 8 | Use: "start", Short: "启动管理的程序", Long: "启动管理的某个程序", Args: cobra.ExactValidArgs(1), 9 | Example: "sudis [console] start ", 10 | PreRunE: preRune, PostRun: runPost, 11 | Run: func(cmd *cobra.Command, args []string) { 12 | request := makeRequest(cmd, "start", args...) 13 | sendRequest(cmd, request) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /cmds/console/stop.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var stopCmd = &cobra.Command{ 8 | Use: "stop", Short: "停止管理的程序", Long: "停止正在运行的某个程序", Args: cobra.ExactValidArgs(1), 9 | Example: "sudis [console] stop ", 10 | PreRunE: preRune, PostRun: runPost, 11 | Run: func(cmd *cobra.Command, args []string) { 12 | request := makeRequest(cmd, "stop", args...) 13 | sendRequest(cmd, request) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /cmds/console/status.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var statusCmd = &cobra.Command{ 8 | Use: "status", Short: "查看运行状态", Long: "查看某个程序的运行状态", Args: cobra.ExactValidArgs(1), 9 | Example: "sudis [console] status ", 10 | PreRunE: preRune, PostRun: runPost, 11 | Run: func(cmd *cobra.Command, args []string) { 12 | request := makeRequest(cmd, "status", args...) 13 | sendRequest(cmd, request) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /docs/email.template.tpl: -------------------------------------------------------------------------------- 1 | {{ if eq .Type "process" }} 2 |

程序通知:

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
程序节点{{.Process.Node}}名称{{.Process.Name}}
From:{{.FromStatus}}To:{{.ToStatus}}
13 | {{else}} 14 |

节点通知:

15 | {{.Node}} 状态变更为:{{.Status}} 16 | {{end}} -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution 2 | 3 | - If you encounter a bug, you can submit it to the dev branch directly. 4 | - If you encounter a problem, you can feedback through the issue. 5 | - The project is under development, and there is still a lot of room for improvement. If you can contribute code, please submit PR to the dev branch. 6 | - If there is feedback on new features, you can feedback via issues or qq group , dingtalk group. 7 | 8 | ![QQ支持群](./docs/qq.png) 9 | ![钉钉支持群](./docs/dd.png) -------------------------------------------------------------------------------- /nodes/notify/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "net/http" 7 | ) 8 | 9 | func WebHook(address, accessToken string, body []byte) error { 10 | resp, err := http.DefaultClient.Post(address+"?access_token="+accessToken, "application/json;charset=UTF-8", bytes.NewBuffer(body)) 11 | if err != nil { 12 | return err 13 | } 14 | if resp.StatusCode != 200 { 15 | return errors.New("请求异常:" + resp.Status) 16 | } 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /docs/TODOLIST.md: -------------------------------------------------------------------------------- 1 | # TODOLIST 2 | 3 | - [X] server节点加入和退出优化,webui自主控制 4 | - [X] 认证方式修改一下,自己的认证方式有点,呵呵 5 | - [X] 连接主控节点,每个节点都有自己的token(salt现在使用的是主节点盐值) 6 | - [ ] 通知消息优化,防止闪断造成的通知频繁问题。(参数配置内容) 7 | - [ ] 程序关系 8 | - 1、程序依赖关系,启动前必须启动的程序,启动后需要启动的程序。(问题:如果不是本机服务通知管理程序去启动) 9 | - 2、程序是否是一次运行程序,依赖的程序可能只是初始操作之列的任务 10 | - [ ] 程序自动部署模板支持(使用ansible部署) 11 | - [ ] acl 权限控制(使用RABC方式) 12 | - [ ] 小程序或者app管理提供 13 | - [X] web CDN资源加载 14 | 15 | 注:先后顺序即为功能优先级 16 | 17 | # fixbug: 18 | 19 | - [ ] 内存显示问题 20 | 21 | -------------------------------------------------------------------------------- /nodes/http/http_static_file.go: -------------------------------------------------------------------------------- 1 | // +build !bindata 2 | 3 | package http 4 | 5 | import ( 6 | "github.com/ihaiker/gokit/logs" 7 | "github.com/kataras/iris/v12" 8 | ) 9 | 10 | func httpStatic(app *iris.Application) { 11 | logs.Info("使用 file resources") 12 | app.Favicon("webui/dist/favicon.ico") 13 | app.HandleDir("/static", iris.Dir("./webui/dist/static")) 14 | app.RegisterView(iris.HTML("webui/dist", ".html")) 15 | app.Get("/", func(ctx iris.Context) { 16 | ctx.View("index.html") 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /cmds/console/detail.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var detailCmd = &cobra.Command{ 8 | Use: "detail", Aliases: []string{"inspect"}, 9 | Short: "查看配置信息,JSON", Long: "查看某个程序的配置信息,JSON格式", Args: cobra.ExactValidArgs(1), 10 | Example: "sudis [console] detail ", 11 | PreRunE: preRune, PostRun: runPost, 12 | Run: func(cmd *cobra.Command, args []string) { 13 | request := makeRequest(cmd, "detail", args...) 14 | sendRequest(cmd, request) 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /nodes/http/http_static_bindata.go: -------------------------------------------------------------------------------- 1 | // +build bindata 2 | 3 | package http 4 | 5 | import ( 6 | "github.com/ihaiker/gokit/logs" 7 | "github.com/kataras/iris/v12" 8 | ) 9 | 10 | func httpStatic(app *iris.Application) { 11 | logs.Info("使用bindata") 12 | app.Get("/favicon.ico", func(ctx iris.Context) { 13 | _, _ = ctx.Write([]byte(_faviconIco)) 14 | }) 15 | app.HandleDir("/", AssetFile()) 16 | app.Get("/", func(ctx iris.Context) { 17 | //ctx.View("index.html") 18 | bs, _ := indexHtmlBytes() 19 | ctx.Write(bs) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /daemon/utils.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | var ErrAsyncTimeout = errors.New("Timeout") 9 | 10 | func Async(f func() error) chan error { 11 | ch := make(chan error) 12 | go func() { 13 | ch <- f() 14 | }() 15 | return ch 16 | } 17 | 18 | func AsyncTimeout(timeout time.Duration, f func() error) chan error { 19 | ch := make(chan error) 20 | go func() { 21 | select { 22 | case err := <-Async(f): 23 | ch <- err 24 | case <-time.After(timeout): 25 | ch <- ErrAsyncTimeout 26 | } 27 | }() 28 | return ch 29 | } 30 | -------------------------------------------------------------------------------- /webui/src/plugins/vTitle.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /webui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | }, 17 | globals: { 18 | 'Vue': true, 19 | 'VueRouter': true, 20 | 'BootstrapVue': true, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /nodes/notify/mail/mail_test.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestSendMail(t *testing.T) { 9 | host := os.Getenv("SMTP_HOST") 10 | user := os.Getenv("SMTP_USER") 11 | passwd := os.Getenv("SMTP_PASSWD") 12 | to := os.Getenv("SMTP_TO") 13 | 14 | server := NewServer(host, 465, user, passwd) 15 | if err := server.Start(); err != nil { 16 | t.Fatal(err) 17 | } 18 | defer func() { 19 | t.Log(server.Close()) 20 | }() 21 | 22 | m := NewMessage(user, to, "标题", "

aaa

") 23 | m.From.Name = "异常通知" 24 | 25 | if err := server.Send(m); err != nil { 26 | t.Fatal(err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13.6-alpine3.11 as builder 2 | 3 | ENV GO111MODULE="on" 4 | ARG LDFLAGS="" 5 | 6 | ADD . /sudis 7 | WORKDIR /sudis 8 | 9 | RUN go get -u github.com/shuLhan/go-bindata/cmd/go-bindata 10 | RUN go generate generator.go 11 | RUN apk add --no-cache make git build-base 12 | RUN go build -tags bindata -ldflags "${LDFLAGS}" -o sudis sudis.go 13 | 14 | 15 | FROM alpine:3.11 16 | MAINTAINER Haiker ni@renzhen.la 17 | 18 | WORKDIR /opt/sudis 19 | 20 | ADD ./libs/config/sudis.example.yaml /etc/sudis/sudis.yaml 21 | COPY --from=builder /sudis/sudis /opt/sudis/sudis 22 | 23 | ENV PATH="$PATH:/opt/sudis" 24 | EXPOSE 5983 5984 25 | VOLUME /etc/sudis 26 | 27 | ENTRYPOINT ["sudis"] -------------------------------------------------------------------------------- /daemon/process_logger_test.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestProcessLogger(t *testing.T) { 11 | logger, _ := NewLogger("") 12 | 13 | logger.Tail("1", func(id, line string) { 14 | fmt.Println(id, " = ", line) 15 | }, 10) 16 | 17 | for i := 0; i < 100; i++ { 18 | time.Sleep(time.Millisecond * 100) 19 | if i == 50 { 20 | logger.CtrlC("1") 21 | } else if i == 20 { 22 | logger.Tail("2", func(id, line string) { 23 | fmt.Println(id, " = ", line) 24 | }, 10) 25 | } 26 | if _, err := logger.Write([]byte(strconv.Itoa(i) + "----\n")); err != nil { 27 | t.Fatal(err) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/webhook.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "process", 3 | "process": { 4 | "pid": 44593, 5 | "status": "running", 6 | "id": 2, 7 | "node": "sudis", 8 | "daemon": "0", 9 | "name": "ping", 10 | "tags": [], 11 | "workDir": "/Users/haiker", 12 | "user": "haiker", 13 | "start": { 14 | "command": "ping", 15 | "args": ["172.16.100.2"] 16 | }, 17 | "stop": {}, 18 | "startDuration": 3, 19 | "startRetries": 3, 20 | "stopSign": 3, 21 | "stopTimeout": 7, 22 | "addTime": "2020-03-24T12:47:41.025598+08:00", 23 | "updateTime": "2020-03-24T12:47:41.025598+08:00", 24 | "cpu": 0, 25 | "rss": 4187 26 | }, 27 | "fromStatus": "starting", 28 | "toStatus": "running" 29 | } -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v3.0.0 4 | 5 | - **又双叒叕** 调整程序整体结构和设计。整整更健康 6 | - 添加多管理节点功能 7 | - fixbug: 修复部分BUG 8 | 9 | ## v2.3.1 10 | 11 | - fixbug: send on closed channel 问题 12 | - fixbug: 添加程序后丢失程序的运行状态 13 | - fixbug: 重新连接后远程调用总是失败。 14 | - 管理程序添加描述内容 15 | - 程序状态变更通知 16 | - 添加地址IP地址解析,感谢纯真IP提供的服务。 17 | 18 | ## v2.2.0 19 | 20 | - 程序运行日志查看 21 | - 开机启动方式支持 22 | - 管理程序顺序总是混轮问题,按照时间排序, 23 | 24 | ## v2.1.0 25 | 26 | - fixbug: 修复server节点和主节点断开连接后,不会对此重连BUG 27 | - 主界面添加master节点内存和CPU使用情况 28 | - 程序详情界面添加,程序说明 & 内存CPU使用情况图表 29 | 30 | ## v2.0.1 31 | - 编辑程序管理,自动启动属性丢失,启动次数属性解析异常问题 32 | - 编辑程序时后台程序健康检查属性丢失。 33 | - 程序管理界面按钮颜色调整,之前的颜色太多显的有些乱 34 | - 程序编辑方式优化,更加方便的处理一些启动参数问题。 35 | - FIXBUG: 运行程序关闭后状态错误. 36 | -------------------------------------------------------------------------------- /cmds/console/delete.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | "strconv" 7 | ) 8 | 9 | var deleteCmd = &cobra.Command{ 10 | Use: "delete", Aliases: []string{"remove"}, Short: "删除管理的程序", Long: "删除被管理的程序", Args: cobra.MinimumNArgs(1), 11 | Example: "sudis [console] delete ", 12 | PreRunE: preRune, PostRun: runPost, 13 | Run: func(cmd *cobra.Command, args []string) { 14 | request := makeRequest(cmd, "delete", args...) 15 | if viper.GetBool("skip") { 16 | request.Header("skip", strconv.FormatBool(true)) 17 | } 18 | sendRequest(cmd, request) 19 | }, 20 | } 21 | 22 | func init() { 23 | deleteCmd.PersistentFlags().BoolP("skip", "", false, "不停止程序删除") 24 | } 25 | -------------------------------------------------------------------------------- /daemon/program_test.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/ihaiker/gokit/files" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestNewProgram(t *testing.T) { 11 | p := NewProgram() 12 | p.Name = "pingbaidu" 13 | p.Start = &Command{ 14 | Command: "ping", 15 | Args: []string{ 16 | "baidu.com", 17 | }, 18 | } 19 | p.AddTime = time.Now() 20 | p.UpdateTime = time.Now() 21 | 22 | if bs, err := json.MarshalIndent(p, "\t", "\n"); err != nil { 23 | t.Fatal(err) 24 | } else { 25 | w, err := files.New("../conf/programs/" + p.Name + ".json").GetWriter(false) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | defer w.Close() 30 | if _, err = w.Write(bs); err != nil { 31 | t.Fatal(err) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /daemon/manager_local_test.go: -------------------------------------------------------------------------------- 1 | package daemon_test 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/logs" 5 | "github.com/ihaiker/sudis/daemon" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestDaemonManager_Start(t *testing.T) { 11 | dm := daemon.NewDaemonManager("/tmp") 12 | 13 | dm.SetStatusListener(func(process *daemon.Process, oldStatus, newStatus daemon.FSMState) { 14 | logs.Info("program:", process.Program.Name, ", from:", oldStatus, ", to:", newStatus) 15 | }) 16 | if err := dm.Start(); err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | if err := dm.StartProgram("ping", nil); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | <-time.After(time.Second * 30) 25 | 26 | if err := dm.StopProgram("ping"); err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | dm.Stop() 31 | } 32 | -------------------------------------------------------------------------------- /daemon/process_info.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ihaiker/gokit/maths" 6 | "github.com/shirou/gopsutil/process" 7 | "strconv" 8 | ) 9 | 10 | func GetProcessInfo(pid int32) (cupPercent float64, useMem uint64, err error) { 11 | var p *process.Process 12 | p, err = process.NewProcess(pid) 13 | if err != nil { 14 | return 15 | } 16 | 17 | var memInfo *process.MemoryInfoStat 18 | memInfo, err = p.MemoryInfo() 19 | if err != nil { 20 | return 21 | } 22 | useMem = uint64(maths.Divide64(maths.Add64(float64(memInfo.VMS), float64(memInfo.RSS)), 1024.0*1024.0)) 23 | 24 | cupPercent, err = p.CPUPercent() 25 | if err != nil { 26 | return 27 | } 28 | 29 | cupPercent, _ = strconv.ParseFloat(fmt.Sprintf("%0.4f", cupPercent), 10) 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /libs/config/sudis.example.yaml: -------------------------------------------------------------------------------- 1 | #### 是否已debug模式运行 2 | #debug: false 3 | 4 | #### node 节点唯一ID,不指定将会获取hostname,如果多机器下hostname重复需要明确指定 5 | #key: sudis 6 | 7 | #### 数据存储位置, 8 | # 1、如果用未指定数据存储位置也没有设置配置文件,数据位置为: $HOME/.sudis 9 | # 2、如果用户设置了配置文件,单位设置数据位置,默认为: dir(conf) 10 | #data-path: $HOME/.sudis 11 | 12 | #### OpenApi,WebAdmin 开放地址,默认不开放 13 | address: 0.0.0.0:5984 14 | 15 | #### 是否禁用web管理台,默认不禁用,此参数只有配置address起效 16 | #disable-webui: false 17 | 18 | #### 数据存储配置 19 | #database: 20 | # type: sqlite3 21 | # url: sudis.db 22 | 23 | #### 是否开发管理节点,默认不开放 24 | manager: 0.0.0.0:5983 25 | 26 | #### 托管加入地址,默认不加入任何地址 27 | #join: [] 28 | #### or 29 | #join: 30 | # - 192.168.1.254:5983 31 | # - 192.168.1.253:5983 32 | 33 | #### 程序默认等待时间,例如关闭时间 34 | #maxwait: 15s 35 | 36 | #### 安全加密盐值,用户 37 | salt: whosyourdaddy -------------------------------------------------------------------------------- /webui/src/plugins/delete.vue: -------------------------------------------------------------------------------- 1 | 6 | 30 | -------------------------------------------------------------------------------- /cmds/initd/cmd.go: -------------------------------------------------------------------------------- 1 | package initd 2 | 3 | import ( 4 | "errors" 5 | "github.com/spf13/cobra" 6 | "os/user" 7 | "runtime" 8 | ) 9 | 10 | func isAdminUser() error { 11 | if u, err := user.Current(); err != nil { 12 | return err 13 | } else if u.Name == "root" || u.Name == "Administrator" { 14 | return nil 15 | } else { 16 | return errors.New("mast run as root or Administrator") 17 | } 18 | } 19 | 20 | var Cmd = &cobra.Command{ 21 | Use: "initd", Short: "添加开机启动项", Example: "sudis initd", 22 | RunE: func(cmd *cobra.Command, args []string) error { 23 | if err := isAdminUser(); err != nil { 24 | return err 25 | } 26 | 27 | switch runtime.GOOS { 28 | case "linux": 29 | return linuxAutoStart() 30 | case "windows": 31 | return windowsAutoStart() 32 | default: 33 | return errors.New("not support") 34 | } 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /webui/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 30 | -------------------------------------------------------------------------------- /cmds/console/tag.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | var tagCommand = &cobra.Command{ 9 | Use: "tag", Short: "添加程序标签", Long: "给程序添加标签", Args: cobra.ExactArgs(2), 10 | Example: `sudis [console|cli] tag name tag1 11 | sudis [console|cli] tag --delete name tag1 12 | `, 13 | PreRunE: preRune, PostRun: runPost, 14 | RunE: func(cmd *cobra.Command, args []string) error { 15 | request := makeRequest(cmd, "tag", args...) 16 | if viper.GetBool("delete") { 17 | request.Header("delete", "true") 18 | } else if del, err := cmd.PersistentFlags().GetBool("delete"); err == nil && del { 19 | request.Header("delete", "true") 20 | } 21 | sendRequest(cmd, request) 22 | return nil 23 | }, 24 | } 25 | 26 | func init() { 27 | tagCommand.PersistentFlags().BoolP("delete", "", false, "是否是删除标签") 28 | } 29 | -------------------------------------------------------------------------------- /daemon/fsm.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | type ( 4 | FSMState string 5 | 6 | FSMStatusEvent struct { 7 | Process *Process `json:"process"` 8 | FromStatus FSMState `json:"fromStatus"` 9 | ToStatus FSMState `json:"toStatus"` 10 | } 11 | 12 | FSMStatusListener func(event FSMStatusEvent) 13 | ) 14 | 15 | const ( 16 | Ready = FSMState("ready") 17 | Starting = FSMState("starting") 18 | Running = FSMState("running") 19 | Fail = FSMState("fail") 20 | RetryWait = FSMState("retry") 21 | Stopping = FSMState("stopping") 22 | Stoped = FSMState("stoped") 23 | ) 24 | 25 | func (f FSMState) String() string { 26 | return string(f) 27 | } 28 | 29 | func (f FSMState) IsRunning() bool { 30 | return f == Running || f == Starting 31 | } 32 | 33 | func StdoutStatusListener(event FSMStatusEvent) { 34 | logger.Debugf("program(%s) from %s to %s", event.Process.Name, event.FromStatus, event.ToStatus) 35 | } 36 | -------------------------------------------------------------------------------- /nodes/dao/notify.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Notify struct { 8 | Name string `json:"name" yaml:"name" toml:"name" xorm:"varchar(64) notnull pk 'name'"` 9 | Config string `json:"config" yaml:"config" toml:"config" xorm:"config"` 10 | CreateTime time.Time `json:"createTime" yaml:"createTime" toml:"createTime" xorm:"createTime"` 11 | } 12 | 13 | type notifyDao struct { 14 | } 15 | 16 | func (self *notifyDao) Get(name string) (notify *Notify, has bool, err error) { 17 | notify = new(Notify) 18 | has, err = engine.Where("name = ?", name).Get(notify) 19 | return 20 | } 21 | 22 | func (self *notifyDao) Add(notify *Notify) error { 23 | _, err := engine.InsertOne(notify) 24 | return err 25 | } 26 | 27 | func (self *notifyDao) Remove(name string) error { 28 | _, err := engine.Delete(&Notify{Name: name}) 29 | return err 30 | } 31 | 32 | var NotifyDao = new(notifyDao) 33 | -------------------------------------------------------------------------------- /webui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Sudis,分布式守护进程管理器 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build webui bindata release clean 2 | 3 | binout=bin/sudis 4 | 5 | Version=$(shell git describe --tags `git rev-list --tags --max-count=1`) 6 | BuildDate=$(shell date +"%F %T") 7 | GitCommit=$(shell git rev-parse --short HEAD) 8 | debug=-w -s 9 | param=-X main.VERSION=${Version} -X main.GITLOG_VERSION=${GitCommit} -X 'main.BUILD_TIME=${BuildDate}' 10 | 11 | gobinddata=$(shell command -v go-bindata) 12 | 13 | ifeq ($(gobinddata),'') 14 | go get -u github.com/shuLhan/go-bindata/cmd/go-bindata 15 | endif 16 | 17 | build: bindata 18 | go mod download 19 | go build -tags bindata -ldflags "${debug} ${param}" -o ${binout} 20 | 21 | docker: 22 | docker build --build-arg LDFLAGS="${debug} ${param}" -t xhaiker/sudis:${Version} . 23 | 24 | bindata: 25 | go generate generator.go 26 | 27 | webui: 28 | make -C webui build 29 | 30 | clean: 31 | @rm -rf bin 32 | @rm -rf webui/dist 33 | @rm -rf webui/node_modules 34 | @rm -f nodes/http/http_static_bindata_assets.go 35 | 36 | 37 | -------------------------------------------------------------------------------- /nodes/dao/json.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | type JSON map[string]interface{} 9 | 10 | func (json JSON) GetString(name string) (string, bool) { 11 | value, has := json[name] 12 | if has { 13 | return fmt.Sprintf("%v", value), has 14 | } else { 15 | return "", has 16 | } 17 | } 18 | 19 | func (json JSON) String(name string) string { 20 | value, _ := json.GetString(name) 21 | return value 22 | } 23 | 24 | func (json JSON) GetInt(name string) (int, bool) { 25 | value, has := json[name] 26 | if !has { 27 | return 0, false 28 | } else if intValue, match := value.(int); match { 29 | return intValue, true 30 | } else { 31 | if i, err := strconv.Atoi(fmt.Sprintf("%v", value)); err != nil { 32 | return 0, false 33 | } else { 34 | return i, true 35 | } 36 | } 37 | } 38 | 39 | func (json JSON) Int(name string, def int) int { 40 | value, has := json.GetInt(name) 41 | if !has { 42 | return def 43 | } 44 | return value 45 | } 46 | -------------------------------------------------------------------------------- /nodes/dao/tags.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | type Tag struct { 4 | Name string `json:"name" yaml:"name" toml:"name" xorm:"varchar(64) notnull pk 'name'"` 5 | Class string `json:"class" yaml:"class" toml:"class" xorm:"class"` 6 | } 7 | 8 | type tagDao struct { 9 | } 10 | 11 | func (self *tagDao) List() (tags []*Tag, err error) { 12 | tags = make([]*Tag, 0) 13 | err = engine.Find(&tags) 14 | return 15 | } 16 | 17 | func (self *tagDao) AddOrUpdate(name, class string) error { 18 | tag := new(Tag) 19 | if has, err := engine.Where("name = ?", name).Get(tag); err != nil { 20 | return err 21 | } else if has { 22 | tag.Class = class 23 | _, err = engine.Update(tag, &Tag{Name: name}) 24 | return err 25 | } else { 26 | tag.Name = name 27 | tag.Class = class 28 | _, err = engine.InsertOne(tag) 29 | return err 30 | } 31 | } 32 | 33 | func (self *tagDao) Remove(name string) error { 34 | _, err := engine.Delete(&Tag{Name: name}) 35 | return err 36 | } 37 | 38 | var TagDao = new(tagDao) 39 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # Sudis 2 | 3 | ![sudis logo](webui/src/assets/images/logo.png) 4 | 5 | Sudis !! 分布式程序控制系统。 6 | 7 | Supervister程序使用Go语言实现分布式一站式管理。 8 | 9 | 中文说明 | [English](README.md) 10 | 11 | 程序构架方式: 12 | 13 | ![sudis.svg](./docs/views/sudis.svg) 14 | 15 | 界面预览: 16 | ![dashboard.png](./docs/views/dashboard.png) 17 | ![nodes.png](./docs/views/nodes.png) 18 | ![programs.png](./docs/views/programs.png) 19 | ![tags.png](./docs/views/tags.png) 20 | 21 | ## 安装 22 | 23 | 阅读[安装说明](docs/INSTALL.md) 24 | 25 | ## 功能 26 | 27 | - 提供分布式多机房一站式管理方式。 28 | - 程序状态变更发送通知。 29 | 30 | ## 如何贡献 31 | 32 | 热诚欢迎提交您的意见和建议! 33 | 34 | 无论是简单的BUG修复、主要的新功能、还是其他建议。阅读[贡献指南](docs/CONTRIBUTING.md)以获取更多详细信息。 35 | 36 | 查阅 [贡献指南](docs/CONTRIBUTING.md) 37 | 38 | ## 变更记录 39 | 40 | [变更记录](docs/CHANGELOG.md) 41 | 42 | ## 未实现功能 43 | 44 | - [ ] 发送程序状态变更通知. (webhook, 邮件, 短信, gNotify). [gNotify](https://github.com/ihaiker/gNotify) 一个多渠道集成推送管理平台,处于设计和开发阶段。 45 | 46 | ## 技术支持 47 | ![QQ支持群](./docs/qq.png) 48 | ![QQ支持群](./docs/dd.png) 49 | 50 | -------------------------------------------------------------------------------- /webui/dist/static/js/chunk-9a6bc50a.801e34ca.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-9a6bc50a"],{a1d2:function(t,a,e){"use strict";e.r(a);var l=function(){var t=this,a=t.$createElement,e=t._self._c||a;return e("div",[e("vTitle",{attrs:{title:"日志管理","title-class":"icon-list"}}),e("div",{staticClass:"animated fadeIn"})],1)},s=[],i=e("c0cf"),c={name:"Logs",components:{vTitle:i["a"]}},n=c,r=e("9ca4"),u=Object(r["a"])(n,l,s,!1,null,null,null);a["default"]=u.exports},c0cf:function(t,a,e){"use strict";var l=function(){var t=this,a=t.$createElement,e=t._self._c||a;return e("ol",{staticClass:"breadcrumb breadcrumb-fixed"},[e("li",{staticClass:"breadcrumb-item"},[e("i",{staticClass:"fa",class:t.titleClass}),t._v(" "+t._s(t.title)+"\n ")]),e("li",{staticClass:"ml-auto"},[t._t("default")],2)])},s=[],i={name:"vTitle",props:{title:String,titleClass:{type:String,default:""}}},c=i,n=e("9ca4"),r=Object(n["a"])(c,l,s,!1,null,null,null);a["a"]=r.exports}}]); 2 | //# sourceMappingURL=chunk-9a6bc50a.801e34ca.js.map -------------------------------------------------------------------------------- /webui/src/tools/utils.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ram(limit) { 3 | let size = ""; 4 | if (limit < 0.1 * 1024) { //如果小于0.1KB转化成B 5 | size = limit.toFixed(2) + "B"; 6 | } else if (limit < 0.1 * 1024 * 1024) {//如果小于0.1MB转化成KB 7 | size = (limit / 1024).toFixed(2) + "KB"; 8 | } else if (limit < 0.1 * 1024 * 1024 * 1024) { //如果小于0.1GB转化成MB 9 | size = (limit / (1024 * 1024)).toFixed(2) + "MB"; 10 | } else { //其他转化成GB 11 | size = (limit / (1024 * 1024 * 1024)).toFixed(2) + "GB"; 12 | } 13 | return size; 14 | }, 15 | now() { 16 | let date = new Date(); 17 | let hour = date.getHours(), minute = date.getMinutes(), second = date.getSeconds(); 18 | return { 19 | name: date.toString(), show: 20 | (hour < 10 ? "0" + hour : hour) + ":" + (minute < 10 ? "0" + minute : minute) + ":" + 21 | (second < 10 ? "0" + second : second) 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /nodes/http/ctl.tag.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/errors" 5 | . "github.com/ihaiker/sudis/libs/errors" 6 | "github.com/ihaiker/sudis/nodes/dao" 7 | "github.com/kataras/iris/v12" 8 | ) 9 | 10 | type TagsController struct { 11 | } 12 | 13 | func (self *TagsController) queryTag(ctx iris.Context) []*dao.Tag { 14 | tags, err := dao.TagDao.List() 15 | errors.Assert(err) 16 | return tags 17 | } 18 | 19 | func (self *TagsController) addOrModify(ctx iris.Context) int { 20 | json := &dao.JSON{} 21 | errors.Assert(ctx.ReadJSON(json)) 22 | 23 | name := json.String("name") 24 | errors.True(name != "", ErrNameEmpty) 25 | 26 | class := json.String("class") 27 | errors.True(class != "", ErrClassEmpty) 28 | 29 | err := dao.TagDao.AddOrUpdate(name, class) 30 | errors.Assert(err) 31 | return iris.StatusNoContent 32 | } 33 | 34 | func (self *TagsController) removeTag(ctx iris.Context) int { 35 | name := ctx.Params().GetString("name") 36 | errors.Assert(dao.TagDao.Remove(name)) 37 | return iris.StatusNoContent 38 | } 39 | -------------------------------------------------------------------------------- /webui/dist/static/css/chunk-5cdb028c.864801b0.css: -------------------------------------------------------------------------------- 1 | .modal .modal-dialog .modal-content[data-v-220add12]{-webkit-transition:all .15s;transition:all .15s}.modal .modal-dialog.aside[data-v-220add12]{-webkit-transform-style:preserve-3d;transform-style:preserve-3d}.modal .modal-dialog.aside.modal-stack-1 .modal-content[data-v-220add12]{-webkit-transform:scale(.9) translate(-2rem,-50px);transform:scale(.9) translate(-2rem,-50px);-webkit-transform-origin:top left;transform-origin:top left}.modal .modal-dialog.aside.modal-stack-2 .modal-content[data-v-220add12]{-webkit-transform:scale(.9) translate(-4rem,-100px);transform:scale(.9) translate(-4rem,-100px);-webkit-transform-origin:top left;transform-origin:top left}.modal .modal-dialog.aside.modal-stack-3 .modal-content[data-v-220add12]{-webkit-transform:scale(.9) translate(-6rem,-150px);transform:scale(.9) translate(-6rem,-150px);-webkit-transform-origin:top left;transform-origin:top left}.aside .modal-visible-aside[data-v-220add12]{display:block}.aside .modal-invisible-aside[data-v-220add12],.modal-visible-aside[data-v-220add12]{display:none} -------------------------------------------------------------------------------- /webui/dist/static/css/chunk-d2642d9c.864801b0.css: -------------------------------------------------------------------------------- 1 | .modal .modal-dialog .modal-content[data-v-220add12]{-webkit-transition:all .15s;transition:all .15s}.modal .modal-dialog.aside[data-v-220add12]{-webkit-transform-style:preserve-3d;transform-style:preserve-3d}.modal .modal-dialog.aside.modal-stack-1 .modal-content[data-v-220add12]{-webkit-transform:scale(.9) translate(-2rem,-50px);transform:scale(.9) translate(-2rem,-50px);-webkit-transform-origin:top left;transform-origin:top left}.modal .modal-dialog.aside.modal-stack-2 .modal-content[data-v-220add12]{-webkit-transform:scale(.9) translate(-4rem,-100px);transform:scale(.9) translate(-4rem,-100px);-webkit-transform-origin:top left;transform-origin:top left}.modal .modal-dialog.aside.modal-stack-3 .modal-content[data-v-220add12]{-webkit-transform:scale(.9) translate(-6rem,-150px);transform:scale(.9) translate(-6rem,-150px);-webkit-transform-origin:top left;transform-origin:top left}.aside .modal-visible-aside[data-v-220add12]{display:block}.aside .modal-invisible-aside[data-v-220add12],.modal-visible-aside[data-v-220add12]{display:none} -------------------------------------------------------------------------------- /cmds/console/add.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "errors" 5 | "github.com/ihaiker/gokit/files" 6 | "github.com/spf13/cobra" 7 | "io/ioutil" 8 | "os" 9 | ) 10 | 11 | var addCmd = &cobra.Command{ 12 | Use: "add", Short: "添加程序管理", Long: "添加被管理的程序", 13 | Example: `sudis [console] add [jsonfile]... 14 | cat jsonfile | sudis [console] add`, 15 | PreRunE: preRune, PostRun: runPost, 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | programs := make([]string, 0) 18 | for _, arg := range args { 19 | if fileContent, err := files.New(arg).ToString(); err != nil { 20 | return err 21 | } else { 22 | programs = append(programs, fileContent) 23 | } 24 | } 25 | 26 | if info, _ := os.Stdin.Stat(); info.Size() > 0 { 27 | body, _ := ioutil.ReadAll(os.Stdin) 28 | programs = append(programs, string(body)) 29 | } 30 | 31 | if len(programs) == 0 { 32 | return errors.New("json file not found") 33 | } 34 | 35 | request := makeRequest(cmd, "add", programs...) 36 | sendRequest(cmd, request) 37 | return nil 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /daemon/interface.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | type Manager interface { 4 | Start() error 5 | Stop() error 6 | 7 | GetProcess(name string) (*Process, error) 8 | 9 | AddProgram(program *Program) error 10 | RemoveProgram(name string, skip bool) error 11 | ModifyProgram(program *Program) error 12 | 13 | ModifyTag(name string, add bool, tag string) error 14 | 15 | ListProgramNames() ([]string, error) 16 | ListProcess() ([]*Process, error) 17 | 18 | SetStatusListener(lis FSMStatusListener) 19 | 20 | StartProgram(name string, determinedResult chan *Process) error 21 | StopProgram(name string) error 22 | 23 | MustGetProcess(name string) *Process 24 | MustAddProgram(program *Program) 25 | MustRemoveProgram(name string, skip bool) 26 | MustModifyProgram(program *Program) 27 | MustModifyTag(name string, add bool, tag string) 28 | MustListProgramNames() []string 29 | MustListProcess() []*Process 30 | 31 | MustStartProgram(name string, determinedResult chan *Process) 32 | MustStopProgram(name string) 33 | 34 | SubscribeLogger(uid string, name string, tail TailLogger, firstLine int) error 35 | UnSubscribeLogger(uid, name string) error 36 | } 37 | -------------------------------------------------------------------------------- /nodes/notify/mail/mail.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/logs" 5 | "gopkg.in/gomail.v2" 6 | ) 7 | 8 | type MailServer struct { 9 | Host string 10 | Port int 11 | User string 12 | Password string 13 | stmp gomail.SendCloser 14 | } 15 | 16 | func NewServer(host string, port int, user, passwd string) *MailServer { 17 | return &MailServer{ 18 | Host: host, Port: port, 19 | User: user, Password: passwd, 20 | } 21 | } 22 | 23 | func (self *MailServer) Start() (err error) { 24 | m := gomail.NewDialer(self.Host, self.Port, self.User, self.Password) 25 | self.stmp, err = m.Dial() 26 | return 27 | } 28 | 29 | func (self *MailServer) Send(msg ...*Message) error { 30 | if self.stmp == nil { 31 | logs.GetLogger("mail").Debug("Mail service is not configured") 32 | return nil 33 | } 34 | 35 | goMails := make([]*gomail.Message, len(msg)) 36 | for i, message := range msg { 37 | goMails[i] = message.Build() 38 | } 39 | return gomail.Send(self.stmp, goMails...) 40 | } 41 | 42 | func (self *MailServer) Close() error { 43 | if self.stmp != nil { 44 | return self.stmp.Close() 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /webui/src/assets/scss/vendors/chart.js/chart.scss: -------------------------------------------------------------------------------- 1 | // Import variables 2 | @import '../variables'; 3 | 4 | .chart-legend, 5 | .bar-legend, 6 | .line-legend, 7 | .pie-legend, 8 | .radar-legend, 9 | .polararea-legend, 10 | .doughnut-legend { 11 | list-style-type: none; 12 | margin-top: 5px; 13 | text-align: center; 14 | -webkit-padding-start: 0; 15 | -moz-padding-start: 0; 16 | padding-left: 0; 17 | } 18 | .chart-legend li, 19 | .bar-legend li, 20 | .line-legend li, 21 | .pie-legend li, 22 | .radar-legend li, 23 | .polararea-legend li, 24 | .doughnut-legend li { 25 | display: inline-block; 26 | white-space: nowrap; 27 | position: relative; 28 | margin-bottom: 4px; 29 | @include border-radius($border-radius); 30 | padding: 2px 8px 2px 28px; 31 | font-size: smaller; 32 | cursor: default; 33 | } 34 | .chart-legend li span, 35 | .bar-legend li span, 36 | .line-legend li span, 37 | .pie-legend li span, 38 | .radar-legend li span, 39 | .polararea-legend li span, 40 | .doughnut-legend li span { 41 | display: block; 42 | position: absolute; 43 | left: 0; 44 | top: 0; 45 | width: 20px; 46 | height: 20px; 47 | @include border-radius($border-radius); 48 | } 49 | -------------------------------------------------------------------------------- /daemon/process_list.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import "github.com/ihaiker/sudis/libs/errors" 4 | 5 | type ProcessGroup []*Process 6 | 7 | func (pl ProcessGroup) Len() int { 8 | return len(pl) 9 | } 10 | 11 | func (pl ProcessGroup) Less(i, j int) bool { 12 | return pl[j].Program.Id > pl[i].Program.Id 13 | } 14 | 15 | func (pl ProcessGroup) Swap(i, j int) { 16 | tmp := (pl)[i] 17 | (pl)[i] = (pl)[j] 18 | (pl)[j] = tmp 19 | } 20 | 21 | func (pl ProcessGroup) Names() []string { 22 | names := make([]string, len(pl)) 23 | for i := 0; i < len(pl); i++ { 24 | names[i] = pl[i].Program.Name 25 | } 26 | return names 27 | } 28 | 29 | func (pl ProcessGroup) Get(name string) (*Process, error) { 30 | for _, p := range pl { 31 | if p.Program.Name == name { 32 | return p, nil 33 | } 34 | } 35 | return nil, errors.ErrProgramNotFound 36 | } 37 | 38 | func (pl *ProcessGroup) Remove(name string) error { 39 | removeIdx := -1 40 | for idx, p := range *pl { 41 | if p.Program.Name == name { 42 | removeIdx = idx 43 | break 44 | } 45 | } 46 | if removeIdx == -1 { 47 | return errors.ErrProgramNotFound 48 | } 49 | *pl = append((*pl)[:removeIdx], (*pl)[removeIdx+1:]...) 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sudis 2 | 3 | ![sudis logo](webui/src/assets/images/logo.png) 4 | 5 | Sudis !! Distributed supervisor process control system. 6 | 7 | [中文说明](README_ZH.md) | English 8 | 9 | ![sudis.svg](./docs/views/sudis.svg) 10 | 11 | 12 | ![dashboard.png](./docs/views/dashboard.png) 13 | ![nodes.png](./docs/views/nodes.png) 14 | ![programs.png](./docs/views/programs.png) 15 | ![tags.png](./docs/views/tags.png) 16 | 17 | ## Installation 18 | 19 | Step-by-step instruction are provided in [docs/INSTALL.md](docs/INSTALL.md) 20 | 21 | ## Futures 22 | 23 | - Support for distributed management. 24 | - Program status change notification. 25 | 26 | 27 | ## How to Contribute 28 | 29 | Contributions are warmly welcome! Be it trivial cleanup, major new feature or other suggestion. 30 | Read this how to contribute guide for more details. 31 | 32 | See [CONTRIBUTING.md](docs/CONTRIBUTING.md) 33 | 34 | ## Changelog 35 | see [CHANGELOG.md](docs/CHANGELOG.md) 36 | 37 | ## TODOLIST 38 | 39 | - [X] Send status change notification. (webhook, email, sms, gNotify). [gNotify](https://github.com/ihaiker/gNotify) is in the design and development stage 40 | 41 | ## Technical Support 42 | ![QQ支持群](./docs/qq.png) 43 | ![QQ支持群](./docs/dd.png) 44 | -------------------------------------------------------------------------------- /webui/dist/static/css/chunk-8d988810.fe8a04b4.css: -------------------------------------------------------------------------------- 1 | .loader{position:absolute;top:0;left:0;right:0;bottom:0;z-index:99999998}.loader .content,.loader .header{position:absolute;top:50%;left:50%;padding:5rem;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.loader .header{font-size:1rem;font-weight:700}.dot{width:3rem;height:3rem;background:#3ac;border-radius:100%;display:inline-block;-webkit-animation:slide 1s infinite;animation:slide 1s infinite}.dot:first-child{-webkit-animation-delay:.1s;animation-delay:.1s;background:#32aacc}.dot:nth-child(2){-webkit-animation-delay:.2s;animation-delay:.2s;background:#64aacc}.dot:nth-child(3){-webkit-animation-delay:.3s;animation-delay:.3s;background:#96aacc}.dot:nth-child(4){-webkit-animation-delay:.4s;animation-delay:.4s;background:#c8aacc}.dot:nth-child(5){-webkit-animation-delay:.5s;animation-delay:.5s;background:#faaacc}@-webkit-keyframes slide{0%{-webkit-transform:scale(1);transform:scale(1);opacity:0}50%{opacity:.3;-webkit-transform:scale(2);transform:scale(2)}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes slide{0%{-webkit-transform:scale(1);transform:scale(1);opacity:0}50%{opacity:.3;-webkit-transform:scale(2);transform:scale(2)}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}} -------------------------------------------------------------------------------- /cmds/console/join.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | var joinCmd = &cobra.Command{ 9 | Use: "join", Short: "加入主控节点", Long: "将当前节点托管到其他节点管理", Args: cobra.ExactValidArgs(1), 10 | Example: "sudis [console|cli] join [--must] ", 11 | PreRunE: preRune, PostRun: runPost, 12 | RunE: func(cmd *cobra.Command, args []string) error { 13 | 14 | request := makeRequest(cmd, "join", args...) 15 | if viper.GetBool("must") { 16 | request.Header("must", "true") 17 | } 18 | if token := viper.GetString("token"); token != "" { 19 | request.Header("token", token) 20 | } 21 | sendRequest(cmd, request) 22 | return nil 23 | }, 24 | } 25 | 26 | var leaveCmd = &cobra.Command{ 27 | Use: "leave", Short: "离开主节点", Long: "离开主节点,如果存在多个主节点需要明确指定,否则或全部离开", 28 | Example: "sudis [console|cli] leave [address,...]", 29 | PreRunE: preRune, PostRun: runPost, 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | request := makeRequest(cmd, "leave", args...) 32 | if len(args) == 0 { 33 | request.Body = []byte("[]") 34 | } 35 | sendRequest(cmd, request) 36 | return nil 37 | }, 38 | } 39 | 40 | func init() { 41 | joinCmd.PersistentFlags().StringP("token", "", "", "连接使用的token") 42 | } 43 | -------------------------------------------------------------------------------- /nodes/dao/user.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import "github.com/ihaiker/sudis/libs/errors" 4 | 5 | type User struct { 6 | Name string `json:"name" yaml:"name" toml:"name" xorm:"varchar(32) notnull pk 'name'"` 7 | Passwd string `json:"-" xorm:"passwd"` 8 | Time string `json:"time" yaml:"time" toml:"time" xorm:"time"` 9 | } 10 | 11 | type userDao struct { 12 | } 13 | 14 | func (dao *userDao) Get(name string) (user *User, has bool, err error) { 15 | user = new(User) 16 | has, err = engine.Where("name = ?", name).Get(user) 17 | return 18 | } 19 | 20 | func (dao *userDao) List() (users []*User, err error) { 21 | users = []*User{} 22 | err = engine.Find(&users) 23 | return 24 | } 25 | 26 | func (dao *userDao) Remove(name string) error { 27 | _, err := engine.Delete(&User{Name: name}) 28 | return err 29 | } 30 | 31 | func (dao *userDao) ModifyPasswd(name, passwd string) error { 32 | if _, has, err := dao.Get(name); err != nil { 33 | return err 34 | } else if !has { 35 | return errors.ErrNotFound 36 | } else { 37 | _, err = engine.Update(&User{Passwd: passwd}, &User{Name: name}) 38 | return err 39 | } 40 | } 41 | 42 | func (dao *userDao) Insert(user *User) error { 43 | _, err := engine.InsertOne(user) 44 | return err 45 | } 46 | 47 | var UserDao = new(userDao) 48 | -------------------------------------------------------------------------------- /nodes/http/ctl.user.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/errors" 5 | "github.com/ihaiker/sudis/nodes/dao" 6 | "github.com/kataras/iris/v12" 7 | "time" 8 | ) 9 | 10 | type UserController struct{} 11 | 12 | func (self *UserController) queryUser(ctx iris.Context) []*dao.User { 13 | users, err := dao.UserDao.List() 14 | errors.Assert(err) 15 | return users 16 | } 17 | 18 | func (self *UserController) addUser(ctx iris.Context) int { 19 | json := &dao.JSON{} 20 | errors.Assert(ctx.ReadJSON(json)) 21 | user := &dao.User{ 22 | Name: json.String("name"), 23 | Passwd: json.String("passwd"), 24 | Time: time.Now().Format("2006-01-02 15:04:05"), 25 | } 26 | errors.Assert(dao.UserDao.Insert(user)) 27 | return iris.StatusNoContent 28 | } 29 | 30 | func (self *UserController) deleteUser(ctx iris.Context) int { 31 | name := ctx.Params().GetString("name") 32 | err := dao.UserDao.Remove(name) 33 | errors.Assert(err) 34 | return iris.StatusNoContent 35 | } 36 | 37 | func (self *UserController) modifyPasswd(ctx iris.Context) int { 38 | json := &dao.JSON{} 39 | errors.Assert(ctx.ReadJSON(json)) 40 | name := json.String("name") 41 | passwd := json.String("passwd") 42 | errors.Assert(dao.UserDao.ModifyPasswd(name, passwd)) 43 | return iris.StatusNoContent 44 | } 45 | -------------------------------------------------------------------------------- /cmds/console/modify.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/ihaiker/gokit/files" 6 | "github.com/ihaiker/sudis/daemon" 7 | "github.com/ihaiker/sudis/libs/errors" 8 | "github.com/spf13/cobra" 9 | "io/ioutil" 10 | "os" 11 | ) 12 | 13 | var modifyCmd = &cobra.Command{ 14 | Use: "modify", Short: "修改程序", Long: "修改被管理的程序", Args: cobra.RangeArgs(1, 2), 15 | Example: `sudis [console] modify [jsonfile] 16 | cat jsonfile | sudis [console] modify `, 17 | PreRunE: preRune, PostRun: runPost, 18 | RunE: func(cmd *cobra.Command, args []string) (err error) { 19 | request := makeRequest(cmd, "modify") 20 | 21 | var content []byte 22 | if len(args) == 2 { 23 | if content, err = files.New(args[1]).ToBytes(); err != nil { 24 | return 25 | } 26 | } else { 27 | if info, _ := os.Stdin.Stat(); info.Size() > 0 { 28 | if content, err = ioutil.ReadAll(os.Stdin); err != nil { 29 | return 30 | } 31 | } 32 | } 33 | 34 | if len(content) == 0 { 35 | return errors.New("no content") 36 | } 37 | 38 | program := daemon.NewProgram() 39 | if err = json.Unmarshal(content, program); err != nil { 40 | return 41 | } 42 | program.Name = args[0] 43 | request.Body, _ = json.Marshal(program) 44 | 45 | sendRequest(cmd, request) 46 | return nil 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ihaiker/sudis 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/Joker/hpp v1.0.0 // indirect 7 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect 8 | github.com/blang/semver v3.5.1+incompatible 9 | github.com/codeskyblue/kexec v0.0.0-20180119015717-5a4bed90d99a 10 | github.com/go-ole/go-ole v1.2.4 // indirect 11 | github.com/go-sql-driver/mysql v1.5.0 12 | github.com/ihaiker/gokit v1.7.5 13 | github.com/iris-contrib/go.uuid v2.0.0+incompatible 14 | github.com/iris-contrib/middleware/cors v0.0.0-20200913183508-5d1bed0e6ea4 15 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect 16 | github.com/kataras/iris/v12 v12.2.0-alpha 17 | github.com/kataras/neffos v0.0.16 18 | github.com/mattn/go-colorable v0.1.2 // indirect 19 | github.com/mattn/go-isatty v0.0.9 // indirect 20 | github.com/mattn/go-sqlite3 v2.0.3+incompatible 21 | github.com/nats-io/nats-server/v2 v2.1.8 // indirect 22 | github.com/shirou/gopsutil v2.20.8+incompatible 23 | github.com/spf13/cobra v1.0.0 24 | github.com/spf13/viper v1.7.1 25 | github.com/yudai/pp v2.0.1+incompatible // indirect 26 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 27 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 28 | gopkg.in/yaml.v2 v2.3.0 29 | xorm.io/xorm v1.0.5 30 | ) 31 | -------------------------------------------------------------------------------- /webui/src/views/program/CPU.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 44 | -------------------------------------------------------------------------------- /nodes/http/auth/basic.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/base64" 5 | "github.com/ihaiker/gokit/errors" 6 | . "github.com/ihaiker/sudis/libs/errors" 7 | "github.com/ihaiker/sudis/nodes/dao" 8 | "github.com/kataras/iris/v12" 9 | "strings" 10 | ) 11 | 12 | type basicService struct{} 13 | 14 | func (self *basicService) Login(data *dao.JSON) *LoginToken { 15 | username := data.String("name") 16 | password := data.String("passwd") 17 | 18 | self.mustCheck(username, password) 19 | token := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) 20 | return &LoginToken{Token: token} 21 | } 22 | 23 | func (self *basicService) mustCheck(username, password string) { 24 | user, has, err := dao.UserDao.Get(username) 25 | errors.Assert(err) 26 | errors.True(has, ErrUser) 27 | errors.True(password == user.Passwd, ErrUser) 28 | } 29 | 30 | func (self *basicService) Check(ctx iris.Context) { 31 | authorization := ctx.GetHeader("Authorization") 32 | if strings.HasPrefix(authorization, "Basic ") { 33 | if out, err := base64.StdEncoding.DecodeString(authorization[6:]); err == nil { 34 | nameAndValue := strings.SplitN(string(out), ":", 2) 35 | err := errors.SafeExec(func() { self.mustCheck(nameAndValue[0], nameAndValue[1]) }) 36 | if err == nil { 37 | ctx.Next() 38 | return 39 | } 40 | } 41 | } 42 | ctx.StatusCode(iris.StatusUnauthorized) 43 | } 44 | -------------------------------------------------------------------------------- /cmds/console/list.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | var listCmd = &cobra.Command{ 11 | Use: "list", Aliases: []string{"ls"}, Args: cobra.NoArgs, 12 | Short: "查看程序列表", Long: "查看管理程序的列表(名称)", 13 | Example: "sudis [console] list --inspect", 14 | PreRunE: preRune, PostRun: runPost, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | request := makeRequest(cmd, "list") 17 | if viper.GetBool("inspect") { 18 | request.Header("inspect", "true") 19 | } 20 | if viper.GetBool("all") { 21 | request.Header("all", "true") 22 | } 23 | if viper.GetBool("quiet") { 24 | request.Header("quiet", "true") 25 | } 26 | 27 | resp := sendRequest(cmd, request, true) 28 | if resp.Error != nil { 29 | fmt.Println(resp.Error) 30 | } else if viper.GetBool("inspect") { 31 | fmt.Println(string(resp.Body)) 32 | } else { 33 | items := make([]string, 0) 34 | if err := json.Unmarshal(resp.Body, &items); err != nil { 35 | fmt.Println(err) 36 | } else { 37 | for _, item := range items { 38 | fmt.Println(item) 39 | } 40 | } 41 | } 42 | }, 43 | } 44 | 45 | func init() { 46 | listCmd.PersistentFlags().BoolP("inspect", "i", false, "显示详情") 47 | listCmd.PersistentFlags().BoolP("all", "a", false, "显示全部程序") 48 | listCmd.PersistentFlags().BoolP("quiet", "q", false, "仅仅显示名称") 49 | } 50 | -------------------------------------------------------------------------------- /nodes/notify/webhook/send.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/ihaiker/gokit/logs" 7 | "github.com/ihaiker/sudis/nodes/dao" 8 | "text/template" 9 | ) 10 | 11 | var logger = logs.GetLogger("webhook") 12 | 13 | func SendWebhook(data interface{}) { 14 | cfg, has, err := dao.NotifyDao.Get("webhook") 15 | if err != nil { 16 | logger.Warn("WebHook获取异常:", err) 17 | return 18 | } 19 | if !has { 20 | return 21 | } 22 | 23 | config := new(dao.JSON) 24 | if err = json.Unmarshal([]byte(cfg.Config), config); err != nil { 25 | logger.Warn("WebHook配置异常: ", err) 26 | return 27 | } 28 | address := config.String("address") 29 | token := config.String("token") 30 | content := config.String("content") 31 | 32 | var body []byte 33 | if content == "" { 34 | body, err = json.Marshal(data) 35 | } else { 36 | body, err = render(data, content) 37 | } 38 | if err != nil { 39 | logger.Warn("发送通知错误:", err) 40 | return 41 | } 42 | if err = WebHook(address, token, body); err != nil { 43 | logger.Warn("WebHook通知异常:", err) 44 | } 45 | } 46 | 47 | func render(data interface{}, content string) ([]byte, error) { 48 | out := bytes.NewBuffer([]byte{}) 49 | if t, err := template.New("master").Parse(content); err != nil { 50 | return nil, err 51 | } else if err = t.Execute(out, data); err != nil { 52 | return nil, err 53 | } 54 | return out.Bytes(), nil 55 | } 56 | -------------------------------------------------------------------------------- /nodes/http/ctl.node.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/errors" 5 | "github.com/ihaiker/sudis/libs/config" 6 | . "github.com/ihaiker/sudis/libs/errors" 7 | "github.com/ihaiker/sudis/nodes/cluster" 8 | "github.com/ihaiker/sudis/nodes/dao" 9 | "github.com/kataras/iris/v12" 10 | ) 11 | 12 | type NodeController struct { 13 | clusterManger *cluster.DaemonManager 14 | } 15 | 16 | func (self *NodeController) queryNodeList() []*dao.Node { 17 | return self.clusterManger.QueryNode() 18 | } 19 | 20 | func (self *NodeController) addOrModifyNodeToken(json *dao.JSON) int { 21 | key := json.String("key") 22 | errors.True(key != "", ErrNodeIsEmpty) 23 | 24 | token := json.String("token") 25 | errors.True(token != "", ErrNodeIsEmpty) 26 | 27 | errors.Assert(self.clusterManger.ModifyNodeToken(key, token)) 28 | return iris.StatusNoContent 29 | } 30 | 31 | func (self *NodeController) modifyNodeTag(json *dao.JSON) int { 32 | 33 | key := json.String("key") 34 | errors.True(key != "", ErrNodeIsEmpty) 35 | 36 | if key == config.Config.Key { 37 | return iris.StatusNoContent 38 | } 39 | tag := json.String("tag") 40 | 41 | errors.Assert(self.clusterManger.ModifyNodeTag(key, tag)) 42 | 43 | return iris.StatusNoContent 44 | } 45 | 46 | func (self *NodeController) removeNode(key string) int { 47 | errors.Assert(self.clusterManger.RemoveJoin(key), "删除节点") 48 | return iris.StatusNoContent 49 | } 50 | -------------------------------------------------------------------------------- /webui/src/views/program/args.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 45 | -------------------------------------------------------------------------------- /nodes/dao/database.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | _ "github.com/go-sql-driver/mysql" 5 | "github.com/ihaiker/gokit/files" 6 | "github.com/ihaiker/sudis/libs/config" 7 | _ "github.com/mattn/go-sqlite3" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/ihaiker/gokit/logs" 13 | "xorm.io/xorm" 14 | ) 15 | 16 | var engine *xorm.Engine 17 | 18 | func CreateEngine(path string, config *config.Database) (err error) { 19 | datasource := config.Url 20 | dbType := config.Type 21 | if !strings.HasPrefix(datasource, "/") { 22 | datasource = filepath.Join(path, datasource) 23 | } 24 | logs.Infof("connect %s %s", dbType, datasource) 25 | 26 | notExists := files.NotExist(datasource) 27 | engine, err = xorm.NewEngine(dbType, datasource) 28 | if err != nil { 29 | return err 30 | } 31 | engine.SetLogger(coreLogger) 32 | if logs.IsDebugMode() { 33 | engine.ShowSQL(true) 34 | } 35 | if notExists { 36 | err = createTables() 37 | } 38 | return 39 | } 40 | 41 | func createTables() error { 42 | tables := []interface{}{ 43 | &Node{}, &Tag{}, 44 | &User{}, &Notify{}, 45 | } 46 | if err := engine.CreateTables(tables...); err != nil { 47 | return err 48 | } 49 | user := &User{ 50 | Name: "admin", Passwd: "12345678", Time: Timestamp(), 51 | } 52 | if _, err := engine.InsertOne(user); err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | 58 | func Timestamp() string { 59 | return time.Now().Format("2006-01-03 15:04:05") 60 | } 61 | -------------------------------------------------------------------------------- /cmds/console/tail.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "fmt" 5 | runtimeKit "github.com/ihaiker/gokit/runtime" 6 | "github.com/ihaiker/sudis/libs/config" 7 | uuid "github.com/iris-contrib/go.uuid" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | SUBSCRIBE = "true" 16 | UNSUBSCRIBE = "false" 17 | ) 18 | 19 | var tailCmd = &cobra.Command{ 20 | Use: "tail", Aliases: []string{"tailf"}, Short: "查看日志", Long: "查看程序控制控制态输出日志", Args: cobra.ExactValidArgs(1), 21 | Example: "sudis [console] tail[f] [-n ] ", 22 | PreRunE: preRune, PostRun: runPost, 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | 25 | name := args[0] 26 | subscribeId, _ := uuid.NewV4() //channel id 27 | subId := strings.ReplaceAll(subscribeId.String(), "-", "") 28 | 29 | //订阅成功就可以,在启动客户端连接的试试已经做好了日志的打印 30 | request := makeRequest(cmd, "tail", name, SUBSCRIBE, subId) 31 | request.Header("num", strconv.Itoa(viper.GetInt("num"))) 32 | if response := sendRequest(cmd, request); response.Error != nil { 33 | fmt.Println(response.Error) 34 | return nil 35 | } 36 | 37 | kill := runtimeKit.NewListener() 38 | kill.AddStop(func() error { 39 | resp := sendRequest(cmd, makeRequest(cmd, "tail", name, UNSUBSCRIBE, subId)) 40 | return resp.Error 41 | }) 42 | return kill.WaitTimeout(config.Config.MaxWaitTimeout) 43 | }, 44 | } 45 | 46 | func init() { 47 | tailCmd.PersistentFlags().IntP("num", "n", 10, "日志首次条目") 48 | } 49 | -------------------------------------------------------------------------------- /nodes/dao/logger.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/logs" 5 | "xorm.io/xorm/log" 6 | ) 7 | 8 | var logger = logs.GetLogger("dao") 9 | 10 | var coreLogger *XormLogger 11 | 12 | func init() { 13 | coreLogger = new(XormLogger) 14 | } 15 | 16 | type XormLogger struct { 17 | showsql bool 18 | } 19 | 20 | func (self *XormLogger) Level() log.LogLevel { 21 | return log.LogLevel(int(logger.Level())) 22 | } 23 | 24 | func (self *XormLogger) SetLevel(l log.LogLevel) { 25 | logger.SetLevel(logs.Level(int(l))) 26 | } 27 | 28 | func (self *XormLogger) Debug(v ...interface{}) { 29 | logger.Debug(v...) 30 | } 31 | 32 | func (self *XormLogger) Debugf(format string, v ...interface{}) { 33 | logger.Debugf(format, v...) 34 | } 35 | 36 | func (self *XormLogger) Error(v ...interface{}) { 37 | logger.Error(v...) 38 | } 39 | 40 | func (self *XormLogger) Errorf(format string, v ...interface{}) { 41 | logger.Errorf(format, v...) 42 | } 43 | 44 | func (self *XormLogger) Info(v ...interface{}) { 45 | logger.Info(v...) 46 | } 47 | 48 | func (self *XormLogger) Infof(format string, v ...interface{}) { 49 | logger.Infof(format, v...) 50 | } 51 | 52 | func (self *XormLogger) Warn(v ...interface{}) { 53 | logger.Warn(v...) 54 | } 55 | 56 | func (self *XormLogger) Warnf(format string, v ...interface{}) { 57 | logger.Warnf(format, v...) 58 | } 59 | 60 | func (self *XormLogger) ShowSQL(show ...bool) { 61 | self.showsql = show[0] 62 | } 63 | 64 | func (self *XormLogger) IsShowSQL() bool { 65 | return self.showsql 66 | } 67 | -------------------------------------------------------------------------------- /webui/src/views/program/RAM.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 48 | -------------------------------------------------------------------------------- /daemon/process_test.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/logs" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func init() { 10 | logs.SetDebugMode(true) 11 | } 12 | 13 | func TestForegroundProcess(t *testing.T) { 14 | program := NewProgram() 15 | program.Name = "ping" 16 | program.Start = &Command{ 17 | Command: "ping", 18 | Args: []string{ 19 | "baidu.com", 20 | }, 21 | } 22 | program.Logger = "/tmp/sudis.log" 23 | process := NewProcess(program) 24 | 25 | process.statusListener = func(pro *Process, fromStatus, toStatus FSMState) { 26 | logger.Debugf("from %s to %s", fromStatus, toStatus) 27 | } 28 | 29 | if err := process.startCommand(nil); err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | <-time.After(time.Second * 10) 34 | 35 | if err := process.stopCommand(); err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | <-time.After(time.Second) 40 | } 41 | 42 | func TestDaemonProcess(t *testing.T) { 43 | program := NewProgram() 44 | program.Name = "nginx" 45 | program.IgnoreAlreadyStarted = true 46 | program.Start = &Command{ 47 | Command: "nginx", 48 | CheckHealth: &CheckHealth{ 49 | CheckAddress: "http://127.0.0.1", 50 | CheckMode: HTTP, 51 | SecretToken: "", 52 | CheckTtl: 3, 53 | }, 54 | } 55 | program.Stop = &Command{ 56 | Command: "nginx", 57 | Args: []string{"-s", "quit"}, 58 | } 59 | 60 | process := NewProcess(program) 61 | 62 | if err := process.startCommand(nil); err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | <-time.After(time.Hour) 67 | 68 | if err := process.stopCommand(); err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | <-time.After(time.Second) 73 | } 74 | -------------------------------------------------------------------------------- /libs/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/commons" 5 | "os" 6 | "time" 7 | ) 8 | 9 | type ( 10 | Database struct { 11 | Type string `mapstructure:"database.type"` 12 | Url string `mapstructure:"database.url"` 13 | } 14 | 15 | sudisConfig struct { 16 | Debug bool `mapstructure:"debug"` 17 | 18 | //集群唯一标识 19 | Key string `mapstructure:"key"` 20 | 21 | //绑定地址 22 | Address string `mapstructure:"address"` 23 | 24 | DisableWebUI bool `mapstructure:"disable-webui"` 25 | 26 | //数据存储位置 27 | DataPath string `mapstructure:"data-path"` 28 | 29 | Database *Database 30 | 31 | //管理节点的绑定地址 32 | Manager string `mapstructure:"manager"` 33 | 34 | //节点盐值,如果设置了此值,所有节点的将统一使用此值,如果没有设置,所有节点的盐值都是单独的。 35 | Salt string `mapstructure:"salt"` 36 | 37 | //连接主节点 38 | Join []string `mapstructure:"join"` 39 | 40 | //管理程序关闭最大等待时间,防止有些程序不能很快停止而导致的直接kill 41 | MaxWaitTimeout time.Duration `mapstructure:"maxwait"` 42 | 43 | //时间通知是否同步通知,及只有上一个通知成功后,才可以进行下一个的通知, 44 | NotifySynchronize bool `mapstructure:"notify-sync"` 45 | 46 | StartTime time.Time `mapstructure:"-"` 47 | } 48 | ) 49 | 50 | var Config = defaultConfig() 51 | 52 | func autoKey() string { 53 | name, err := os.Hostname() 54 | if err != nil || name == "" { 55 | name = commons.GetHost([]string{"docker0"}, []string{}) 56 | } 57 | return name 58 | } 59 | 60 | func defaultConfig() *sudisConfig { 61 | return &sudisConfig{ 62 | Key: autoKey(), 63 | Database: &Database{ 64 | Type: "sqlite3", Url: "sudis.db", 65 | }, 66 | Join: []string{}, 67 | MaxWaitTimeout: time.Second * 15, 68 | StartTime: time.Now(), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /webui/src/views/system/Systems.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 46 | -------------------------------------------------------------------------------- /nodes/notify/mail/message.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "gopkg.in/gomail.v2" 5 | "time" 6 | ) 7 | 8 | type Address struct { 9 | Address string 10 | Name string 11 | } 12 | 13 | func NewAddress(address, name string) *Address { 14 | return &Address{ 15 | Address: address, 16 | Name: name, 17 | } 18 | } 19 | 20 | type Body struct { 21 | ContentType string 22 | Body string 23 | Attachments []gomail.PartSetting 24 | } 25 | 26 | type Message struct { 27 | From *Address `mail:"From"` 28 | To []*Address `mail:"To"` //接受者 29 | Cc []*Address `mail:"Cc"` //抄送 30 | Bcc []*Address `mail:"Bcc"` //密送 31 | Subject string `mail:"Subject"` //标题 32 | Body *Body 33 | } 34 | 35 | func NewMessage(from, to, subject, body string) *Message { 36 | return &Message{ 37 | From: NewAddress(from, ""), 38 | To: []*Address{NewAddress(to, "")}, 39 | Subject: subject, 40 | Body: &Body{ 41 | ContentType: "text/html", 42 | Body: body, 43 | Attachments: nil, 44 | }, 45 | } 46 | } 47 | 48 | func set(m *gomail.Message, header string, address ...*Address) { 49 | if address != nil { 50 | gAddress := make([]string, len(address)) 51 | for i, address := range address { 52 | gAddress[i] = m.FormatAddress(address.Address, address.Name) 53 | } 54 | m.SetHeader(header, gAddress...) 55 | } 56 | } 57 | 58 | func (msg Message) Build() *gomail.Message { 59 | m := gomail.NewMessage() 60 | set(m, "From", msg.From) 61 | set(m, "To", msg.To...) 62 | set(m, "Cc", msg.Cc...) 63 | set(m, "Bcc", msg.Bcc...) 64 | m.SetDateHeader("X-Date", time.Now()) 65 | m.SetHeader("Subject", msg.Subject) 66 | if msg.Body != nil { 67 | m.SetBody(msg.Body.ContentType, msg.Body.Body, msg.Body.Attachments...) 68 | } 69 | return m 70 | } 71 | -------------------------------------------------------------------------------- /nodes/notify/mail/send.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/ihaiker/gokit/logs" 7 | "github.com/ihaiker/sudis/nodes/dao" 8 | "html/template" 9 | "os" 10 | ) 11 | 12 | var logger = logs.GetLogger("email") 13 | 14 | func loadEmailServer() (server *MailServer, config *dao.JSON, err error) { 15 | var email *dao.Notify 16 | var has bool 17 | if email, has, err = dao.NotifyDao.Get("email"); err != nil || !has { 18 | err = os.ErrNotExist 19 | return 20 | } 21 | config = new(dao.JSON) 22 | if err = json.Unmarshal([]byte(email.Config), config); err != nil { 23 | return 24 | } 25 | server = NewServer( 26 | config.String("address"), config.Int("port", 465), 27 | config.String("name"), config.String("passwd"), 28 | ) 29 | err = server.Start() 30 | return 31 | } 32 | 33 | func SendEmail(data interface{}) { 34 | server, config, err := loadEmailServer() 35 | if err == os.ErrNotExist { 36 | return 37 | } else if err != nil { 38 | logger.Error("获取邮件设置错误:", err) 39 | return 40 | } 41 | defer func() { _ = server.Close() }() 42 | 43 | from := config.String("name") 44 | to := config.String("to") 45 | content := config.String("content") 46 | 47 | if message, err := render(data, content); err != nil { 48 | logger.Warn("邮件模板错误: ", err) 49 | } else { 50 | msg := NewMessage(from, to, "Sudis Notify", message) 51 | if err = server.Send(msg); err != nil { 52 | logger.Warn("发送邮件:", err) 53 | } 54 | } 55 | } 56 | 57 | func render(data interface{}, content string) (string, error) { 58 | out := bytes.NewBuffer([]byte{}) 59 | if t, err := template.New("master").Parse(content); err != nil { 60 | return "", err 61 | } else if err = t.Execute(out, data); err != nil { 62 | return "", err 63 | } 64 | return out.String(), nil 65 | } 66 | -------------------------------------------------------------------------------- /webui/src/tools/http.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import axios from 'axios' 3 | import main from '../main' 4 | 5 | axios.defaults.baseURL = process.env.VUE_APP_URL; 6 | axios.defaults.timeout = 15000; 7 | axios.defaults.withCredentials = true; 8 | axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'; 9 | 10 | // 添加请求拦截器 11 | axios.interceptors.request.use(function (config) { 12 | let token = localStorage.getItem('token'); 13 | if (token) { 14 | config.headers['Authorization'] = token; 15 | } 16 | return config 17 | }); 18 | 19 | //添加响应拦截器 20 | axios.interceptors.response.use((response) => { 21 | if (response.status === 200 && response.data) { 22 | return response.data 23 | } 24 | return response 25 | }, function (err) { 26 | if (err.response) { 27 | if (err.response.status === 401) { 28 | localStorage.removeItem("token"); 29 | if (err.response.data && err.response.data.redirect) { 30 | window.location.href = err.response.data.redirect; 31 | } else { 32 | main.$router.push({path: '/signin', replace: true}); 33 | } 34 | } else { 35 | return Promise.reject(err.response.data) 36 | } 37 | } else { 38 | return Promise.reject({e: err, message: err.message}) 39 | } 40 | }); 41 | 42 | let config = { 43 | transformRequest: [function (data) { 44 | let ret = ''; 45 | for (let it in data) { 46 | ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&' 47 | } 48 | return ret 49 | }], 50 | headers: { 51 | 'Content-Type': 'application/x-www-form-urlencoded' 52 | } 53 | }; 54 | 55 | export default { 56 | axios: axios, 57 | form: config 58 | } 59 | -------------------------------------------------------------------------------- /webui/src/main.js: -------------------------------------------------------------------------------- 1 | import 'core-js/es6/promise' 2 | import 'core-js/es6/string' 3 | import 'core-js/es7/array' 4 | 5 | // import Vue from 'vue' 6 | import BootstrapVue from 'bootstrap-vue' 7 | import App from './App' 8 | import router from './router' 9 | import mixins from "./tools/mixins" 10 | //全局插件组件 11 | import plugins from './tools/plugins' 12 | import http from "./tools/http" 13 | 14 | Vue.use(BootstrapVue); 15 | Vue.use(plugins); 16 | Vue.mixin(mixins); 17 | 18 | Vue.config.productionTip = false; 19 | 20 | Vue.prototype.$axios = http.axios; 21 | Vue.prototype.$form = http.form; 22 | 23 | 24 | import {Alert, Confirm} from 'vue-m-dialog'; 25 | 26 | //https://mengdu.github.io/m-dialog/example/index.html#example 27 | Alert.config({ 28 | "title": "提示?", "show-close": false, 29 | "confromButtonText": "确定", 30 | "confirmButtonClassName": "btn btn-info btn-block" 31 | }); 32 | Vue.prototype.$alert = Alert; 33 | 34 | Confirm.config({ 35 | "title": "确定?", "show-close": false, 36 | closeOnClickModal: false, closeOnPressEscape: false, showClose: false, 37 | cancelButtonText: "取消", cancelButtonClassName: "btn btn-light w-25", 38 | confromButtonText: "确定", confirmButtonClassName: "btn btn-info w-25", 39 | }); 40 | Vue.prototype.$confirm = Confirm; 41 | 42 | //https://github.com/bajian/vue-toast 43 | import Toast from 'vue-bajiantoast' 44 | import '@/assets/toast.css'; 45 | 46 | Vue.prototype.$toast = Toast; 47 | Toast.config({ 48 | duration: 3000, 49 | position: 'top right', showCloseBtn: true, 50 | }); 51 | 52 | 53 | 54 | import VueParticles from 'vue-particles' 55 | Vue.use(VueParticles); 56 | 57 | let vm = new Vue({ 58 | el: '#app', 59 | router, 60 | data: {loadingShow: false, loadingTitle: ""}, 61 | template: '', 62 | components: {App} 63 | }); 64 | 65 | export default vm 66 | -------------------------------------------------------------------------------- /webui/src/tools/mixins.js: -------------------------------------------------------------------------------- 1 | import utils from "./utils"; 2 | 3 | let mixins = { 4 | data: () => ({ 5 | gid: ('m' + Math.floor(Math.random() * 10000000000)), 6 | timers: {} 7 | }), 8 | methods: { 9 | gvid(prefix, subfix) { 10 | if (undefined !== prefix && undefined !== subfix) { 11 | return prefix + this.gid + subfix; 12 | } else if (prefix) { 13 | return this.gid + prefix; 14 | } else { 15 | return this.gid; 16 | } 17 | }, 18 | loading(isLoading, title) { 19 | this.$root.loadingShow = isLoading; 20 | this.$root.loadingTitle = title; 21 | }, 22 | startLoading(title) { 23 | this.loading(true, title || "loading......"); 24 | }, 25 | loaddingStatus(title) { 26 | this.$root.loadingTitle = title; 27 | }, 28 | finishLoading() { 29 | this.loading(false, ""); 30 | }, 31 | request(title, request) { 32 | this.startLoading(title); 33 | request.finally(this.finishLoading) 34 | }, 35 | ramShow(limit) { 36 | return utils.ram(limit) 37 | }, 38 | now() { 39 | return utils.now() 40 | }, 41 | twoJsonMerge(json1, json2) { 42 | var length1 = 0, length2 = 0, jsonStr, str; 43 | for (var ever in json1) length1++; 44 | for (var ever in json2) length2++; 45 | if (length1 && length2) str = ','; 46 | else str = ''; 47 | jsonStr = ((JSON.stringify(json1)).replace(/,}/, '}') + (JSON.stringify(json2)).replace(/,}/, '}')).replace(/}{/, str); 48 | return JSON.parse(jsonStr); 49 | }, 50 | 51 | } 52 | }; 53 | export default mixins; 54 | -------------------------------------------------------------------------------- /webui/src/views/nodes/ModifyTag.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /webui/src/assets/scss/_custom.scss: -------------------------------------------------------------------------------- 1 | // Here you can add other styles 2 | 3 | body { 4 | background-color: white; 5 | } 6 | 7 | .breadcrumb { 8 | margin-bottom: 0; 9 | } 10 | 11 | $btn-padding-x-xs: .20rem !default; 12 | $btn-padding-y-xs: .12rem !default; 13 | $input-btn-line-height-xs: 1.1 !default; 14 | 15 | .btn.btn-xs { 16 | // line-height: ensure proper height of button next to small input 17 | @include button-size($btn-padding-y-xs, $btn-padding-x-xs, $font-size-sm, $input-btn-line-height-xs, $btn-border-radius-sm); 18 | } 19 | 20 | .sidebar .nav-link.active { 21 | color: #fff; 22 | background: #226190; 23 | } 24 | 25 | .btn-default { 26 | color: #333; 27 | background-color: #fff; 28 | border-color: #ccc; 29 | } 30 | .btn-default:hover, 31 | .btn-default:focus, 32 | .btn-default:active, 33 | .btn-default.active, 34 | .open .dropdown-toggle.btn-default { 35 | color: #333; 36 | background-color: #ebebeb; 37 | border-color: #adadad; 38 | } 39 | .btn-default:active, 40 | .btn-default.active, 41 | .open .dropdown-toggle.btn-default { 42 | background-image: none; 43 | } 44 | .btn-default.disabled, 45 | .btn-default[disabled], fieldset[disabled] .btn-default, 46 | .btn-default.disabled:hover, 47 | .btn-default[disabled]:hover, fieldset[disabled] .btn-default:hover, 48 | .btn-default.disabled:focus, 49 | .btn-default[disabled]:focus, fieldset[disabled] .btn-default:focus, 50 | .btn-default.disabled:active, 51 | .btn-default[disabled]:active, fieldset[disabled] .btn-default:active, 52 | .btn-default.disabled.active, 53 | .btn-default[disabled].active, fieldset[disabled] .btn-default.active { 54 | background-color: #fff; 55 | border-color: #ccc; 56 | } 57 | .btn-default .badge { 58 | color: #fff; 59 | background-color: #333; 60 | } 61 | 62 | .bg-default { 63 | color: #333; 64 | background-color: #ebebeb !important; 65 | border-color: #adadad; 66 | } 67 | -------------------------------------------------------------------------------- /webui/src/router.js: -------------------------------------------------------------------------------- 1 | // import Vue from 'vue' 2 | // import VueRouter from 'vue-router' 3 | 4 | const AppContainer = () => import('@/containers/AppContainer'); 5 | const Login = () => import('@/views/Login'); 6 | 7 | const Dashboard = () => import('@/views/dashboard/Dashboard'); 8 | const Nodes = () => import('@/views/nodes/Nodes'); 9 | const Programs = () => import('@/views/program/ProgramList'); 10 | const ProgramInfo = () => import('@/views/program/ProgramInfo'); 11 | const ProgramLogger = () => import('@/views/program/ProgramLogger'); 12 | const Tags = () => import('@/views/tags/Tags'); 13 | const Users = () => import('@/views/users/Users'); 14 | const Systems = () => import('@/views/system/Systems'); 15 | const Logs = () => import('@/views/logs/Logs'); 16 | 17 | Vue.use(VueRouter); 18 | 19 | export default new VueRouter({ 20 | mode: 'hash', 21 | linkActiveClass: 'open active', 22 | scrollBehavior: () => ({y: 0}), 23 | routes: [ 24 | {path: "/signin", name: 'signin', component: Login}, 25 | { 26 | path: "/admin", component: AppContainer, 27 | children: [ 28 | {path: "", redirect: "dashboard"}, 29 | 30 | {path: "dashboard", component: Dashboard}, 31 | {path: "nodes", component: Nodes}, 32 | {path: "programs", component: Programs}, 33 | {path: "programs/:tag", component: Programs}, 34 | 35 | {path: "program", component: ProgramInfo}, 36 | {path: "program/logs", component: ProgramLogger}, 37 | 38 | {path: "tags", component: Tags}, 39 | {path: "users", component: Users}, 40 | {path: "systems", component: Systems}, 41 | {path: "logs", component: Logs}, 42 | 43 | 44 | {path: '*', redirect: 'dashboard'} 45 | ] 46 | }, 47 | {path: '*', redirect: '/admin'} 48 | ] 49 | }) 50 | -------------------------------------------------------------------------------- /libs/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "github.com/kataras/iris/v12" 6 | ) 7 | 8 | type Error struct { 9 | Status int `json:"-"` 10 | Code string `json:"error"` 11 | Message string `json:"message"` 12 | } 13 | 14 | func (err *Error) Error() string { 15 | return err.Code + ":" + err.Message 16 | } 17 | 18 | func New(format string, args ...interface{}) error { 19 | return NewError(iris.StatusInternalServerError, "InternalServerError", fmt.Sprintf(format, args...)) 20 | } 21 | 22 | func NewError(status int, code string, message string) *Error { 23 | return &Error{Status: status, Code: code, Message: message} 24 | } 25 | 26 | var ( 27 | ErrProgramNotFound = NewError(iris.StatusNotFound, "ErrProgramNotFound", "管托程序未发现") 28 | ErrProgramExists = NewError(iris.StatusNotImplemented, "ErrProgramExists", "托管程序已经存在") 29 | 30 | ErrProgramIsRunning = NewError(iris.StatusNotFound, "ErrProgramIsRunning", "管托程序正在运行") 31 | 32 | ErrNodeIsEmpty = NewError(iris.StatusBadRequest, "NodeIsEmpty", "Node不能为空") 33 | ErrTagEmpty = NewError(iris.StatusBadRequest, "TagIsEmpty", "Tag不能为空") 34 | ErrNameEmpty = NewError(iris.StatusBadRequest, "NameIsEmpty", "Name不能为空") 35 | ErrClassEmpty = NewError(iris.StatusBadRequest, "ClassEmpty", "class不能为空") 36 | ErrUser = NewError(iris.StatusBadRequest, "UserError", "用户不存在或者密码不正确") 37 | 38 | ErrNotFound = NewError(iris.StatusBadRequest, "NotFound", "未发现") 39 | ErrNotFoundConfig = NewError(iris.StatusBadRequest, "NotFoundConfig", "配置未发现") 40 | 41 | ErrNodeNotFound = NewError(iris.StatusNotFound, "NodeNotFound", "节点未发现!") 42 | ErrNodeKeyExists = NewError(iris.StatusNotFound, "ErrNodeKeyExists", "节点主键已经存在,不可用!") 43 | 44 | ErrTimeout = NewError(iris.StatusInternalServerError, "ErrTimeout", "运行超时") 45 | 46 | ErrClientToken = NewError(iris.StatusUnauthorized, "ErrClientToken", "客户端Token错误") 47 | 48 | ErrToken = NewError(iris.StatusForbidden, "ErrToken", "错误的TOKEN") 49 | ) 50 | -------------------------------------------------------------------------------- /libs/ipapi/api.go: -------------------------------------------------------------------------------- 1 | package ipapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/ihaiker/gokit/errors" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | /* 12 | { 13 | "status": "success", 14 | "country": "China", 15 | "countryCode": "CN", 16 | "region": "BJ", 17 | "regionName": "Beijing", 18 | "city": "Beijing", 19 | "zip": "", 20 | "lat": 39.9949, 21 | "lon": 116.316, 22 | "timezone": "Asia/Shanghai", 23 | "isp": "IDC, China Telecommunications Corporation", 24 | "org": "", 25 | "as": "AS23724 IDC, China Telecommunications Corporation", 26 | "query": "220.181.38.148" 27 | } 28 | */ 29 | 30 | type IPInfo struct { 31 | Status string `json:"status"` 32 | Country string `json:"country"` 33 | CountryCode string `json:"countryCode"` 34 | Region string `json:"region"` 35 | RegionName string `json:"regionName"` 36 | City string `json:"city"` 37 | Zip string `json:"zip"` 38 | Lat float64 `json:"lat"` 39 | Lon float64 `json:"lon"` 40 | Timezone string `json:"timezone"` 41 | Isp string `json:"isp"` 42 | Org string `json:"org"` 43 | As string `json:"as"` 44 | Query string `json:"query"` 45 | } 46 | 47 | func (ip IPInfo) String() string { 48 | if ip.Status != "success" { 49 | return "" 50 | } 51 | return fmt.Sprintf("%s %s %s", ip.Country, ip.RegionName, ip.City) 52 | } 53 | 54 | func Get(ip string) IPInfo { 55 | info := IPInfo{ 56 | Status: "fail", 57 | } 58 | defer errors.Catch(func(re error) { 59 | info.Country = re.Error() 60 | }) 61 | request, err := http.NewRequest(http.MethodGet, "http://ip-api.com/json/"+ip, nil) 62 | errors.Assert(err, "make request") 63 | 64 | client := http.Client{Timeout: time.Second * 3} 65 | resp, err := client.Do(request) 66 | errors.Assert(err, "request") 67 | 68 | defer func() { _ = resp.Body.Close() }() 69 | errors.Assert(json.NewDecoder(resp.Body).Decode(&info), "request decoder") 70 | return info 71 | } 72 | -------------------------------------------------------------------------------- /webui/src/views/dashboard/SocialBoxChart.vue: -------------------------------------------------------------------------------- 1 | 59 | -------------------------------------------------------------------------------- /nodes/http/ctl.notify.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/ihaiker/gokit/errors" 6 | . "github.com/ihaiker/sudis/libs/errors" 7 | "github.com/ihaiker/sudis/nodes/dao" 8 | "github.com/ihaiker/sudis/nodes/notify/mail" 9 | "github.com/ihaiker/sudis/nodes/notify/webhook" 10 | "github.com/kataras/iris/v12" 11 | "time" 12 | ) 13 | 14 | type NotifyController struct{} 15 | 16 | func (self *NotifyController) get(ctx iris.Context) *dao.Notify { 17 | name := ctx.Params().GetString("name") 18 | notify, has, err := dao.NotifyDao.Get(name) 19 | errors.Assert(err) 20 | errors.True(has, ErrNotFoundConfig) 21 | return notify 22 | } 23 | 24 | func (self *NotifyController) delete(name string) int { 25 | errors.Assert(dao.NotifyDao.Remove(name), "删除通知配置异常") 26 | return iris.StatusNoContent 27 | } 28 | 29 | func (self *NotifyController) modity(ctx iris.Context) int { 30 | notify := new(dao.Notify) 31 | errors.Assert(ctx.ReadJSON(notify)) 32 | 33 | errors.Assert(dao.NotifyDao.Remove(notify.Name)) 34 | 35 | notify.CreateTime = time.Now() 36 | errors.Assert(dao.NotifyDao.Add(notify)) 37 | return iris.StatusNoContent 38 | } 39 | 40 | func (self *NotifyController) test(ctx iris.Context) int { 41 | 42 | nt := new(dao.Notify) 43 | errors.Assert(ctx.ReadJSON(nt)) 44 | 45 | config := new(dao.JSON) 46 | errors.Assert(json.Unmarshal([]byte(nt.Config), config)) 47 | 48 | if nt.Name == "email" { 49 | from := config.String("name") 50 | to := config.String("to") 51 | content := config.String("content") 52 | server := mail.NewServer( 53 | config.String("address"), config.Int("port", 465), 54 | from, config.String("passwd"), 55 | ) 56 | errors.Assert(server.Start()) 57 | defer server.Close() 58 | 59 | errors.Assert(server.Send(mail.NewMessage(from, to, "Sudis通知", content))) 60 | 61 | } else { 62 | 63 | errors.Assert(webhook.WebHook( 64 | config.String("address"), 65 | config.String("token"), 66 | []byte(config.String("content")), 67 | )) 68 | 69 | } 70 | return iris.StatusNoContent 71 | } 72 | -------------------------------------------------------------------------------- /daemon/process_logger.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/concurrent/atomic" 5 | "github.com/ihaiker/gokit/errors" 6 | "github.com/ihaiker/gokit/logs" 7 | "io" 8 | ) 9 | 10 | const LOGGER_LINES_NUM = 100 11 | 12 | type TailLogger func(id, line string) 13 | 14 | type ProcessLogger struct { 15 | file io.Writer 16 | lines []string 17 | lineIdx *atomic.AtomicInt 18 | tails map[string]TailLogger 19 | } 20 | 21 | func NewLogger(loggerFile string) (logger *ProcessLogger, err error) { 22 | logger = new(ProcessLogger) 23 | if loggerFile != "" { 24 | if logger.file, err = logs.NewDailyRolling(loggerFile); err != nil { 25 | return nil, err 26 | } 27 | } 28 | logger.lineIdx = atomic.NewAtomicInt(0) 29 | logger.lines = make([]string, LOGGER_LINES_NUM) 30 | logger.tails = map[string]TailLogger{} 31 | return 32 | } 33 | 34 | func (self *ProcessLogger) Tail(id string, tail TailLogger, lines int) { 35 | logger.Debug("logger tail : ", id) 36 | self.tails[id] = tail 37 | if lines > LOGGER_LINES_NUM { 38 | lines = LOGGER_LINES_NUM 39 | } 40 | idx := self.lineIdx.Get() 41 | if idx < lines { 42 | idx = 0 43 | } else if idx < LOGGER_LINES_NUM { 44 | idx -= lines 45 | } 46 | for i := idx; i < idx+lines; i++ { 47 | line := self.lines[(idx+i)%LOGGER_LINES_NUM] 48 | if line != "" { 49 | tail(id, line) 50 | } 51 | } 52 | } 53 | 54 | func (self *ProcessLogger) CtrlC(id string) { 55 | logger.Debug("stop logger tail: ", id) 56 | delete(self.tails, id) 57 | } 58 | 59 | func (self *ProcessLogger) Write(p []byte) (int, error) { 60 | line := string(p) 61 | self.lines[self.lineIdx.GetAndIncrement(1)%LOGGER_LINES_NUM] = line 62 | for id, tail := range self.tails { 63 | errors.Try(func() { 64 | tail(id, line) 65 | }) 66 | } 67 | if self.file != nil { 68 | return self.file.Write(p) 69 | } 70 | return len(p), nil 71 | } 72 | 73 | func (self *ProcessLogger) Close() error { 74 | if self.file != nil { 75 | if closer, match := self.file.(io.Closer); match { 76 | return closer.Close() 77 | } 78 | } 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /webui/dist/static/js/chunk-f106c87c.4fd2d7d6.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-f106c87c"],{"29ed":function(t,e,n){"use strict";n.r(e);var s=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"h-100"},[n("VTitle",{attrs:{"title-class":"fa-file-text",title:"日志查看【"+t.name+"】"}}),n("div",{staticClass:"logger p-3"},t._l(t.lines,(function(e){return n("div",[t._v(t._s(e))])})),0)],1)},i=[],a=(n("f548"),n("cc57"),n("c0cf")),c={name:"ProgramLogger",components:{VTitle:a["a"]},data:function(){return{node:"",name:"",lines:[]}},mounted:function(){this.node=this.$route.query.node,this.name=this.$route.query.name,this.init()},destroyed:function(){this.socket.close()},methods:{getDomain:function(){var t="";if(void 0!==t&&""!==t)return t;var e=window.location.href.substr(0,window.location.href.indexOf("/",8));return e=e.replace("https://","wss://"),e=e.replace("http://","ws://"),e},init:function(){var t=this;if("undefined"===typeof WebSocket)alert("您的浏览器不支持socket");else{var e=this;this.socket=new WebSocket(e.getDomain()+"/admin/program/logs"),this.socket.onopen=function(){e.socket.send(JSON.stringify({name:t.name,node:t.node,user:localStorage.getItem("x-user"),ticket:localStorage.getItem("x-ticket")}))},this.socket.onmessage=this.getMessage}},getMessage:function(t){this.lines.push(t.data)}}},o=c,r=n("9ca4"),l=Object(r["a"])(o,s,i,!1,null,null,null);e["default"]=l.exports},c0cf:function(t,e,n){"use strict";var s=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("ol",{staticClass:"breadcrumb breadcrumb-fixed"},[n("li",{staticClass:"breadcrumb-item"},[n("i",{staticClass:"fa",class:t.titleClass}),t._v(" "+t._s(t.title)+"\n ")]),n("li",{staticClass:"ml-auto"},[t._t("default")],2)])},i=[],a={name:"vTitle",props:{title:String,titleClass:{type:String,default:""}}},c=a,o=n("9ca4"),r=Object(o["a"])(c,s,i,!1,null,null,null);e["a"]=r.exports},cc57:function(t,e,n){var s=n("064e").f,i=Function.prototype,a=/^\s*function ([^ (]*)/,c="name";c in i||n("149f")&&s(i,c,{configurable:!0,get:function(){try{return(""+this).match(a)[1]}catch(t){return""}}})}}]); 2 | //# sourceMappingURL=chunk-f106c87c.4fd2d7d6.js.map -------------------------------------------------------------------------------- /nodes/http/ctl.websocket.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/ihaiker/gokit/errors" 7 | "github.com/ihaiker/sudis/nodes/cluster" 8 | "github.com/ihaiker/sudis/nodes/dao" 9 | "github.com/kataras/iris/v12" 10 | "github.com/kataras/iris/v12/websocket" 11 | "github.com/kataras/neffos" 12 | "strings" 13 | ) 14 | 15 | type LoggerController struct { 16 | wsServer *neffos.Server 17 | manger *cluster.DaemonManager 18 | } 19 | 20 | func NewLoggerController(manger *cluster.DaemonManager) *LoggerController { 21 | connectIds := map[string][]string{} 22 | 23 | ws := websocket.New(websocket.DefaultGorillaUpgrader, websocket.Events{ 24 | websocket.OnNativeMessage: func(nsConn *websocket.NSConn, msg websocket.Message) error { 25 | params := &dao.JSON{} 26 | if err := json.Unmarshal(msg.Body, params); err != nil { 27 | return err 28 | } 29 | //token := params.String("token") 30 | name := params.String("name") 31 | node := params.String("node") 32 | line := params.Int("line", 30) 33 | uid := strings.ReplaceAll(nsConn.Conn.ID(), "-", "") 34 | logger.Debugf("订阅日志:%s,%s,%s ", name, node, uid) 35 | if err := manger.SubscribeLogger(uid, node, name, func(id, line string) { 36 | defer errors.Catch(func(err error) {}) 37 | //nsConn.Emit("", []byte(line)) 38 | fmt.Println(line) 39 | nsConn.Conn.Write(neffos.Message{Body: []byte(line)}) 40 | }, line); err != nil { 41 | //nsConn.Emit("", []byte(err.Error())) 42 | nsConn.Conn.Write(neffos.Message{Body: []byte(err.Error())}) 43 | } 44 | connectIds[uid] = []string{name, node} 45 | return nil 46 | }, 47 | }) 48 | ws.OnDisconnect = func(c *websocket.Conn) { 49 | uid := strings.ReplaceAll(c.ID(), "-", "") 50 | if nameAndNode, has := connectIds[uid]; has { 51 | name, node := nameAndNode[0], nameAndNode[1] 52 | logger.Debugf("取消订阅日志:%s,%s,%s ", name, node, uid) 53 | _ = manger.UnsubscribeLogger(uid, node, name) 54 | } 55 | } 56 | return &LoggerController{wsServer: ws, manger: manger} 57 | } 58 | 59 | func (self *LoggerController) Handler() iris.Handler { 60 | return websocket.Handler(self.wsServer) 61 | } 62 | -------------------------------------------------------------------------------- /webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sudis/admin", 3 | "version": "2.0.0", 4 | "description": "Sudis admin", 5 | "author": "haiker", 6 | "homepage": "http://shui.renzhen.la", 7 | "copyright": "Copyright 2018 haiker", 8 | "scripts": { 9 | "serve": "vue-cli-service serve", 10 | "build": "vue-cli-service build", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "@coreui/coreui": "^2.1.9", 15 | "@coreui/coreui-plugin-chartjs-custom-tooltips": "^1.3.1", 16 | "@coreui/icons": "0.3.0", 17 | "@coreui/vue": "^2.1.2", 18 | "axios": "^0.19.0", 19 | "bootstrap": "^4.3.1", 20 | "bootstrap-vue": "2.16.0", 21 | "chart.js": "^2.8.0", 22 | "core-js": "^2.6.5", 23 | "css-vars-ponyfill": "^1.17.2", 24 | "echarts": "^4.5.0", 25 | "font-awesome": "^4.7.0", 26 | "perfect-scrollbar": "^1.4.0", 27 | "simple-line-icons": "^2.4.1", 28 | "vue": "^2.6.10", 29 | "vue-bajiantoast": "^1.0.12", 30 | "vue-chartjs": "^3.4.2", 31 | "vue-echarts": "^4.1.0", 32 | "vue-m-dialog": "^1.2.1", 33 | "vue-multiselect": "^2.1.6", 34 | "vue-particles": "^1.0.9", 35 | "vue-perfect-scrollbar": "^0.1.0", 36 | "vue-router": "^3.0.2" 37 | }, 38 | "devDependencies": { 39 | "@vue/cli-plugin-babel": "^3.5.5", 40 | "@vue/cli-plugin-e2e-nightwatch": "^3.5.1", 41 | "@vue/cli-plugin-eslint": "^3.5.1", 42 | "@vue/cli-plugin-unit-jest": "^3.5.3", 43 | "@vue/cli-service": "^3.5.3", 44 | "@vue/test-utils": "^1.0.0-beta.29", 45 | "babel-core": "^7.0.0-bridge.0", 46 | "babel-jest": "^23.6.0", 47 | "growl": "^1.10.5", 48 | "https-proxy-agent": "^2.2.1", 49 | "node-sass": "^4.11.0", 50 | "sass-loader": "^7.1.0", 51 | "vue-template-compiler": "^2.6.10" 52 | }, 53 | "browserslist": [ 54 | "> 1%", 55 | "last 2 versions", 56 | "not ie <= 9" 57 | ], 58 | "engines": { 59 | "node": ">= 8.10.x", 60 | "npm": ">= 5.6.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /webui/src/plugins/XPage.vue: -------------------------------------------------------------------------------- 1 | 26 | 63 | -------------------------------------------------------------------------------- /cmds/initd/windows.go: -------------------------------------------------------------------------------- 1 | package initd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ihaiker/gokit/errors" 6 | "github.com/ihaiker/gokit/files" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "text/template" 12 | ) 13 | 14 | const windowCfgServer = ` 15 | 16 | {{.Id}} 17 | {{.Name}} 18 | {{.Description}} 19 | 20 | {{.WorkDir}} 21 | Normal 22 | 23 | {{.Start}} 24 | {{.StartArgs}} 25 | 26 | Automatic 27 | 28 | ` 29 | 30 | type WindowsServerConfig struct { 31 | Id string 32 | Name string 33 | Description string 34 | WorkDir string 35 | Start string 36 | StartArgs string 37 | } 38 | 39 | func windowsAutoStart() error { 40 | defer errors.Catch() 41 | //创建文件夹 42 | dir, _ := filepath.Abs("./conf") 43 | errors.Assert(writeConfig(dir)) 44 | 45 | workDir, err := filepath.Abs(filepath.Dir(os.Args[0])) 46 | errors.Assert(err) 47 | 48 | exePath, err := filepath.Abs(os.Args[0]) 49 | errors.Assert(err) 50 | 51 | data := &WindowsServerConfig{ 52 | Id: "sudis", Name: "sudis", 53 | Description: "The sudis endpoint", 54 | WorkDir: workDir, 55 | Start: exePath, StartArgs: "", 56 | } 57 | 58 | out, err := files.New(workDir + "/sudis-server.xml").GetWriter(false) 59 | errors.Assert(err) 60 | 61 | t, err := template.New("master").Parse(windowCfgServer) 62 | errors.Assert(err) 63 | errors.Assert(t.Execute(out, data)) 64 | 65 | fmt.Println("下载启动服务插件") 66 | serverExe := files.New(filepath.Join(workDir, "sudis-server.exe")) 67 | resp, err := http.Get("https://github.com/kohsuke/winsw/releases/download/winsw-v2.3.0/WinSW.NET4.exe") 68 | errors.Assert(err) 69 | 70 | defer func() { _ = resp.Body.Close() }() 71 | fw, err := serverExe.GetWriter(false) 72 | errors.Assert(err) 73 | 74 | defer func() { _ = fw.Close() }() 75 | _, err = io.Copy(fw, resp.Body) 76 | errors.Assert(err) 77 | 78 | err, outlines := runs(serverExe.GetPath(), "install") 79 | errors.Assert(err) 80 | fmt.Println(outlines) 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /webui/dist/index.html: -------------------------------------------------------------------------------- 1 | Sudis,分布式守护进程管理器
-------------------------------------------------------------------------------- /nodes/join/client.go: -------------------------------------------------------------------------------- 1 | package join 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "github.com/ihaiker/gokit/errors" 7 | "github.com/ihaiker/gokit/logs" 8 | "github.com/ihaiker/gokit/remoting" 9 | "github.com/ihaiker/gokit/remoting/rpc" 10 | "math" 11 | "time" 12 | ) 13 | 14 | var logger = logs.GetLogger("join") 15 | 16 | type joinClient struct { 17 | client rpc.RpcClient 18 | address, key, token string 19 | shutdown bool 20 | } 21 | 22 | func newClient(address, token, key string, onRpcMessage rpc.OnMessage) *joinClient { 23 | joinClient := &joinClient{ 24 | address: address, token: token, key: key, 25 | } 26 | joinClient.client = rpc.NewClient(address, onRpcMessage, joinClient.reconnect) 27 | return joinClient 28 | } 29 | 30 | func (self *joinClient) reconnect(channel remoting.Channel) { 31 | if self.shutdown { 32 | return 33 | } 34 | 35 | go func() { 36 | maxWaitSeconds := 5 * 60 37 | logger.Debug("尝试连接主控节点") 38 | for i := 0; !self.shutdown; i++ { 39 | _ = errors.Safe(self.client.Close) 40 | 41 | if err := self.Start(); err == nil { 42 | logger.Info("重连与TCP主控节点连接成功:", self.address) 43 | return 44 | } else { 45 | logger.Warn("重连主控节点异常:", err) 46 | } 47 | 48 | seconds := int(math.Pow(2, float64(i))) 49 | if seconds > maxWaitSeconds { 50 | seconds = maxWaitSeconds 51 | } 52 | time.Sleep(time.Second * time.Duration(seconds)) 53 | } 54 | }() 55 | } 56 | 57 | func (self *joinClient) Notify(req *rpc.Request) { 58 | self.client.Oneway(req, time.Second*5) 59 | } 60 | 61 | func (self *joinClient) authRequest() *rpc.Request { 62 | req := new(rpc.Request) 63 | req.URL = "auth" 64 | timestamp := time.Now().Format("20060102150405") 65 | req.Header("timestamp", timestamp) 66 | req.Header("key", self.key) 67 | code := fmt.Sprintf("%x", md5.Sum([]byte(timestamp+self.token+self.key))) 68 | req.Body = []byte(code) 69 | return req 70 | } 71 | 72 | func (self *joinClient) Start() (err error) { 73 | self.shutdown = false 74 | if err = self.client.Start(); err != nil { 75 | return 76 | } 77 | if resp := self.client.Send(self.authRequest(), time.Second*10); resp.Error != nil { 78 | err = resp.Error 79 | } 80 | return 81 | } 82 | 83 | func (self *joinClient) Stop() error { 84 | self.shutdown = true 85 | logger.Info("断开主控节点连接") 86 | return self.client.Close() 87 | } 88 | -------------------------------------------------------------------------------- /nodes/http/ctl.program.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/errors" 5 | "github.com/ihaiker/sudis/daemon" 6 | . "github.com/ihaiker/sudis/libs/errors" 7 | "github.com/ihaiker/sudis/nodes/cluster" 8 | "github.com/ihaiker/sudis/nodes/dao" 9 | "github.com/kataras/iris/v12" 10 | ) 11 | 12 | type ProgramController struct { 13 | clusterManger *cluster.DaemonManager 14 | } 15 | 16 | func (self *ProgramController) queryPrograms(ctx iris.Context) *dao.Page { 17 | name := ctx.URLParam("name") 18 | node := ctx.URLParam("node") 19 | tag := ctx.URLParam("tag") 20 | status := ctx.URLParam("status") 21 | page := ctx.URLParamIntDefault("page", 1) 22 | limit := ctx.URLParamIntDefault("limit", 12) 23 | return self.clusterManger.ListPrograms(name, node, tag, status, page, limit) 24 | } 25 | 26 | func (self *ProgramController) modifyProgramTag(json *dao.JSON) int { 27 | name := json.String("name") 28 | node := json.String("node") 29 | tag := json.String("tag") 30 | add := json.Int("add", 1) 31 | errors.True(tag != "", ErrTagEmpty) 32 | errors.Assert(self.clusterManger.ModifyProgramTag(name, node, tag, add == 1), "更新失败") 33 | return iris.StatusNoContent 34 | } 35 | 36 | func (self *ProgramController) addOrModifyProgram(ctx iris.Context) int { 37 | form := daemon.NewProgram() 38 | errors.Assert(ctx.ReadJSON(form)) 39 | 40 | //check 41 | errors.True(form.Node != "", "请选择您添加程序的节点") 42 | errors.True(form.Name != "", "程序名称不能为空!且必须是字母、数字下滑线组合") 43 | if !form.IsForeground() { 44 | errors.True(form.Stop != nil && form.Stop.Command != "", "后台运行程序必须提供停止命令") 45 | errors.True(form.Start.CheckHealth != nil && form.Start.CheckHealth.CheckAddress != "", "后台程序必须提供健康检查方式") 46 | } 47 | 48 | if _, err := self.clusterManger.GetProgram(form.Node, form.Name); err == nil { 49 | self.clusterManger.MustModifyProgram(form.Node, form.Name, form) 50 | } else { 51 | self.clusterManger.MustAddProgram(form.Node, form) 52 | } 53 | 54 | return iris.StatusNoContent 55 | } 56 | 57 | func (self *ProgramController) commandProgram(json *dao.JSON) int { 58 | name := json.String("name") 59 | node := json.String("node") 60 | command := json.String("command") 61 | logger.Debug("program command: ", name, " ", node, " ", command) 62 | self.clusterManger.MustCommand(node, name, command) 63 | logger.Debug("program command: ", name, " ", node, " ", command, " success") 64 | return iris.StatusNoContent 65 | } 66 | 67 | func (self *ProgramController) programDetail(ctx iris.Context) *daemon.Process { 68 | name := ctx.URLParam("name") 69 | node := ctx.URLParam("node") 70 | return self.clusterManger.MustGetProcess(node, name) 71 | } 72 | -------------------------------------------------------------------------------- /webui/src/views/program/ProgramLogger.vue: -------------------------------------------------------------------------------- 1 | 9 | 70 | -------------------------------------------------------------------------------- /nodes/http/ctl.dashboard.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/ihaiker/sudis/libs/errors" 8 | "github.com/ihaiker/sudis/nodes/cluster" 9 | "github.com/ihaiker/sudis/nodes/dao" 10 | "io/ioutil" 11 | "net/http" 12 | "time" 13 | ) 14 | 15 | type GithubRelease struct { 16 | HTMLURL string `json:"html_url"` 17 | TagName string `json:"tag_name"` 18 | Name string `json:"name"` 19 | } 20 | 21 | var release *GithubRelease 22 | var lastRefreshTime = time.Now() 23 | 24 | func getRelease() (*GithubRelease, error) { 25 | if release != nil && time.Now().Before(lastRefreshTime.Add(time.Hour)) { 26 | return release, nil 27 | } 28 | lastRefreshTime = time.Now() 29 | release = &GithubRelease{HTMLURL: "", TagName: "", Name: ""} 30 | 31 | if request, err := http.NewRequest("", "https://api.github.com/repos/ihaiker/sudis/releases/latest", nil); err != nil { 32 | return nil, err 33 | } else { 34 | client := &http.Client{ 35 | Transport: &http.Transport{ 36 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 37 | }, 38 | Timeout: time.Second * 5, 39 | } 40 | if response, err := client.Do(request); err != nil { 41 | return release, err 42 | } else if response.StatusCode != 200 { 43 | return release, errors.New(response.Status) 44 | } else { 45 | defer response.Body.Close() 46 | if bs, err := ioutil.ReadAll(response.Body); err != nil { 47 | return release, err 48 | } else { 49 | return release, json.Unmarshal(bs, release) 50 | } 51 | } 52 | } 53 | } 54 | 55 | func dashboard(manger *cluster.DaemonManager) interface{} { 56 | return func() *dao.JSON { 57 | release, _ := getRelease() 58 | 59 | nodeProcess := manger.CacheAll() 60 | 61 | all := 0 //所有陈谷 62 | started := 0 //已经启动的程序 63 | allNode := len(nodeProcess) //所有节点 64 | onlineNode := 0 //存活节点 65 | cpu := float64(0) //总共使用CUP量 66 | ram := uint64(0) //使用内存量 67 | 68 | for node, processes := range nodeProcess { 69 | if node.Status == dao.NodeStatusOnline { 70 | onlineNode += 1 71 | } 72 | all += len(processes) 73 | for _, process := range processes { 74 | if process.GetStatus().IsRunning() { 75 | cpu += process.Cpu 76 | ram += process.Rss 77 | started += 1 78 | } 79 | } 80 | } 81 | 82 | return &dao.JSON{ 83 | "node": &dao.JSON{ 84 | "all": allNode, 85 | "online": onlineNode, 86 | }, 87 | "process": &dao.JSON{ 88 | "all": all, 89 | "started": started, 90 | }, 91 | "info": &dao.JSON{ 92 | "CPU": fmt.Sprintf("%0.4f", cpu), 93 | "RAM": ram, 94 | }, 95 | "version": release, 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /webui/src/views/tags/Tags.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 72 | -------------------------------------------------------------------------------- /nodes/manager/join_server.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/ihaiker/gokit/logs" 8 | "github.com/ihaiker/gokit/remoting" 9 | "github.com/ihaiker/gokit/remoting/rpc" 10 | "github.com/ihaiker/sudis/daemon" 11 | "github.com/ihaiker/sudis/nodes/cluster" 12 | ) 13 | 14 | var logger = logs.GetLogger("manager") 15 | 16 | type joinServer struct { 17 | salt string 18 | rpc.RpcServer 19 | dm *cluster.DaemonManager 20 | } 21 | 22 | func NewJoinServer(address, salt string, dm *cluster.DaemonManager) *joinServer { 23 | server := &joinServer{salt: salt, dm: dm} 24 | server.RpcServer = rpc.NewServer( 25 | address, server.onMessage, server.onClientClosed, 26 | ) 27 | return server 28 | } 29 | 30 | func (self *joinServer) checkClientAuth(channel remoting.Channel, request *rpc.Request) *rpc.Response { 31 | if request.URL == "auth" { 32 | address := channel.GetRemoteAddress() 33 | timestamp, exits := request.GetHeader("timestamp") 34 | key, has := request.GetHeader("key") 35 | if exits && has { 36 | code := string(request.Body) 37 | daemonManger := NewManager(key, address, self) 38 | if err := self.dm.NodeJoin(key, address, timestamp, code, daemonManger); err == nil { 39 | channel.SetAttr("key", key) 40 | logger.Infof("node join key: %s, address: %s", key, address) 41 | return rpc.OK(channel, request) 42 | } else { 43 | logger.Warnf("node join key: %s, error: %s", key, err) 44 | return rpc.NewErrorResponse(request.ID(), err) 45 | } 46 | } 47 | return rpc.NewErrorResponse(request.ID(), errors.New("NoAuthHeader")) 48 | } 49 | 50 | if _, has := channel.GetAttr("key"); has { 51 | return nil 52 | } else { 53 | return rpc.NewErrorResponse(request.ID(), errors.New("ErrUnauthorized")) 54 | } 55 | } 56 | 57 | func (self *joinServer) onClientClosed(channel remoting.Channel) { 58 | if key, has := channel.GetAttr("key"); has { 59 | logger.Infof("node %s %s leave", key, channel.GetRemoteAddress()) 60 | self.dm.NodeLeave(fmt.Sprint(key), channel.GetRemoteAddress()) 61 | } 62 | } 63 | 64 | func (self *joinServer) onMessage(channel remoting.Channel, request *rpc.Request) *rpc.Response { 65 | if resp := self.checkClientAuth(channel, request); resp != nil { 66 | return resp 67 | } 68 | switch request.URL { 69 | case "program.status": 70 | event := new(daemon.FSMStatusEvent) 71 | if err := json.Unmarshal(request.Body, event); err != nil { 72 | logger.Warn("decoder event error ", err) 73 | } else { 74 | self.dm.OnStatusEvent(*event) 75 | } 76 | return rpc.OK(channel, request) 77 | case "tail.logger": 78 | id, _ := request.GetHeader("id") 79 | line := string(request.Body) 80 | self.dm.OnLogger(id, line) 81 | } 82 | return rpc.NewErrorResponse(request.ID(), rpc.ErrNotFount) 83 | } 84 | -------------------------------------------------------------------------------- /webui/dist/static/js/chunk-2a30e9d6.88f246a6.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2a30e9d6"],{a55b:function(t,s,a){"use strict";a.r(s);var e=function(){var t=this,s=t.$createElement,e=t._self._c||s;return e("div",{staticClass:"app flex-row align-items-center bg-dark"},[e("vue-particles",{staticClass:"position-absolute w-100 h-100",attrs:{linesColor:"#dedede",color:"#dedede"}}),e("div",{staticClass:"container"},[e("b-row",{staticClass:"justify-content-center"},[e("b-col",{attrs:{md:"8"}},[e("b-card-group",[e("b-card",{staticClass:"p-4",attrs:{"no-body":""}},[e("b-card-body",[e("b-form",[e("h1",{staticClass:"text-dark"},[t._v("Login")]),e("p",{staticClass:"text-muted"},[t._v("Sign In to your account")]),e("b-input-group",{staticClass:"mb-3"},[e("b-input-group-prepend",[e("b-input-group-text",[e("i",{staticClass:"icon-user"})])],1),e("b-form-input",{staticClass:"form-control",attrs:{type:"text",placeholder:"Username"},model:{value:t.name,callback:function(s){t.name=s},expression:"name"}})],1),e("b-input-group",{staticClass:"mb-4"},[e("b-input-group-prepend",[e("b-input-group-text",[e("i",{staticClass:"icon-lock"})])],1),e("b-form-input",{staticClass:"form-control",attrs:{type:"password",placeholder:"Password"},model:{value:t.passwd,callback:function(s){t.passwd=s},expression:"passwd"}})],1),e("b-row",[e("b-col",{attrs:{cols:"6"}},[e("b-button",{staticClass:"px-4 btn-block",attrs:{variant:"primary"},on:{click:t.login}},[t._v("Login\n ")])],1),e("b-col",{staticClass:"text-right",attrs:{cols:"6"}})],1)],1)],1)],1),e("b-card",{staticClass:"text-white bg-primary py-3 d-md-down-none",staticStyle:{width:"44%"}},[e("b-card-body",{staticClass:"text-center"},[e("div",[e("img",{staticStyle:{width:"3rem"},attrs:{src:a("dd88")}}),e("h4",[t._v("Sudis")]),e("p",[t._v("Distributed supervisor process control system .")]),e("a",{staticClass:"btn btn-primary active mt-3",attrs:{href:"https://github.com/ihaiker/sudis",target:"_blank"}},[t._v("Read More!")])])])],1)],1)],1)],1)],1)],1)},o=[],n=(a("cc57"),{name:"Login",data:function(){return{name:"",passwd:""}},mounted:function(){var t=localStorage.getItem("token");t&&this.$router.push("/admin/dashboard")},methods:{login:function(){var t=this;t.$axios.post("/login",{name:t.name,passwd:t.passwd}).then((function(s){var a=s.token;localStorage.setItem("token",a),t.$toast.success("登录成功!"),t.$router.push("/admin/dashboard")})).catch((function(s){t.$alert(s.message)}))}}}),r=n,i=a("9ca4"),c=Object(i["a"])(r,e,o,!1,null,null,null);s["default"]=c.exports},cc57:function(t,s,a){var e=a("064e").f,o=Function.prototype,n=/^\s*function ([^ (]*)/,r="name";r in o||a("149f")&&e(o,r,{configurable:!0,get:function(){try{return(""+this).match(n)[1]}catch(t){return""}}})},dd88:function(t,s,a){t.exports=a.p+"static/img/logo2.66d14f79.png"}}]); 2 | //# sourceMappingURL=chunk-2a30e9d6.88f246a6.js.map -------------------------------------------------------------------------------- /sudis.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ihaiker/gokit/errors" 6 | "github.com/ihaiker/gokit/files" 7 | "github.com/ihaiker/gokit/logs" 8 | "github.com/ihaiker/sudis/cmds/console" 9 | "github.com/ihaiker/sudis/cmds/initd" 10 | "github.com/ihaiker/sudis/cmds/node" 11 | "github.com/ihaiker/sudis/libs/config" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | "math/rand" 15 | "os" 16 | "path/filepath" 17 | "runtime" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | var ( 23 | VERSION = "v3.0.0" 24 | BUILD_TIME = "2012-12-12 12:12:12" 25 | GITLOG_VERSION = "0000" 26 | ) 27 | 28 | var rootCmd = &cobra.Command{ 29 | Use: filepath.Base(os.Args[0]), Version: fmt.Sprintf(" %s ", VERSION), 30 | Long: fmt.Sprintf(`SUDIS V3, 一个分布式进程控制程序。 31 | Build: %s, Go: %s, GitLog: %s`, BUILD_TIME, runtime.Version(), GITLOG_VERSION), 32 | } 33 | 34 | func init() { 35 | cobra.OnInitialize(func() { 36 | viper.SetEnvPrefix("SUDIS") 37 | 38 | dataPath := viper.GetString("data-path") 39 | if conf := viper.GetString("conf"); conf != "" { 40 | viper.SetConfigFile(conf) 41 | } else if dataPath != "" && files.IsExistFile(filepath.Join(dataPath, "sudis.conf")) { 42 | viper.SetConfigFile(filepath.Join(dataPath, "sudis.conf")) 43 | } else { 44 | viper.SetConfigName("sudis") 45 | viper.SetConfigType("yaml") 46 | for _, configPath := range []string{ 47 | "etc", "conf", "etc/sudis", 48 | "/etc/sudis", os.ExpandEnv("$HOME/.sudis"), 49 | } { 50 | viper.AddConfigPath(configPath) 51 | } 52 | } 53 | viper.AutomaticEnv() 54 | if err := viper.ReadInConfig(); err != nil { 55 | fmt.Println("read config error: ", err) 56 | } 57 | if err := viper.Unmarshal(config.Config); err != nil { 58 | fmt.Println("unmarshal error ", err) 59 | } 60 | if config.Config.DataPath == "" { 61 | if useConfig := viper.ConfigFileUsed(); useConfig != "" { 62 | config.Config.DataPath = filepath.Dir(useConfig) 63 | } else { 64 | config.Config.DataPath = "$HOME/.sudis" 65 | } 66 | } 67 | config.Config.DataPath = os.ExpandEnv(config.Config.DataPath) 68 | }) 69 | 70 | rootCmd.PersistentFlags().BoolP("debug", "d", config.Config.Debug, "Debug模式") 71 | 72 | rootCmd.AddCommand(node.NodeCommand) 73 | rootCmd.AddCommand(console.Commands...) 74 | rootCmd.AddCommand(console.ConsoleCommands) 75 | rootCmd.AddCommand(initd.Cmd) 76 | _ = viper.BindPFlags(rootCmd.PersistentFlags()) 77 | 78 | errors.StackFilter = func(frame runtime.Frame) bool { 79 | return strings.HasPrefix(frame.Function, "github.com/ihaiker/sudis") 80 | } 81 | } 82 | 83 | func main() { 84 | logs.Open("sudis") 85 | defer logs.CloseAll() 86 | rand.Seed(time.Now().UnixNano()) 87 | runtime.GOMAXPROCS(runtime.NumCPU()) 88 | node.SetDefaultCommand(rootCmd) 89 | if err := rootCmd.Execute(); err != nil { 90 | os.Exit(1) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /nodes/notify/server.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/concurrent/executors" 5 | "github.com/ihaiker/gokit/logs" 6 | "github.com/ihaiker/sudis/daemon" 7 | "github.com/ihaiker/sudis/nodes/cluster" 8 | "github.com/ihaiker/sudis/nodes/join" 9 | "github.com/ihaiker/sudis/nodes/notify/mail" 10 | "github.com/ihaiker/sudis/nodes/notify/webhook" 11 | ) 12 | 13 | var logger = logs.GetLogger("notity") 14 | 15 | type ( 16 | NotifyEventType string 17 | 18 | NotifyEvent struct { 19 | Type NotifyEventType `json:"type"` 20 | *cluster.NodeEvent `json:",omitempty"` 21 | *daemon.FSMStatusEvent `json:",omitempty"` 22 | } 23 | 24 | notifyServer struct { 25 | clusterManger *cluster.DaemonManager 26 | joinManager *join.ToJoinManager 27 | executor executors.ExecutorService 28 | } 29 | ) 30 | 31 | const ( 32 | Node NotifyEventType = "node" 33 | Process NotifyEventType = "process" 34 | ) 35 | 36 | func New(sync bool, clusterManger *cluster.DaemonManager, joinManager *join.ToJoinManager) *notifyServer { 37 | s := ¬ifyServer{ 38 | clusterManger: clusterManger, joinManager: joinManager, 39 | } 40 | if sync { 41 | s.executor = executors.Single(30 /*任务堆个数*/) 42 | } else { 43 | s.executor = executors.Fixed(5 /*工作人数*/, 30 /*任务对个数*/) 44 | } 45 | return s 46 | } 47 | 48 | func (self *notifyServer) Start() error { 49 | self.clusterManger.SetStatusListener(func(event daemon.FSMStatusEvent) { 50 | err := self.executor.Submit(func() { 51 | self.onProcessStatusEvent(event) 52 | }) 53 | if err != nil { 54 | logger.Warn("send process status notify error ", err) 55 | } 56 | }) 57 | self.clusterManger.SetNodeListener(func(event cluster.NodeEvent) { 58 | err := self.executor.Submit(func() { 59 | self.onNodeStatusEvent(event) 60 | }) 61 | if err != nil { 62 | logger.Warn("send node status notify error", err) 63 | } 64 | }) 65 | return nil 66 | } 67 | 68 | func (self *notifyServer) onProcessStatusEvent(event daemon.FSMStatusEvent) { 69 | //通知主控节点 70 | self.joinManager.OnProgramStatusEvent(event) 71 | 72 | //本地处理 73 | logger.Infof("node: %s, name: %s, from: %s, to: %s", 74 | event.Process.Node, event.Process.Name, event.FromStatus, event.ToStatus) 75 | 76 | self.notify(NotifyEvent{ 77 | Type: Process, 78 | FSMStatusEvent: &event, 79 | }) 80 | } 81 | 82 | func (self *notifyServer) onNodeStatusEvent(event cluster.NodeEvent) { 83 | logger.Infof("node: %s, status: %s", event.Node, event.Status) 84 | 85 | self.notify(NotifyEvent{ 86 | Type: Node, 87 | NodeEvent: &event, 88 | }) 89 | } 90 | 91 | func (self *notifyServer) notify(data interface{}) { 92 | webhook.SendWebhook(data) 93 | mail.SendEmail(data) 94 | } 95 | 96 | func (self *notifyServer) Stop() error { 97 | self.executor.Shutdown() 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /webui/src/plugins/loadings.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 112 | -------------------------------------------------------------------------------- /cmds/node/cmd.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "github.com/ihaiker/sudis/libs/config" 5 | "github.com/spf13/cobra" 6 | "github.com/spf13/viper" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | var NodeCommand = &cobra.Command{ 12 | Use: "node", Short: "节点启动", 13 | RunE: func(cmd *cobra.Command, args []string) error { 14 | return Start() 15 | }, 16 | } 17 | 18 | func init() { 19 | NodeCommand.PersistentFlags().StringP("conf", "f", "", "配置文件") 20 | 21 | NodeCommand.PersistentFlags().StringP("key", "", config.Config.Key, "节点唯一ID") 22 | NodeCommand.PersistentFlags().StringP("address", "", config.Config.Address, "API绑定地址") 23 | NodeCommand.PersistentFlags().BoolP("disable-webui", "", config.Config.DisableWebUI, "禁用webui") 24 | 25 | NodeCommand.PersistentFlags().StringP("data-path", "", config.Config.DataPath, "数据存储位置 (default: $HOME/.sudis)") 26 | NodeCommand.PersistentFlags().StringP("database.type", "", config.Config.Database.Type, "数据存储方式") 27 | NodeCommand.PersistentFlags().StringP("database.url", "", config.Config.Database.Url, "数据存储地址") 28 | 29 | NodeCommand.PersistentFlags().StringP("salt", "", config.Config.Salt, "安全加密盐值,如果设置了此值,所有节点加入管理默认将使用此值,若未设置节点将使用单独的设置") 30 | NodeCommand.PersistentFlags().String("manager", config.Config.Manager, "管理托管绑定地址") 31 | NodeCommand.PersistentFlags().StringSliceP("join", "", config.Config.Join, "托管连接地址") 32 | NodeCommand.PersistentFlags().DurationP("maxwait", "", config.Config.MaxWaitTimeout, "程序关闭最大等待时间") 33 | NodeCommand.PersistentFlags().BoolP("notify-sync", "", false, "事件通知是否同步通知。") 34 | 35 | _ = viper.BindPFlags(NodeCommand.PersistentFlags()) 36 | } 37 | 38 | func SetDefaultCommand(root *cobra.Command) { 39 | setDef := NodeCommand 40 | //set node is default command 41 | if runCommand, args, err := root.Find(os.Args[1:]); err == nil { 42 | if runCommand == root { 43 | root.InitDefaultHelpFlag() 44 | _ = root.ParseFlags(args) 45 | 46 | if help, err := root.Flags().GetBool("help"); err == nil && help { 47 | // show help 48 | } else { 49 | idx := 1 50 | for _, arg := range args { 51 | if strings.HasPrefix(arg, "-") { 52 | flagName := strings.TrimLeft(arg, "-") 53 | hasValue := strings.Index(flagName, "=") 54 | if hasValue > 0 { 55 | flagName = flagName[:hasValue] 56 | } 57 | if f := root.PersistentFlags().Lookup(flagName); f != nil { 58 | if f.Value.Type() == "bool" || hasValue > 0 { 59 | idx += 1 60 | } else if f.Value.String() != "" { 61 | idx += 2 62 | } 63 | continue 64 | } 65 | if len(flagName) == 1 { 66 | if f := root.PersistentFlags().ShorthandLookup(flagName); f != nil { 67 | if f.Value.Type() == "bool" || hasValue > 0 { 68 | idx += 1 69 | } else if f.Value.String() != "" { 70 | idx += 2 71 | } 72 | continue 73 | } 74 | } 75 | } 76 | break 77 | } 78 | root.SetArgs(append(os.Args[1:idx], append([]string{setDef.Name()}, os.Args[idx:]...)...)) 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /nodes/join/manager.go: -------------------------------------------------------------------------------- 1 | package join 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/ihaiker/gokit/errors" 7 | "github.com/ihaiker/gokit/remoting/rpc" 8 | "github.com/ihaiker/sudis/daemon" 9 | sudisError "github.com/ihaiker/sudis/libs/errors" 10 | "math" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type ToJoinManager struct { 16 | key, salt string 17 | joined map[string]*joinClient 18 | shutdown bool 19 | OnRpcMessage rpc.OnMessage 20 | } 21 | 22 | func New(key, salt string) *ToJoinManager { 23 | return &ToJoinManager{ 24 | key: key, salt: salt, 25 | joined: make(map[string]*joinClient), 26 | } 27 | } 28 | 29 | func (self *ToJoinManager) MustJoinIt(address, token string) { 30 | maxWaitSeconds := 5 * 60 31 | go func() { 32 | for i := 0; !self.shutdown; i++ { 33 | if err := self.Join(address, token); err == nil { 34 | return 35 | } else if strings.Contains(err.Error(), sudisError.ErrToken.Code) { 36 | logger.Warn("Token错误,忽略加入 ", address) 37 | return 38 | } 39 | seconds := int(math.Pow(2, float64(i))) 40 | if seconds > maxWaitSeconds { 41 | seconds = maxWaitSeconds 42 | } 43 | next := time.Second * time.Duration(seconds) 44 | logger.Debugf("%s 重试连接主控节点:%s", next.String(), address) 45 | time.Sleep(next) 46 | } 47 | }() 48 | } 49 | 50 | func (self *ToJoinManager) Join(address, token string) (err error) { 51 | //已经连接成功了,这里的操作是为了客户端连接主控节点异常后, 52 | //使用命令主动再次连接的判断,因为客户端使用了指数递增方式等待,所以后面的等待是时间将会很长 53 | if _, has := self.joined[address]; has { 54 | return 55 | } 56 | logger.Infof("连接主控节点:%s", address) 57 | client := newClient(address, token, self.key, self.OnRpcMessage) 58 | err = client.Start() 59 | if err != nil { 60 | logger.Warn("连接主控异常:", err) 61 | _ = errors.Safe(client.Stop) 62 | return 63 | } 64 | self.joined[address] = client 65 | return err 66 | } 67 | 68 | func (self *ToJoinManager) Leave(address ...string) error { 69 | if len(address) == 0 { 70 | for addr, _ := range self.joined { 71 | address = append(address, addr) 72 | } 73 | } 74 | for _, addr := range address { 75 | if cli, has := self.joined[addr]; has { 76 | logger.Infof("to leave join : %s", addr) 77 | if err := cli.Stop(); err != nil { 78 | return err 79 | } 80 | delete(self.joined, addr) 81 | } else { 82 | return fmt.Errorf("leave %s : not found", addr) 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | func (self *ToJoinManager) OnProgramStatusEvent(event daemon.FSMStatusEvent) { 89 | defer errors.Catch() 90 | request := &rpc.Request{URL: "program.status"} 91 | request.Body, _ = json.Marshal(&event) 92 | for _, client := range self.joined { 93 | client.Notify(request) 94 | } 95 | } 96 | 97 | func (self *ToJoinManager) Start() error { 98 | return nil 99 | } 100 | 101 | func (self *ToJoinManager) Stop() error { 102 | logger.Info("multi join stop") 103 | self.shutdown = true 104 | for _, client := range self.joined { 105 | _ = errors.Safe(client.Stop) 106 | } 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /webui/src/views/nodes/TokenForm.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 87 | -------------------------------------------------------------------------------- /cmds/node/node.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ihaiker/gokit/errors" 6 | "github.com/ihaiker/gokit/logs" 7 | "github.com/ihaiker/gokit/remoting/rpc" 8 | runtimeKit "github.com/ihaiker/gokit/runtime" 9 | "github.com/ihaiker/sudis/daemon" 10 | . "github.com/ihaiker/sudis/libs/config" 11 | "github.com/ihaiker/sudis/nodes/cluster" 12 | "github.com/ihaiker/sudis/nodes/command" 13 | "github.com/ihaiker/sudis/nodes/dao" 14 | "github.com/ihaiker/sudis/nodes/http" 15 | "github.com/ihaiker/sudis/nodes/join" 16 | "github.com/ihaiker/sudis/nodes/manager" 17 | "github.com/ihaiker/sudis/nodes/notify" 18 | "github.com/spf13/viper" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | ) 23 | 24 | func Start() (err error) { 25 | defer errors.Catch(func(re error) { 26 | err = re 27 | }) 28 | logs.SetDebugMode(Config.Debug) 29 | logs.Info("Using config file:", viper.ConfigFileUsed()) 30 | 31 | //启动数据库 32 | errors.Assert(dao.CreateEngine(Config.DataPath, Config.Database)) 33 | 34 | signal := runtimeKit.NewListener() 35 | 36 | clusterManger := makeDaemonManager(signal) 37 | 38 | //管理节点启动 39 | if Config.Manager != "" { 40 | signal.Add(manager.NewJoinServer(Config.Manager, Config.Salt, clusterManger)) 41 | } 42 | 43 | joinManager := join.New(Config.Key, Config.Salt) 44 | 45 | //open api and web ui 46 | if Config.Address != "" { 47 | signal.Add(http.NewHttpServer(Config.Address, Config.DisableWebUI, clusterManger, joinManager)) 48 | } 49 | 50 | makeSockConsoleListener(signal, clusterManger, joinManager) 51 | makeJoinManager(signal, joinManager) 52 | 53 | signal.Add(notify.New(Config.NotifySynchronize, clusterManger, joinManager)) 54 | 55 | return signal.WaitTimeout(Config.MaxWaitTimeout) 56 | } 57 | 58 | func makeDaemonManager(signal *runtimeKit.SignalListener) *cluster.DaemonManager { 59 | localDaemon := daemon.NewDaemonManager(filepath.Join(Config.DataPath, "programs"), Config.Key) 60 | clusterManger := cluster.NewDaemonManger(Config.Key, Config.Salt, localDaemon) 61 | signal.Add(clusterManger) 62 | return clusterManger 63 | } 64 | 65 | func makeSockConsoleListener(signal *runtimeKit.SignalListener, daemonManger *cluster.DaemonManager, joinManager *join.ToJoinManager) { 66 | sock, _ := filepath.Abs(filepath.Join(Config.DataPath, "sudis.sock")) 67 | _ = os.Remove(sock) 68 | sockAddress := fmt.Sprintf("unix:/%s", sock) 69 | joinManager.OnRpcMessage = command.MakeCommand(daemonManger, joinManager) 70 | signal.Add(rpc.NewServer(sockAddress, joinManager.OnRpcMessage, nil)) 71 | } 72 | 73 | func makeJoinManager(signal *runtimeKit.SignalListener, joinManager *join.ToJoinManager) { 74 | signal.Add(joinManager) 75 | signal.AddStart(func() error { 76 | for _, joinAttr := range Config.Join { 77 | addressAndToken := strings.SplitN(joinAttr, ",", 2) 78 | if len(addressAndToken) == 1 { 79 | if Config.Salt == "" { 80 | logs.Warnf("ignore to join %s. flag `--slat` and token is empty", joinAttr) 81 | continue 82 | } 83 | addressAndToken = append(addressAndToken, Config.Salt) 84 | } 85 | joinManager.MustJoinIt(addressAndToken[0], addressAndToken[1]) 86 | } 87 | return nil 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /nodes/dao/node.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "github.com/ihaiker/sudis/libs/errors" 5 | "github.com/ihaiker/sudis/libs/ipapi" 6 | ) 7 | 8 | type ( 9 | NodeStatus string 10 | 11 | Node struct { 12 | Tag string `json:"tag" xorm:"tag"` 13 | Key string `json:"key" xorm:"varchar(32) notnull pk 'key'"` 14 | Token string `json:"token" xorm:"token"` 15 | Ip string `json:"ip" xorm:"ip"` 16 | Address string `json:"address" xorm:"address"` 17 | ProgramNum int `json:"programNum" xorm:"programNum"` 18 | Status NodeStatus `json:"status" xorm:"-"` 19 | Time string `json:"time" xorm:"time"` 20 | } 21 | 22 | nodeDao struct{} 23 | ) 24 | 25 | const ( 26 | NodeStatusOnline NodeStatus = "online" 27 | NodeStatusOutline NodeStatus = "outline" 28 | ) 29 | 30 | func (self *nodeDao) List() (nodes []*Node, err error) { 31 | nodes = make([]*Node, 0) 32 | err = engine.Find(&nodes) 33 | return 34 | } 35 | 36 | //添加新节点token 37 | func (self *nodeDao) ModifyToken(key, token string) error { 38 | if _, has, err := self.Get(key); err != nil { 39 | return err 40 | } else if has { 41 | _, err := engine.Update(&Node{Token: token, Time: Timestamp()}, &Node{Key: key}) 42 | return err 43 | } else { 44 | _, err := engine.InsertOne(&Node{Key: key, Token: token, Time: Timestamp(), Status: NodeStatusOutline}) 45 | return err 46 | } 47 | } 48 | 49 | func (self *nodeDao) Join(ip, key string) error { 50 | if node, has, err := self.Get(key); err != nil { 51 | return err 52 | } else if !has { 53 | address := ipapi.Get(ip) 54 | node = &Node{Key: key, Ip: ip, Address: address.String(), Status: NodeStatusOnline, Time: Timestamp()} 55 | if _, err = engine.InsertOne(node); err != nil { 56 | return err 57 | } 58 | } else { 59 | if node.Address == "" || node.Ip != ip { 60 | node.Address = ipapi.Get(ip).String() 61 | } 62 | if _, err := engine.Update(&Node{Ip: ip, Address: node.Address, Time: Timestamp(), Status: NodeStatusOnline}, &Node{Key: key}); err != nil { 63 | return err 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | func (self *nodeDao) Remove(key string) error { 70 | _, err := engine.Delete(&Node{Key: key}) 71 | return err 72 | } 73 | 74 | func (self *nodeDao) Lost(ip, key string) error { 75 | if node, has, err := self.Get(key); err != nil { 76 | return err 77 | } else if has { 78 | node.Time = Timestamp() 79 | node.Status = NodeStatusOutline 80 | if _, err = engine.Update(node, &Node{Key: key}); err != nil { 81 | return err 82 | } 83 | } 84 | return nil 85 | } 86 | 87 | func (self *nodeDao) Get(key string) (node *Node, has bool, err error) { 88 | node = new(Node) 89 | has, err = engine.Where("key = ?", key).Limit(1).Get(node) 90 | return 91 | } 92 | 93 | func (self *nodeDao) ModifyTag(key, tag string) error { 94 | if node, has, err := self.Get(key); err != nil { 95 | return err 96 | } else if !has { 97 | return errors.ErrNotFound 98 | } else { 99 | node.Tag = tag 100 | if _, err = engine.Cols("tag").Update(&Node{Tag: node.Tag}, &Node{Key: key}); err != nil { 101 | return err 102 | } 103 | return nil 104 | } 105 | } 106 | 107 | var NodeDao = new(nodeDao) 108 | -------------------------------------------------------------------------------- /cmds/initd/linux.go: -------------------------------------------------------------------------------- 1 | package initd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/blang/semver" 7 | "github.com/ihaiker/gokit/errors" 8 | "github.com/ihaiker/gokit/files" 9 | "github.com/ihaiker/sudis/libs/config" 10 | "gopkg.in/yaml.v2" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "strings" 15 | ) 16 | 17 | const serviceContent = ` 18 | [Unit] 19 | Description=The sudis endpoint. 20 | After=network.target remote-fs.target nss-lookup.target 21 | 22 | [Service] 23 | WorkingDirectory=/opt/sudis 24 | ExecStart=/opt/sudis/sudis 25 | ExecStop=/bin/kill -2 $MAINPID 26 | KillSignal=SIGQUIT 27 | TimeoutStopSec=15 28 | KillMode=process 29 | PrivateTmp=true 30 | 31 | [Install] 32 | WantedBy=multi-user.target 33 | ` 34 | 35 | func gt26() bool { 36 | defer errors.Catch() 37 | 38 | v26 := semver.MustParse("2.6.0") 39 | if err, version := runs("uname", "-r"); err == nil { 40 | idx := strings.Index(version, "-") 41 | version = version[0:idx] 42 | if vCheck, err := semver.Parse(version); err == nil { 43 | return vCheck.GT(v26) 44 | } 45 | } 46 | return true 47 | } 48 | 49 | func mkdir(path string) error { 50 | fmt.Println("创建目录:", path) 51 | if files.IsExistDir(path) { 52 | return nil 53 | } 54 | if err := mkdir(filepath.Dir(path)); err != nil { 55 | return err 56 | } 57 | return os.Mkdir(path, 0700) 58 | } 59 | 60 | func runs(args ...string) (error, string) { 61 | 62 | fmt.Println("运行:", strings.Join(args, " ")) 63 | 64 | out := bytes.NewBuffer([]byte{}) 65 | cmd := exec.Command(args[0], args[1:]...) 66 | cmd.Stderr = out 67 | cmd.Stdout = out 68 | err := cmd.Run() 69 | return err, string(out.Bytes()) 70 | } 71 | 72 | func echo(content, path string) error { 73 | fmt.Println("输出内容到:", path) 74 | fmt.Println("<<----------------------------") 75 | fmt.Println(content) 76 | fmt.Println("---------------------------->>") 77 | service := files.New(path) 78 | if w, err := service.GetWriter(false); err != nil { 79 | return err 80 | } else if _, err = w.WriteString(content); err != nil { 81 | return err 82 | } 83 | return nil 84 | } 85 | 86 | func writeConfig(confDir string) error { 87 | configFile := filepath.Join(confDir, "sudis.yaml") 88 | cfg, _ := yaml.Marshal(config.Config) 89 | return echo(string(cfg), configFile) 90 | } 91 | 92 | func linuxGt26() (err error) { 93 | defer errors.Catch(func(re error) { err = re }) 94 | 95 | errors.Assert(mkdir("/etc/sudis/programs")) 96 | errors.Assert(writeConfig("/etc/sudis")) 97 | 98 | fileName := "/lib/systemd/system/sudis.service" 99 | errors.Assert(echo(serviceContent, fileName)) 100 | 101 | err, _ = runs("chmod", "+x", fileName) 102 | errors.Assert(err) 103 | 104 | from, _ := filepath.Abs(os.Args[0]) 105 | to := "/opt/sudis/sudis" 106 | if from != to { 107 | _, _ = runs("rm", "-f", to) 108 | err, _ = runs("cp", "-r", from, to) 109 | errors.Assert(err) 110 | err, _ = runs("chmod", "+x", to) 111 | errors.Assert(err) 112 | } 113 | 114 | //启动开机启动 115 | err, out := runs("systemctl", "enable", fileName) 116 | errors.Assert(err) 117 | fmt.Println(out) 118 | return 119 | } 120 | 121 | func linuxAutoStart() error { 122 | if gt26() { 123 | return linuxGt26() 124 | } else { 125 | return errors.New("not support") 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /cmds/console/console.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import "C" 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "github.com/ihaiker/gokit/logs" 8 | "github.com/ihaiker/gokit/remoting" 9 | "github.com/ihaiker/gokit/remoting/rpc" 10 | "github.com/ihaiker/sudis/libs/config" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | "path/filepath" 14 | "time" 15 | ) 16 | 17 | var logger = logs.GetLogger("console") 18 | 19 | var client rpc.RpcClient 20 | 21 | func onMessage(channel remoting.Channel, request *rpc.Request) *rpc.Response { 22 | if request.URL == "tail.logger" { 23 | fmt.Print(string(request.Body)) 24 | return nil 25 | } else { 26 | return rpc.OK(channel, request) 27 | } 28 | } 29 | 30 | func preRune(cmd *cobra.Command, args []string) (err error) { 31 | sockCfg := filepath.Join(config.Config.DataPath, "sudis.sock") 32 | if param := viper.GetString("sock"); param != "" { 33 | sockCfg = param 34 | } else if param, err := cmd.Flags().GetString("sock"); err == nil && param != "" { 35 | sockCfg = param 36 | } 37 | sock, _ := filepath.Abs(sockCfg) 38 | sock = "unix:/" + sock 39 | logger.Debug("连接服务端sock: ", sock) 40 | client = rpc.NewClient(sock, onMessage, nil) 41 | if err = client.Start(); err != nil { 42 | logger.Warn("连接服务错误: ", err) 43 | return 44 | } 45 | return nil 46 | } 47 | 48 | func runPost(cmd *cobra.Command, args []string) { 49 | _ = client.Close() 50 | } 51 | 52 | func makeRequest(cmd *cobra.Command, command string, body ...string) *rpc.Request { 53 | request := new(rpc.Request) 54 | request.URL = command 55 | if node := viper.GetString("node"); node != "" { 56 | request.Header("node", node) 57 | } else if node, err := cmd.Flags().GetString("node"); err == nil && node != "" { 58 | request.Header("node", node) 59 | } 60 | if len(body) > 0 { 61 | request.Body, _ = json.Marshal(body) 62 | } 63 | return request 64 | } 65 | 66 | func sendRequest(cmd *cobra.Command, request *rpc.Request, disablePrintln ...bool) *rpc.Response { 67 | seconds := viper.GetDuration("timeout") 68 | request.Header("timeout", fmt.Sprintf("%.0f", seconds.Seconds())) 69 | 70 | resp := client.Send(request, seconds) 71 | if len(disablePrintln) > 0 && disablePrintln[0] { 72 | // 73 | } else { 74 | if resp.Error != nil { 75 | fmt.Println(resp.Error) 76 | } else { 77 | fmt.Println(string(resp.Body)) 78 | } 79 | } 80 | return resp 81 | } 82 | 83 | var ConsoleCommands = &cobra.Command{ 84 | Use: "console", Short: "管理端命令", Long: "管理端命令", Aliases: []string{"cli"}, 85 | } 86 | 87 | var Commands = []*cobra.Command{ 88 | startCmd, statusCmd, stopCmd, 89 | listCmd, addCmd, deleteCmd, modifyCmd, 90 | detailCmd, tailCmd, tagCommand, 91 | joinCmd, leaveCmd, 92 | } 93 | 94 | func addFlags(cmd *cobra.Command) { 95 | cmd.PersistentFlags().StringP("sock", "s", "", "连接服务端sock地址.(default: ${data-path}/sudis.sock)") 96 | cmd.PersistentFlags().DurationP("timeout", "t", time.Second*15, "wait timeout") 97 | cmd.PersistentFlags().StringP("node", "", "", "执行的节点") 98 | } 99 | 100 | func init() { 101 | for _, command := range Commands { 102 | addFlags(command) 103 | ConsoleCommands.AddCommand(command) 104 | _ = viper.BindPFlags(command.PersistentFlags()) 105 | } 106 | _ = viper.BindPFlags(ConsoleCommands.PersistentFlags()) 107 | } 108 | -------------------------------------------------------------------------------- /daemon/program.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "encoding/json" 5 | "os/user" 6 | "syscall" 7 | "time" 8 | ) 9 | 10 | type CheckMode string 11 | 12 | const ( 13 | HTTP CheckMode = "http" 14 | HTTPS CheckMode = "https" 15 | TCP CheckMode = "tcp" 16 | ) 17 | 18 | type Tags []string 19 | 20 | func (tags *Tags) Add(tag string) { 21 | *tags = append(*tags, tag) 22 | } 23 | 24 | func (tags *Tags) Remove(tag string) { 25 | for i, t := range *tags { 26 | if t == tag { 27 | *tags = append((*tags)[:i], (*tags)[i+1:]...) 28 | break 29 | } 30 | } 31 | } 32 | 33 | type ( 34 | CheckHealth struct { 35 | CheckAddress string `json:"url"` 36 | CheckMode CheckMode `json:"type"` 37 | CheckTtl int `json:"ttl"` 38 | //访问安全token定义,这里面是要定义key=value的,这样兼容性更高一些 39 | SecretToken string `json:"securityKey,omitempty"` 40 | } 41 | 42 | //程序命令,(启动或停止) 43 | Command struct { 44 | //程序运行体 45 | Command string `json:"command,omitempty"` 46 | 47 | //启动参数 48 | Args []string `json:"args,omitempty"` 49 | 50 | //监控检车接口 51 | CheckHealth *CheckHealth `json:"health,omitempty"` 52 | } 53 | //监控程序 54 | Program struct { 55 | //程序唯一性ID,使用UUID方式 56 | Id uint64 `json:"id"` 57 | 58 | Node string `json:"node,omitempty"` 59 | 60 | Daemon string `json:"daemon"` 61 | 62 | //程序名称 63 | Name string `json:"name"` 64 | 65 | Description string `json:"description,omitempty"` 66 | 67 | //程序标签 68 | Tags Tags `json:"tags"` 69 | 70 | //工作目录 71 | WorkDir string `json:"workDir,omitempty"` 72 | 73 | //启动使用用户 74 | User string `json:"user,omitempty"` 75 | 76 | //环境参数变量 77 | Envs []string `json:"envs,omitempty"` 78 | 79 | //是不是守护程序,如果是需要提供启动和停止命令 前台程序 80 | Start *Command `json:"start"` 81 | 82 | //启动停止命令 83 | Stop *Command `json:"stop,omitempty"` 84 | 85 | //忽略,deamon类型的程序已经启动,也会直接加入管理 86 | IgnoreAlreadyStarted bool `json:"ignoreStarted,omitempty"` 87 | 88 | //是否自动启动 89 | AutoStart bool `json:"autoStart,omitempty"` 90 | 91 | //启动周期 92 | StartDuration int `json:"startDuration,omitempty"` 93 | 94 | //启动重试次数 95 | StartRetries int `json:"startRetries,omitempty"` 96 | 97 | StopSign syscall.Signal `json:"stopSign,omitempty"` 98 | 99 | //结束运行超时时间 100 | StopTimeout int `json:"stopTimeout,omitempty"` 101 | 102 | AddTime time.Time `json:"addTime,omitempty"` 103 | UpdateTime time.Time `json:"updateTime,omitempty"` 104 | 105 | //日志文件位置 106 | Logger string `json:"logger,omitempty"` 107 | } 108 | ) 109 | 110 | func (this *Program) IsForeground() bool { 111 | return this.Daemon == "0" 112 | } 113 | 114 | func (this *Program) JSON() string { 115 | bs, _ := json.Marshal(this) 116 | return string(bs) 117 | } 118 | func (this *Program) JSONByte() []byte { 119 | bs, _ := json.Marshal(this) 120 | return bs 121 | } 122 | 123 | func NewProgram() *Program { 124 | currentUser, _ := user.Current() 125 | return &Program{ 126 | Daemon: "0", 127 | Tags: Tags{}, 128 | WorkDir: currentUser.HomeDir, 129 | User: currentUser.Username, 130 | AutoStart: false, 131 | StartDuration: 7, 132 | StartRetries: 3, 133 | Envs: []string{}, 134 | StopSign: syscall.SIGQUIT, 135 | StopTimeout: 7, 136 | AddTime: time.Now(), 137 | UpdateTime: time.Now(), 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /webui/src/views/tags/TagForm.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 82 | 83 | 86 | -------------------------------------------------------------------------------- /nodes/http/ctl.routers.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/ihaiker/gokit/errors" 5 | "github.com/ihaiker/sudis/libs/config" 6 | . "github.com/ihaiker/sudis/libs/errors" 7 | "github.com/ihaiker/sudis/nodes/cluster" 8 | "github.com/ihaiker/sudis/nodes/dao" 9 | "github.com/ihaiker/sudis/nodes/http/auth" 10 | "github.com/ihaiker/sudis/nodes/join" 11 | "github.com/kataras/iris/v12" 12 | "github.com/kataras/iris/v12/hero" 13 | "net" 14 | ) 15 | 16 | func Routers(app *iris.Application, clusterManger *cluster.DaemonManager, joinManager *join.ToJoinManager) { 17 | h := hero.New() 18 | h.Register(func(ctx iris.Context) *dao.JSON { 19 | data := &dao.JSON{} 20 | errors.Assert(ctx.ReadJSON(data)) 21 | return data 22 | }) 23 | 24 | authService := auth.NewService() 25 | app.Post("/login", h.Handler(authService.Login)) 26 | 27 | admin := app.Party("/admin", authService.Check) 28 | { 29 | admin.Get("/dashboard", h.Handler(dashboard(clusterManger))) 30 | 31 | node := admin.Party("/node") 32 | { 33 | ctl := &NodeController{clusterManger: clusterManger} 34 | node.Post("/token", h.Handler(ctl.addOrModifyNodeToken)) 35 | node.Get("/list", h.Handler(ctl.queryNodeList)) //列表 36 | node.Post("/tag", h.Handler(ctl.modifyNodeTag)) //打标签 37 | node.Delete("/{key}", h.Handler(ctl.removeNode)) 38 | } 39 | 40 | program := admin.Party("/program") 41 | { 42 | ctl := &ProgramController{clusterManger: clusterManger} 43 | program.Get("/list", h.Handler(ctl.queryPrograms)) 44 | program.Post("/tag", h.Handler(ctl.modifyProgramTag)) 45 | program.Post("/addOrModify", h.Handler(ctl.addOrModifyProgram)) 46 | program.Put("/command", h.Handler(ctl.commandProgram)) 47 | program.Get("/detail", h.Handler(ctl.programDetail)) 48 | } 49 | 50 | wsServer := NewLoggerController(clusterManger) 51 | app.Any("/admin/program/logs", wsServer.Handler()) 52 | 53 | tag := admin.Party("/tag") 54 | { 55 | ctl := TagsController{} 56 | tag.Get("/list", h.Handler(ctl.queryTag)) 57 | tag.Post("/addOrModify", h.Handler(ctl.addOrModify)) 58 | tag.Delete("/{name}", h.Handler(ctl.removeTag)) 59 | } 60 | 61 | user := admin.Party("/user") 62 | { 63 | userCtl := &UserController{} 64 | user.Get("/list", h.Handler(userCtl.queryUser)) 65 | user.Post("/add", h.Handler(userCtl.addUser)) 66 | user.Delete("/{name}", h.Handler(userCtl.deleteUser)) 67 | user.Post("/passwd", h.Handler(userCtl.modifyPasswd)) 68 | } 69 | 70 | notify := admin.Party("/notify") 71 | { 72 | ctl := new(NotifyController) 73 | notify.Get("/{name}", h.Handler(ctl.get)) 74 | notify.Post("/test", h.Handler(ctl.test)) 75 | notify.Post("", h.Handler(ctl.modity)) 76 | notify.Delete("/{name}", h.Handler(ctl.delete)) 77 | } 78 | 79 | admin.Any("/join", h.Handler(func(ctx iris.Context) int { 80 | address := ctx.URLParamTrim("address") 81 | errors.True(address != "", "加入地址为空") 82 | _, _, err := net.SplitHostPort(address) 83 | errors.Assert(err, "加入地址错误") 84 | 85 | token := ctx.URLParamTrim("token") 86 | if token == "" { 87 | token = config.Config.Salt 88 | } 89 | errors.True(token == "", ErrToken.Error()) 90 | 91 | must := ctx.URLParamTrim("must") 92 | if must == "true" { 93 | joinManager.MustJoinIt(address, token) 94 | } else { 95 | errors.Assert(joinManager.Join(address, token)) 96 | } 97 | return iris.StatusNoContent 98 | })) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /webui/src/views/system/webhook.vue: -------------------------------------------------------------------------------- 1 |