├── .gitignore ├── docs └── 编排 │ ├── plugin │ ├── exec.md │ └── file.md │ ├── example │ ├── example.conf │ ├── index.html │ └── main.yaml │ └── README.md ├── run ├── wechat_QR.jpg ├── code ├── api │ ├── logging │ │ ├── errors.go │ │ ├── h_remove.go │ │ ├── build_config.go │ │ ├── h_stop.go │ │ ├── h_start.go │ │ ├── handler.go │ │ ├── report.go │ │ └── h_config.go │ ├── api.go │ ├── plugin │ │ ├── h_reload.go │ │ ├── h_uninstall.go │ │ ├── file.go │ │ ├── h_list.go │ │ ├── handler.go │ │ └── h_install.go │ ├── agent │ │ ├── h_exists.go │ │ ├── h_sniffer.go │ │ ├── stop.go │ │ ├── h_start.go │ │ ├── h_uninstall.go │ │ ├── h_restart.go │ │ ├── h_install.go │ │ ├── service.go │ │ ├── h_upgrade.go │ │ ├── h_config.go │ │ ├── handler.go │ │ └── report.go │ ├── layout │ │ ├── errors.go │ │ ├── task_test.go │ │ ├── def.go │ │ ├── h_status.go │ │ ├── handler.go │ │ ├── h_run.go │ │ ├── build.go │ │ ├── hi_exec.go │ │ ├── task.go │ │ ├── runner.go │ │ └── hi_file.go │ ├── install │ │ ├── h_status.go │ │ ├── h_run.go │ │ └── handler.go │ ├── host │ │ ├── h_info.go │ │ ├── h_search.go │ │ ├── h_list.go │ │ └── handler.go │ ├── file │ │ ├── h_upload_handle.go │ │ ├── utils.go │ │ ├── h_upload_from.go │ │ ├── h_ls.go │ │ ├── handler.go │ │ ├── h_download.go │ │ └── h_upload.go │ ├── server │ │ ├── h_info.go │ │ └── handler.go │ ├── cmd │ │ ├── h_pty.go │ │ ├── h_status.go │ │ ├── h_kill.go │ │ ├── h_ps.go │ │ ├── client.go │ │ ├── h_channel.go │ │ ├── h_run.go │ │ ├── handler.go │ │ ├── h_syncrun.go │ │ └── process.go │ ├── errors.go │ ├── scaffolding │ │ ├── handler.go │ │ └── h_foo.go │ └── hm │ │ ├── handler.go │ │ └── h_static.go ├── utils │ ├── recover.go │ ├── decrypt.go │ ├── md5.go │ ├── task.go │ ├── bytes.go │ ├── data.go │ └── version.go ├── sshcli │ ├── pty.go │ └── client.go ├── client │ ├── send_scaffolding.go │ ├── send_hm.go │ ├── send_install.go │ ├── send_exec.go │ ├── send_file.go │ ├── send_logging.go │ ├── clients.go │ └── client.go ├── agent │ ├── utils.go │ ├── uninstall.go │ ├── upgrade.go │ └── install.go ├── app │ ├── api.go │ ├── ws.go │ └── app.go ├── main.go └── conf │ ├── plugin.go │ └── conf.go ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── conf └── server.conf ├── Makefile ├── go.mod ├── README.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /release 3 | /.vscode 4 | /vendor -------------------------------------------------------------------------------- /docs/编排/plugin/exec.md: -------------------------------------------------------------------------------- 1 | # 执行插件 2 | 3 | `cmd`: 需执行的命令 4 | 5 | `output`: 输出结果保存到变量名称 -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ./build 3 | killall server 4 | ./bin/server -conf conf/server.conf -------------------------------------------------------------------------------- /wechat_QR.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkstack/smartagent-server/HEAD/wechat_QR.jpg -------------------------------------------------------------------------------- /docs/编排/example/example.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8090; 3 | root /var/www/example; 4 | } -------------------------------------------------------------------------------- /code/api/logging/errors.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import "errors" 4 | 5 | var errNoCollector = errors.New("no collector") 6 | -------------------------------------------------------------------------------- /code/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "time" 4 | 5 | const RequestTimeout = 10 * time.Second 6 | const TotalTasksLabel = "total_tasks" 7 | -------------------------------------------------------------------------------- /docs/编排/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello World 4 | 5 | 6 | Hello World 7 | 8 | -------------------------------------------------------------------------------- /code/utils/recover.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/lwch/logging" 4 | 5 | func Recover(key string) { 6 | if err := recover(); err != nil { 7 | logging.Error("%s: %v", key, err) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /code/api/logging/h_remove.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "server/code/client" 5 | 6 | "github.com/lwch/api" 7 | ) 8 | 9 | func (h *Handler) remove(clients *client.Clients, ctx *api.Context) { 10 | ctx.OK(nil) 11 | } 12 | -------------------------------------------------------------------------------- /code/api/plugin/h_reload.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "server/code/client" 5 | 6 | "github.com/lwch/api" 7 | ) 8 | 9 | func (h *Handler) reload(clients *client.Clients, ctx *api.Context) { 10 | h.cfg.LoadPlugin() 11 | ctx.OK(nil) 12 | } 13 | -------------------------------------------------------------------------------- /docs/编排/plugin/file.md: -------------------------------------------------------------------------------- 1 | # 文件插件 2 | 3 | ## 上传文件 4 | 5 | `action`: push 6 | 7 | `src`: 文件来源,相对路径为相对于压缩包内的路径,绝对路径为server所在服务器上的文件路径 8 | 9 | `dst`: 保存路径,绝对路径 10 | 11 | ## 下载文件 12 | 13 | `action`: pull 14 | 15 | `src`: 文件来源,绝对路径 16 | 17 | `dst`: 保存路径,绝对路径 -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - uses: jkstack/smartagent-build@1.18.5 -------------------------------------------------------------------------------- /code/sshcli/pty.go: -------------------------------------------------------------------------------- 1 | package sshcli 2 | 3 | type writer struct { 4 | data []byte 5 | } 6 | 7 | func (w *writer) Write(p []byte) (int, error) { 8 | w.data = append(w.data, p...) 9 | return len(p), nil 10 | } 11 | 12 | func (w writer) String() string { 13 | return string(w.data) 14 | } 15 | -------------------------------------------------------------------------------- /code/api/agent/h_exists.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "server/code/client" 5 | 6 | "github.com/lwch/api" 7 | ) 8 | 9 | func (h *Handler) exists(clients *client.Clients, ctx *api.Context) { 10 | id := ctx.XStr("id") 11 | if clients.Get(id) == nil { 12 | ctx.NotFound("id") 13 | } 14 | ctx.OK(nil) 15 | } 16 | -------------------------------------------------------------------------------- /code/api/layout/errors.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var errTimeout = errors.New("timeout") 9 | 10 | func errClientNotfound(id string) error { 11 | return fmt.Errorf("client not found: %s", id) 12 | } 13 | 14 | func errPluginNotInstalled(name string) error { 15 | return fmt.Errorf("plugin not installed: %s", name) 16 | } 17 | -------------------------------------------------------------------------------- /code/api/install/h_status.go: -------------------------------------------------------------------------------- 1 | package install 2 | 3 | import ( 4 | "server/code/client" 5 | 6 | "github.com/lwch/api" 7 | ) 8 | 9 | func (h *Handler) status(clients *client.Clients, ctx *api.Context) { 10 | taskID := ctx.XStr("task_id") 11 | 12 | h.RLock() 13 | info := h.data[taskID] 14 | h.RUnlock() 15 | 16 | if info == nil { 17 | ctx.NotFound("task") 18 | return 19 | } 20 | 21 | ctx.OK(*info) 22 | } 23 | -------------------------------------------------------------------------------- /code/api/host/h_info.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "server/code/client" 5 | 6 | "github.com/lwch/api" 7 | ) 8 | 9 | func (h *Handler) info(clients *client.Clients, ctx *api.Context) { 10 | id := ctx.XStr("id") 11 | 12 | cli := clients.Get(id) 13 | if cli == nil { 14 | ctx.NotFound("client") 15 | } 16 | 17 | ctx.OK(map[string]interface{}{ 18 | "hostname": cli.HostName(), 19 | "os": cli.OS(), 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: [ v*.*.* ] 6 | 7 | jobs: 8 | 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: build 15 | uses: jkstack/smartagent-build@1.18.5 16 | 17 | - name: release 18 | uses: jkstack/smartagent-release@master 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /code/api/host/h_search.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "server/code/client" 5 | 6 | "github.com/lwch/api" 7 | ) 8 | 9 | func (h *Handler) search(clients *client.Clients, ctx *api.Context) { 10 | keyword := ctx.XStr("keyword") 11 | 12 | var id string 13 | clients.Range(func(c *client.Client) bool { 14 | if c.IP() == keyword || c.Mac() == keyword { 15 | id = c.ID() 16 | return false 17 | } 18 | return true 19 | }) 20 | 21 | ctx.OK(id) 22 | } 23 | -------------------------------------------------------------------------------- /code/api/layout/task_test.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import "testing" 4 | 5 | func TestParseIf(t *testing.T) { 6 | var info taskInfo 7 | err := info.parseIf("1=1") 8 | if err != nil { 9 | t.Fatal(err) 10 | } 11 | err = info.parseIf("1 = 1") 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | err = info.parseIf("$a=$b") 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | err = info.parseIf("$a>=$b") 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /code/api/plugin/h_uninstall.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "server/code/client" 7 | 8 | "github.com/lwch/api" 9 | "github.com/lwch/runtime" 10 | ) 11 | 12 | func (h *Handler) uninstall(clients *client.Clients, ctx *api.Context) { 13 | name := ctx.XStr("name") 14 | version := ctx.XStr("version") 15 | 16 | runtime.Assert(os.RemoveAll(path.Join(h.cfg.PluginDir, name, version))) 17 | 18 | h.cfg.LoadPlugin() 19 | 20 | ctx.OK(version) 21 | } 22 | -------------------------------------------------------------------------------- /code/utils/decrypt.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/base64" 5 | "strings" 6 | 7 | "github.com/jkstack/anet" 8 | "github.com/lwch/runtime" 9 | ) 10 | 11 | func DecryptPass(pass string) string { 12 | if strings.HasPrefix(pass, "%1%") { 13 | raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(pass, "%1%")) 14 | runtime.Assert(err) 15 | dec, err := anet.Decrypt(raw) 16 | runtime.Assert(err) 17 | pass = string(dec) 18 | } 19 | return pass 20 | } 21 | -------------------------------------------------------------------------------- /conf/server.conf: -------------------------------------------------------------------------------- 1 | # 所有相对路径为相对于可执行文件上级目录的相对路径 2 | 3 | listen = 13081 # 监听端口号 4 | 5 | plugin_dir = /opt/smartagent-server/plugins # 插件保存目录 6 | cache_dir = /opt/smartagent-server/cache # 缓存目录 7 | cache_threshold = 80 # 缓存目录阈值,超过这个值时开始限流 8 | data_dir = /opt/smartagent-server/data # 数据存储目录 9 | log_dir = /opt/smartagent-server/logs # 日志保存路径 10 | log_size = 50M # 日志分割大小 11 | log_rotate = 7 # 日志保留份数 12 | 13 | # logging 14 | #logging_report = http://127.0.0.1:11011 # 日志上报服务地址 -------------------------------------------------------------------------------- /code/api/agent/h_sniffer.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "net" 5 | "server/code/client" 6 | 7 | "github.com/lwch/api" 8 | "github.com/lwch/runtime" 9 | ) 10 | 11 | func (h *Handler) sniffer(clients *client.Clients, ctx *api.Context) { 12 | addr := ctx.XStr("addr") 13 | remote, err := net.ResolveTCPAddr("tcp", addr) 14 | runtime.Assert(err) 15 | c, err := net.DialTCP("tcp", nil, remote) 16 | runtime.Assert(err) 17 | defer c.Close() 18 | ctx.OK(c.LocalAddr().(*net.TCPAddr).IP.String()) 19 | } 20 | -------------------------------------------------------------------------------- /code/client/send_scaffolding.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "server/code/conf" 5 | "server/code/utils" 6 | 7 | "github.com/jkstack/anet" 8 | ) 9 | 10 | func (cli *Client) SendFoo(p *conf.PluginInfo) (string, error) { 11 | id, err := utils.TaskID() 12 | if err != nil { 13 | return "", err 14 | } 15 | var msg anet.Msg 16 | msg.Type = anet.TypeFoo 17 | msg.TaskID = id 18 | msg.Plugin = fillPlugin(p) 19 | cli.Lock() 20 | cli.taskRead[id] = make(chan *anet.Msg) 21 | cli.Unlock() 22 | cli.chWrite <- &msg 23 | return id, nil 24 | } 25 | -------------------------------------------------------------------------------- /code/api/plugin/file.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "server/code/client" 5 | "strings" 6 | 7 | "github.com/lwch/api" 8 | "github.com/lwch/logging" 9 | ) 10 | 11 | func (h *Handler) serveFile(clients *client.Clients, ctx *api.Context) { 12 | uri := strings.TrimPrefix(ctx.URI(), "/file/plugin/") 13 | tmp := strings.SplitN(uri, "/", 2) 14 | logging.Info("download plugin %s, md5=%s", tmp[0], tmp[1]) 15 | info := h.cfg.PluginByMD5(tmp[0], tmp[1]) 16 | if info == nil { 17 | ctx.NotFound("plugin") 18 | } 19 | ctx.ServeFile(info.Dir) 20 | } 21 | -------------------------------------------------------------------------------- /code/client/send_hm.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "server/code/conf" 5 | "server/code/utils" 6 | 7 | "github.com/jkstack/anet" 8 | ) 9 | 10 | func (cli *Client) SendHMStatic(p *conf.PluginInfo) (string, error) { 11 | id, err := utils.TaskID() 12 | if err != nil { 13 | return "", err 14 | } 15 | var msg anet.Msg 16 | msg.Type = anet.TypeHMStaticReq 17 | msg.TaskID = id 18 | msg.Plugin = fillPlugin(p) 19 | cli.Lock() 20 | cli.taskRead[id] = make(chan *anet.Msg) 21 | cli.Unlock() 22 | cli.chWrite <- &msg 23 | return id, nil 24 | } 25 | -------------------------------------------------------------------------------- /code/utils/md5.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/md5" 5 | "io" 6 | "os" 7 | ) 8 | 9 | func MD5Checksum(dir string) ([md5.Size]byte, error) { 10 | var ret [md5.Size]byte 11 | f, err := os.Open(dir) 12 | if err != nil { 13 | return ret, err 14 | } 15 | defer f.Close() 16 | return MD5From(f) 17 | } 18 | 19 | func MD5From(r io.Reader) ([md5.Size]byte, error) { 20 | var ret [md5.Size]byte 21 | enc := md5.New() 22 | _, err := io.Copy(enc, r) 23 | if err != nil { 24 | return ret, err 25 | } 26 | copy(ret[:], enc.Sum(nil)) 27 | return ret, nil 28 | } 29 | -------------------------------------------------------------------------------- /code/api/file/h_upload_handle.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "server/code/client" 5 | "strings" 6 | 7 | "github.com/lwch/api" 8 | ) 9 | 10 | func (h *Handler) uploadHandle(clients *client.Clients, ctx *api.Context) { 11 | id := strings.TrimPrefix(ctx.URI(), "/file/upload/") 12 | h.RLock() 13 | cache := h.uploadCache[id] 14 | h.RUnlock() 15 | if cache == nil { 16 | ctx.HTTPNotFound("file") 17 | return 18 | } 19 | if ctx.Token() != cache.token { 20 | ctx.HTTPForbidden("access denied") 21 | return 22 | } 23 | ctx.ServeFile(cache.dir) 24 | h.RemoveUploadCache(id) 25 | } 26 | -------------------------------------------------------------------------------- /code/api/server/h_info.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "server/code/client" 7 | 8 | "github.com/lwch/api" 9 | ) 10 | 11 | func created(dir string) int64 { 12 | fi, err := os.Stat(path.Join(dir, ".version")) 13 | if err != nil { 14 | return 0 15 | } 16 | return fi.ModTime().Unix() 17 | } 18 | 19 | func (h *Handler) info(clients *client.Clients, ctx *api.Context) { 20 | ctx.OK(map[string]interface{}{ 21 | "clients": clients.Size(), 22 | "plugins": h.cfg.PluginCount(), 23 | "version": h.version, 24 | "created": created(h.cfg.WorkDir), 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /code/agent/utils.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "server/code/sshcli" 7 | "strings" 8 | ) 9 | 10 | func sudo(cli *sshcli.Client, cmd, pass string) (string, error) { 11 | str, err := cli.Do(context.Background(), "sudo -k", "") 12 | if err != nil { 13 | return fmt.Sprintf("重设sudo密码失败:%s", str), err 14 | } 15 | str, err = cli.Do(context.Background(), "sudo -S -b "+cmd+" && sleep 1", pass+"\n") 16 | if err != nil { 17 | return str, err 18 | } 19 | idx := strings.Index(str, "\r\n") 20 | if idx != -1 { 21 | str = str[idx+2:] 22 | } 23 | return str, err 24 | } 25 | -------------------------------------------------------------------------------- /code/api/cmd/h_pty.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io" 5 | "server/code/client" 6 | 7 | "github.com/lwch/api" 8 | ) 9 | 10 | func (h *Handler) pty(clients *client.Clients, ctx *api.Context) { 11 | id := ctx.XStr("id") 12 | pid := ctx.XInt("pid") 13 | 14 | cli := h.cliFrom(id) 15 | if cli == nil { 16 | ctx.HTTPNotFound("client") 17 | return 18 | } 19 | 20 | cli.RLock() 21 | p := cli.process[pid] 22 | cli.RUnlock() 23 | 24 | if p == nil { 25 | ctx.HTTPNotFound("process") 26 | } 27 | 28 | data, err := p.read() 29 | if err != nil && err != io.EOF { 30 | panic(err) 31 | } 32 | 33 | ctx.Body(data) 34 | } 35 | -------------------------------------------------------------------------------- /code/utils/task.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/lwch/runtime" 9 | ) 10 | 11 | var taskDate string 12 | var taskCnt uint32 13 | var taskLock sync.Mutex 14 | 15 | func TaskID() (string, error) { 16 | now := time.Now() 17 | taskLock.Lock() 18 | defer taskLock.Unlock() 19 | if now.Format("20060102") != taskDate { 20 | taskDate = now.Format("20060102") 21 | taskCnt = 0 22 | } 23 | rand, err := runtime.UUID(16, "0123456789abcdef") 24 | if err != nil { 25 | return "", err 26 | } 27 | taskCnt++ 28 | return fmt.Sprintf("%s-%05d-%s", now.Format("20060102"), taskCnt%99999, rand), nil 29 | } 30 | -------------------------------------------------------------------------------- /code/api/cmd/h_status.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "server/code/client" 5 | 6 | "github.com/lwch/api" 7 | ) 8 | 9 | func (h *Handler) status(clients *client.Clients, ctx *api.Context) { 10 | id := ctx.XStr("id") 11 | pid := ctx.XInt("pid") 12 | 13 | cli := h.cliFrom(id) 14 | if cli == nil { 15 | ctx.NotFound("client") 16 | } 17 | 18 | cli.RLock() 19 | p := cli.process[pid] 20 | cli.RUnlock() 21 | 22 | if p == nil { 23 | ctx.NotFound("process") 24 | } 25 | 26 | ctx.OK(map[string]interface{}{ 27 | "id": p.id, 28 | "channel": p.taskID, 29 | "created": p.created.Unix(), 30 | "running": p.running, 31 | "code": p.code, 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /code/api/agent/stop.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | "server/code/client" 6 | "server/code/sshcli" 7 | "server/code/utils" 8 | 9 | "github.com/lwch/api" 10 | ) 11 | 12 | func (h *Handler) stop(clients *client.Clients, ctx *api.Context) { 13 | addr := ctx.XStr("addr") 14 | user := ctx.XStr("user") 15 | pass := utils.DecryptPass(ctx.XStr("pass")) 16 | 17 | cli, err := sshcli.New(addr, user, pass) 18 | if err != nil { 19 | ctx.ERR(1, fmt.Sprintf("ssh连接失败:%v", err)) 20 | return 21 | } 22 | 23 | str, err := service(cli, pass, "smartagent", "stop") 24 | if err != nil { 25 | ctx.ERR(2, fmt.Sprintf("停止失败:%s", str)) 26 | return 27 | } 28 | 29 | ctx.OK(nil) 30 | 31 | } 32 | -------------------------------------------------------------------------------- /code/api/agent/h_start.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | "server/code/client" 6 | "server/code/sshcli" 7 | "server/code/utils" 8 | 9 | "github.com/lwch/api" 10 | ) 11 | 12 | func (h *Handler) start(clients *client.Clients, ctx *api.Context) { 13 | addr := ctx.XStr("addr") 14 | user := ctx.XStr("user") 15 | pass := utils.DecryptPass(ctx.XStr("pass")) 16 | 17 | cli, err := sshcli.New(addr, user, pass) 18 | if err != nil { 19 | ctx.ERR(1, fmt.Sprintf("ssh连接失败:%v", err)) 20 | return 21 | } 22 | 23 | str, err := service(cli, pass, "smartagent", "start") 24 | if err != nil { 25 | ctx.ERR(2, fmt.Sprintf("启动失败:%s", str)) 26 | return 27 | } 28 | 29 | ctx.OK(nil) 30 | 31 | } 32 | -------------------------------------------------------------------------------- /code/api/agent/h_uninstall.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | "server/code/agent" 6 | "server/code/client" 7 | "server/code/sshcli" 8 | "server/code/utils" 9 | 10 | "github.com/lwch/api" 11 | ) 12 | 13 | func (h *Handler) uninstall(clients *client.Clients, ctx *api.Context) { 14 | addr := ctx.XStr("addr") 15 | user := ctx.XStr("user") 16 | pass := utils.DecryptPass(ctx.XStr("pass")) 17 | 18 | cli, err := sshcli.New(addr, user, pass) 19 | if err != nil { 20 | ctx.ERR(1, fmt.Sprintf("ssh连接失败:%v", err)) 21 | return 22 | } 23 | 24 | version, err := agent.Uninstall(cli, pass) 25 | if err != nil { 26 | ctx.ERR(2, err.Error()) 27 | return 28 | } 29 | ctx.OK(version) 30 | } 31 | -------------------------------------------------------------------------------- /code/api/agent/h_restart.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | "server/code/client" 6 | "server/code/sshcli" 7 | "server/code/utils" 8 | 9 | "github.com/lwch/api" 10 | ) 11 | 12 | func (h *Handler) restart(clients *client.Clients, ctx *api.Context) { 13 | addr := ctx.XStr("addr") 14 | user := ctx.XStr("user") 15 | pass := utils.DecryptPass(ctx.XStr("pass")) 16 | 17 | cli, err := sshcli.New(addr, user, pass) 18 | if err != nil { 19 | ctx.ERR(1, fmt.Sprintf("ssh连接失败:%v", err)) 20 | return 21 | } 22 | 23 | str, err := service(cli, pass, "smartagent", "restart") 24 | if err != nil { 25 | ctx.ERR(2, fmt.Sprintf("重启失败:%s", str)) 26 | return 27 | } 28 | 29 | ctx.OK(nil) 30 | 31 | } 32 | -------------------------------------------------------------------------------- /code/utils/bytes.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/dustin/go-humanize" 4 | 5 | // Bytes yaml bytes 6 | type Bytes uint64 7 | 8 | // MarshalKV marshal bytes 9 | func (b Bytes) MarshalKV() (string, error) { 10 | return humanize.Bytes(uint64(b)), nil 11 | } 12 | 13 | // UnmarshalKV unmarshal bytes 14 | func (data *Bytes) UnmarshalKV(value string) error { 15 | n, err := humanize.ParseBytes(value) 16 | if err != nil { 17 | return err 18 | } 19 | *data = Bytes(n) 20 | return nil 21 | } 22 | 23 | // Bytes get bytes data 24 | func (data *Bytes) Bytes() uint64 { 25 | return uint64(*data) 26 | } 27 | 28 | // String format to string 29 | func (data Bytes) String() string { 30 | return humanize.Bytes(uint64(data)) 31 | } 32 | -------------------------------------------------------------------------------- /code/api/errors.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "fmt" 4 | 5 | type PluginNotInstalled string 6 | 7 | func (e PluginNotInstalled) Error() string { 8 | return fmt.Sprintf("plugin %s not installed", string(e)) 9 | } 10 | 11 | func PluginNotInstalledErr(name string) { 12 | panic(PluginNotInstalled(name)) 13 | } 14 | 15 | type BadParam string 16 | 17 | func (e BadParam) Error() string { 18 | return fmt.Sprintf("bad param: %s", string(e)) 19 | } 20 | 21 | func BadParamErr(param string) { 22 | panic(BadParam(param)) 23 | } 24 | 25 | type Notfound string 26 | 27 | func (e Notfound) Error() string { 28 | return fmt.Sprintf("not found: %s", string(e)) 29 | } 30 | 31 | func NotfoundErr(what string) { 32 | panic(Notfound(what)) 33 | } 34 | -------------------------------------------------------------------------------- /code/api/plugin/h_list.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "server/code/client" 7 | 8 | "github.com/lwch/api" 9 | "github.com/lwch/logging" 10 | ) 11 | 12 | func (h *Handler) list(clients *client.Clients, ctx *api.Context) { 13 | type item struct { 14 | Name string `json:"name"` 15 | Version string `json:"version"` 16 | Created int64 `json:"created"` 17 | } 18 | var list []item 19 | h.cfg.RangePlugin(func(name, version string) { 20 | dir := path.Join(h.cfg.PluginDir, name, version) 21 | logging.Info("plugin.dir=%s", dir) 22 | fi, _ := os.Stat(dir) 23 | list = append(list, item{ 24 | Name: name, 25 | Version: version, 26 | Created: fi.ModTime().Unix(), 27 | }) 28 | }) 29 | ctx.OK(list) 30 | } 31 | -------------------------------------------------------------------------------- /code/client/send_install.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "server/code/conf" 5 | "server/code/utils" 6 | 7 | "github.com/jkstack/anet" 8 | ) 9 | 10 | func (cli *Client) SendInstall(p *conf.PluginInfo, uri, url, dir string, 11 | timeout int, auth, user, pass string) (string, error) { 12 | id, err := utils.TaskID() 13 | if err != nil { 14 | return "", err 15 | } 16 | var msg anet.Msg 17 | msg.Type = anet.TypeInstallArgs 18 | msg.TaskID = id 19 | msg.Plugin = fillPlugin(p) 20 | msg.InstallArgs = &anet.InstallArgs{ 21 | URI: uri, 22 | URL: url, 23 | Dir: dir, 24 | Timeout: timeout, 25 | Auth: auth, 26 | User: user, 27 | Pass: pass, 28 | } 29 | cli.Lock() 30 | cli.taskRead[id] = make(chan *anet.Msg) 31 | cli.Unlock() 32 | cli.chWrite <- &msg 33 | return id, nil 34 | } 35 | -------------------------------------------------------------------------------- /code/api/layout/def.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | type Main struct { 4 | Name string `yaml:"name" json:"name"` 5 | Timeout uint `yaml:"timeout" json:"timeout"` 6 | Tasks []Task `yaml:"tasks" json:"tasks"` 7 | } 8 | 9 | type Task struct { 10 | // plugin info 11 | Exec `yaml:",inline" json:",inline"` 12 | File `yaml:",inline" json:",inline"` 13 | 14 | Name string `yaml:"name" json:"name"` 15 | Plugin string `yaml:"plugin" json:"plugin"` 16 | Auth string `yaml:"auth" json:"auth"` 17 | If string `yaml:"if" json:"if"` 18 | Timeout uint `yaml:"timeout" json:"timeout"` 19 | } 20 | 21 | type Exec struct { 22 | Cmd string `yaml:"cmd" json:"cmd"` 23 | Output string `yaml:"output" json:"output"` 24 | } 25 | 26 | type File struct { 27 | Action string `yaml:"action" json:"action"` 28 | Src string `yaml:"src" json:"src"` 29 | Dst string `yaml:"dst" json:"dst"` 30 | } 31 | -------------------------------------------------------------------------------- /code/agent/uninstall.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "server/code/sshcli" 7 | ) 8 | 9 | func Uninstall(cli *sshcli.Client, pass string) (string, error) { 10 | version, err := cli.Do(context.Background(), "cat "+DefaultInstallDir+"/.version", "") 11 | if err != nil { 12 | return "", fmt.Errorf("获取版本号失败:%v", err) 13 | } 14 | 15 | _, err = sudo(cli, "systemctl stop smartagent", pass) 16 | if err != nil { 17 | _, err = sudo(cli, "service smartagent stop", pass) 18 | } 19 | if err != nil { 20 | sudo(cli, "/etc/init.d/smartagent stop", pass) 21 | } 22 | 23 | sudo(cli, "systemctl disable smartagent", pass) 24 | sudo(cli, "update-rc.d smartagent remove", pass) 25 | 26 | sudo(cli, DefaultInstallDir+"/bin/smartagent -conf "+ 27 | DefaultInstallDir+"/conf/client.conf -action uninstall", pass) 28 | 29 | sudo(cli, "rm -fr "+DefaultInstallDir, pass) 30 | 31 | return version, nil 32 | } 33 | -------------------------------------------------------------------------------- /code/api/file/utils.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "server/code/utils" 7 | 8 | "github.com/jkstack/anet" 9 | ) 10 | 11 | const blockSize = 4096 12 | 13 | func fillFile(f *os.File, size uint64) error { 14 | left := size 15 | dummy := make([]byte, blockSize) 16 | for left > 0 { 17 | if left >= blockSize { 18 | _, err := f.Write(dummy) 19 | if err != nil { 20 | return err 21 | } 22 | left -= blockSize 23 | continue 24 | } 25 | dummy = make([]byte, left) 26 | n, err := f.Write(dummy) 27 | if err != nil { 28 | return err 29 | } 30 | left -= uint64(n) 31 | } 32 | return nil 33 | } 34 | 35 | func writeFile(f *os.File, data *anet.DownloadData) (int, error) { 36 | _, err := f.Seek(int64(data.Offset), io.SeekStart) 37 | if err != nil { 38 | return 0, err 39 | } 40 | dec, err := utils.DecodeData(data.Data) 41 | if err != nil { 42 | return 0, err 43 | } 44 | return f.Write(dec) 45 | } 46 | -------------------------------------------------------------------------------- /code/api/cmd/h_kill.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | lapi "server/code/api" 5 | "server/code/client" 6 | 7 | "github.com/lwch/api" 8 | "github.com/lwch/logging" 9 | ) 10 | 11 | func (h *Handler) kill(clients *client.Clients, ctx *api.Context) { 12 | id := ctx.XStr("id") 13 | pid := ctx.XInt("pid") 14 | 15 | cli := clients.Get(id) 16 | if cli == nil { 17 | ctx.NotFound("client") 18 | } 19 | 20 | runCli := h.cli(cli) 21 | if cli == nil { 22 | ctx.NotFound("client") 23 | } 24 | 25 | runCli.RLock() 26 | process := runCli.process[pid] 27 | runCli.RUnlock() 28 | 29 | if process == nil { 30 | ctx.NotFound("process") 31 | } 32 | 33 | p := h.cfg.GetPlugin("exec", cli.OS(), cli.Arch()) 34 | if p == nil { 35 | lapi.PluginNotInstalledErr("exec") 36 | } 37 | 38 | taskID := process.sendKill(p) 39 | 40 | h.stTotalTasks.Inc() 41 | 42 | logging.Info("kill [%d] on %s, task_id=%s, plugin.version=%s", pid, id, taskID, p.Version) 43 | 44 | ctx.OK(nil) 45 | } 46 | -------------------------------------------------------------------------------- /code/api/host/h_list.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "server/code/client" 5 | 6 | "github.com/lwch/api" 7 | ) 8 | 9 | func (h *Handler) list(clients *client.Clients, ctx *api.Context) { 10 | ids := make(map[string]bool) 11 | for _, id := range ctx.OCsv("ids", []string{}) { 12 | ids[id] = true 13 | } 14 | type cli struct { 15 | ID string `json:"id"` 16 | IP string `json:"ip"` 17 | MAC string `json:"mac"` 18 | OS string `json:"os"` 19 | Platform string `json:"platform"` 20 | Arch string `json:"arch"` 21 | Version string `json:"version"` 22 | } 23 | var list []cli 24 | clients.Range(func(c *client.Client) bool { 25 | if len(ids) > 0 && !ids[c.ID()] { 26 | return true 27 | } 28 | list = append(list, cli{ 29 | ID: c.ID(), 30 | IP: c.IP(), 31 | MAC: c.Mac(), 32 | OS: c.OS(), 33 | Platform: c.Platform(), 34 | Arch: c.Arch(), 35 | Version: c.Version(), 36 | }) 37 | return true 38 | }) 39 | ctx.OK(list) 40 | } 41 | -------------------------------------------------------------------------------- /code/api/scaffolding/handler.go: -------------------------------------------------------------------------------- 1 | package scaffolding 2 | 3 | import ( 4 | "server/code/client" 5 | "server/code/conf" 6 | "sync" 7 | 8 | "github.com/jkstack/anet" 9 | "github.com/jkstack/jkframe/stat" 10 | "github.com/lwch/api" 11 | ) 12 | 13 | // Handler server handler 14 | type Handler struct { 15 | sync.RWMutex 16 | cfg *conf.Configure 17 | } 18 | 19 | // New new cmd handler 20 | func New() *Handler { 21 | return &Handler{} 22 | } 23 | 24 | // Init init handler 25 | func (h *Handler) Init(cfg *conf.Configure, stats *stat.Mgr) { 26 | h.cfg = cfg 27 | } 28 | 29 | // HandleFuncs get handle functions 30 | func (h *Handler) HandleFuncs() map[string]func(*client.Clients, *api.Context) { 31 | return map[string]func(*client.Clients, *api.Context){ 32 | "/scaffolding/foo": h.foo, 33 | } 34 | } 35 | 36 | func (h *Handler) OnConnect(cli *client.Client) { 37 | } 38 | 39 | // OnClose agent on close 40 | func (h *Handler) OnClose(string) { 41 | } 42 | 43 | func (h *Handler) OnMessage(*client.Client, *anet.Msg) { 44 | } 45 | -------------------------------------------------------------------------------- /code/api/server/handler.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "server/code/client" 5 | "server/code/conf" 6 | 7 | "github.com/jkstack/anet" 8 | "github.com/jkstack/jkframe/stat" 9 | "github.com/lwch/api" 10 | ) 11 | 12 | // Handler server handler 13 | type Handler struct { 14 | cfg *conf.Configure 15 | version string 16 | } 17 | 18 | // New new cmd handler 19 | func New(version string) *Handler { 20 | return &Handler{version: version} 21 | } 22 | 23 | // Init init handler 24 | func (h *Handler) Init(cfg *conf.Configure, stats *stat.Mgr) { 25 | h.cfg = cfg 26 | } 27 | 28 | // HandleFuncs get handle functions 29 | func (h *Handler) HandleFuncs() map[string]func(*client.Clients, *api.Context) { 30 | return map[string]func(*client.Clients, *api.Context){ 31 | "/server/info": h.info, 32 | } 33 | } 34 | 35 | func (h *Handler) OnConnect(*client.Client) { 36 | } 37 | 38 | // OnClose agent on close 39 | func (h *Handler) OnClose(string) { 40 | } 41 | 42 | func (h *Handler) OnMessage(*client.Client, *anet.Msg) { 43 | } 44 | -------------------------------------------------------------------------------- /code/api/cmd/h_ps.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "server/code/client" 5 | "sort" 6 | "time" 7 | 8 | "github.com/lwch/api" 9 | ) 10 | 11 | func (h *Handler) ps(clients *client.Clients, ctx *api.Context) { 12 | id := ctx.XStr("id") 13 | 14 | type item struct { 15 | ID int `json:"id"` 16 | Channel string `json:"channel"` 17 | Name string `json:"name"` 18 | Start int64 `json:"start_time"` 19 | Up int64 `json:"up_time"` 20 | } 21 | 22 | cli := h.cliFrom(id) 23 | if cli == nil { 24 | ctx.OK([]item{}) 25 | return 26 | } 27 | 28 | var list []item 29 | 30 | cli.RLock() 31 | for _, p := range cli.process { 32 | if !p.running { 33 | continue 34 | } 35 | list = append(list, item{ 36 | ID: p.id, 37 | Channel: p.taskID, 38 | Name: p.cmd, 39 | Start: p.created.Unix(), 40 | Up: int64(time.Since(p.created).Seconds()), 41 | }) 42 | } 43 | cli.RUnlock() 44 | 45 | sort.Slice(list, func(i, j int) bool { 46 | return list[i].Start < list[j].Start 47 | }) 48 | 49 | ctx.OK(list) 50 | } 51 | -------------------------------------------------------------------------------- /code/api/host/handler.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "server/code/client" 5 | "server/code/conf" 6 | "sync" 7 | 8 | "github.com/jkstack/anet" 9 | "github.com/jkstack/jkframe/stat" 10 | "github.com/lwch/api" 11 | ) 12 | 13 | // Handler cmd handler 14 | type Handler struct { 15 | sync.RWMutex 16 | cfg *conf.Configure 17 | } 18 | 19 | // New new cmd handler 20 | func New() *Handler { 21 | return &Handler{} 22 | } 23 | 24 | // Init init handler 25 | func (h *Handler) Init(cfg *conf.Configure, stats *stat.Mgr) { 26 | h.cfg = cfg 27 | } 28 | 29 | // HandleFuncs get handle functions 30 | func (h *Handler) HandleFuncs() map[string]func(*client.Clients, *api.Context) { 31 | return map[string]func(*client.Clients, *api.Context){ 32 | "/host/list": h.list, 33 | "/host/info": h.info, 34 | "/host/search": h.search, 35 | } 36 | } 37 | 38 | func (h *Handler) OnConnect(*client.Client) { 39 | } 40 | 41 | // OnClose agent on close 42 | func (h *Handler) OnClose(string) { 43 | } 44 | 45 | func (h *Handler) OnMessage(*client.Client, *anet.Msg) { 46 | } 47 | -------------------------------------------------------------------------------- /code/api/plugin/handler.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "server/code/client" 5 | "server/code/conf" 6 | 7 | "github.com/jkstack/anet" 8 | "github.com/jkstack/jkframe/stat" 9 | "github.com/lwch/api" 10 | ) 11 | 12 | // Handler handler 13 | type Handler struct { 14 | cfg *conf.Configure 15 | } 16 | 17 | // New new handler 18 | func New() *Handler { 19 | return &Handler{} 20 | } 21 | 22 | // Init init handler 23 | func (h *Handler) Init(cfg *conf.Configure, stats *stat.Mgr) { 24 | h.cfg = cfg 25 | } 26 | 27 | // HandleFuncs get handle functions 28 | func (h *Handler) HandleFuncs() map[string]func(*client.Clients, *api.Context) { 29 | return map[string]func(*client.Clients, *api.Context){ 30 | "/file/plugin/": h.serveFile, 31 | "/plugin/reload": h.reload, 32 | "/plugin/install": h.install, 33 | "/plugin/list": h.list, 34 | "/plugin/uninstall": h.uninstall, 35 | } 36 | } 37 | 38 | func (h *Handler) OnConnect(*client.Client) { 39 | } 40 | 41 | // OnClose agent on close 42 | func (h *Handler) OnClose(string) { 43 | } 44 | 45 | func (h *Handler) OnMessage(*client.Client, *anet.Msg) { 46 | } 47 | -------------------------------------------------------------------------------- /code/api/agent/h_install.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "server/code/agent" 7 | "server/code/client" 8 | "server/code/sshcli" 9 | "server/code/utils" 10 | "strings" 11 | 12 | "github.com/lwch/api" 13 | "github.com/lwch/runtime" 14 | ) 15 | 16 | func (h *Handler) install(clients *client.Clients, ctx *api.Context) { 17 | addr := ctx.XStr("addr") 18 | user := ctx.XStr("user") 19 | pass := utils.DecryptPass(ctx.XStr("pass")) 20 | f, _, err := ctx.File("file") 21 | runtime.Assert(err) 22 | defer f.Close() 23 | 24 | cli, err := sshcli.New(addr, user, pass) 25 | if err != nil { 26 | ctx.ERR(1, fmt.Sprintf("ssh连接失败:%v", err)) 27 | return 28 | } 29 | defer cli.Close() 30 | 31 | data, err := io.ReadAll(f) 32 | if err != nil { 33 | ctx.ERR(3, fmt.Sprintf("文件上传失败:%v", err)) 34 | return 35 | } 36 | err = agent.Extract(cli, pass, data) 37 | switch { 38 | case err == nil: 39 | ctx.OK(nil) 40 | case err.Error() == "无法重复安装": 41 | ctx.ERR(2, err.Error()) 42 | case strings.Contains(err.Error(), "文件上传失败:"): 43 | ctx.ERR(3, err.Error()) 44 | default: 45 | ctx.ERR(4, err.Error()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /code/api/layout/h_status.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "server/code/client" 5 | 6 | "github.com/lwch/api" 7 | ) 8 | 9 | func (h *Handler) status(clients *client.Clients, ctx *api.Context) { 10 | taskID := ctx.XStr("task_id") 11 | 12 | h.RLock() 13 | r := h.runners[taskID] 14 | h.RUnlock() 15 | 16 | if r == nil { 17 | ctx.NotFound("task") 18 | return 19 | } 20 | 21 | var ret struct { 22 | Done bool `json:"done"` 23 | Created int64 `json:"created"` 24 | Finished int64 `json:"finished"` 25 | TotalCnt int `json:"total_count"` 26 | FinishedCnt int `json:"finished_count"` 27 | Nodes []status `json:"nodes"` 28 | } 29 | 30 | ret.Done = r.done 31 | ret.Created = r.created 32 | ret.Finished = r.finished 33 | ret.TotalCnt = len(r.hosts) 34 | 35 | var finished int 36 | for _, id := range r.hosts { 37 | r.RLock() 38 | st := r.nodes[id] 39 | r.RUnlock() 40 | if st == nil { 41 | continue 42 | } 43 | if st.Finished > 0 { 44 | finished++ 45 | } 46 | ret.Nodes = append(ret.Nodes, *st) 47 | } 48 | ret.FinishedCnt = finished 49 | 50 | ctx.OK(ret) 51 | } 52 | -------------------------------------------------------------------------------- /code/api/plugin/h_install.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "io" 7 | "os" 8 | "path" 9 | "server/code/client" 10 | 11 | "github.com/lwch/api" 12 | "github.com/lwch/runtime" 13 | ) 14 | 15 | func (h *Handler) install(clients *client.Clients, ctx *api.Context) { 16 | name := ctx.XStr("name") 17 | version := ctx.XStr("version") 18 | file, _, err := ctx.File("file") 19 | runtime.Assert(err) 20 | defer file.Close() 21 | dir := path.Join(h.cfg.PluginDir, name, version) 22 | runtime.Assert(os.MkdirAll(dir, 0755)) 23 | gr, err := gzip.NewReader(file) 24 | runtime.Assert(err) 25 | defer gr.Close() 26 | write := func(dir string, r io.Reader) { 27 | f, err := os.Create(dir) 28 | runtime.Assert(err) 29 | defer f.Close() 30 | _, err = io.Copy(f, r) 31 | runtime.Assert(err) 32 | } 33 | tr := tar.NewReader(gr) 34 | for { 35 | hdr, err := tr.Next() 36 | if err != nil { 37 | if err == io.EOF { 38 | break 39 | } 40 | panic(err) 41 | } 42 | if hdr.FileInfo().IsDir() { 43 | continue 44 | } 45 | write(path.Join(dir, hdr.Name), tr) 46 | } 47 | 48 | h.cfg.LoadPlugin() 49 | 50 | ctx.OK(nil) 51 | } 52 | -------------------------------------------------------------------------------- /code/api/scaffolding/h_foo.go: -------------------------------------------------------------------------------- 1 | package scaffolding 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | lapi "server/code/api" 7 | "server/code/client" 8 | "time" 9 | 10 | "github.com/jkstack/anet" 11 | "github.com/lwch/api" 12 | "github.com/lwch/runtime" 13 | ) 14 | 15 | func (h *Handler) foo(clients *client.Clients, ctx *api.Context) { 16 | id := ctx.XStr("id") 17 | 18 | cli := clients.Get(id) 19 | if cli == nil { 20 | ctx.NotFound("client") 21 | } 22 | 23 | p := h.cfg.GetPlugin("scaffolding", cli.OS(), cli.Arch()) 24 | if p == nil { 25 | lapi.PluginNotInstalledErr("scaffolding") 26 | } 27 | 28 | taskID, err := cli.SendFoo(p) 29 | runtime.Assert(err) 30 | 31 | defer cli.ChanClose(taskID) 32 | 33 | var msg *anet.Msg 34 | select { 35 | case msg = <-cli.ChanRead(taskID): 36 | case <-time.After(api.RequestTimeout): 37 | ctx.Timeout() 38 | } 39 | 40 | switch { 41 | case msg.Type == anet.TypeError: 42 | ctx.ERR(http.StatusServiceUnavailable, msg.ErrorMsg) 43 | return 44 | case msg.Type != anet.TypeBar: 45 | ctx.ERR(http.StatusInternalServerError, fmt.Sprintf("invalid message type: %d", msg.Type)) 46 | return 47 | } 48 | 49 | ctx.OK(nil) 50 | } 51 | -------------------------------------------------------------------------------- /code/api/hm/handler.go: -------------------------------------------------------------------------------- 1 | package hm 2 | 3 | import ( 4 | lapi "server/code/api" 5 | "server/code/client" 6 | "server/code/conf" 7 | "sync" 8 | 9 | "github.com/jkstack/anet" 10 | "github.com/jkstack/jkframe/stat" 11 | "github.com/lwch/api" 12 | ) 13 | 14 | // Handler cmd handler 15 | type Handler struct { 16 | sync.RWMutex 17 | cfg *conf.Configure 18 | stUsage *stat.Counter 19 | stTotalTasks *stat.Counter 20 | } 21 | 22 | // New new cmd handler 23 | func New() *Handler { 24 | return &Handler{} 25 | } 26 | 27 | // Init init handler 28 | func (h *Handler) Init(cfg *conf.Configure, stats *stat.Mgr) { 29 | h.cfg = cfg 30 | h.stUsage = stats.NewCounter("plugin_count_hm") 31 | h.stTotalTasks = stats.NewCounter(lapi.TotalTasksLabel) 32 | } 33 | 34 | // HandleFuncs get handle functions 35 | func (h *Handler) HandleFuncs() map[string]func(*client.Clients, *api.Context) { 36 | return map[string]func(*client.Clients, *api.Context){ 37 | "/hm/static": h.static, 38 | } 39 | } 40 | 41 | func (h *Handler) OnConnect(*client.Client) { 42 | } 43 | 44 | // OnClose agent on close 45 | func (h *Handler) OnClose(string) { 46 | } 47 | 48 | func (h *Handler) OnMessage(*client.Client, *anet.Msg) { 49 | } 50 | -------------------------------------------------------------------------------- /code/api/logging/build_config.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/lwch/api" 7 | ) 8 | 9 | type k8sConfig struct { 10 | Namespace string `json:"ns"` 11 | Names []string `json:"name"` 12 | Dir string `json:"dir"` 13 | Api string `json:"api"` 14 | Token string `json:"token"` 15 | } 16 | 17 | func (cfg *k8sConfig) build(ctx *api.Context) error { 18 | cfg.Namespace = ctx.XStr("ns") 19 | cfg.Names = ctx.XCsv("names") 20 | cfg.Dir = ctx.OStr("dir", "") 21 | cfg.Api = ctx.XStr("api") 22 | cfg.Token = ctx.XStr("token") 23 | sort.Strings(cfg.Names) 24 | return nil 25 | } 26 | 27 | type fileConfig struct { 28 | Dir string `json:"dir"` 29 | } 30 | 31 | func (cfg *fileConfig) build(ctx *api.Context) error { 32 | cfg.Dir = ctx.OStr("dir", "") 33 | return nil 34 | } 35 | 36 | type dockerConfig struct { 37 | ContainerName string `json:"ct_name"` 38 | ContainerTag string `json:"ct_tag"` 39 | Dir string `json:"dir"` 40 | } 41 | 42 | func (cfg *dockerConfig) build(ctx *api.Context) error { 43 | cfg.ContainerName = ctx.XStr("ct_name") 44 | cfg.ContainerTag = ctx.OStr("ct_tag", "") 45 | cfg.Dir = ctx.OStr("dir", "") 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /code/api/install/h_run.go: -------------------------------------------------------------------------------- 1 | package install 2 | 3 | import ( 4 | "net/http" 5 | lapi "server/code/api" 6 | "server/code/client" 7 | "time" 8 | 9 | "github.com/lwch/api" 10 | "github.com/lwch/runtime" 11 | ) 12 | 13 | func (h *Handler) run(clients *client.Clients, ctx *api.Context) { 14 | id := ctx.XStr("id") 15 | uri := ctx.OStr("uri", "") 16 | url := ctx.OStr("url", "") 17 | dir := ctx.OStr("dir", "") 18 | timeout := ctx.OInt("timeout", 3600) 19 | auth := ctx.OStr("auth", "") 20 | user := ctx.OStr("user", "") 21 | pass := ctx.OStr("pass", "") 22 | 23 | cli := clients.Get(id) 24 | if cli == nil { 25 | ctx.NotFound("client") 26 | } 27 | 28 | p := h.cfg.GetPlugin("install", cli.OS(), cli.Arch()) 29 | if p == nil { 30 | lapi.PluginNotInstalledErr("install") 31 | } 32 | 33 | if len(uri) == 0 && len(url) == 0 { 34 | ctx.ERR(http.StatusBadRequest, "missing url/uri param") 35 | return 36 | } 37 | 38 | taskID, err := cli.SendInstall(p, uri, url, dir, timeout, auth, user, pass) 39 | runtime.Assert(err) 40 | 41 | h.stUsage.Inc() 42 | h.stTotalTasks.Inc() 43 | 44 | info := &Info{updated: time.Now()} 45 | h.Lock() 46 | h.data[taskID] = info 47 | h.Unlock() 48 | go h.loop(cli, taskID) 49 | 50 | ctx.OK(taskID) 51 | } 52 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | 3 | OUTDIR=release 4 | 5 | VERSION=2.0.5 6 | TIMESTAMP=`date +%s` 7 | 8 | BRANCH=`git rev-parse --abbrev-ref HEAD` 9 | HASH=`git log -n1 --pretty=format:%h` 10 | REVERSION=`git log --oneline|wc -l|tr -d ' '` 11 | BUILD_TIME=`date +'%Y-%m-%d %H:%M:%S'` 12 | LDFLAGS="-X 'main.gitBranch=$(BRANCH)' \ 13 | -X 'main.gitHash=$(HASH)' \ 14 | -X 'main.gitReversion=$(REVERSION)' \ 15 | -X 'main.buildTime=$(BUILD_TIME)' \ 16 | -X 'main.version=$(VERSION)'" 17 | 18 | all: 19 | go mod vendor 20 | rm -fr $(OUTDIR)/$(VERSION) 21 | mkdir -p $(OUTDIR)/$(VERSION)/opt/smartagent-server/bin \ 22 | $(OUTDIR)/$(VERSION)/opt/smartagent-server/conf 23 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -mod vendor -ldflags $(LDFLAGS) \ 24 | -o $(OUTDIR)/$(VERSION)/opt/smartagent-server/bin/smartagent-server code/*.go 25 | cp conf/server.conf $(OUTDIR)/$(VERSION)/opt/smartagent-server/conf/server.conf 26 | echo $(VERSION) > $(OUTDIR)/$(VERSION)/opt/smartagent-server/.version 27 | cd $(OUTDIR)/$(VERSION) && fakeroot tar -czvf smartagent-server_$(VERSION).tar.gz \ 28 | --warning=no-file-changed opt 29 | rm -fr $(OUTDIR)/$(VERSION)/opt $(OUTDIR)/$(VERSION)/etc 30 | cp CHANGELOG.md $(OUTDIR)/CHANGELOG.md 31 | version: 32 | @echo $(VERSION) 33 | distclean: 34 | rm -fr $(OUTDIR) 35 | -------------------------------------------------------------------------------- /code/api/agent/service.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "server/code/sshcli" 7 | "strings" 8 | 9 | "github.com/lwch/logging" 10 | ) 11 | 12 | func service(cli *sshcli.Client, pass, name, action string) (string, error) { 13 | _, err := sudo(cli, "systemctl "+action+" "+name, pass) 14 | if err == nil { 15 | return "", nil 16 | } 17 | logging.Info("systemctl %s %s failed: %v", action, name, err) 18 | _, err = sudo(cli, "service "+name+" "+action, pass) 19 | if err == nil { 20 | return "", nil 21 | } 22 | logging.Info("service %s %s failed: %v", name, action, err) 23 | str, err := sudo(cli, "/etc/init.d/"+name+" "+action, pass) 24 | if err == nil { 25 | return "", nil 26 | } 27 | logging.Info("/etc/init.d/%s %s failed: %v", name, action, err) 28 | return str, err 29 | } 30 | 31 | func sudo(cli *sshcli.Client, cmd, pass string) (string, error) { 32 | str, err := cli.Do(context.Background(), "sudo -k", "") 33 | if err != nil { 34 | return fmt.Sprintf("重设sudo密码失败:%s", str), err 35 | } 36 | str, err = cli.Do(context.Background(), "sudo -S -b "+cmd+" && sleep 1", pass+"\n") 37 | if err != nil { 38 | return str, err 39 | } 40 | idx := strings.Index(str, "\r\n") 41 | if idx != -1 { 42 | str = str[idx+2:] 43 | } 44 | return str, err 45 | } 46 | -------------------------------------------------------------------------------- /code/utils/data.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | func EncodeData(data []byte) string { 14 | var str string 15 | if strings.Contains(http.DetectContentType(data), "text/plain") { 16 | var buf bytes.Buffer 17 | w := gzip.NewWriter(&buf) 18 | if _, err := w.Write(data); err == nil { 19 | w.Close() 20 | str = "$1$" + base64.StdEncoding.EncodeToString(buf.Bytes()) 21 | } 22 | } 23 | if len(str) == 0 { 24 | str = "$0$" + base64.StdEncoding.EncodeToString(data) 25 | } 26 | return str 27 | } 28 | 29 | func DecodeData(str string) ([]byte, error) { 30 | switch { 31 | case strings.HasPrefix(str, "$0$"): 32 | str := strings.TrimPrefix(str, "$0$") 33 | return base64.StdEncoding.DecodeString(str) 34 | case strings.HasPrefix(str, "$1$"): 35 | str := strings.TrimPrefix(str, "$1$") 36 | b64 := base64.NewDecoder(base64.StdEncoding, strings.NewReader(str)) 37 | r, err := gzip.NewReader(b64) 38 | if err != nil { 39 | return nil, err 40 | } 41 | var buf bytes.Buffer 42 | _, err = io.Copy(&buf, r) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return buf.Bytes(), nil 47 | default: 48 | return nil, fmt.Errorf("invalid data: %s", str) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /code/utils/version.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type Version struct { 11 | data [3]int 12 | } 13 | 14 | func ParseVersion(str string) (Version, error) { 15 | var ret Version 16 | tmp := strings.SplitN(str, ".", 3) 17 | if len(tmp) != 3 { 18 | return ret, errors.New("invalid version") 19 | } 20 | n, err := strconv.ParseInt(tmp[0], 10, 64) 21 | if err != nil { 22 | return ret, errors.New("invalid major version") 23 | } 24 | ret.data[0] = int(n) 25 | n, err = strconv.ParseInt(tmp[1], 10, 64) 26 | if err != nil { 27 | return ret, errors.New("invalid minor version") 28 | } 29 | ret.data[1] = int(n) 30 | n, err = strconv.ParseInt(tmp[2], 10, 64) 31 | if err != nil { 32 | return ret, errors.New("invalid patch version") 33 | } 34 | ret.data[2] = int(n) 35 | return ret, nil 36 | } 37 | 38 | func (v Version) Greater(v2 Version) bool { 39 | for i := 0; i < 3; i++ { 40 | if v.data[i] > v2.data[i] { 41 | return true 42 | } 43 | } 44 | return false 45 | } 46 | 47 | func (v Version) Equal(v2 Version) bool { 48 | for i := 0; i < 3; i++ { 49 | if v.data[i] != v2.data[i] { 50 | return false 51 | } 52 | } 53 | return true 54 | } 55 | 56 | func (v Version) String() string { 57 | return fmt.Sprintf("%d.%d.%d", v.data[0], v.data[1], v.data[2]) 58 | } 59 | -------------------------------------------------------------------------------- /code/api/cmd/client.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "server/code/client" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type cmdClient struct { 10 | sync.RWMutex 11 | cli *client.Client 12 | process map[int]*process // pid => process 13 | } 14 | 15 | func newClient(c *client.Client) *cmdClient { 16 | cli := &cmdClient{ 17 | cli: c, 18 | process: make(map[int]*process), 19 | } 20 | go cli.clear() 21 | return cli 22 | } 23 | 24 | func (cli *cmdClient) clear() { 25 | run := func() { 26 | var list []*process 27 | cli.Lock() 28 | for _, p := range cli.process { 29 | if time.Since(p.updated) > clearTimeout { 30 | list = append(list, p) 31 | } 32 | } 33 | cli.Unlock() 34 | 35 | for _, p := range list { 36 | p.close() 37 | cli.Lock() 38 | delete(cli.process, p.id) 39 | cli.Unlock() 40 | } 41 | } 42 | for { 43 | run() 44 | time.Sleep(time.Minute) 45 | } 46 | } 47 | 48 | func (cli *cmdClient) addProcess(p *process) { 49 | cli.Lock() 50 | cli.process[p.id] = p 51 | cli.Unlock() 52 | } 53 | 54 | func (cli *cmdClient) close() { 55 | list := make([]*process, 0, len(cli.process)) 56 | cli.RLock() 57 | for _, p := range cli.process { 58 | list = append(list, p) 59 | } 60 | cli.RUnlock() 61 | for _, p := range list { 62 | p.close() 63 | } 64 | cli.Lock() 65 | cli.process = make(map[int]*process) 66 | cli.Unlock() 67 | } 68 | -------------------------------------------------------------------------------- /code/api/agent/h_upgrade.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "server/code/agent" 7 | "server/code/client" 8 | "server/code/sshcli" 9 | "server/code/utils" 10 | "strings" 11 | 12 | "github.com/lwch/api" 13 | "github.com/lwch/runtime" 14 | ) 15 | 16 | func (h *Handler) upgrade(clients *client.Clients, ctx *api.Context) { 17 | addr := ctx.XStr("addr") 18 | user := ctx.XStr("user") 19 | pass := utils.DecryptPass(ctx.XStr("pass")) 20 | restart := ctx.XBool("restart") 21 | f, _, err := ctx.File("file") 22 | runtime.Assert(err) 23 | defer f.Close() 24 | 25 | cli, err := sshcli.New(addr, user, pass) 26 | runtime.Assert(err) 27 | defer cli.Close() 28 | 29 | data, err := io.ReadAll(f) 30 | if err != nil { 31 | ctx.ERR(1, fmt.Sprintf("文件上传失败:%v", err)) 32 | return 33 | } 34 | version, err := agent.Upgrade(cli, pass, data, restart) 35 | switch { 36 | case err == nil: 37 | ctx.OK(version) 38 | case strings.Contains(err.Error(), "文件上传失败:"): 39 | ctx.ERR(1, err.Error()) 40 | case strings.Contains(err.Error(), "安装包解压失败:"): 41 | ctx.ERR(2, err.Error()) 42 | case strings.Contains(err.Error(), "生成配置文件失败:"), 43 | strings.Contains(err.Error(), "移动配置文件失败:"): 44 | ctx.ERR(3, err.Error()) 45 | case strings.Contains(err.Error(), "修改安装目录失败:"), 46 | strings.Contains(err.Error(), "修改启动脚本中的安装目录失败:"): 47 | ctx.ERR(4, err.Error()) 48 | default: 49 | ctx.ERR(5, err.Error()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/编排/example/main.yaml: -------------------------------------------------------------------------------- 1 | name: deploy nginx 2 | timeout: 600 3 | tasks: 4 | - name: flow control 5 | plugin: exec 6 | cmd: echo "do even things" 7 | if: $EVEN = 1 8 | timeout: 1 9 | - name: flow control 10 | plugin: exec 11 | cmd: echo "do odd things" 12 | if: $ODD = 1 13 | - name: check nginx if installed 14 | plugin: exec 15 | cmd: apt list --installed 2>/dev/null | grep nginx | wc -l 16 | auth: su 17 | output: installed 18 | - name: stop nginx if installed 19 | plugin: exec 20 | cmd: service nginx stop 21 | auth: su 22 | if: $installed > 0 23 | - name: remove nginx if installed 24 | plugin: exec 25 | cmd: apt-get purge -y nginx 26 | auth: su 27 | if: $installed > 0 28 | - name: install nginx 29 | plugin: exec 30 | cmd: apt-get install -y nginx 31 | auth: su 32 | - name: start nginx 33 | plugin: exec 34 | cmd: service nginx start 35 | auth: su 36 | - name: add server 37 | plugin: file 38 | action: push 39 | src: example.conf 40 | dst: /etc/nginx/conf.d/example.conf 41 | auth: su 42 | - name: mkdir /var/www/example 43 | plugin: exec 44 | cmd: mkdir -p /var/www/example 45 | auth: su 46 | - name: add index.html 47 | plugin: file 48 | action: push 49 | src: index.html 50 | dst: /var/www/example/index.html 51 | auth: su 52 | - name: reload service 53 | plugin: exec 54 | cmd: service nginx reload 55 | auth: su -------------------------------------------------------------------------------- /code/client/send_exec.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/base64" 5 | "server/code/conf" 6 | "server/code/utils" 7 | 8 | "github.com/jkstack/anet" 9 | ) 10 | 11 | func (cli *Client) SendExec(p *conf.PluginInfo, cmd string, args []string, timeout int, 12 | auth, user, pass, workdir string, env []string, deferRM string) (string, error) { 13 | id, err := utils.TaskID() 14 | if err != nil { 15 | return "", err 16 | } 17 | if len(pass) > 0 { 18 | enc, err := anet.Encrypt([]byte(pass)) 19 | if err != nil { 20 | return "", err 21 | } 22 | pass = "$1$" + base64.StdEncoding.EncodeToString(enc) 23 | } 24 | var msg anet.Msg 25 | msg.Type = anet.TypeExec 26 | msg.TaskID = id 27 | msg.Plugin = fillPlugin(p) 28 | msg.Exec = &anet.ExecPayload{ 29 | Cmd: cmd, 30 | Args: args, 31 | Timeout: timeout, 32 | Auth: auth, 33 | User: user, 34 | Pass: pass, 35 | WorkDir: workdir, 36 | Env: env, 37 | DeferRM: deferRM, 38 | } 39 | ch := make(chan *anet.Msg, 10) 40 | cli.Lock() 41 | cli.taskRead[id] = ch 42 | cli.Unlock() 43 | cli.chWrite <- &msg 44 | return id, nil 45 | } 46 | 47 | func (cli *Client) SendKill(p *conf.PluginInfo, pid int) (string, error) { 48 | id, err := utils.TaskID() 49 | if err != nil { 50 | return "", err 51 | } 52 | var msg anet.Msg 53 | msg.Type = anet.TypeExecKill 54 | msg.TaskID = id 55 | msg.Plugin = fillPlugin(p) 56 | msg.ExecKill = &anet.ExecKill{ 57 | Pid: pid, 58 | } 59 | cli.chWrite <- &msg 60 | return id, nil 61 | } 62 | -------------------------------------------------------------------------------- /code/api/agent/h_config.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "server/code/agent" 5 | "server/code/client" 6 | "server/code/sshcli" 7 | "server/code/utils" 8 | "strings" 9 | 10 | "github.com/lwch/api" 11 | "github.com/lwch/runtime" 12 | ) 13 | 14 | func (h *Handler) config(clients *client.Clients, ctx *api.Context) { 15 | addr := ctx.XStr("addr") 16 | user := ctx.XStr("user") 17 | pass := ctx.XStr("pass") 18 | 19 | var cfg agent.Config 20 | cfg.ID = ctx.XStr("id") 21 | cfg.Server = ctx.XStr("server") 22 | cfg.User = ctx.OStr("owner", "nobody") 23 | cfg.PluginDir = ctx.OStr("plugin_dir", agent.DefaultInstallDir+"/plugin") 24 | cfg.LogDir = ctx.OStr("log_dir", agent.DefaultInstallDir+"/logs") 25 | cfg.LogSize = utils.Bytes(ctx.OInt("log_size", 50*1024*1024)) 26 | cfg.LogRotate = ctx.OInt("log_rotate", 7) 27 | cfg.CPU = uint32(ctx.OInt("cpu_limit", 1)) 28 | cfg.Memory = utils.Bytes(ctx.OInt("memory_limit", 100*1024*1024)) 29 | 30 | cli, err := sshcli.New(addr, user, pass) 31 | runtime.Assert(err) 32 | defer cli.Close() 33 | 34 | content, err := agent.Install(cli, pass, cfg) 35 | 36 | switch { 37 | case err == nil: 38 | ctx.OK(content) 39 | case strings.Contains(err.Error(), "生成配置文件失败:"), 40 | strings.Contains(err.Error(), "移动配置文件失败:"): 41 | ctx.ERR(1, err.Error()) 42 | case strings.Contains(err.Error(), "目录创建失败:"), 43 | strings.Contains(err.Error(), "目录属主修改失败:"): 44 | ctx.ERR(2, err.Error()) 45 | case strings.Contains(err.Error(), "启动失败:"): 46 | ctx.ERR(3, err.Error()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /code/api/cmd/h_channel.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "server/code/client" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | "github.com/lwch/api" 13 | "github.com/lwch/logging" 14 | "github.com/lwch/runtime" 15 | ) 16 | 17 | var upgrader = websocket.Upgrader{ 18 | EnableCompression: true, 19 | } 20 | 21 | func (h *Handler) channel(clients *client.Clients, ctx *api.Context) { 22 | uri := strings.TrimPrefix(ctx.URI(), "/cmd/channel/") 23 | 24 | tmp := strings.SplitN(uri, "/", 2) 25 | cli := h.cliFrom(tmp[0]) 26 | if cli == nil { 27 | ctx.HTTPNotFound("client") 28 | return 29 | } 30 | 31 | pid, err := strconv.ParseInt(tmp[1], 10, 64) 32 | runtime.Assert(err) 33 | 34 | cli.RLock() 35 | p := cli.process[int(pid)] 36 | cli.RUnlock() 37 | 38 | if p == nil { 39 | ctx.HTTPNotFound("process") 40 | return 41 | } 42 | 43 | var conn *websocket.Conn 44 | 45 | ctx.RawCallback(func(w http.ResponseWriter, r *http.Request) { 46 | var err error 47 | conn, err = upgrader.Upgrade(w, r, nil) 48 | runtime.Assert(err) 49 | }) 50 | 51 | for { 52 | data, err := p.read() 53 | if err != nil { 54 | if err == io.EOF { 55 | return 56 | } 57 | logging.Error("chan read: %v", err) 58 | panic(err) 59 | } 60 | if len(data) == 0 { 61 | time.Sleep(time.Second) 62 | continue 63 | } 64 | err = conn.WriteMessage(websocket.TextMessage, data) 65 | if err != nil { 66 | logging.Error("write message: %v", err) 67 | panic(err) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /code/app/api.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | lapi "server/code/api" 7 | "server/code/client" 8 | "strings" 9 | 10 | "github.com/lwch/api" 11 | "github.com/lwch/logging" 12 | ) 13 | 14 | func (app *App) reg(uri string, cb func(*client.Clients, *api.Context)) { 15 | http.HandleFunc(uri, func(w http.ResponseWriter, r *http.Request) { 16 | if app.blocked { 17 | http.Error(w, "raise limit", http.StatusServiceUnavailable) 18 | return 19 | } 20 | statName := strings.TrimSuffix(uri, "/") 21 | statName = strings.ReplaceAll(statName, "/", ":") 22 | counter := app.stats.NewCounter("api_counter" + statName) 23 | counter.Inc() 24 | tick := app.stats.NewTick("api_pref" + statName) 25 | defer tick.Close() 26 | ctx := api.NewContext(w, r) 27 | defer func() { 28 | if err := recover(); err != nil { 29 | switch err := err.(type) { 30 | case api.MissingParam: 31 | ctx.ERR(http.StatusBadRequest, err.Error()) 32 | case api.BadParam: 33 | ctx.ERR(http.StatusBadRequest, err.Error()) 34 | case api.NotFound: 35 | ctx.ERR(http.StatusNotFound, err.Error()) 36 | case api.Timeout: 37 | ctx.ERR(http.StatusBadGateway, err.Error()) 38 | case lapi.PluginNotInstalled: 39 | ctx.ERR(http.StatusNotAcceptable, err.Error()) 40 | case lapi.Notfound: 41 | ctx.ERR(http.StatusNotFound, err.Error()) 42 | default: 43 | ctx.ERR(http.StatusInternalServerError, fmt.Sprintf("%v", err)) 44 | logging.Error("err: %v", err) 45 | } 46 | } 47 | }() 48 | cb(app.clients, ctx) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/dustin/go-humanize v1.0.0 7 | github.com/gorilla/websocket v1.5.0 8 | github.com/jkstack/anet v0.0.0-20220701093727-7f4683edc043 9 | github.com/jkstack/jkframe v1.0.4 10 | github.com/kardianos/service v1.2.1 11 | github.com/lwch/api v0.0.0-20220418064400-20942890aacd 12 | github.com/lwch/kvconf v0.0.0-20211201080405-98b2290c1921 13 | github.com/lwch/l2cache v0.0.0-20211028034138-10159a793fac 14 | github.com/lwch/logging v0.0.0-20220322084100-ec48185d95ab 15 | github.com/lwch/runtime v0.0.0-20190520054850-8c97e19e0c6d 16 | github.com/pkg/sftp v1.13.4 17 | github.com/prometheus/client_golang v1.12.2 18 | github.com/shirou/gopsutil/v3 v3.22.2 19 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292 20 | gopkg.in/yaml.v3 v3.0.0 21 | ) 22 | 23 | require ( 24 | github.com/beorn7/perks v1.0.1 // indirect 25 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 26 | github.com/go-ole/go-ole v1.2.6 // indirect 27 | github.com/golang/protobuf v1.5.2 // indirect 28 | github.com/kr/fs v0.1.0 // indirect 29 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 30 | github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect 31 | github.com/prometheus/client_model v0.2.0 // indirect 32 | github.com/prometheus/common v0.32.1 // indirect 33 | github.com/prometheus/procfs v0.7.3 // indirect 34 | github.com/yusufpapurcu/wmi v1.2.2 // indirect 35 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect 36 | google.golang.org/protobuf v1.26.0 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /code/api/logging/h_stop.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "path/filepath" 7 | "server/code/client" 8 | "time" 9 | 10 | "github.com/jkstack/anet" 11 | "github.com/lwch/api" 12 | "github.com/lwch/runtime" 13 | ) 14 | 15 | func (h *Handler) stop(clients *client.Clients, ctx *api.Context) { 16 | pid := ctx.XInt64("pid") 17 | 18 | h.RLock() 19 | t := h.data[pid] 20 | h.RUnlock() 21 | 22 | if t == nil { 23 | ctx.NotFound("project") 24 | return 25 | } 26 | 27 | if !t.Started { 28 | ctx.ERR(1, "project is not started") 29 | return 30 | } 31 | 32 | for _, cid := range t.Targets { 33 | cli := clients.Get(cid) 34 | if cli == nil { 35 | ctx.NotFound("agent") 36 | return 37 | } 38 | 39 | taskID, err := cli.SendLoggingStop(t.ID) 40 | runtime.Assert(err) 41 | defer cli.ChanClose(taskID) 42 | 43 | h.stTotalTasks.Inc() 44 | 45 | var msg *anet.Msg 46 | select { 47 | case msg = <-cli.ChanRead(taskID): 48 | case <-time.After(api.RequestTimeout): 49 | ctx.Timeout() 50 | } 51 | 52 | switch { 53 | case msg.Type == anet.TypeError: 54 | ctx.ERR(http.StatusServiceUnavailable, msg.ErrorMsg) 55 | return 56 | case msg.Type != anet.TypeLoggingStatusRep: 57 | ctx.ERR(http.StatusInternalServerError, fmt.Sprintf("invalid message type: %d", msg.Type)) 58 | return 59 | } 60 | 61 | if !msg.LoggingStatusRep.OK { 62 | if msg.LoggingStatusRep.Msg != anet.LoggingNotRunningMsg { 63 | ctx.ERR(http.StatusInternalServerError, msg.LoggingStatusRep.Msg) 64 | return 65 | } 66 | } 67 | } 68 | 69 | t.Started = false 70 | dir := filepath.Join(h.cfg.DataDir, "logging", fmt.Sprintf("%d.json", t.ID)) 71 | saveConfig(dir, *t) 72 | 73 | ctx.OK(nil) 74 | } 75 | -------------------------------------------------------------------------------- /code/api/logging/h_start.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "path/filepath" 7 | "server/code/client" 8 | "time" 9 | 10 | "github.com/jkstack/anet" 11 | "github.com/lwch/api" 12 | "github.com/lwch/runtime" 13 | ) 14 | 15 | func (h *Handler) start(clients *client.Clients, ctx *api.Context) { 16 | pid := ctx.XInt64("pid") 17 | 18 | h.RLock() 19 | t := h.data[pid] 20 | h.RUnlock() 21 | 22 | if t == nil { 23 | ctx.NotFound("project") 24 | return 25 | } 26 | 27 | if t.Started { 28 | ctx.ERR(1, "project is started") 29 | return 30 | } 31 | 32 | for _, cid := range t.Targets { 33 | cli := clients.Get(cid) 34 | if cli == nil { 35 | ctx.NotFound("agent") 36 | return 37 | } 38 | 39 | taskID, err := cli.SendLoggingStart(t.ID) 40 | runtime.Assert(err) 41 | defer cli.ChanClose(taskID) 42 | 43 | h.stTotalTasks.Inc() 44 | 45 | var msg *anet.Msg 46 | select { 47 | case msg = <-cli.ChanRead(taskID): 48 | case <-time.After(api.RequestTimeout): 49 | ctx.Timeout() 50 | } 51 | 52 | switch { 53 | case msg.Type == anet.TypeError: 54 | ctx.ERR(http.StatusServiceUnavailable, msg.ErrorMsg) 55 | return 56 | case msg.Type != anet.TypeLoggingStatusRep: 57 | ctx.ERR(http.StatusInternalServerError, fmt.Sprintf("invalid message type: %d", msg.Type)) 58 | return 59 | } 60 | 61 | if !msg.LoggingStatusRep.OK { 62 | if msg.LoggingStatusRep.Msg != anet.LoggingAlreadyRunningMsg { 63 | ctx.ERR(http.StatusInternalServerError, msg.LoggingStatusRep.Msg) 64 | return 65 | } 66 | } 67 | } 68 | 69 | t.Started = true 70 | dir := filepath.Join(h.cfg.DataDir, "logging", fmt.Sprintf("%d.json", t.ID)) 71 | saveConfig(dir, *t) 72 | 73 | ctx.OK(nil) 74 | } 75 | -------------------------------------------------------------------------------- /code/api/agent/handler.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "server/code/client" 5 | "server/code/conf" 6 | 7 | "github.com/jkstack/anet" 8 | "github.com/jkstack/jkframe/stat" 9 | "github.com/lwch/api" 10 | "github.com/prometheus/client_golang/prometheus" 11 | ) 12 | 13 | // Handler server handler 14 | type Handler struct { 15 | cfg *conf.Configure 16 | stAgentVersion *prometheus.GaugeVec 17 | stAgentInfo *prometheus.GaugeVec 18 | stPlugin *prometheus.GaugeVec 19 | } 20 | 21 | // New new cmd handler 22 | func New() *Handler { 23 | return &Handler{} 24 | } 25 | 26 | // Init init handler 27 | func (h *Handler) Init(cfg *conf.Configure, stats *stat.Mgr) { 28 | h.cfg = cfg 29 | h.stAgentVersion = stats.RawVec("agent_version", []string{"id", "agent_type", "version", "go_version"}) 30 | h.stAgentInfo = stats.RawVec("agent_info", []string{"id", "agent_type", "tag"}) 31 | h.stPlugin = stats.RawVec("plugin_info", []string{"id", "agent_type", "name", "tag"}) 32 | } 33 | 34 | // HandleFuncs get handle functions 35 | func (h *Handler) HandleFuncs() map[string]func(*client.Clients, *api.Context) { 36 | return map[string]func(*client.Clients, *api.Context){ 37 | "/agent/exists": h.exists, 38 | "/agent/sniffer": h.sniffer, 39 | "/agent/install": h.install, 40 | "/agent/config": h.config, 41 | "/agent/uninstall": h.uninstall, 42 | "/agent/restart": h.restart, 43 | "/agent/start": h.start, 44 | "/agent/stop": h.stop, 45 | "/agent/upgrade": h.upgrade, 46 | } 47 | } 48 | 49 | func (h *Handler) OnConnect(*client.Client) { 50 | } 51 | 52 | // OnClose agent on close 53 | func (h *Handler) OnClose(string) { 54 | } 55 | 56 | func (h *Handler) OnMessage(cli *client.Client, msg *anet.Msg) { 57 | if msg.Type != anet.TypeAgentInfo { 58 | return 59 | } 60 | h.basicInfo(cli.ID(), msg.AgentInfo) 61 | h.pluginInfo(cli.ID(), msg.AgentInfo) 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartAgent Server 2 | 3 | [![smartagent-server](https://github.com/jkstack/smartagent-server/actions/workflows/build.yml/badge.svg)](https://github.com/jkstack/smartagent-server/actions/workflows/build.yml) 4 | [![go-mod](https://img.shields.io/github/go-mod/go-version/jkstack/smartagent-server)](https://github.com/jkstack/smartagent-server) 5 | [![license](https://img.shields.io/github/license/jkstack/smartagent-server)](https://www.gnu.org/licenses/agpl-3.0.txt) 6 | [![platform](https://img.shields.io/badge/platform-linux-lightgrey.svg)](https://github.com/jkstack/smartagent-server) 7 | 8 | SmartAgent 采用C/S架构的模型来运行,两者之间采用Websocket协议保障在企业级网络策略中通信能力。为提高可扩展性,在Agent端使用多进程的方式运行多种插件,来提供业务方的扩展能力。 9 | 10 |
11 | 12 | ### 服务端 13 | Server为SmartAgent的控制端,负责控制所有主机极其运行插件。 14 | 15 |
16 | 17 | ### 客户端 18 | Agent为SmartAgent的受控端,目前通过插件的方式已支持:远程命令(脚本)执行、获取主机文件列表、文件上传下载、远程命令行等。Agent端支持对执行插件的CPU和内存限制,以此来保障Agent端不会因为自生原因而影响宿主机上的其他服务。 19 | 20 | 规划中插件能力: 21 | - Docker插件 22 | - 自动化机器人(RPA)插件 23 | 24 | 25 |
26 | 27 | ## 产品特色 28 | ### 安全性 29 | SmartAgent服务端在接受新的链接后会等待客户端的握手消息,其中包含客户端所在主机的操作系统、CPU、内存、主机名等基础信息。若服务端在5秒内无法收到客户端的握手消息或者握手消息格式有误时,服务端将会主动断开链接。 30 |
31 | 32 | ### 插件化 33 | 为丰富SmartAgent自身功能,SmartAgent提供了插件化的能力。在服务器端接收到客户端的握手消息并确认无误后,将会根据客户端的操作系统分发对应操作系统的可执行插件。 34 |
35 | 36 | ### 跨平台 37 | SmartAgent主程序采用go语言进行开发,应此兼容市面上大部分操作系统,如centos、redhat、debian、ubuntu、suse、solaris、xp、win2003、win2008、2012、2016、2019等,go语言本身要求linux内核版本号不低于2.6.23,理论上SmartAgent支持该内核版本以上的所有linux操作系统,包括嵌入式Arm、MIPS平台等。 38 |
39 | 40 | ### Restful接口 41 | Server端提供了完整的API接口获取Agent列表、主机信息、任务执行等操作,可以将SmartAgent作为底层通信组件便于集成到上层分布式业务系统中。 42 | 43 | 44 | 45 |
46 |
47 |
48 | 49 | SmartAgent Client 50 | https://github.com/jkstack/SmartAgent 51 | 52 |
53 | 54 | SmartAgent Server 55 | https://github.com/jkstack/SmartAgent-server 56 | 57 |
58 |
59 | 60 | SmartAgent 开源站点
61 | 62 | http://open.jkstack.com 63 | 64 | 65 | SmartAgent 用户微信群 66 | 67 | -------------------------------------------------------------------------------- /code/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | rt "runtime" 9 | "server/code/app" 10 | "server/code/conf" 11 | 12 | _ "net/http/pprof" 13 | 14 | "github.com/kardianos/service" 15 | "github.com/lwch/runtime" 16 | ) 17 | 18 | var ( 19 | version string = "0.0.0" 20 | gitBranch string = "" 21 | gitHash string = "" 22 | gitReversion string = "0" 23 | buildTime string = "0000-00-00 00:00:00" 24 | ) 25 | 26 | func showVersion() { 27 | fmt.Printf("程序版本: %s\n代码版本: %s.%s.%s\n时间: %s\ngo版本: %s\n", 28 | version, 29 | gitBranch, gitHash, gitReversion, 30 | buildTime, 31 | rt.Version()) 32 | } 33 | 34 | func main() { 35 | cf := flag.String("conf", "", "配置文件所在路径") 36 | ver := flag.Bool("version", false, "查看版本号") 37 | act := flag.String("action", "", "install或uninstall") 38 | flag.Parse() 39 | 40 | if *ver { 41 | showVersion() 42 | return 43 | } 44 | 45 | if len(*cf) == 0 { 46 | fmt.Println("缺少-conf参数") 47 | os.Exit(1) 48 | } 49 | 50 | var user string 51 | var depends []string 52 | if rt.GOOS != "windows" { 53 | user = "root" 54 | depends = append(depends, "After=network.target") 55 | } 56 | 57 | dir, err := filepath.Abs(*cf) 58 | runtime.Assert(err) 59 | 60 | opt := make(service.KeyValue) 61 | opt["LimitNOFILE"] = 65535 62 | 63 | appCfg := &service.Config{ 64 | Name: "smartagent-server", 65 | DisplayName: "smartagent-server", 66 | Description: "smartagent server", 67 | UserName: user, 68 | Arguments: []string{"-conf", dir}, 69 | Dependencies: depends, 70 | Option: opt, 71 | } 72 | 73 | dir, err = os.Executable() 74 | runtime.Assert(err) 75 | 76 | cfg := conf.Load(*cf, filepath.Join(filepath.Dir(dir), "/../")) 77 | 78 | app := app.New(cfg, version) 79 | sv, err := service.New(app, appCfg) 80 | runtime.Assert(err) 81 | 82 | switch *act { 83 | case "install": 84 | runtime.Assert(sv.Install()) 85 | case "uninstall": 86 | runtime.Assert(sv.Uninstall()) 87 | default: 88 | runtime.Assert(sv.Run()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /code/api/file/h_upload_from.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | lapi "server/code/api" 7 | "server/code/client" 8 | "time" 9 | 10 | "github.com/jkstack/anet" 11 | "github.com/lwch/api" 12 | "github.com/lwch/logging" 13 | "github.com/lwch/runtime" 14 | ) 15 | 16 | func (h *Handler) uploadFrom(clients *client.Clients, ctx *api.Context) { 17 | id := ctx.XStr("id") 18 | dir := ctx.XStr("dir") 19 | name := ctx.XStr("name") 20 | uri := ctx.XStr("uri") 21 | auth := ctx.OStr("auth", "") 22 | user := ctx.OStr("user", "") 23 | pass := ctx.OStr("pass", "") 24 | mod := ctx.OInt("mod", 0644) 25 | ownUser := ctx.OStr("own_user", "") 26 | ownGroup := ctx.OStr("own_group", "") 27 | timeout := ctx.OInt("timeout", 60) 28 | 29 | cli := clients.Get(id) 30 | if cli == nil { 31 | ctx.NotFound("client") 32 | } 33 | 34 | p := h.cfg.GetPlugin("file", cli.OS(), cli.Arch()) 35 | if p == nil { 36 | lapi.PluginNotInstalledErr("file") 37 | } 38 | 39 | taskID, err := cli.SendUpload(p, client.UploadContext{ 40 | Dir: dir, 41 | Name: name, 42 | Auth: auth, 43 | User: user, 44 | Pass: pass, 45 | Mod: mod, 46 | OwnUser: ownUser, 47 | OwnGroup: ownGroup, 48 | Md5Check: false, 49 | Uri: uri, 50 | }, "") 51 | runtime.Assert(err) 52 | defer cli.ChanClose(taskID) 53 | 54 | h.stUsage.Inc() 55 | h.stTotalTasks.Inc() 56 | 57 | logging.Info("upload [%s] to %s on %s, task_id=%s, plugin.version=%s", 58 | name, dir, id, taskID, p.Version) 59 | 60 | var rep *anet.Msg 61 | select { 62 | case rep = <-cli.ChanRead(taskID): 63 | case <-time.After(time.Duration(timeout) * time.Second): 64 | ctx.Timeout() 65 | } 66 | 67 | switch { 68 | case rep.Type == anet.TypeError: 69 | ctx.ERR(http.StatusServiceUnavailable, rep.ErrorMsg) 70 | return 71 | case rep.Type != anet.TypeUploadRep: 72 | ctx.ERR(http.StatusInternalServerError, fmt.Sprintf("invalid message type: %d", rep.Type)) 73 | return 74 | } 75 | 76 | if !rep.UploadRep.OK { 77 | ctx.ERR(1, rep.UploadRep.ErrMsg) 78 | return 79 | } 80 | 81 | ctx.OK(map[string]interface{}{ 82 | "dir": rep.UploadRep.Dir, 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /code/agent/upgrade.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "path" 8 | "server/code/sshcli" 9 | "strings" 10 | 11 | "github.com/lwch/kvconf" 12 | "github.com/lwch/runtime" 13 | ) 14 | 15 | func Upgrade(cli *sshcli.Client, pass string, data []byte, restart bool) (string, error) { 16 | old, err := cli.Do(context.Background(), "cat "+ 17 | path.Join(DefaultInstallDir, ".version"), "") 18 | runtime.Assert(err) 19 | 20 | confDir := path.Join(DefaultInstallDir, "conf", "client.conf") 21 | conf, err := cli.Do(context.Background(), "cat "+confDir, "") 22 | runtime.Assert(err) 23 | 24 | var src map[string]string 25 | runtime.Assert(kvconf.NewDecoder(strings.NewReader(conf)).Decode(&src)) 26 | 27 | err = cli.UploadFrom(bytes.NewReader(data), "smartagent.tar.gz") 28 | if err != nil { 29 | return "", fmt.Errorf("文件上传失败:%v", err) 30 | } 31 | defer cli.Remove("smartagent.tar.gz") 32 | str, err := sudo(cli, "tar -xhf smartagent.tar.gz -C /", pass) 33 | if err != nil { 34 | return "", fmt.Errorf("安装包解压失败:%s", str) 35 | } 36 | 37 | conf, err = cli.Do(context.Background(), "cat "+confDir, "") 38 | runtime.Assert(err) 39 | 40 | var dst map[string]string 41 | runtime.Assert(kvconf.NewDecoder(strings.NewReader(conf)).Decode(&dst)) 42 | 43 | for k, v := range dst { 44 | if _, ok := src[k]; !ok { 45 | src[k] = v 46 | } 47 | } 48 | 49 | var buf bytes.Buffer 50 | runtime.Assert(kvconf.NewEncoder(&buf).Encode(src)) 51 | 52 | err = cli.UploadFrom(&buf, "client.conf") 53 | if err != nil { 54 | return "", fmt.Errorf("生成配置文件失败:%v", err) 55 | } 56 | defer cli.Remove("client.conf") 57 | str, err = sudo(cli, "mv client.conf "+DefaultInstallDir+"/conf/client.conf", pass) 58 | if err != nil { 59 | return "", fmt.Errorf("移动配置文件失败:%s", str) 60 | } 61 | 62 | if restart { 63 | str, err = sudo(cli, "systemctl restart smartagent", pass) 64 | if err != nil { 65 | str, err = sudo(cli, "service smartagent restart", pass) 66 | } 67 | if err != nil { 68 | str, err = sudo(cli, "/etc/init.d/smartagent restart", pass) 69 | } 70 | if err != nil { 71 | return "", fmt.Errorf("重启失败:%s", str) 72 | } 73 | } 74 | 75 | return old, nil 76 | } 77 | -------------------------------------------------------------------------------- /code/api/file/h_ls.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | lapi "server/code/api" 7 | "server/code/client" 8 | "time" 9 | 10 | "github.com/jkstack/anet" 11 | "github.com/lwch/api" 12 | "github.com/lwch/runtime" 13 | ) 14 | 15 | func (h *Handler) ls(clients *client.Clients, ctx *api.Context) { 16 | id := ctx.XStr("id") 17 | dir := ctx.XStr("dir") 18 | 19 | cli := clients.Get(id) 20 | if cli == nil { 21 | ctx.NotFound("client") 22 | } 23 | 24 | p := h.cfg.GetPlugin("file", cli.OS(), cli.Arch()) 25 | if p == nil { 26 | lapi.PluginNotInstalledErr("file") 27 | } 28 | 29 | taskID, err := cli.SendLS(p, dir) 30 | runtime.Assert(err) 31 | defer cli.ChanClose(taskID) 32 | 33 | h.stUsage.Inc() 34 | h.stTotalTasks.Inc() 35 | 36 | var msg *anet.Msg 37 | select { 38 | case msg = <-cli.ChanRead(taskID): 39 | case <-time.After(lapi.RequestTimeout): 40 | ctx.Timeout() 41 | } 42 | 43 | switch { 44 | case msg.Type == anet.TypeError: 45 | ctx.ERR(http.StatusServiceUnavailable, msg.ErrorMsg) 46 | return 47 | case msg.Type != anet.TypeLsRep: 48 | ctx.ERR(http.StatusInternalServerError, fmt.Sprintf("invalid message type: %d", msg.Type)) 49 | return 50 | } 51 | 52 | if !msg.LSRep.OK { 53 | if msg.LSRep.ErrMsg == "directory not found" { 54 | ctx.NotFound("directory") 55 | } 56 | ctx.ERR(http.StatusInternalServerError, msg.LSRep.ErrMsg) 57 | return 58 | } 59 | 60 | type item struct { 61 | Name string `json:"name"` 62 | Auth uint32 `json:"auth"` 63 | User string `json:"user"` 64 | Group string `json:"group"` 65 | Size uint64 `json:"size"` 66 | ModTime int64 `json:"mod_time"` 67 | IsDir bool `json:"is_dir"` 68 | IsLink bool `json:"is_link"` 69 | } 70 | items := make([]item, len(msg.LSRep.Files)) 71 | for i, file := range msg.LSRep.Files { 72 | items[i] = item{ 73 | Name: file.Name, 74 | Auth: uint32(file.Mod), 75 | User: file.User, 76 | Group: file.Group, 77 | Size: file.Size, 78 | ModTime: file.ModTime.Unix(), 79 | IsDir: file.Mod.IsDir(), 80 | IsLink: file.IsLink, 81 | } 82 | } 83 | ctx.OK(map[string]interface{}{ 84 | "dir": msg.LSRep.Dir, 85 | "files": items, 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /code/api/cmd/h_run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | lapi "server/code/api" 7 | "server/code/client" 8 | "time" 9 | 10 | "github.com/jkstack/anet" 11 | "github.com/lwch/api" 12 | "github.com/lwch/logging" 13 | "github.com/lwch/runtime" 14 | ) 15 | 16 | func (h *Handler) run(clients *client.Clients, ctx *api.Context) { 17 | id := ctx.XStr("id") 18 | cmd := ctx.XStr("cmd") 19 | args := ctx.OCsv("args", []string{}) 20 | timeout := ctx.OInt("timeout", 3600) 21 | auth := ctx.OStr("auth", "") 22 | user := ctx.OStr("user", "") 23 | pass := ctx.OStr("pass", "") 24 | workdir := ctx.OStr("workdir", "") 25 | env := ctx.OCsv("env", []string{}) 26 | deferRM := ctx.OStr("defer_rm", "") 27 | callback := ctx.OStr("callback", "") 28 | 29 | cli := clients.Get(id) 30 | if cli == nil { 31 | ctx.NotFound("client") 32 | } 33 | 34 | p := h.cfg.GetPlugin("exec", cli.OS(), cli.Arch()) 35 | if p == nil { 36 | lapi.PluginNotInstalledErr("exec") 37 | } 38 | 39 | if timeout <= 0 { 40 | timeout = 3600 41 | } 42 | 43 | runCli := h.cliFrom(id) 44 | if runCli == nil { 45 | runCli = h.cliNew(cli) 46 | } 47 | 48 | taskID, err := cli.SendExec(p, cmd, args, timeout, auth, user, pass, workdir, env, deferRM) 49 | runtime.Assert(err) 50 | 51 | h.stUsage.Inc() 52 | h.stTotalTasks.Inc() 53 | 54 | logging.Info("run [%s] on %s, task_id=%s, plugin.version=%s", cmd, id, taskID, p.Version) 55 | 56 | var msg *anet.Msg 57 | select { 58 | case msg = <-cli.ChanRead(taskID): 59 | case <-time.After(api.RequestTimeout): 60 | ctx.Timeout() 61 | } 62 | 63 | switch { 64 | case msg.Type == anet.TypeError: 65 | ctx.ERR(http.StatusServiceUnavailable, msg.ErrorMsg) 66 | return 67 | case msg.Type != anet.TypeExecd: 68 | ctx.ERR(http.StatusInternalServerError, fmt.Sprintf("invalid message type: %d", msg.Type)) 69 | return 70 | } 71 | 72 | if !msg.Execd.OK { 73 | ctx.ERR(1, msg.Execd.Msg) 74 | return 75 | } 76 | 77 | process, err := newProcess(runCli, h.cfg, msg.Execd.Pid, cmd, taskID, callback) 78 | runtime.Assert(err) 79 | go process.recv() 80 | runCli.addProcess(process) 81 | 82 | ctx.OK(map[string]interface{}{ 83 | "channel_id": taskID, 84 | "pid": msg.Execd.Pid, 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /docs/编排/README.md: -------------------------------------------------------------------------------- 1 | # 编排脚本 2 | 3 | 提供执行yaml编排脚本的能力,对标ansible 4 | 5 | ## 已支持插件 6 | 7 | 1. [exec](plugin/exec.md): 执行脚本或命令 8 | 2. [file](plugin/file.md): 文件传输 9 | 10 | [api接口](../api/yaml/run.md) 11 | 12 | ## 示例 13 | 14 | name: deploy nginx 15 | timeout: 600 16 | tasks: 17 | - name: flow control 18 | plugin: exec 19 | cmd: echo "do even things" 20 | if: $EVEN = 1 21 | timeout: 1 22 | - name: flow control 23 | plugin: exec 24 | cmd: echo "do odd things" 25 | if: $ODD = 1 26 | - name: check if nginx installed 27 | plugin: exec 28 | cmd: apt list --installed | grep nginx | wc -l 29 | timeout: 300 30 | output: cnt 31 | auth: sudo 32 | - name: uninstall nginx 33 | plugin: exec 34 | cmd: apt purge -y nginx 35 | if: $cnt > 0 36 | auth: sudo 37 | - name: install nginx 38 | plugin: exec 39 | cmd: apt install -y nginx 40 | auth: sudo 41 | - name: add server 42 | plugin: file 43 | action: push 44 | src: example.conf 45 | dst: /etc/nginx/conf.d/example.conf 46 | auth: sudo 47 | - name: mkdir /var/www/example 48 | plugin: exec 49 | cmd: mkdir -p /var/www/example 50 | auth: sudo 51 | - name: add index.html 52 | plugin: file 53 | action: push 54 | src: index.html 55 | dst: /var/www/example/index.html 56 | auth: sudo 57 | - name: reload service 58 | plugin: exec 59 | cmd: systemctl reload nginx 60 | auth: sudo 61 | 62 | ## 通用字段 63 | 64 | `name`: 任务名称,全局作用域表示总任务名称,tasks中表示子任务名称 65 | 66 | `plugin`: 子任务所需的插件名称,详见已支持插件章节 67 | 68 | `auth`: 提权方式,仅linux有效 69 | 70 | `timeout`: 超时时间,最外层为所有任务总超时时间,内层为单个任务的超时时间 71 | 72 | `if`: 条件匹配,仅当给定表达式结果为`true`时执行当前子任务,目前仅支持单条表达式, 73 | 支持的匹配运算符如下: 74 | - `>`: 左值大于右值为true 75 | - `<`: 左值小于右值为true 76 | - `=`: 左值等于右值为true 77 | - `!=`: 左值不等于右值为true 78 | - `<=`: 左值小于等于右值为true 79 | - `>=`: 左值大于等于右值为true 80 | - 以上比较过程若左值和右值都能转成整数时按照整数进行比较,否则按照字符串进行比较 81 | 82 | ## 变量 83 | 84 | 编排文件中的变量以$开头,在脚本运行时会内置以下系统变量,所有系统内置变量名称均为大写: 85 | 86 | 1. `$ID`: 当前主机ID 87 | 2. `$IDX`: 当前主机在列表中的下标 88 | 3. `$EVEN`/`$ODD`: 奇偶标志位0或1 89 | 4. `$DEADLINE`: 超时时间戳 -------------------------------------------------------------------------------- /code/api/cmd/handler.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | lapi "server/code/api" 5 | "server/code/client" 6 | "server/code/conf" 7 | "sync" 8 | "time" 9 | 10 | "github.com/jkstack/anet" 11 | "github.com/jkstack/jkframe/stat" 12 | "github.com/lwch/api" 13 | ) 14 | 15 | const clearTimeout = 30 * time.Minute 16 | 17 | // Handler cmd handler 18 | type Handler struct { 19 | sync.RWMutex 20 | cfg *conf.Configure 21 | clients map[string]*cmdClient // cid => client 22 | stUsage *stat.Counter 23 | stTotalTasks *stat.Counter 24 | } 25 | 26 | // New new cmd handler 27 | func New() *Handler { 28 | return &Handler{ 29 | clients: make(map[string]*cmdClient), 30 | } 31 | } 32 | 33 | // Init init handler 34 | func (h *Handler) Init(cfg *conf.Configure, stats *stat.Mgr) { 35 | h.cfg = cfg 36 | h.stUsage = stats.NewCounter("plugin_count_exec") 37 | h.stTotalTasks = stats.NewCounter(lapi.TotalTasksLabel) 38 | } 39 | 40 | // HandleFuncs get handle functions 41 | func (h *Handler) HandleFuncs() map[string]func(*client.Clients, *api.Context) { 42 | return map[string]func(*client.Clients, *api.Context){ 43 | "/cmd/run": h.run, 44 | "/cmd/ps": h.ps, 45 | "/cmd/pty": h.pty, 46 | "/cmd/kill": h.kill, 47 | "/cmd/status": h.status, 48 | "/cmd/sync_run": h.syncRun, 49 | "/cmd/channel/": h.channel, 50 | } 51 | } 52 | 53 | func (h *Handler) cli(cli *client.Client) *cmdClient { 54 | h.Lock() 55 | defer h.Unlock() 56 | if cli, ok := h.clients[cli.ID()]; ok { 57 | return cli 58 | } 59 | c := newClient(cli) 60 | h.clients[cli.ID()] = c 61 | return c 62 | } 63 | 64 | func (h *Handler) cliNew(cli *client.Client) *cmdClient { 65 | c := newClient(cli) 66 | h.Lock() 67 | h.clients[cli.ID()] = c 68 | h.Unlock() 69 | return c 70 | } 71 | 72 | func (h *Handler) cliFrom(id string) *cmdClient { 73 | h.RLock() 74 | defer h.RUnlock() 75 | return h.clients[id] 76 | } 77 | 78 | func (h *Handler) OnConnect(*client.Client) { 79 | } 80 | 81 | // OnClose agent on close 82 | func (h *Handler) OnClose(id string) { 83 | h.RLock() 84 | cli := h.clients[id] 85 | h.RUnlock() 86 | if cli != nil { 87 | cli.close() 88 | h.Lock() 89 | delete(h.clients, id) 90 | h.Unlock() 91 | } 92 | } 93 | 94 | func (h *Handler) OnMessage(*client.Client, *anet.Msg) { 95 | } 96 | -------------------------------------------------------------------------------- /code/api/agent/report.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "github.com/jkstack/anet" 5 | "github.com/prometheus/client_golang/prometheus" 6 | ) 7 | 8 | func setValue(vec *prometheus.GaugeVec, id, tag string, n float64) { 9 | vec.With(prometheus.Labels{ 10 | "id": id, 11 | "agent_type": "smart", 12 | "tag": tag, 13 | }).Set(n) 14 | } 15 | 16 | func (h *Handler) basicInfo(id string, info *anet.AgentInfo) { 17 | h.stAgentVersion.With(prometheus.Labels{ 18 | "id": id, 19 | "agent_type": "smart", 20 | "version": info.Version, 21 | "go_version": info.GoVersion, 22 | }).Set(1) 23 | setValue(h.stAgentInfo, id, "cpu_usage", float64(info.CpuUsage)) 24 | setValue(h.stAgentInfo, id, "memory_usage", float64(info.MemoryUsage)) 25 | setValue(h.stAgentInfo, id, "threads", float64(info.Threads)) 26 | setValue(h.stAgentInfo, id, "routines", float64(info.Routines)) 27 | setValue(h.stAgentInfo, id, "startup", float64(info.Startup)) 28 | setValue(h.stAgentInfo, id, "heap_in_use", float64(info.HeapInuse)) 29 | setValue(h.stAgentInfo, id, "gc_0", info.GC["0"]) 30 | setValue(h.stAgentInfo, id, "gc_0.25", info.GC["25"]) 31 | setValue(h.stAgentInfo, id, "gc_0.5", info.GC["50"]) 32 | setValue(h.stAgentInfo, id, "gc_0.75", info.GC["75"]) 33 | setValue(h.stAgentInfo, id, "gc_1", info.GC["100"]) 34 | setValue(h.stAgentInfo, id, "in_packets", float64(info.InPackets)) 35 | setValue(h.stAgentInfo, id, "in_bytes", float64(info.InBytes)) 36 | setValue(h.stAgentInfo, id, "out_packets", float64(info.OutPackets)) 37 | setValue(h.stAgentInfo, id, "out_bytes", float64(info.OutBytes)) 38 | } 39 | 40 | func setPluginValue(vec *prometheus.GaugeVec, id, name, tag string, n float64) { 41 | vec.With(prometheus.Labels{ 42 | "id": id, 43 | "agent_type": "smart", 44 | "name": name, 45 | "tag": tag, 46 | }).Set(n) 47 | } 48 | 49 | func (h *Handler) pluginInfo(id string, info *anet.AgentInfo) { 50 | setValue(h.stAgentInfo, id, "plugin_execd", float64(info.PluginExecd)) 51 | setValue(h.stAgentInfo, id, "plugin_running", float64(info.PluginRunning)) 52 | for k, v := range info.PluginUseCount { 53 | setPluginValue(h.stPlugin, id, k, "use", float64(v)) 54 | } 55 | for k, v := range info.PluginOutPackets { 56 | setPluginValue(h.stPlugin, id, k, "out_packets", float64(v)) 57 | } 58 | for k, v := range info.PluginOutBytes { 59 | setPluginValue(h.stPlugin, id, k, "out_bytes", float64(v)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /code/api/layout/handler.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | lapi "server/code/api" 5 | "server/code/api/file" 6 | "server/code/client" 7 | "server/code/conf" 8 | "sync" 9 | "time" 10 | 11 | "github.com/jkstack/anet" 12 | "github.com/jkstack/jkframe/stat" 13 | "github.com/lwch/api" 14 | ) 15 | 16 | type taskHandler interface { 17 | Name() string 18 | Clone(Task, taskInfo) taskHandler 19 | Check(Task) error 20 | WantRun(map[string]string) bool 21 | Run(id, dir, user, pass string, args map[string]string) error 22 | } 23 | 24 | // Handler server handler 25 | type Handler struct { 26 | sync.RWMutex 27 | cfg *conf.Configure 28 | runners map[string]*runner 29 | handlers map[string]taskHandler 30 | idx uint32 31 | fh *file.Handler 32 | stExecUsage *stat.Counter 33 | stFileUsage *stat.Counter 34 | stTotalTasks *stat.Counter 35 | } 36 | 37 | // New new cmd handler 38 | func New(fh *file.Handler) *Handler { 39 | h := &Handler{ 40 | runners: make(map[string]*runner), 41 | handlers: make(map[string]taskHandler), 42 | fh: fh, 43 | } 44 | h.handlers["exec"] = &execHandler{} 45 | h.handlers["file"] = &fileHandler{} 46 | go h.clear() 47 | return h 48 | } 49 | 50 | // Init init handler 51 | func (h *Handler) Init(cfg *conf.Configure, stats *stat.Mgr) { 52 | h.cfg = cfg 53 | h.stExecUsage = stats.NewCounter("plugin_count_exec") 54 | h.stFileUsage = stats.NewCounter("plugin_count_file") 55 | h.stTotalTasks = stats.NewCounter(lapi.TotalTasksLabel) 56 | } 57 | 58 | // HandleFuncs get handle functions 59 | func (h *Handler) HandleFuncs() map[string]func(*client.Clients, *api.Context) { 60 | return map[string]func(*client.Clients, *api.Context){ 61 | "/layout/run": h.run, 62 | "/layout/status": h.status, 63 | } 64 | } 65 | 66 | func (h *Handler) OnConnect(*client.Client) { 67 | } 68 | 69 | // OnClose agent on close 70 | func (h *Handler) OnClose(string) { 71 | } 72 | 73 | func (h *Handler) OnMessage(*client.Client, *anet.Msg) { 74 | } 75 | 76 | func (h *Handler) clear() { 77 | run := func() { 78 | remove := make([]string, 0, len(h.runners)) 79 | h.RLock() 80 | for id, r := range h.runners { 81 | if time.Since(r.deadline).Hours() > 1 { 82 | remove = append(remove, id) 83 | } 84 | } 85 | h.RUnlock() 86 | 87 | h.Lock() 88 | for _, id := range remove { 89 | delete(h.runners, id) 90 | } 91 | h.Unlock() 92 | } 93 | for { 94 | run() 95 | time.Sleep(time.Minute) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /code/api/cmd/h_syncrun.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "net/http" 8 | lapi "server/code/api" 9 | "server/code/client" 10 | "time" 11 | 12 | "github.com/jkstack/anet" 13 | "github.com/lwch/api" 14 | "github.com/lwch/logging" 15 | "github.com/lwch/runtime" 16 | ) 17 | 18 | func (h *Handler) syncRun(clients *client.Clients, ctx *api.Context) { 19 | id := ctx.XStr("id") 20 | cmd := ctx.XStr("cmd") 21 | args := ctx.OCsv("args", []string{}) 22 | timeout := ctx.OInt("timeout", 60) 23 | auth := ctx.OStr("auth", "") 24 | user := ctx.OStr("user", "") 25 | pass := ctx.OStr("pass", "") 26 | workdir := ctx.OStr("workdir", "") 27 | env := ctx.OCsv("env", []string{}) 28 | deferRM := ctx.OStr("defer_rm", "") 29 | 30 | if timeout > 60 { 31 | timeout = 60 32 | } else if timeout <= 0 { 33 | timeout = 60 34 | } 35 | 36 | cli := clients.Get(id) 37 | if cli == nil { 38 | ctx.NotFound("client") 39 | } 40 | 41 | p := h.cfg.GetPlugin("exec", cli.OS(), cli.Arch()) 42 | if p == nil { 43 | lapi.PluginNotInstalledErr("exec") 44 | } 45 | 46 | runCli := h.cliFrom(id) 47 | if runCli == nil { 48 | runCli = h.cliNew(cli) 49 | } 50 | 51 | taskID, err := cli.SendExec(p, cmd, args, timeout, auth, user, pass, workdir, env, deferRM) 52 | runtime.Assert(err) 53 | 54 | h.stUsage.Inc() 55 | h.stTotalTasks.Inc() 56 | 57 | logging.Info("sync_run [%s] on %s, task_id=%s, plugin.version=%s", cmd, id, taskID, p.Version) 58 | 59 | var msg *anet.Msg 60 | select { 61 | case msg = <-cli.ChanRead(taskID): 62 | case <-time.After(api.RequestTimeout): 63 | ctx.Timeout() 64 | } 65 | 66 | switch { 67 | case msg.Type == anet.TypeError: 68 | ctx.ERR(http.StatusServiceUnavailable, msg.ErrorMsg) 69 | return 70 | case msg.Type != anet.TypeExecd: 71 | ctx.ERR(http.StatusInternalServerError, fmt.Sprintf("invalid message type: %d", msg.Type)) 72 | return 73 | } 74 | 75 | if !msg.Execd.OK { 76 | ctx.ERR(1, msg.Execd.Msg) 77 | return 78 | } 79 | 80 | process, err := newProcess(runCli, h.cfg, msg.Execd.Pid, cmd, taskID, "") 81 | runtime.Assert(err) 82 | defer process.close() 83 | process.ctx, process.cancel = context.WithTimeout(process.ctx, time.Duration(timeout)*time.Second) 84 | defer process.cancel() 85 | go process.recv() 86 | process.wait() 87 | 88 | data, err := process.read() 89 | runtime.Assert(err) 90 | 91 | ctx.OK(map[string]interface{}{ 92 | "code": process.code, 93 | "data": base64.StdEncoding.EncodeToString(data), 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /code/client/send_file.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/md5" 5 | "os" 6 | "server/code/conf" 7 | "server/code/utils" 8 | 9 | "github.com/jkstack/anet" 10 | ) 11 | 12 | type UploadContext struct { 13 | Dir string 14 | Name string 15 | Auth string 16 | User string 17 | Pass string 18 | Mod int 19 | OwnUser string 20 | OwnGroup string 21 | Size uint64 22 | Md5 [md5.Size]byte 23 | Md5Check bool 24 | // send data 25 | Data []byte 26 | // send file 27 | Uri string 28 | Token string 29 | } 30 | 31 | func (cli *Client) SendLS(p *conf.PluginInfo, dir string) (string, error) { 32 | id, err := utils.TaskID() 33 | if err != nil { 34 | return "", err 35 | } 36 | var msg anet.Msg 37 | msg.Type = anet.TypeLsReq 38 | msg.TaskID = id 39 | msg.Plugin = fillPlugin(p) 40 | msg.LSReq = &anet.LsReq{Dir: dir} 41 | cli.Lock() 42 | cli.taskRead[id] = make(chan *anet.Msg) 43 | cli.Unlock() 44 | cli.chWrite <- &msg 45 | return id, nil 46 | } 47 | 48 | func (cli *Client) SendDownload(p *conf.PluginInfo, dir string) (string, error) { 49 | id, err := utils.TaskID() 50 | if err != nil { 51 | return "", err 52 | } 53 | var msg anet.Msg 54 | msg.Type = anet.TypeDownloadReq 55 | msg.TaskID = id 56 | msg.Plugin = fillPlugin(p) 57 | msg.DownloadReq = &anet.DownloadReq{Dir: dir} 58 | cli.Lock() 59 | cli.taskRead[id] = make(chan *anet.Msg) 60 | cli.Unlock() 61 | cli.chWrite <- &msg 62 | return id, nil 63 | } 64 | 65 | func (cli *Client) SendUpload(p *conf.PluginInfo, ctx UploadContext, id string) (string, error) { 66 | if len(id) == 0 { 67 | var err error 68 | id, err = utils.TaskID() 69 | if err != nil { 70 | return "", err 71 | } 72 | } 73 | var msg anet.Msg 74 | msg.Type = anet.TypeUpload 75 | msg.TaskID = id 76 | msg.Plugin = fillPlugin(p) 77 | msg.Upload = &anet.Upload{ 78 | Dir: ctx.Dir, 79 | Name: ctx.Name, 80 | Auth: ctx.Auth, 81 | User: ctx.User, 82 | Pass: ctx.Pass, 83 | Mod: os.FileMode(ctx.Mod), 84 | OwnUser: ctx.OwnUser, 85 | OwnGroup: ctx.OwnGroup, 86 | Size: ctx.Size, 87 | MD5: ctx.Md5, 88 | } 89 | if len(ctx.Data) > 0 { 90 | msg.Upload.Data = utils.EncodeData(ctx.Data) 91 | } else if len(ctx.Uri) > 0 { 92 | msg.Upload.URI = ctx.Uri 93 | msg.Upload.Token = ctx.Token 94 | } 95 | cli.Lock() 96 | cli.taskRead[id] = make(chan *anet.Msg) 97 | cli.Unlock() 98 | cli.chWrite <- &msg 99 | return id, nil 100 | } 101 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 2.0.5 4 | 5 | 修正客户端重连后无法继续运行脚本的问题 6 | 7 | ## 2.0.4 8 | 9 | 1. 修正客户端连接握手失败时的崩溃问题 10 | 2. go版本升级到1.18.5 11 | 12 | ## 2.0.3 13 | 14 | 1. 支持docker容器的log采集 15 | 2. 增加基于prometheus的埋点监控 16 | 3. /cmd/run接口增加defer_rm和callback参数 17 | 4. /cmd/sync_run接口增加defer_rm参数 18 | 5. go版本升级到1.18.4 19 | 20 | ## 2.0.2 21 | 22 | 1. 修正/hm/static接口返回值与文档不一致的问题 23 | 2. 新增/scaffolding/foo接口支持脚手架项目 24 | 25 | ## 2.0.1 26 | 27 | 1. 修正上传文件大于1M时无法正确上传的问题 28 | 2. 新增yaml编排文件支持 29 | 3. 支持logging项目 30 | 31 | ## 2.0.0 32 | 33 | 1. 适配agent版本2.x 34 | 2. 通信协议修改为websocket+json 35 | 3. 修改插件目录结构 36 | 37 | ### 关于插件 38 | 39 | 1. 修改/cmd/run接口,增加env参数 40 | 2. 修改/cmd/pty接口参数定义 41 | 3. 增加/cmd/sync_run接口 42 | 4. 增加/host/search接口 43 | 5. 远程执行命令增加磁盘缓存功能,解决返回数据量过大时的内存占用过大问题 44 | 6. 文件上传下载增加对文本内容的压缩功能 45 | 7. 调整上传文件超过1M时的处理方式 46 | 47 | ## 1.3.2 48 | 49 | ### 新增功能 50 | 51 | 1. 增加/agent/stop和/agent/start接口 52 | 2. 增加/agent/generate接口 53 | 54 | ## 1.3.1 55 | 56 | ### 新增功能 57 | 58 | 1. 支持插件的启动/停止 59 | 2. /cmd/ps接口增加cmd返回字段 60 | 3. /plugin/install接口返回插件原始版本号 61 | 4. 新增/plugin/start和/plugin/stop接口 62 | 5. 修改插件列表若干接口,增加运行状态的返回结果 63 | 6. /cmd/ps接口增加cmd字段 64 | 65 | ### 修复BUG 66 | 67 | 1. 修正插件卸载日志中的版本号问题 68 | 3. 修正已停止插件的监控数据问题 69 | 70 | ## 1.3.0 71 | 72 | ### 新增功能 73 | 74 | 1. 支持samp平台化管理 75 | 2. 新增agent安装、重启、更新接口 76 | 77 | ### 新增优化 78 | 79 | 1. 调整plugin的目录结构 80 | 2. client握手超时时间增加到30秒 81 | 82 | ### 修复BUG 83 | 84 | 无 85 | 86 | ## 1.2.2 87 | 88 | ### 新增功能 89 | 90 | 无 91 | 92 | ### 新增优化 93 | 94 | 1. 分发插件时增加重传功能 95 | 2. jkplugin插件升级,支持suse 96 | 97 | ### 修复BUG 98 | 99 | 无 100 | 101 | ## 1.2.1 102 | 103 | ### 新增功能 104 | 105 | 无 106 | 107 | ### 新增优化 108 | 109 | 1. host.monitor去除动态数据上报 110 | 2. host.monitor静态数据上报修改为1天一次 111 | 3. /hm/static接口支持被动通知host.monitor上报数据 112 | 113 | ### 修复BUG 114 | 115 | 1. 修正plugin无法更新的问题 116 | 117 | ## 1.2.0 118 | 119 | ### 新增功能 120 | 121 | 1. agent支持同时连接多个server 122 | 123 | ### 新增优化 124 | 125 | 无 126 | 127 | ### 修复BUG 128 | 129 | 无 130 | 131 | ## 1.1.0 132 | 133 | ### 新增功能 134 | 135 | 1. 增加插件的CPU内存监控页面 136 | 2. 增加host.monitor插件 137 | 3. shell功能支持windows 138 | 4. web页面增加版本号显示,所有程序支持-version参数查看版本号 139 | 140 | ### 新增优化 141 | 142 | 1. 优化插件内存监控时的CPU占用问题 143 | 2. 切换到gogo的protobuf库,优化gc 144 | 145 | ### 修复BUG 146 | 147 | 1. 修正agent端未配置limit时插件进程被杀死的问题 148 | 2. 修正上传空文件的报错问题 149 | 3. 修正panic的插件无法升级的问题 150 | 151 | ## 1.0.0 152 | 153 | ### 新增功能 154 | 155 | 1. 远程执行命令 156 | 2. 文件查询上传下载 157 | 3. 命令行工具 158 | 159 | ### 新增优化 160 | 161 | 无 162 | 163 | ### 修复BUG 164 | 165 | 无 166 | -------------------------------------------------------------------------------- /code/api/layout/h_run.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "archive/tar" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | lapi "server/code/api" 10 | "server/code/client" 11 | "sync/atomic" 12 | 13 | "github.com/lwch/api" 14 | "github.com/lwch/logging" 15 | "github.com/lwch/runtime" 16 | ) 17 | 18 | func (h *Handler) run(clients *client.Clients, ctx *api.Context) { 19 | ids := ctx.XCsv("ids") 20 | mode := ctx.OStr("mode", "sequence") 21 | errContinue := ctx.OBool("continue", false) 22 | user := ctx.OStr("user", "") 23 | pass := ctx.OStr("pass", "") 24 | file, _, err := ctx.File("file") 25 | runtime.Assert(err) 26 | 27 | if mode != "sequence" && 28 | mode != "parallel" && 29 | mode != "evenodd" { 30 | lapi.BadParamErr("mode") 31 | return 32 | } 33 | 34 | uniq := make(map[string]bool) 35 | for _, id := range ids { 36 | if uniq[id] { 37 | lapi.BadParamErr("ids") 38 | return 39 | } 40 | cli := clients.Get(id) 41 | if cli == nil { 42 | lapi.NotfoundErr(id) 43 | return 44 | } 45 | uniq[id] = true 46 | } 47 | 48 | idx := atomic.AddUint32(&h.idx, 1) 49 | runner := newRunner(h, clients, idx, ids, mode, errContinue, user, pass) 50 | dir, err := h.extract(file, runner.id) 51 | runtime.Assert(err) 52 | 53 | err = runner.checkAndBuild(dir, h.handlers) 54 | if err != nil { 55 | ctx.ERR(1, err.Error()) 56 | return 57 | } 58 | 59 | h.Lock() 60 | h.runners[runner.id] = runner 61 | h.Unlock() 62 | go runner.run(dir) 63 | 64 | ctx.OK(runner.id) 65 | } 66 | 67 | func (h *Handler) extract(file io.Reader, taskID string) (string, error) { 68 | os.MkdirAll(h.cfg.CacheDir, 0755) 69 | dir, err := ioutil.TempDir(h.cfg.CacheDir, "layout") 70 | if err != nil { 71 | return "", err 72 | } 73 | tr := tar.NewReader(file) 74 | write := func(dir string, r io.Reader) error { 75 | f, err := os.Create(dir) 76 | if err != nil { 77 | return err 78 | } 79 | defer f.Close() 80 | _, err = io.Copy(f, r) 81 | return err 82 | } 83 | for { 84 | hdr, err := tr.Next() 85 | if err != nil { 86 | if err == io.EOF { 87 | return dir, nil 88 | } 89 | os.RemoveAll(dir) 90 | return "", err 91 | } 92 | if hdr.Typeflag == tar.TypeDir { 93 | continue 94 | } 95 | if hdr.Typeflag != tar.TypeReg { 96 | logging.Info("skip file: %s", hdr.Name) 97 | continue 98 | } 99 | target := filepath.Join(dir, hdr.Name) 100 | os.MkdirAll(filepath.Dir(target), 0755) 101 | err = write(target, tr) 102 | if err != nil { 103 | os.RemoveAll(dir) 104 | return "", err 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /code/agent/install.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path" 9 | "server/code/sshcli" 10 | "server/code/utils" 11 | 12 | "github.com/lwch/kvconf" 13 | "github.com/lwch/runtime" 14 | ) 15 | 16 | const DefaultInstallDir = "/opt/smartagent" 17 | 18 | func Extract(cli *sshcli.Client, pass string, data []byte) error { 19 | _, err := cli.Stat(DefaultInstallDir + "/.success") 20 | if !os.IsNotExist(err) { 21 | return errors.New("无法重复安装") 22 | } 23 | err = cli.UploadFrom(bytes.NewReader(data), "smartagent.tar.gz") 24 | if err != nil { 25 | return fmt.Errorf("文件上传失败:%v", err) 26 | } 27 | defer cli.Remove("smartagent.tar.gz") 28 | str, err := sudo(cli, "tar -xhf smartagent.tar.gz -C /", pass) 29 | if err != nil { 30 | return fmt.Errorf("安装包解压失败:%s", str) 31 | } 32 | return nil 33 | } 34 | 35 | type Config struct { 36 | ID string `kv:"id"` 37 | Server string `kv:"server"` 38 | User string `kv:"user"` 39 | PluginDir string `kv:"plugin_dir"` 40 | LogDir string `kv:"log_dir"` 41 | LogSize utils.Bytes `kv:"log_size"` 42 | LogRotate int `kv:"log_rotate"` 43 | CPU uint32 `kv:"cpu_limit"` 44 | Memory utils.Bytes `kv:"memory_limit"` 45 | } 46 | 47 | func Install(cli *sshcli.Client, pass string, cfg Config) (string, error) { 48 | if !path.IsAbs(cfg.PluginDir) { 49 | cfg.PluginDir = DefaultInstallDir + "/" + cfg.PluginDir 50 | } 51 | if !path.IsAbs(cfg.LogDir) { 52 | cfg.LogDir = DefaultInstallDir + "/" + cfg.LogDir 53 | } 54 | 55 | var buf bytes.Buffer 56 | runtime.Assert(kvconf.NewEncoder(&buf).Encode(cfg)) 57 | content := buf.String() 58 | 59 | err := cli.UploadFrom(&buf, "client.conf") 60 | if err != nil { 61 | return "", fmt.Errorf("生成配置文件失败:%v", err) 62 | } 63 | defer cli.Remove("client.conf") 64 | str, err := sudo(cli, "mv client.conf "+DefaultInstallDir+"/conf/client.conf", pass) 65 | if err != nil { 66 | return "", fmt.Errorf("移动配置文件失败:%s", str) 67 | } 68 | 69 | sudo(cli, DefaultInstallDir+"/bin/smartagent -conf "+ 70 | DefaultInstallDir+"/conf/client.conf -action install", pass) 71 | sudo(cli, "systemctl enable smartagent", pass) 72 | sudo(cli, "update-rc.d smartagent defaults", pass) 73 | 74 | str, err = sudo(cli, "systemctl restart smartagent", pass) 75 | if err != nil { 76 | str, err = sudo(cli, "service smartagent restart", pass) 77 | } 78 | if err != nil { 79 | str, err = sudo(cli, "/etc/init.d/smartagent restart", pass) 80 | } 81 | if err != nil { 82 | return "", fmt.Errorf("启动失败:%s", str) 83 | } 84 | 85 | sudo(cli, "touch "+DefaultInstallDir+"/.success", pass) 86 | 87 | return content, nil 88 | } 89 | -------------------------------------------------------------------------------- /code/app/ws.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "server/code/client" 10 | "time" 11 | 12 | "github.com/gorilla/websocket" 13 | "github.com/jkstack/anet" 14 | "github.com/lwch/logging" 15 | "github.com/lwch/runtime" 16 | ) 17 | 18 | var upgrader = websocket.Upgrader{ 19 | EnableCompression: true, 20 | } 21 | 22 | func (app *App) agent(w http.ResponseWriter, r *http.Request, 23 | onConnect chan *client.Client, cancel context.CancelFunc) *client.Client { 24 | conn, err := upgrader.Upgrade(w, r, nil) 25 | if err != nil { 26 | logging.Error("upgrade websocket: %v", err) 27 | http.Error(w, err.Error(), http.StatusInternalServerError) 28 | return nil 29 | } 30 | come, err := app.waitCome(conn) 31 | if err != nil { 32 | logging.Error("wait come message(%s): %v", conn.RemoteAddr().String(), err) 33 | return nil 34 | } 35 | if app.handshake(conn, come) { 36 | app.stAgentCount.Inc() 37 | logging.Info("client %s connection on, os=%s, arch=%s, mac=%s", 38 | come.ID, come.OS, come.Arch, come.MAC) 39 | cli := app.clients.New(conn, come, cancel) 40 | app.clients.Add(cli) 41 | onConnect <- cli 42 | return cli 43 | } 44 | return nil 45 | } 46 | 47 | func (app *App) waitCome(conn *websocket.Conn) (*anet.ComePayload, error) { 48 | conn.SetReadDeadline(time.Now().Add(time.Minute)) 49 | var msg anet.Msg 50 | err := conn.ReadJSON(&msg) 51 | if err != nil { 52 | return nil, err 53 | } 54 | if msg.Type != anet.TypeCome { 55 | return nil, errors.New("invalid come message") 56 | } 57 | return msg.Come, nil 58 | } 59 | 60 | func (app *App) handshake(conn *websocket.Conn, come *anet.ComePayload) (ok bool) { 61 | var errMsg string 62 | defer func() { 63 | var rep anet.Msg 64 | rep.Type = anet.TypeHandshake 65 | rep.Important = true 66 | if ok { 67 | rep.Handshake = &anet.HandshakePayload{ 68 | OK: true, 69 | ID: come.ID, 70 | // TODO: redirect 71 | } 72 | } else { 73 | rep.Handshake = &anet.HandshakePayload{ 74 | OK: false, 75 | Msg: errMsg, 76 | } 77 | } 78 | data, err := json.Marshal(rep) 79 | if err != nil { 80 | logging.Error("build handshake message: %v", err) 81 | return 82 | } 83 | conn.WriteMessage(websocket.TextMessage, data) 84 | }() 85 | app.connectLock.Lock() 86 | defer app.connectLock.Unlock() 87 | if len(come.ID) == 0 { 88 | id, err := runtime.UUID(16, "0123456789abcdef") 89 | if err != nil { 90 | errMsg = fmt.Sprintf("generate agent id: %v", err) 91 | logging.Error(errMsg) 92 | return false 93 | } 94 | come.ID = fmt.Sprintf("agent-%s-%s", time.Now().Format("20060102"), id) 95 | } else if app.clients.Get(come.ID) != nil { 96 | errMsg = "agent id conflict" 97 | return false 98 | } 99 | return true 100 | } 101 | -------------------------------------------------------------------------------- /code/api/layout/build.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | func (r *runner) checkAndBuild(dir string, handlers map[string]taskHandler) error { 14 | var main *Main 15 | var err error 16 | if _, err := os.Stat(filepath.Join(dir, "main.yaml")); !os.IsNotExist(err) { 17 | main, err = r.decodeYaml(dir) 18 | } else if _, err := os.Stat(filepath.Join(dir, "main.json")); !os.IsNotExist(err) { 19 | main, err = r.decodeJson(dir) 20 | } else { 21 | return errors.New("missing main.yaml or main.json file") 22 | } 23 | if err != nil { 24 | return err 25 | } 26 | 27 | if len(main.Name) == 0 { 28 | return errors.New("missing layout name") 29 | } 30 | if main.Timeout == 0 { 31 | main.Timeout = 3600 32 | } 33 | 34 | for i, t := range main.Tasks { 35 | if len(t.Name) == 0 { 36 | return fmt.Errorf("missing task name for task index %d", i+1) 37 | } 38 | if len(t.Plugin) == 0 { 39 | return fmt.Errorf("missing plugin for task [%s]", t.Name) 40 | } 41 | handler, ok := handlers[t.Plugin] 42 | if !ok { 43 | return fmt.Errorf("plugin %s not supported", t.Plugin) 44 | } 45 | err = handler.Check(t) 46 | if err != nil { 47 | return err 48 | } 49 | var info taskInfo 50 | info.parent = r 51 | info.name = t.Name 52 | info.auth = t.Auth 53 | info.timeout = time.Duration(t.Timeout) * time.Second 54 | if len(info.auth) > 0 { 55 | switch info.auth { 56 | case "sudo": 57 | case "su": 58 | default: 59 | return fmt.Errorf("unexpected auth argument for task [%s]", t.Name) 60 | } 61 | if len(r.user) == 0 { 62 | return fmt.Errorf("specify auth argument for task [%s] but missing user argument on calling", t.Name) 63 | } 64 | } 65 | if len(t.If) > 0 { 66 | err = info.parseIf(t.If) 67 | if err != nil { 68 | return fmt.Errorf("parse if expression failed for task [%s]: %v", t.Name, err) 69 | } 70 | } 71 | r.tasks = append(r.tasks, handler.Clone(t, info)) 72 | } 73 | 74 | r.deadline = time.Now().Add(time.Duration(main.Timeout) * time.Second) 75 | 76 | return nil 77 | } 78 | 79 | func (r *runner) decodeYaml(dir string) (*Main, error) { 80 | f, err := os.Open(filepath.Join(dir, "main.yaml")) 81 | if err != nil { 82 | return nil, err 83 | } 84 | defer f.Close() 85 | var main Main 86 | err = yaml.NewDecoder(f).Decode(&main) 87 | if err != nil { 88 | return nil, fmt.Errorf("decode main.yaml: %v", err) 89 | } 90 | return &main, err 91 | } 92 | 93 | func (r *runner) decodeJson(dir string) (*Main, error) { 94 | f, err := os.Open(filepath.Join(dir, "main.json")) 95 | if err != nil { 96 | return nil, err 97 | } 98 | defer f.Close() 99 | var main Main 100 | err = yaml.NewDecoder(f).Decode(&main) 101 | if err != nil { 102 | return nil, fmt.Errorf("decode main.json: %v", err) 103 | } 104 | return &main, err 105 | } 106 | -------------------------------------------------------------------------------- /code/api/file/handler.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "os" 5 | lapi "server/code/api" 6 | "server/code/client" 7 | "server/code/conf" 8 | "sync" 9 | "time" 10 | 11 | "github.com/jkstack/anet" 12 | "github.com/jkstack/jkframe/stat" 13 | "github.com/lwch/api" 14 | "github.com/lwch/logging" 15 | ) 16 | 17 | // Handler cmd handler 18 | type Handler struct { 19 | sync.RWMutex 20 | cfg *conf.Configure 21 | uploadCache map[string]*uploadInfo 22 | stUsage *stat.Counter 23 | stTotalTasks *stat.Counter 24 | } 25 | 26 | // New new cmd handler 27 | func New() *Handler { 28 | h := &Handler{ 29 | uploadCache: make(map[string]*uploadInfo), 30 | } 31 | go h.clear() 32 | return h 33 | } 34 | 35 | // Init init handler 36 | func (h *Handler) Init(cfg *conf.Configure, stats *stat.Mgr) { 37 | h.cfg = cfg 38 | h.stUsage = stats.NewCounter("plugin_count_file") 39 | h.stTotalTasks = stats.NewCounter(lapi.TotalTasksLabel) 40 | } 41 | 42 | // HandleFuncs get handle functions 43 | func (h *Handler) HandleFuncs() map[string]func(*client.Clients, *api.Context) { 44 | return map[string]func(*client.Clients, *api.Context){ 45 | "/file/ls": h.ls, 46 | "/file/download": h.download, 47 | "/file/upload": h.upload, 48 | "/file/upload/": h.uploadHandle, 49 | "/file/upload_from": h.uploadFrom, 50 | } 51 | } 52 | 53 | func (h *Handler) OnConnect(*client.Client) { 54 | } 55 | 56 | // OnClose agent on close 57 | func (h *Handler) OnClose(string) { 58 | } 59 | 60 | func (h *Handler) OnMessage(*client.Client, *anet.Msg) { 61 | } 62 | 63 | func (h *Handler) clear() { 64 | run := func() { 65 | list := make([]string, 0, len(h.uploadCache)) 66 | h.RLock() 67 | for id, cache := range h.uploadCache { 68 | if time.Now().After(cache.timeout) { 69 | list = append(list, id) 70 | } 71 | } 72 | h.RUnlock() 73 | if len(list) > 0 { 74 | logging.Info("collected %d files to remove from upload cache", len(list)) 75 | } 76 | var cnt int 77 | for _, id := range list { 78 | if h.RemoveUploadCache(id) { 79 | cnt++ 80 | } 81 | } 82 | if len(list) > 0 { 83 | logging.Info("%d files removed from upload cache", cnt) 84 | } 85 | } 86 | for { 87 | run() 88 | time.Sleep(time.Minute) 89 | } 90 | } 91 | 92 | func (h *Handler) LogUploadCache(taskID, dir, token string, 93 | deadline time.Time, rm bool) { 94 | h.Lock() 95 | h.uploadCache[taskID] = &uploadInfo{ 96 | token: token, 97 | dir: dir, 98 | rm: rm, 99 | timeout: deadline, 100 | } 101 | h.Unlock() 102 | logging.Info("log cache: %s", taskID) 103 | } 104 | 105 | func (h *Handler) RemoveUploadCache(id string) bool { 106 | h.Lock() 107 | cache := h.uploadCache[id] 108 | delete(h.uploadCache, id) 109 | h.Unlock() 110 | if cache == nil { 111 | return false 112 | } 113 | logging.Info("removed cache: %s", id) 114 | if cache.rm { 115 | os.Remove(cache.dir) 116 | } 117 | return true 118 | } 119 | -------------------------------------------------------------------------------- /code/api/install/handler.go: -------------------------------------------------------------------------------- 1 | package install 2 | 3 | import ( 4 | lapi "server/code/api" 5 | "server/code/client" 6 | "server/code/conf" 7 | "server/code/utils" 8 | "sync" 9 | "time" 10 | 11 | "github.com/jkstack/anet" 12 | "github.com/jkstack/jkframe/stat" 13 | "github.com/lwch/api" 14 | ) 15 | 16 | type Action struct { 17 | Action string `json:"action"` 18 | Time int64 `json:"time"` 19 | Name string `json:"name"` 20 | OK bool `json:"ok"` 21 | Msg string `json:"msg"` 22 | } 23 | 24 | type Info struct { 25 | updated time.Time 26 | Done bool `json:"done"` 27 | Actions []Action `json:"actions"` 28 | } 29 | 30 | // Handler cmd handler 31 | type Handler struct { 32 | sync.RWMutex 33 | cfg *conf.Configure 34 | data map[string]*Info 35 | stUsage *stat.Counter 36 | stTotalTasks *stat.Counter 37 | } 38 | 39 | // New new cmd handler 40 | func New() *Handler { 41 | h := &Handler{data: make(map[string]*Info)} 42 | go h.clear() 43 | return h 44 | } 45 | 46 | // Init init handler 47 | func (h *Handler) Init(cfg *conf.Configure, stats *stat.Mgr) { 48 | h.cfg = cfg 49 | h.stUsage = stats.NewCounter("plugin_count_install") 50 | h.stTotalTasks = stats.NewCounter(lapi.TotalTasksLabel) 51 | } 52 | 53 | // HandleFuncs get handle functions 54 | func (h *Handler) HandleFuncs() map[string]func(*client.Clients, *api.Context) { 55 | return map[string]func(*client.Clients, *api.Context){ 56 | "/install/run": h.run, 57 | "/install/status": h.status, 58 | } 59 | } 60 | 61 | func (h *Handler) OnConnect(*client.Client) { 62 | } 63 | 64 | // OnClose agent on close 65 | func (h *Handler) OnClose(string) { 66 | } 67 | 68 | func (h *Handler) OnMessage(*client.Client, *anet.Msg) { 69 | } 70 | 71 | func (h *Handler) loop(cli *client.Client, taskID string) { 72 | ch := cli.ChanRead(taskID) 73 | h.RLock() 74 | info := h.data[taskID] 75 | h.RUnlock() 76 | for { 77 | msg := <-ch 78 | switch msg.Type { 79 | case anet.TypeInstallRep: 80 | rep := msg.InstallRep 81 | add := func(act string) { 82 | info.updated = time.Now() 83 | info.Actions = append(info.Actions, Action{ 84 | Action: act, 85 | Time: rep.Time, 86 | Name: rep.Name, 87 | OK: rep.OK, 88 | Msg: rep.Msg, 89 | }) 90 | } 91 | switch rep.Action { 92 | case anet.InstallActionDownload: 93 | add("download") 94 | case anet.InstallActionInstall: 95 | add("install") 96 | case anet.InstallActionDone: 97 | add("done") 98 | info.Done = true 99 | } 100 | case anet.TypeError: 101 | info.updated = time.Now() 102 | info.Actions = append(info.Actions, Action{ 103 | Action: "done", 104 | Time: time.Now().Unix(), 105 | OK: false, 106 | Msg: msg.ErrorMsg, 107 | }) 108 | info.Done = true 109 | } 110 | } 111 | } 112 | 113 | func (h *Handler) clear() { 114 | for { 115 | time.Sleep(time.Minute) 116 | h.remove() 117 | } 118 | } 119 | 120 | func (h *Handler) remove() { 121 | defer utils.Recover("remove") 122 | remove := make([]string, 0, len(h.data)) 123 | h.RLock() 124 | for id, info := range h.data { 125 | if time.Since(info.updated).Hours() > 24 { 126 | remove = append(remove, id) 127 | } 128 | } 129 | h.RUnlock() 130 | 131 | h.Lock() 132 | for _, id := range remove { 133 | delete(h.data, id) 134 | } 135 | h.Unlock() 136 | } 137 | -------------------------------------------------------------------------------- /code/client/send_logging.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "server/code/utils" 5 | 6 | "github.com/jkstack/anet" 7 | ) 8 | 9 | func (cli *Client) SendLoggingConfigK8s(pid int64, ext string, batch, buffer, interval int, report string, 10 | ns string, names []string, dir, api, token string) (string, error) { 11 | taskID, err := utils.TaskID() 12 | if err != nil { 13 | return "", err 14 | } 15 | var msg anet.Msg 16 | msg.Type = anet.TypeLoggingConfig 17 | msg.TaskID = taskID 18 | msg.LoggingConfig = &anet.LoggingConfig{ 19 | Pid: pid, 20 | T: anet.LoggingTypeK8s, 21 | Exclude: ext, 22 | Batch: batch, 23 | Buffer: buffer, 24 | Interval: interval, 25 | Report: report, 26 | K8s: &anet.LoggingConfigK8s{ 27 | Namespace: ns, 28 | Names: names, 29 | Dir: dir, 30 | Api: api, 31 | Token: token, 32 | }, 33 | } 34 | cli.chWrite <- &msg 35 | return taskID, nil 36 | } 37 | 38 | func (cli *Client) SendLoggingConfigDocker(pid int64, ext string, batch, buffer, interval int, report string, 39 | containerName, containerTag, dir string) (string, error) { 40 | taskID, err := utils.TaskID() 41 | if err != nil { 42 | return "", err 43 | } 44 | var msg anet.Msg 45 | msg.Type = anet.TypeLoggingConfig 46 | msg.TaskID = taskID 47 | msg.LoggingConfig = &anet.LoggingConfig{ 48 | Pid: pid, 49 | T: anet.LoggingTypeDocker, 50 | Exclude: ext, 51 | Batch: batch, 52 | Buffer: buffer, 53 | Interval: interval, 54 | Report: report, 55 | Docker: &anet.LoggingConfigDocker{ 56 | ContainerName: containerName, 57 | ContainerTag: containerTag, 58 | Dir: dir, 59 | }, 60 | } 61 | cli.chWrite <- &msg 62 | return taskID, nil 63 | } 64 | 65 | func (cli *Client) SendLoggingConfigFile(pid int64, ext string, batch, buffer, interval int, report string, 66 | dir string) (string, error) { 67 | taskID, err := utils.TaskID() 68 | if err != nil { 69 | return "", err 70 | } 71 | var msg anet.Msg 72 | msg.Type = anet.TypeLoggingConfig 73 | msg.TaskID = taskID 74 | msg.LoggingConfig = &anet.LoggingConfig{ 75 | Pid: pid, 76 | T: anet.LoggingTypeFile, 77 | Exclude: ext, 78 | Batch: batch, 79 | Buffer: buffer, 80 | Interval: interval, 81 | Report: report, 82 | File: &anet.LoggingConfigFile{ 83 | Dir: dir, 84 | }, 85 | } 86 | cli.chWrite <- &msg 87 | return taskID, nil 88 | } 89 | 90 | func (cli *Client) SendLoggingStart(id int64) (string, error) { 91 | taskID, err := utils.TaskID() 92 | if err != nil { 93 | return "", err 94 | } 95 | var msg anet.Msg 96 | msg.Type = anet.TypeLoggingStatusReq 97 | msg.TaskID = taskID 98 | msg.LoggingStatusReq = &anet.LoggingStatusReq{ 99 | ID: id, 100 | Running: true, 101 | } 102 | cli.Lock() 103 | cli.taskRead[taskID] = make(chan *anet.Msg) 104 | cli.Unlock() 105 | cli.chWrite <- &msg 106 | return taskID, nil 107 | } 108 | 109 | func (cli *Client) SendLoggingStop(id int64) (string, error) { 110 | taskID, err := utils.TaskID() 111 | if err != nil { 112 | return "", err 113 | } 114 | var msg anet.Msg 115 | msg.Type = anet.TypeLoggingStatusReq 116 | msg.TaskID = taskID 117 | msg.LoggingStatusReq = &anet.LoggingStatusReq{ 118 | ID: id, 119 | Running: false, 120 | } 121 | cli.Lock() 122 | cli.taskRead[taskID] = make(chan *anet.Msg) 123 | cli.Unlock() 124 | cli.chWrite <- &msg 125 | return taskID, nil 126 | } 127 | -------------------------------------------------------------------------------- /code/api/logging/handler.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | lapi "server/code/api" 8 | "server/code/client" 9 | "server/code/conf" 10 | "sync" 11 | 12 | "github.com/jkstack/anet" 13 | "github.com/jkstack/jkframe/stat" 14 | "github.com/lwch/api" 15 | "github.com/lwch/runtime" 16 | "github.com/prometheus/client_golang/prometheus" 17 | ) 18 | 19 | // Handler server handler 20 | type Handler struct { 21 | sync.RWMutex 22 | cfg *conf.Configure 23 | data map[int64]*context 24 | stTotalTasks *stat.Counter 25 | stK8s *prometheus.GaugeVec 26 | stDocker *prometheus.GaugeVec 27 | stFile *prometheus.GaugeVec 28 | stAgentVersion *prometheus.GaugeVec 29 | stAgent *prometheus.GaugeVec 30 | stReporter *prometheus.GaugeVec 31 | } 32 | 33 | // New new cmd handler 34 | func New() *Handler { 35 | return &Handler{ 36 | data: make(map[int64]*context), 37 | } 38 | } 39 | 40 | // Init init handler 41 | func (h *Handler) Init(cfg *conf.Configure, stats *stat.Mgr) { 42 | h.cfg = cfg 43 | runtime.Assert(h.loadConfig(filepath.Join(h.cfg.DataDir, "logging"))) 44 | h.stTotalTasks = stats.NewCounter(lapi.TotalTasksLabel) 45 | h.stK8s = stats.RawVec("agent_logging_k8s_info", []string{"id", "agent_type", "tag"}) 46 | h.stDocker = stats.RawVec("agent_logging_docker_info", []string{"id", "agent_type", "tag"}) 47 | h.stFile = stats.RawVec("agent_logging_file_info", []string{"id", "agent_type", "tag"}) 48 | h.stAgentVersion = stats.RawVec("agent_version", []string{"id", "agent_type", "version", "go_version"}) 49 | h.stAgent = stats.RawVec("agent_info", []string{"id", "agent_type", "tag"}) 50 | h.stReporter = stats.RawVec("agent_logging_reporter_info", []string{"id", "agent_type", "tag"}) 51 | } 52 | 53 | // HandleFuncs get handle functions 54 | func (h *Handler) HandleFuncs() map[string]func(*client.Clients, *api.Context) { 55 | return map[string]func(*client.Clients, *api.Context){ 56 | "/logging/config": h.config, 57 | "/logging/start": h.start, 58 | "/logging/stop": h.stop, 59 | "/logging/remove": h.remove, 60 | } 61 | } 62 | 63 | func (h *Handler) OnConnect(cli *client.Client) { 64 | var send []*context 65 | h.RLock() 66 | for _, ctx := range h.data { 67 | if ctx.in(cli.ID()) { 68 | send = append(send, ctx) 69 | } 70 | } 71 | h.RUnlock() 72 | // TODO: collect not running 73 | for _, ctx := range send { 74 | ctx.reSend(cli, h.cfg.LoggingReport) 75 | } 76 | } 77 | 78 | // OnClose agent on close 79 | func (h *Handler) OnClose(string) { 80 | } 81 | 82 | func (h *Handler) OnMessage(cli *client.Client, msg *anet.Msg) { 83 | if msg.Type != anet.TypeLoggingReport { 84 | return 85 | } 86 | h.onReport(cli, *msg.LoggingReport) 87 | } 88 | 89 | func (h *Handler) loadConfig(dir string) error { 90 | os.MkdirAll(dir, 0755) 91 | files, err := filepath.Glob(filepath.Join(dir, "*.json")) 92 | if err != nil { 93 | return err 94 | } 95 | load := func(dir string) error { 96 | f, err := os.Open(dir) 97 | if err != nil { 98 | return err 99 | } 100 | defer f.Close() 101 | var ctx context 102 | err = json.NewDecoder(f).Decode(&ctx) 103 | if err != nil { 104 | return err 105 | } 106 | ctx.Args.parent = &ctx 107 | ctx.parent = h 108 | h.Lock() 109 | h.data[ctx.ID] = &ctx 110 | h.Unlock() 111 | return nil 112 | } 113 | for _, file := range files { 114 | err = load(file) 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /code/client/clients.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/gorilla/websocket" 10 | "github.com/jkstack/anet" 11 | "github.com/jkstack/jkframe/stat" 12 | "github.com/lwch/logging" 13 | ) 14 | 15 | // Clients clients 16 | type Clients struct { 17 | sync.RWMutex 18 | data map[string]*Client 19 | stInPackets *stat.Counter 20 | stOutPackets *stat.Counter 21 | stInBytes *stat.Counter 22 | stOutBytes *stat.Counter 23 | } 24 | 25 | func NewClients(stats *stat.Mgr) *Clients { 26 | clients := &Clients{ 27 | data: make(map[string]*Client), 28 | stInPackets: stats.NewCounter("in_packets"), 29 | stOutPackets: stats.NewCounter("out_packets"), 30 | stInBytes: stats.NewCounter("in_bytes"), 31 | stOutBytes: stats.NewCounter("out_bytes"), 32 | } 33 | go clients.print() 34 | return clients 35 | } 36 | 37 | // New new client 38 | func (cs *Clients) New(conn *websocket.Conn, come *anet.ComePayload, cancel context.CancelFunc) *Client { 39 | t := "smart" 40 | if come.Name == "godagent" { 41 | t = "god" 42 | } 43 | cli := &Client{ 44 | t: t, 45 | parent: cs, 46 | info: *come, 47 | remote: conn, 48 | chRead: make(chan *anet.Msg, channelBuffer), 49 | chWrite: make(chan *anet.Msg, channelBuffer), 50 | taskRead: make(map[string]chan *anet.Msg, channelBuffer/10), 51 | cancel: cancel, 52 | } 53 | ctx, cancel := context.WithCancel(context.Background()) 54 | go cli.read(ctx, cancel) 55 | go cli.write(ctx, cancel) 56 | go func() { 57 | <-ctx.Done() 58 | 59 | cli.close() 60 | cs.Lock() 61 | delete(cs.data, come.ID) 62 | cs.Unlock() 63 | 64 | cancel() 65 | }() 66 | return cli 67 | } 68 | 69 | // Add add client 70 | func (cs *Clients) Add(cli *Client) { 71 | cs.Lock() 72 | defer cs.Unlock() 73 | if old := cs.data[cli.info.ID]; old != nil { 74 | old.close() 75 | } 76 | cs.data[cli.info.ID] = cli 77 | } 78 | 79 | // Get get client by id 80 | func (cs *Clients) Get(id string) *Client { 81 | cs.RLock() 82 | defer cs.RUnlock() 83 | return cs.data[id] 84 | } 85 | 86 | // Range list clients 87 | func (cs *Clients) Range(cb func(*Client) bool) { 88 | cs.RLock() 89 | defer cs.RUnlock() 90 | for _, c := range cs.data { 91 | next := cb(c) 92 | if !next { 93 | return 94 | } 95 | } 96 | } 97 | 98 | // Size get clients count 99 | func (cs *Clients) Size() int { 100 | return len(cs.data) 101 | } 102 | 103 | func (cs *Clients) print() { 104 | var logs []string 105 | cs.RLock() 106 | for _, cli := range cs.data { 107 | if len(cli.chWrite) > 0 || len(cli.chRead) > 0 { 108 | logs = append(logs, fmt.Sprintf("client %s: write chan=%d, read chan=%d", 109 | cli.ID(), len(cli.chWrite), len(cli.chRead))) 110 | } 111 | } 112 | cs.RUnlock() 113 | logging.Info(strings.Join(logs, "\n")) 114 | } 115 | 116 | func (cs *Clients) Prefix(str string) []*Client { 117 | var ret []*Client 118 | cs.RLock() 119 | for id, cli := range cs.data { 120 | if strings.HasPrefix(id, str) { 121 | ret = append(ret, cli) 122 | } 123 | } 124 | cs.RUnlock() 125 | return ret 126 | } 127 | 128 | func (cs *Clients) Contains(str string) []*Client { 129 | var ret []*Client 130 | cs.RLock() 131 | for id, cli := range cs.data { 132 | if strings.Contains(id, str) { 133 | ret = append(ret, cli) 134 | } 135 | } 136 | cs.RUnlock() 137 | return ret 138 | } 139 | 140 | func (cs *Clients) All() []*Client { 141 | var ret []*Client 142 | cs.RLock() 143 | for _, cli := range cs.data { 144 | ret = append(ret, cli) 145 | } 146 | cs.RUnlock() 147 | return ret 148 | } 149 | -------------------------------------------------------------------------------- /code/conf/plugin.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "server/code/utils" 11 | "sync" 12 | 13 | "github.com/lwch/logging" 14 | "github.com/lwch/runtime" 15 | ) 16 | 17 | type PluginInfo struct { 18 | Name string 19 | Version utils.Version 20 | OS string 21 | Arch string 22 | Dir string 23 | MD5 [md5.Size]byte 24 | URI string 25 | } 26 | 27 | type PluginMD5List struct { 28 | sync.RWMutex 29 | data map[string]PluginInfo // md5 => info 30 | } 31 | 32 | func newPluginMD5List() *PluginMD5List { 33 | return &PluginMD5List{ 34 | data: make(map[string]PluginInfo), 35 | } 36 | } 37 | 38 | func (list *PluginMD5List) update(enc string, info PluginInfo) { 39 | list.Lock() 40 | list.data[enc] = info 41 | list.Unlock() 42 | } 43 | 44 | func (list *PluginMD5List) get(ver utils.Version, os, arch string) *PluginInfo { 45 | if list == nil { 46 | return nil 47 | } 48 | var ret *PluginInfo 49 | list.RLock() 50 | for _, p := range list.data { 51 | if p.Version.Equal(ver) && 52 | p.OS == os && p.Arch == arch { 53 | ret = &p 54 | break 55 | } 56 | } 57 | list.RUnlock() 58 | return ret 59 | } 60 | 61 | func (list *PluginMD5List) by(md5 string) *PluginInfo { 62 | if list == nil { 63 | return nil 64 | } 65 | list.RLock() 66 | defer list.RUnlock() 67 | if p, ok := list.data[md5]; ok { 68 | return &p 69 | } 70 | return nil 71 | } 72 | 73 | type SupportedItem struct { 74 | OS string `json:"os"` 75 | Arch string `json:"arch"` 76 | File string `json:"file"` 77 | } 78 | 79 | type Manifest struct { 80 | Name string `json:"name"` 81 | Version string `json:"version"` 82 | Supported []SupportedItem `json:"supported"` 83 | } 84 | 85 | func (cfg *Configure) LoadPlugin() { 86 | files, err := filepath.Glob(path.Join(cfg.PluginDir, "*")) // name 87 | runtime.Assert(err) 88 | for _, file := range files { 89 | cfg.Lock() 90 | if _, ok := cfg.PluginList[path.Base(file)]; !ok { 91 | cfg.PluginList[path.Base(file)] = newPluginMD5List() 92 | } 93 | cfg.Unlock() 94 | files, err := filepath.Glob(path.Join(file, "*")) // version 95 | runtime.Assert(err) 96 | cfg.Lock() 97 | delete(cfg.PluginLatest, path.Base(file)) 98 | cfg.Unlock() 99 | for _, file := range files { 100 | version := path.Base(file) 101 | dir := path.Join(file, "manifest.json") 102 | if _, err := os.Stat(dir); os.IsNotExist(err) { 103 | continue 104 | } 105 | cfg.loadManifest(dir, version) 106 | } 107 | } 108 | } 109 | 110 | func (cfg *Configure) loadManifest(dir, version string) { 111 | f, err := os.Open(dir) 112 | runtime.Assert(err) 113 | defer f.Close() 114 | var mf Manifest 115 | runtime.Assert(json.NewDecoder(f).Decode(&mf)) 116 | ver, err := utils.ParseVersion(version) 117 | runtime.Assert(err) 118 | for _, it := range mf.Supported { 119 | pd := path.Join(path.Dir(dir), it.File) 120 | md5, err := utils.MD5Checksum(pd) 121 | runtime.Assert(err) 122 | enc := fmt.Sprintf("%x", md5) 123 | cfg.PluginList[mf.Name].update(enc, PluginInfo{ 124 | Name: mf.Name, 125 | Version: ver, 126 | OS: it.OS, 127 | Arch: it.Arch, 128 | Dir: pd, 129 | MD5: md5, 130 | URI: fmt.Sprintf("/file/plugin/%s/%s", mf.Name, enc), 131 | }) 132 | logging.Info("load plugin %s, os=%s, arch=%s, version=%s, md5=%x", 133 | mf.Name, it.OS, it.Arch, ver.String(), md5) 134 | if ver.Greater(cfg.PluginLatest[mf.Name]) { 135 | cfg.Lock() 136 | cfg.PluginLatest[mf.Name] = ver 137 | cfg.Unlock() 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /code/api/logging/report.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "server/code/client" 5 | 6 | "github.com/jkstack/anet" 7 | "github.com/prometheus/client_golang/prometheus" 8 | ) 9 | 10 | func setValue(vec *prometheus.GaugeVec, id, tag string, n float64) { 11 | vec.With(prometheus.Labels{ 12 | "id": id, 13 | "agent_type": "god", 14 | "tag": tag, 15 | }).Set(n) 16 | } 17 | 18 | func (h *Handler) onReport(cli *client.Client, data anet.LoggingReport) { 19 | setValue(h.stK8s, cli.ID(), "count", float64(data.CountK8s)) 20 | setValue(h.stDocker, cli.ID(), "count", float64(data.CountDocker)) 21 | setValue(h.stFile, cli.ID(), "count", float64(data.CountFile)) 22 | h.onReportAgent(cli.ID(), data.AgentInfo) 23 | h.onReportReporter(cli.ID(), data.Info) 24 | h.onReportK8s(cli.ID(), data.K8s) 25 | h.onReportDocker(cli.ID(), data.Docker) 26 | h.onReportFile(cli.ID(), data.File) 27 | } 28 | 29 | func (h *Handler) onReportAgent(id string, info anet.LoggingReportAgentInfo) { 30 | h.stAgentVersion.With(prometheus.Labels{ 31 | "id": id, 32 | "agent_type": "god", 33 | "version": "0.0.0", // TODO 34 | "go_version": info.GoVersion, 35 | }).Set(1) 36 | setValue(h.stAgent, id, "threads", float64(info.Threads)) 37 | setValue(h.stAgent, id, "routines", float64(info.Routines)) 38 | setValue(h.stAgent, id, "startup", float64(info.Startup)) 39 | setValue(h.stAgent, id, "heap_in_use", float64(info.HeapInuse)) 40 | setValue(h.stAgent, id, "gc_0", info.GC["0"]) 41 | setValue(h.stAgent, id, "gc_0.25", info.GC["25"]) 42 | setValue(h.stAgent, id, "gc_0.5", info.GC["50"]) 43 | setValue(h.stAgent, id, "gc_0.75", info.GC["75"]) 44 | setValue(h.stAgent, id, "gc_1", info.GC["100"]) 45 | setValue(h.stAgent, id, "in_packets", float64(info.InPackets)) 46 | setValue(h.stAgent, id, "in_bytes", float64(info.InBytes)) 47 | setValue(h.stAgent, id, "out_packets", float64(info.OutPackets)) 48 | setValue(h.stAgent, id, "out_bytes", float64(info.OutBytes)) 49 | } 50 | 51 | func (h *Handler) onReportReporter(id string, info anet.LoggingReportInfo) { 52 | setValue(h.stReporter, id, "qps", info.QPS) 53 | setValue(h.stReporter, id, "avg_cost", info.AvgCost) 54 | setValue(h.stReporter, id, "p_0", float64(info.P0)) 55 | setValue(h.stReporter, id, "p_50", float64(info.P50)) 56 | setValue(h.stReporter, id, "p_90", float64(info.P90)) 57 | setValue(h.stReporter, id, "p_99", float64(info.P99)) 58 | setValue(h.stReporter, id, "p_100", float64(info.P100)) 59 | setValue(h.stReporter, id, "count", float64(info.Count)) 60 | setValue(h.stReporter, id, "bytes", float64(info.Bytes)) 61 | setValue(h.stReporter, id, "http_error_count", float64(info.HttpErr)) 62 | setValue(h.stReporter, id, "service_error_count", float64(info.SrvErr)) 63 | } 64 | 65 | func (h *Handler) onReportK8s(id string, info anet.LoggingReportK8sData) { 66 | setValue(h.stK8s, id, "qps", info.QPS) 67 | setValue(h.stK8s, id, "avg_cost", info.AvgCost) 68 | setValue(h.stK8s, id, "p_0", float64(info.P0)) 69 | setValue(h.stK8s, id, "p_50", float64(info.P50)) 70 | setValue(h.stK8s, id, "p_90", float64(info.P90)) 71 | setValue(h.stK8s, id, "p_99", float64(info.P99)) 72 | setValue(h.stK8s, id, "p_100", float64(info.P100)) 73 | setValue(h.stK8s, id, "count_service", float64(info.CountService)) 74 | setValue(h.stK8s, id, "count_pod", float64(info.CountPod)) 75 | setValue(h.stK8s, id, "count_container", float64(info.CountContainer)) 76 | } 77 | 78 | func (h *Handler) onReportDocker(id string, info anet.LoggingReportDockerData) { 79 | setValue(h.stDocker, id, "count_container", float64(info.Count)) 80 | } 81 | 82 | func (h *Handler) onReportFile(id string, info anet.LoggingReportFileData) { 83 | setValue(h.stFile, id, "count_files", float64(info.Count)) 84 | } 85 | -------------------------------------------------------------------------------- /code/api/cmd/process.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "path" 11 | "server/code/conf" 12 | "time" 13 | 14 | "github.com/jkstack/anet" 15 | "github.com/lwch/l2cache" 16 | "github.com/lwch/logging" 17 | ) 18 | 19 | const memoryCacheSize = 102400 20 | 21 | var callbackCli = &http.Client{Timeout: time.Minute} 22 | 23 | type process struct { 24 | parent *cmdClient 25 | id int 26 | cmd string 27 | callback string 28 | created time.Time 29 | updated time.Time 30 | taskID string 31 | running bool 32 | code int 33 | cache *l2cache.Cache 34 | ctx context.Context 35 | cancel context.CancelFunc 36 | } 37 | 38 | func newProcess(parent *cmdClient, cfg *conf.Configure, id int, cmd, taskID, callback string) (*process, error) { 39 | cache, err := l2cache.New(memoryCacheSize, path.Join(cfg.CacheDir, "cmd")) 40 | if err != nil { 41 | return nil, err 42 | } 43 | ctx, cancel := context.WithCancel(context.Background()) 44 | return &process{ 45 | parent: parent, 46 | id: id, 47 | cmd: cmd, 48 | callback: callback, 49 | created: time.Now(), 50 | updated: time.Now(), 51 | taskID: taskID, 52 | running: true, 53 | code: -65535, 54 | cache: cache, 55 | ctx: ctx, 56 | cancel: cancel, 57 | }, nil 58 | } 59 | 60 | func (p *process) recv() { 61 | defer p.cancel() 62 | defer p.cb() 63 | cli := p.parent.cli 64 | ch := cli.ChanRead(p.taskID) 65 | defer cli.ChanClose(p.taskID) 66 | for { 67 | var msg *anet.Msg 68 | select { 69 | case msg = <-ch: 70 | case <-p.ctx.Done(): 71 | return 72 | } 73 | if msg == nil { 74 | return 75 | } 76 | p.updated = time.Now() 77 | switch msg.Type { 78 | case anet.TypeError: 79 | p.running = false 80 | p.code = -255 81 | p.cache.Write([]byte(msg.ErrorMsg)) 82 | logging.Error("task %s on [%s] run failed: %s", p.taskID, cli.ID(), msg.ErrorMsg) 83 | return 84 | case anet.TypeExecData: 85 | data, _ := base64.StdEncoding.DecodeString(msg.ExecData.Data) 86 | p.cache.Write(data) 87 | logging.Debug("task %s on [%s] recved %d bytes", p.taskID, cli.ID(), len(data)) 88 | case anet.TypeExecDone: 89 | p.running = false 90 | p.code = msg.ExecDone.Code 91 | logging.Info("task %s on [%s] run done, code=%d", p.taskID, cli.ID(), msg.ExecDone.Code) 92 | return 93 | } 94 | } 95 | } 96 | 97 | func (p *process) close() { 98 | p.cache.Close() 99 | p.cancel() 100 | } 101 | 102 | func (p *process) read() ([]byte, error) { 103 | return ioutil.ReadAll(p.cache) 104 | } 105 | 106 | func (p *process) sendKill(plugin *conf.PluginInfo) string { 107 | id, _ := p.parent.cli.SendKill(plugin, p.id) 108 | return id 109 | } 110 | 111 | func (p *process) wait() { 112 | <-p.ctx.Done() 113 | } 114 | 115 | func (p *process) cb() { 116 | if len(p.callback) == 0 { 117 | return 118 | } 119 | u, err := url.Parse(p.callback) 120 | if err != nil { 121 | logging.Error("parse for callback url [%s]: %v", p.callback, err) 122 | return 123 | } 124 | args := u.Query() 125 | args.Set("agent_id", p.parent.cli.ID()) 126 | args.Set("pid", fmt.Sprintf("%d", p.id)) 127 | u.RawQuery = args.Encode() 128 | resp, err := callbackCli.Get(u.String()) 129 | if err != nil { 130 | logging.Error("callback to [%s]: %v", p.callback, err) 131 | return 132 | } 133 | defer resp.Body.Close() 134 | if resp.StatusCode != http.StatusOK { 135 | data, _ := ioutil.ReadAll(resp.Body) 136 | logging.Error("callback to [%s] is not http200: %d\n%s", resp.StatusCode, string(data)) 137 | return 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /code/sshcli/client.go: -------------------------------------------------------------------------------- 1 | package sshcli 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "os" 8 | "strings" 9 | 10 | "github.com/pkg/sftp" 11 | "golang.org/x/crypto/ssh" 12 | ) 13 | 14 | type Client struct { 15 | cli *ssh.Client 16 | } 17 | 18 | func New(addr, user, pass string) (*Client, error) { 19 | cli, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{ 20 | User: user, 21 | Auth: []ssh.AuthMethod{ssh.Password(pass)}, 22 | HostKeyCallback: func(string, net.Addr, ssh.PublicKey) error { 23 | return nil 24 | }, 25 | }) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &Client{cli}, nil 30 | } 31 | 32 | func (c *Client) Close() error { 33 | return c.cli.Close() 34 | } 35 | 36 | func (c *Client) Do(ctx context.Context, cmd, stdin string) (string, error) { 37 | session, err := c.cli.NewSession() 38 | if err != nil { 39 | return "", err 40 | } 41 | modes := ssh.TerminalModes{ 42 | ssh.ECHO: 1, // enable echoing 43 | ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud 44 | ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud 45 | } 46 | err = session.RequestPty("xterm", 24, 80, modes) 47 | if err != nil { 48 | return "", err 49 | } 50 | var buf writer 51 | if len(stdin) > 0 { 52 | session.Stdin = strings.NewReader(stdin) 53 | } 54 | session.Stdout = &buf 55 | session.Stderr = &buf 56 | err = session.Start(cmd) 57 | if err != nil { 58 | return "", err 59 | } 60 | ch := make(chan error) 61 | go func() { 62 | ch <- session.Wait() 63 | session.Close() 64 | }() 65 | select { 66 | case err = <-ch: 67 | case <-ctx.Done(): 68 | err = ctx.Err() 69 | } 70 | if len(stdin) > 0 { 71 | return strings.TrimPrefix(buf.String(), strings.ReplaceAll(stdin, "\n", "\r\n")), err 72 | } 73 | return buf.String(), err 74 | } 75 | 76 | func (c *Client) Upload(src, dst string) error { 77 | cli, err := sftp.NewClient(c.cli) 78 | if err != nil { 79 | return err 80 | } 81 | defer cli.Close() 82 | srcFile, err := os.Open(src) 83 | if err != nil { 84 | return err 85 | } 86 | defer srcFile.Close() 87 | dstFile, err := cli.Create(dst) 88 | if err != nil { 89 | return err 90 | } 91 | defer dstFile.Close() 92 | _, err = io.Copy(dstFile, srcFile) 93 | if err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | 99 | func (c *Client) UploadFrom(src io.Reader, dst string) error { 100 | cli, err := sftp.NewClient(c.cli) 101 | if err != nil { 102 | return err 103 | } 104 | defer cli.Close() 105 | dstFile, err := cli.Create(dst) 106 | if err != nil { 107 | return err 108 | } 109 | defer dstFile.Close() 110 | _, err = io.Copy(dstFile, src) 111 | if err != nil { 112 | return err 113 | } 114 | return nil 115 | } 116 | 117 | func (c *Client) Remove(dir string) error { 118 | cli, err := sftp.NewClient(c.cli) 119 | if err != nil { 120 | return err 121 | } 122 | defer cli.Close() 123 | return cli.Remove(dir) 124 | } 125 | 126 | func (c Client) Glob(pattern string) ([]string, error) { 127 | cli, err := sftp.NewClient(c.cli) 128 | if err != nil { 129 | return nil, err 130 | } 131 | defer cli.Close() 132 | return cli.Glob(pattern) 133 | } 134 | 135 | func (c Client) Stat(dir string) (os.FileInfo, error) { 136 | cli, err := sftp.NewClient(c.cli) 137 | if err != nil { 138 | return nil, err 139 | } 140 | defer cli.Close() 141 | return cli.Stat(dir) 142 | } 143 | 144 | func (c Client) StatVFS(dir string) (*sftp.StatVFS, error) { 145 | cli, err := sftp.NewClient(c.cli) 146 | if err != nil { 147 | return nil, err 148 | } 149 | defer cli.Close() 150 | return cli.StatVFS(dir) 151 | } 152 | 153 | func (c Client) ReadFile(dir string) ([]byte, error) { 154 | cli, err := sftp.NewClient(c.cli) 155 | if err != nil { 156 | return nil, err 157 | } 158 | defer cli.Close() 159 | f, err := cli.Open(dir) 160 | if err != nil { 161 | return nil, err 162 | } 163 | defer f.Close() 164 | return io.ReadAll(f) 165 | } 166 | -------------------------------------------------------------------------------- /code/api/hm/h_static.go: -------------------------------------------------------------------------------- 1 | package hm 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | lapi "server/code/api" 7 | "server/code/client" 8 | "time" 9 | 10 | "github.com/jkstack/anet" 11 | "github.com/lwch/api" 12 | "github.com/lwch/runtime" 13 | ) 14 | 15 | func (h *Handler) static(clients *client.Clients, ctx *api.Context) { 16 | id := ctx.XStr("id") 17 | 18 | cli := clients.Get(id) 19 | if cli == nil { 20 | ctx.NotFound("client") 21 | } 22 | 23 | p := h.cfg.GetPlugin("host.monitor", cli.OS(), cli.Arch()) 24 | if p == nil { 25 | lapi.PluginNotInstalledErr("host.monitor") 26 | } 27 | 28 | taskID, err := cli.SendHMStatic(p) 29 | runtime.Assert(err) 30 | defer cli.ChanClose(taskID) 31 | 32 | h.stUsage.Inc() 33 | h.stTotalTasks.Inc() 34 | 35 | var msg *anet.Msg 36 | select { 37 | case msg = <-cli.ChanRead(taskID): 38 | case <-time.After(api.RequestTimeout): 39 | ctx.Timeout() 40 | } 41 | 42 | switch { 43 | case msg.Type == anet.TypeError: 44 | ctx.ERR(http.StatusServiceUnavailable, msg.ErrorMsg) 45 | return 46 | case msg.Type != anet.TypeHMStaticRep: 47 | ctx.ERR(http.StatusInternalServerError, fmt.Sprintf("invalid message type: %d", msg.Type)) 48 | return 49 | } 50 | 51 | type core struct { 52 | Processor int32 `json:"processor"` 53 | Model string `json:"model"` 54 | Core int32 `json:"core"` 55 | Cores int32 `json:"cores"` 56 | Physical int32 `json:"physical"` 57 | } 58 | 59 | type disk struct { 60 | Name string `json:"name"` 61 | Type string `json:"type"` 62 | Opts []string `json:"opts"` 63 | Total uint64 `json:"total"` 64 | INodes uint64 `json:"inodes"` 65 | } 66 | 67 | type intf struct { 68 | Index int `json:"index"` 69 | Name string `json:"name"` 70 | Mtu int `json:"mtu"` 71 | Flags []string `json:"flags"` 72 | Mac string `json:"mac"` 73 | Address []string `json:"addrs"` 74 | } 75 | 76 | var ret struct { 77 | Timestamp int64 `json:"timestamp"` 78 | HostName string `json:"host_name"` 79 | UpTime int64 `json:"uptime"` 80 | OSName string `json:"os_name"` 81 | Platform string `json:"platform"` 82 | PlatformVersion string `json:"platform_version"` 83 | KernelArch string `json:"kernel_arch"` 84 | KernelVersion string `json:"kernel_version"` 85 | PhysicalCore int `json:"physical_core"` 86 | LogicalCore int `json:"logical_core"` 87 | Cores []core `json:"cores"` 88 | PhysicalMemory uint64 `json:"physical_memory"` 89 | SwapMemory uint64 `json:"swap_memory"` 90 | Disks []disk `json:"disks"` 91 | Interface []intf `json:"intfs"` 92 | } 93 | 94 | info := msg.HMStatic 95 | ret.Timestamp = info.Time.Unix() 96 | ret.HostName = info.Host.Name 97 | ret.UpTime = int64(info.Host.UpTime.Seconds()) 98 | ret.OSName = info.OS.Name 99 | ret.Platform = info.OS.PlatformName 100 | ret.PlatformVersion = info.OS.PlatformVersion 101 | ret.KernelArch = info.Kernel.Arch 102 | ret.KernelVersion = info.Kernel.Version 103 | ret.PhysicalCore = info.CPU.Physical 104 | ret.LogicalCore = info.CPU.Logical 105 | for _, c := range info.CPU.Cores { 106 | ret.Cores = append(ret.Cores, core{ 107 | Processor: c.Processor, 108 | Model: c.Model, 109 | Core: c.Core, 110 | Cores: c.Cores, 111 | Physical: c.Physical, 112 | }) 113 | } 114 | ret.PhysicalMemory = info.Memory.Physical 115 | ret.SwapMemory = info.Memory.Swap 116 | for _, d := range info.Disks { 117 | ret.Disks = append(ret.Disks, disk{ 118 | Name: d.Name, 119 | Type: d.FSType, 120 | Opts: d.Opts, 121 | Total: d.Total, 122 | INodes: d.INodes, 123 | }) 124 | } 125 | for _, i := range info.Interface { 126 | ret.Interface = append(ret.Interface, intf{ 127 | Index: i.Index, 128 | Name: i.Name, 129 | Flags: i.Flags, 130 | Mac: i.Mac, 131 | Address: i.Address, 132 | }) 133 | } 134 | 135 | ctx.OK(ret) 136 | } 137 | -------------------------------------------------------------------------------- /code/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "server/code/utils" 7 | "sync" 8 | 9 | "github.com/lwch/kvconf" 10 | "github.com/lwch/logging" 11 | "github.com/lwch/runtime" 12 | ) 13 | 14 | const ( 15 | defaultCacheDir = "/opt/smartagent-server/cache" 16 | defaultCacheThreshold = 80 17 | defaultDataDir = "/opt/smartagent-server/data" 18 | defaultPluginDir = "/opt/smartagent-server/plugins" 19 | defaultLogDir = "/opt/smartagent-server/logs" 20 | defaultLogSize = utils.Bytes(50 * 1024 * 1024) 21 | defaultLogRotate = 7 22 | ) 23 | 24 | type Configure struct { 25 | sync.RWMutex 26 | Listen uint16 `kv:"listen"` 27 | CacheDir string `kv:"cache_dir"` 28 | CacheThreshold uint `kv:"cache_threshold"` 29 | DataDir string `kv:"data_dir"` 30 | PluginDir string `kv:"plugin_dir"` 31 | LogDir string `kv:"log_dir"` 32 | LogSize utils.Bytes `kv:"log_size"` 33 | LogRotate int `kv:"log_rotate"` 34 | LoggingReport string `kv:"logging_report"` 35 | // runtime 36 | WorkDir string 37 | PluginList map[string]*PluginMD5List // name => md5 => info 38 | PluginLatest map[string]utils.Version // name => version 39 | } 40 | 41 | func Load(dir, abs string) *Configure { 42 | f, err := os.Open(dir) 43 | runtime.Assert(err) 44 | defer f.Close() 45 | 46 | var ret Configure 47 | runtime.Assert(kvconf.NewDecoder(f).Decode(&ret)) 48 | ret.check(abs) 49 | 50 | ret.WorkDir, _ = os.Getwd() 51 | ret.PluginList = make(map[string]*PluginMD5List) 52 | ret.PluginLatest = make(map[string]utils.Version) 53 | ret.LoadPlugin() 54 | 55 | return &ret 56 | } 57 | 58 | func (cfg *Configure) GetPlugin(name, os, arch string) *PluginInfo { 59 | cfg.RLock() 60 | list := cfg.PluginList[name] 61 | ver := cfg.PluginLatest[name] 62 | cfg.RUnlock() 63 | return list.get(ver, os, arch) 64 | } 65 | 66 | func (cfg *Configure) PluginByMD5(name, md5 string) *PluginInfo { 67 | cfg.RLock() 68 | list := cfg.PluginList[name] 69 | cfg.RUnlock() 70 | return list.by(md5) 71 | } 72 | 73 | func (cfg *Configure) PluginCount() int { 74 | var cnt int 75 | cfg.RangePlugin(func(name, version string) { 76 | if version != "0.0.0" { 77 | cnt++ 78 | } 79 | }) 80 | return cnt 81 | } 82 | 83 | func (cfg *Configure) RangePlugin(cb func(name, version string)) { 84 | cfg.RLock() 85 | defer cfg.RUnlock() 86 | for name, ver := range cfg.PluginLatest { 87 | cb(name, ver.String()) 88 | } 89 | } 90 | 91 | func (cfg *Configure) check(abs string) { 92 | if cfg.Listen == 0 { 93 | panic("invalid listen config") 94 | } 95 | if len(cfg.CacheDir) == 0 { 96 | logging.Info("reset conf.cache_dir to default path: %s", defaultCacheDir) 97 | cfg.CacheDir = defaultCacheDir 98 | } else if !filepath.IsAbs(cfg.CacheDir) { 99 | cfg.CacheDir = filepath.Join(abs, cfg.CacheDir) 100 | } 101 | if len(cfg.PluginDir) == 0 { 102 | logging.Info("reset conf.plugin_dir to default path: %s", defaultPluginDir) 103 | cfg.PluginDir = defaultPluginDir 104 | } else if !filepath.IsAbs(cfg.PluginDir) { 105 | cfg.PluginDir = filepath.Join(abs, cfg.PluginDir) 106 | } 107 | if len(cfg.LogDir) == 0 { 108 | logging.Info("reset conf.log_dir to default path: %s", defaultLogDir) 109 | cfg.LogDir = defaultLogDir 110 | } else if !filepath.IsAbs(cfg.LogDir) { 111 | cfg.LogDir = filepath.Join(abs, cfg.LogDir) 112 | } 113 | if len(cfg.DataDir) == 0 { 114 | logging.Info("reset conf.data_dir to default path: %s", defaultDataDir) 115 | cfg.DataDir = defaultDataDir 116 | } else if !filepath.IsAbs(cfg.DataDir) { 117 | cfg.DataDir = filepath.Join(abs, cfg.DataDir) 118 | } 119 | if cfg.LogSize == 0 { 120 | logging.Info("reset conf.log_size to default size: %s", defaultLogSize.String()) 121 | cfg.LogSize = defaultLogSize 122 | } 123 | if cfg.LogRotate == 0 { 124 | logging.Info("reset conf.log_roate to default count: %d", defaultLogRotate) 125 | cfg.LogRotate = defaultLogRotate 126 | } 127 | if cfg.CacheThreshold == 0 || cfg.CacheThreshold > 100 { 128 | logging.Info("reset conf.cache_threshold to default limit: %d", defaultCacheThreshold) 129 | cfg.CacheThreshold = defaultCacheThreshold 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /code/api/layout/hi_exec.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "server/code/client" 9 | "strings" 10 | "time" 11 | 12 | "github.com/jkstack/anet" 13 | "github.com/lwch/logging" 14 | ) 15 | 16 | type execHandler struct { 17 | taskInfo 18 | cmd string 19 | output string 20 | } 21 | 22 | func (h *execHandler) Check(t Task) error { 23 | if len(t.Cmd) == 0 { 24 | return fmt.Errorf("missing cmd for [%s] task", t.Name) 25 | } 26 | return nil 27 | } 28 | 29 | func (h *execHandler) Clone(t Task, info taskInfo) taskHandler { 30 | return &execHandler{ 31 | taskInfo: info, 32 | cmd: t.Cmd, 33 | output: t.Output, 34 | } 35 | } 36 | 37 | func (h *execHandler) Run(id, dir, user, pass string, args map[string]string) error { 38 | deadline := h.deadline() 39 | args["DEADLINE"] = fmt.Sprintf("%d", deadline.Unix()) 40 | logging.Info("exec [%s] on agent [%s]", h.name, id) 41 | cli := h.parent.GetClient(id) 42 | if cli == nil { 43 | return errClientNotfound(id) 44 | } 45 | p := h.parent.GetPlugin("exec", cli.OS(), cli.Arch()) 46 | if p == nil { 47 | return errPluginNotInstalled("exec") 48 | } 49 | timeout := int(deadline.Sub(time.Now()).Seconds() + .5) 50 | taskID, err := cli.SendExec(p, h.cmd, nil, timeout, h.auth, 51 | h.parent.user, h.parent.pass, "", nil, "") 52 | if err != nil { 53 | return fmt.Errorf("send exec [%s] on agent [%s]: %v", h.name, id, err) 54 | } 55 | defer cli.ChanClose(taskID) 56 | 57 | h.parent.parent.stExecUsage.Inc() 58 | h.parent.parent.stTotalTasks.Inc() 59 | 60 | return h.read(cli, h.name, taskID, time.After(time.Duration(timeout)*time.Second), args) 61 | } 62 | 63 | func (h *execHandler) read(cli *client.Client, 64 | taskName, taskID string, 65 | deadline <-chan time.Time, args map[string]string) error { 66 | pid, err := h.readAck(cli, taskID, deadline) 67 | if err != nil { 68 | return err 69 | } 70 | logging.Info("exec [%s] on agent [%s] successed, pid=%d", taskName, cli.ID(), pid) 71 | return h.readBody(cli, taskName, taskID, deadline, args) 72 | } 73 | 74 | func (h *execHandler) readAck(cli *client.Client, 75 | taskID string, deadline <-chan time.Time) (int, error) { 76 | var msg *anet.Msg 77 | select { 78 | case msg = <-cli.ChanRead(taskID): 79 | case <-deadline: 80 | return 0, errTimeout 81 | } 82 | switch { 83 | case msg.Type == anet.TypeError: 84 | return 0, errors.New(msg.ErrorMsg) 85 | case msg.Type != anet.TypeExecd: 86 | return 0, fmt.Errorf("invalid message type: %d", msg.Type) 87 | } 88 | if !msg.Execd.OK { 89 | return 0, errors.New(msg.Execd.Msg) 90 | } 91 | return msg.Execd.Pid, nil 92 | } 93 | 94 | func (h *execHandler) readBody(cli *client.Client, 95 | taskName, taskID string, 96 | deadline <-chan time.Time, args map[string]string) error { 97 | ch := cli.ChanRead(taskID) 98 | var cache bytes.Buffer 99 | for { 100 | var msg *anet.Msg 101 | select { 102 | case msg = <-ch: 103 | case <-deadline: 104 | return errTimeout 105 | } 106 | if msg == nil { 107 | return nil 108 | } 109 | switch msg.Type { 110 | case anet.TypeError: 111 | return errors.New(msg.ErrorMsg) 112 | case anet.TypeExecData: 113 | data, _ := base64.StdEncoding.DecodeString(msg.ExecData.Data) 114 | logging.Debug("exec [%s] on agent [%s] recved %d bytes", taskName, cli.ID(), len(data)) 115 | _, err := cache.Write(data) 116 | if err != nil { 117 | return fmt.Errorf("write cache: %v", err) 118 | } 119 | case anet.TypeExecDone: 120 | logging.Info("exec [%s] on agent [%s] run done, code=%d", 121 | taskName, cli.ID(), msg.ExecDone.Code) 122 | if msg.ExecDone.Code != 0 { 123 | logging.Info("exec [%s] on agent [%s] failed: %s", 124 | taskName, cli.ID(), cache.String()) 125 | return fmt.Errorf("code=%d", msg.ExecDone.Code) 126 | } 127 | if len(h.output) > 0 { 128 | str := cache.String() 129 | str = strings.TrimLeft(str, "Password: \r\n") 130 | if strings.HasPrefix(str, "[sudo]") { 131 | tmp := strings.SplitN(str, "\n", 2) 132 | str = tmp[1] 133 | } 134 | str = strings.TrimRight(str, "\r\n") 135 | args[h.output] = str 136 | if len(args[h.output]) < 1024 { 137 | logging.Info("exec [%s] on agent [%s] log variable [%s] value:\n%s", 138 | taskName, cli.ID(), h.output, args[h.output]) 139 | } 140 | } 141 | return nil 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /code/api/file/h_download.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path" 11 | lapi "server/code/api" 12 | "server/code/client" 13 | "server/code/utils" 14 | "time" 15 | 16 | "github.com/jkstack/anet" 17 | "github.com/lwch/api" 18 | "github.com/lwch/logging" 19 | "github.com/lwch/runtime" 20 | ) 21 | 22 | func (h *Handler) download(clients *client.Clients, ctx *api.Context) { 23 | id := ctx.XStr("id") 24 | dir := ctx.XStr("dir") 25 | timeout := ctx.OInt("timeout", 600) 26 | 27 | cli := clients.Get(id) 28 | if cli == nil { 29 | ctx.NotFound("client") 30 | } 31 | 32 | p := h.cfg.GetPlugin("file", cli.OS(), cli.Arch()) 33 | if p == nil { 34 | lapi.PluginNotInstalledErr("file") 35 | } 36 | 37 | taskID, err := cli.SendDownload(p, dir) 38 | runtime.Assert(err) 39 | defer cli.ChanClose(taskID) 40 | 41 | h.stUsage.Inc() 42 | h.stTotalTasks.Inc() 43 | 44 | logging.Info("download [%s] on %s, task_id=%s, plugin.version=%s", dir, id, taskID, p.Version) 45 | 46 | var rep *anet.Msg 47 | select { 48 | case rep = <-cli.ChanRead(taskID): 49 | case <-time.After(api.RequestTimeout): 50 | ctx.Timeout() 51 | } 52 | 53 | switch { 54 | case rep.Type == anet.TypeError: 55 | ctx.ERR(http.StatusServiceUnavailable, rep.ErrorMsg) 56 | return 57 | case rep.Type != anet.TypeDownloadRep: 58 | ctx.ERR(http.StatusInternalServerError, fmt.Sprintf("invalid message type: %d", rep.Type)) 59 | return 60 | } 61 | 62 | if !rep.DownloadRep.OK { 63 | logging.Error("download [%s] on %s failed, task_id=%s, msg=%s", dir, id, taskID, rep.DownloadRep.ErrMsg) 64 | ctx.HTTPServiceUnavailable(rep.DownloadRep.ErrMsg) 65 | return 66 | } 67 | logging.Info("download [%s] on %s success, task_id=%s, size=%d, md5=%x", dir, id, taskID, 68 | rep.DownloadRep.Size, rep.DownloadRep.MD5) 69 | 70 | f, err := tmpFile(ctx, h.cfg.CacheDir, rep.DownloadRep.Size) 71 | if err != nil { 72 | logging.Error("download [%s] on %s failed, task_id=%s, err=%v", dir, id, taskID, err) 73 | ctx.HTTPServiceUnavailable(err.Error()) 74 | return 75 | } 76 | defer func() { 77 | f.Close() 78 | os.Remove(f.Name()) 79 | }() 80 | left := rep.DownloadRep.Size 81 | after := time.After(time.Duration(timeout) * time.Second) 82 | for { 83 | var msg *anet.Msg 84 | select { 85 | case msg = <-cli.ChanRead(taskID): 86 | case <-after: 87 | ctx.HTTPTimeout() 88 | return 89 | } 90 | switch msg.Type { 91 | case anet.TypeDownloadData: 92 | n, err := writeFile(f, msg.DownloadData) 93 | if err != nil { 94 | logging.Error("download [%s] on %s, task_id=%s, err=%v", dir, id, taskID, err) 95 | ctx.HTTPServiceUnavailable(err.Error()) 96 | return 97 | } 98 | left -= uint64(n) 99 | if left == 0 { 100 | serveFile(ctx, f, id, dir, taskID, rep.DownloadRep.MD5) 101 | return 102 | } 103 | case anet.TypeDownloadError: 104 | ctx.HTTPServiceUnavailable(msg.DownloadError.Msg) 105 | return 106 | } 107 | } 108 | } 109 | 110 | func tmpFile(ctx *api.Context, cacheDir string, size uint64) (*os.File, error) { 111 | tmp := path.Join(cacheDir, "download") 112 | os.MkdirAll(tmp, 0755) 113 | f, err := os.CreateTemp(tmp, "dl") 114 | if err != nil { 115 | return nil, err 116 | } 117 | defer f.Close() 118 | err = fillFile(f, size) 119 | if err != nil { 120 | f.Close() 121 | os.Remove(f.Name()) 122 | return nil, err 123 | } 124 | f.Close() 125 | f, err = os.OpenFile(f.Name(), os.O_RDWR|os.O_TRUNC, 0644) 126 | if err != nil { 127 | f.Close() 128 | os.Remove(f.Name()) 129 | return nil, err 130 | } 131 | return f, err 132 | } 133 | 134 | func serveFile(ctx *api.Context, f *os.File, id, dir, taskID string, src [md5.Size]byte) { 135 | _, err := f.Seek(0, io.SeekStart) 136 | if err != nil { 137 | logging.Error("download [%s] on %s, task_id=%s, err=%v", dir, id, taskID, err) 138 | ctx.HTTPServiceUnavailable(err.Error()) 139 | return 140 | } 141 | dst, err := utils.MD5From(f) 142 | if err != nil { 143 | logging.Error("download [%s] on %s, task_id=%s, err=%v", dir, id, taskID, err) 144 | ctx.HTTPServiceUnavailable(err.Error()) 145 | return 146 | } 147 | if !bytes.Equal(dst[:], src[:]) { 148 | logging.Error("download [%s] on %s, task_id=%s, invalid md5checksum, src=%x, dst=%x", 149 | dir, id, taskID, src, dst) 150 | ctx.HTTPConflict("invalid checksum") 151 | return 152 | } 153 | f.Close() 154 | ctx.ServeFile(f.Name()) 155 | } 156 | -------------------------------------------------------------------------------- /code/api/layout/task.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type variable struct { 11 | static bool 12 | value string 13 | } 14 | 15 | type op byte 16 | 17 | const ( 18 | opLess op = iota 19 | opGreater 20 | opEqual 21 | opNotEqual 22 | opLessEqual 23 | opGreaterEqual 24 | ) 25 | 26 | func (op op) Run(left, right variable, args map[string]string) bool { 27 | var leftString, rightString string 28 | if left.static { 29 | leftString = left.value 30 | } else { 31 | leftString = args[left.value] 32 | } 33 | if right.static { 34 | rightString = right.value 35 | } else { 36 | rightString = args[right.value] 37 | } 38 | isNumber := true 39 | leftInt, err := strconv.ParseInt(leftString, 10, 64) 40 | if err != nil { 41 | isNumber = false 42 | } 43 | rightInt, err := strconv.ParseInt(rightString, 10, 64) 44 | if err != nil { 45 | isNumber = false 46 | } 47 | switch op { 48 | case opEqual: 49 | if isNumber { 50 | return leftInt == rightInt 51 | } 52 | return leftString == rightString 53 | case opNotEqual: 54 | if isNumber { 55 | return leftInt != rightInt 56 | } 57 | return leftString != rightString 58 | case opLess: 59 | if isNumber { 60 | return leftInt < rightInt 61 | } 62 | return leftString < rightString 63 | case opGreater: 64 | if isNumber { 65 | return leftInt > rightInt 66 | } 67 | return leftString > rightString 68 | case opLessEqual: 69 | if isNumber { 70 | return leftInt <= rightInt 71 | } 72 | return leftString <= rightString 73 | case opGreaterEqual: 74 | if isNumber { 75 | return leftInt >= rightInt 76 | } 77 | return leftString >= rightString 78 | default: 79 | return false 80 | } 81 | } 82 | 83 | type taskInfo struct { 84 | parent *runner 85 | name string 86 | auth string 87 | timeout time.Duration 88 | hasIf bool 89 | left variable 90 | right variable 91 | op op 92 | } 93 | 94 | func isOP(str string) bool { 95 | if len(str) != 1 { 96 | return false 97 | } 98 | return str[0] == '>' || 99 | str[0] == '<' || 100 | str[0] == '!' || 101 | str[0] == '=' 102 | } 103 | 104 | func (info *taskInfo) Name() string { 105 | return info.name 106 | } 107 | 108 | func (info *taskInfo) parseIf(str string) error { 109 | info.hasIf = true 110 | str = strings.TrimSpace(str) 111 | var tmp []string 112 | var buf string 113 | for _, ch := range str { 114 | if isOP(string(ch)) { 115 | if len(buf) > 0 { 116 | tmp = append(tmp, strings.TrimSpace(buf)) 117 | } 118 | tmp = append(tmp, string(ch)) 119 | buf = "" 120 | continue 121 | } 122 | buf += string(ch) 123 | } 124 | if len(buf) > 0 { 125 | tmp = append(tmp, strings.TrimSpace(buf)) 126 | } 127 | trans := make([]string, 0, len(tmp)) 128 | var i int 129 | for { 130 | if i >= len(tmp) { 131 | break 132 | } 133 | if isOP(tmp[i]) { 134 | str := tmp[i] 135 | for j := i + 1; j < len(tmp); j++ { 136 | if !isOP(tmp[j]) { 137 | i = j 138 | break 139 | } 140 | str += tmp[j] 141 | } 142 | trans = append(trans, strings.TrimSpace(str)) 143 | } else { 144 | trans = append(trans, strings.TrimSpace(tmp[i])) 145 | i++ 146 | } 147 | } 148 | if len(trans) != 3 { 149 | return fmt.Errorf("invalid if expression") 150 | } 151 | switch trans[1] { 152 | case "<": 153 | info.op = opLess 154 | case ">": 155 | info.op = opGreater 156 | case "=": 157 | info.op = opEqual 158 | case "!=": 159 | info.op = opNotEqual 160 | case "<=": 161 | info.op = opLessEqual 162 | case ">=": 163 | info.op = opGreaterEqual 164 | default: 165 | return fmt.Errorf("invalid operator") 166 | } 167 | if trans[0][0] == '$' { 168 | info.left.static = false 169 | info.left.value = trans[0][1:] 170 | } else { 171 | info.left.static = true 172 | info.left.value = trans[0] 173 | } 174 | if trans[2][0] == '$' { 175 | info.right.static = false 176 | info.right.value = trans[2][1:] 177 | } else { 178 | info.right.static = true 179 | info.right.value = trans[2] 180 | } 181 | return nil 182 | } 183 | 184 | func (info *taskInfo) deadline() time.Time { 185 | if info.timeout > 0 { 186 | return time.Now().Add(info.timeout) 187 | } 188 | return info.parent.deadline 189 | } 190 | 191 | func (info *taskInfo) WantRun(args map[string]string) bool { 192 | if !info.hasIf { 193 | return true 194 | } 195 | return info.op.Run(info.left, info.right, args) 196 | } 197 | -------------------------------------------------------------------------------- /code/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "server/code/api/agent" 9 | "server/code/api/cmd" 10 | "server/code/api/file" 11 | "server/code/api/hm" 12 | "server/code/api/host" 13 | "server/code/api/install" 14 | "server/code/api/layout" 15 | apilogging "server/code/api/logging" 16 | "server/code/api/plugin" 17 | "server/code/api/scaffolding" 18 | "server/code/api/server" 19 | "server/code/client" 20 | "server/code/conf" 21 | "server/code/utils" 22 | "sync" 23 | "time" 24 | 25 | "github.com/jkstack/anet" 26 | "github.com/jkstack/jkframe/stat" 27 | "github.com/kardianos/service" 28 | "github.com/lwch/api" 29 | "github.com/lwch/logging" 30 | "github.com/lwch/runtime" 31 | "github.com/shirou/gopsutil/v3/disk" 32 | ) 33 | 34 | type handler interface { 35 | Init(*conf.Configure, *stat.Mgr) 36 | HandleFuncs() map[string]func(*client.Clients, *api.Context) 37 | OnConnect(*client.Client) 38 | OnClose(string) 39 | OnMessage(*client.Client, *anet.Msg) 40 | } 41 | 42 | // App app 43 | type App struct { 44 | cfg *conf.Configure 45 | clients *client.Clients 46 | version string 47 | blocked bool 48 | connectLock sync.Mutex 49 | stats *stat.Mgr 50 | 51 | // runtime 52 | stAgentCount *stat.Counter 53 | } 54 | 55 | // New new app 56 | func New(cfg *conf.Configure, version string) *App { 57 | st := stat.New(5 * time.Second) 58 | app := &App{ 59 | cfg: cfg, 60 | clients: client.NewClients(st), 61 | version: version, 62 | blocked: false, 63 | stats: st, 64 | stAgentCount: st.NewCounter("agent_count"), 65 | } 66 | go app.limit() 67 | return app 68 | } 69 | 70 | // Start start app 71 | func (app *App) Start(s service.Service) error { 72 | go func() { 73 | logging.SetSizeRotate(logging.SizeRotateConfig{ 74 | Dir: app.cfg.LogDir, 75 | Name: "smartagent-server", 76 | Size: int64(app.cfg.LogSize.Bytes()), 77 | Rotate: app.cfg.LogRotate, 78 | WriteStdout: true, 79 | WriteFile: true, 80 | }) 81 | defer logging.Flush() 82 | 83 | defer utils.Recover("service") 84 | 85 | os.RemoveAll(app.cfg.CacheDir) 86 | 87 | var mods []handler 88 | mods = append(mods, plugin.New()) 89 | mods = append(mods, cmd.New()) 90 | fh := file.New() 91 | mods = append(mods, fh) 92 | mods = append(mods, hm.New()) 93 | mods = append(mods, host.New()) 94 | mods = append(mods, server.New(app.version)) 95 | mods = append(mods, agent.New()) 96 | mods = append(mods, install.New()) 97 | mods = append(mods, layout.New(fh)) 98 | mods = append(mods, apilogging.New()) 99 | mods = append(mods, scaffolding.New()) 100 | 101 | for _, mod := range mods { 102 | mod.Init(app.cfg, app.stats) 103 | for uri, cb := range mod.HandleFuncs() { 104 | app.reg(uri, cb) 105 | } 106 | } 107 | 108 | http.HandleFunc("/metrics", app.stats.ServeHTTP) 109 | http.HandleFunc("/ws/agent", func(w http.ResponseWriter, r *http.Request) { 110 | onConnect := make(chan *client.Client) 111 | ctx, cancel := context.WithCancel(context.Background()) 112 | defer cancel() 113 | go func() { 114 | select { 115 | case cli := <-onConnect: 116 | for _, mod := range mods { 117 | mod.OnConnect(cli) 118 | } 119 | case <-ctx.Done(): 120 | return 121 | } 122 | }() 123 | cli := app.agent(w, r, onConnect, cancel) 124 | if cli == nil { 125 | return 126 | } 127 | go func() { 128 | for { 129 | select { 130 | case msg := <-cli.Unknown(): 131 | if msg == nil { 132 | return 133 | } 134 | for _, mod := range mods { 135 | mod.OnMessage(cli, msg) 136 | } 137 | case <-ctx.Done(): 138 | return 139 | } 140 | } 141 | }() 142 | <-ctx.Done() 143 | app.stAgentCount.Dec() 144 | logging.Info("client %s connection closed", cli.ID()) 145 | for _, mod := range mods { 146 | mod.OnClose(cli.ID()) 147 | } 148 | }) 149 | 150 | logging.Info("http listen on %d", app.cfg.Listen) 151 | runtime.Assert(http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Listen), nil)) 152 | }() 153 | return nil 154 | } 155 | 156 | func (app *App) Stop(s service.Service) error { 157 | return nil 158 | } 159 | 160 | func (app *App) limit() { 161 | for { 162 | usage, err := disk.Usage(app.cfg.CacheDir) 163 | if err == nil { 164 | if usage.UsedPercent > float64(app.cfg.CacheThreshold) { 165 | app.blocked = true 166 | } else { 167 | app.blocked = false 168 | } 169 | } 170 | time.Sleep(time.Second) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /code/api/file/h_upload.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/hex" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "mime/multipart" 11 | "net/http" 12 | "os" 13 | "path" 14 | lapi "server/code/api" 15 | "server/code/client" 16 | "server/code/utils" 17 | "time" 18 | 19 | "github.com/jkstack/anet" 20 | "github.com/lwch/api" 21 | "github.com/lwch/logging" 22 | "github.com/lwch/runtime" 23 | ) 24 | 25 | const uploadLimit = 1024 * 1024 26 | 27 | type uploadInfo struct { 28 | token string 29 | dir string 30 | rm bool 31 | timeout time.Time 32 | } 33 | 34 | func (h *Handler) upload(clients *client.Clients, ctx *api.Context) { 35 | id := ctx.XStr("id") 36 | dir := ctx.XStr("dir") 37 | enc := ctx.OStr("md5", "") 38 | auth := ctx.OStr("auth", "") 39 | user := ctx.OStr("user", "") 40 | pass := ctx.OStr("pass", "") 41 | mod := ctx.OInt("mod", 0644) 42 | ownUser := ctx.OStr("own_user", "") 43 | ownGroup := ctx.OStr("own_group", "") 44 | file, hdr, err := ctx.File("file") 45 | runtime.Assert(err) 46 | timeout := ctx.OInt("timeout", 60) 47 | 48 | var srcMD5 [md5.Size]byte 49 | if len(enc) > 0 { 50 | encData, err := hex.DecodeString(enc) 51 | runtime.Assert(err) 52 | 53 | copy(srcMD5[:], encData) 54 | } 55 | 56 | cli := clients.Get(id) 57 | if cli == nil { 58 | ctx.NotFound("client") 59 | } 60 | 61 | p := h.cfg.GetPlugin("file", cli.OS(), cli.Arch()) 62 | if p == nil { 63 | lapi.PluginNotInstalledErr("file") 64 | } 65 | 66 | var taskID string 67 | if hdr.Size <= uploadLimit { 68 | var data []byte 69 | data, err = ioutil.ReadAll(file) 70 | runtime.Assert(err) 71 | dstMD5 := md5.Sum(data) 72 | info := client.UploadContext{ 73 | Dir: dir, 74 | Name: hdr.Filename, 75 | Auth: auth, 76 | User: user, 77 | Pass: pass, 78 | Mod: mod, 79 | OwnUser: ownUser, 80 | OwnGroup: ownGroup, 81 | Size: uint64(hdr.Size), 82 | Data: data, 83 | } 84 | if len(enc) > 0 { 85 | if !bytes.Equal(srcMD5[:], dstMD5[:]) { 86 | ctx.ERR(2, "invalid checksum") 87 | return 88 | } 89 | info.Md5 = srcMD5 90 | } else { 91 | info.Md5 = md5.Sum(data) 92 | } 93 | taskID, err = cli.SendUpload(p, info, "") 94 | } else { 95 | var tmpDir string 96 | var dstMD5 [md5.Size]byte 97 | tmpDir, dstMD5, err = dumpFile(file, path.Join(h.cfg.CacheDir, "upload")) 98 | runtime.Assert(err) 99 | defer os.Remove(tmpDir) 100 | if len(enc) > 0 && !bytes.Equal(srcMD5[:], dstMD5[:]) { 101 | ctx.ERR(2, "invalid checksum") 102 | return 103 | } 104 | token, err := runtime.UUID(16, "0123456789abcdef") 105 | runtime.Assert(err) 106 | taskID, err = utils.TaskID() 107 | runtime.Assert(err) 108 | uri := "/file/upload/" + taskID 109 | h.LogUploadCache(taskID, tmpDir, token, 110 | time.Now().Add(time.Duration(timeout)*time.Second), true) 111 | defer h.RemoveUploadCache(taskID) 112 | taskID, err = cli.SendUpload(p, client.UploadContext{ 113 | Dir: dir, 114 | Name: hdr.Filename, 115 | Auth: auth, 116 | User: user, 117 | Pass: pass, 118 | Mod: mod, 119 | OwnUser: ownUser, 120 | OwnGroup: ownGroup, 121 | Size: uint64(hdr.Size), 122 | Md5: dstMD5, 123 | Uri: uri, 124 | Token: token, 125 | }, taskID) 126 | } 127 | runtime.Assert(err) 128 | defer cli.ChanClose(taskID) 129 | 130 | h.stUsage.Inc() 131 | h.stTotalTasks.Inc() 132 | 133 | logging.Info("upload [%s] to %s on %s, task_id=%s, plugin.version=%s", 134 | hdr.Filename, dir, id, taskID, p.Version) 135 | 136 | var rep *anet.Msg 137 | select { 138 | case rep = <-cli.ChanRead(taskID): 139 | case <-time.After(time.Duration(timeout) * time.Second): 140 | ctx.Timeout() 141 | } 142 | 143 | switch { 144 | case rep.Type == anet.TypeError: 145 | ctx.ERR(http.StatusServiceUnavailable, rep.ErrorMsg) 146 | return 147 | case rep.Type != anet.TypeUploadRep: 148 | ctx.ERR(http.StatusInternalServerError, fmt.Sprintf("invalid message type: %d", rep.Type)) 149 | return 150 | } 151 | 152 | if !rep.UploadRep.OK { 153 | ctx.ERR(1, rep.UploadRep.ErrMsg) 154 | return 155 | } 156 | 157 | ctx.OK(nil) 158 | } 159 | 160 | func dumpFile(f multipart.File, dir string) (string, [md5.Size]byte, error) { 161 | var md [md5.Size]byte 162 | os.MkdirAll(dir, 0755) 163 | dst, err := os.CreateTemp(dir, "ul") 164 | if err != nil { 165 | return "", md, err 166 | } 167 | defer dst.Close() 168 | enc := md5.New() 169 | _, err = io.Copy(io.MultiWriter(dst, enc), f) 170 | if err != nil { 171 | return "", md, err 172 | } 173 | copy(md[:], enc.Sum(nil)) 174 | return dst.Name(), md, nil 175 | } 176 | -------------------------------------------------------------------------------- /code/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "server/code/conf" 8 | "server/code/utils" 9 | "sync" 10 | "time" 11 | 12 | "github.com/gorilla/websocket" 13 | "github.com/jkstack/anet" 14 | "github.com/lwch/logging" 15 | ) 16 | 17 | const channelBuffer = 10000 18 | 19 | type Client struct { 20 | sync.RWMutex 21 | t string 22 | parent *Clients 23 | info anet.ComePayload 24 | remote *websocket.Conn 25 | // runtime 26 | chRead chan *anet.Msg 27 | chWrite chan *anet.Msg 28 | taskRead map[string]chan *anet.Msg 29 | cancel context.CancelFunc 30 | } 31 | 32 | func (cli *Client) close() { 33 | cli.cancel() 34 | logging.Info("client %s connection closed", cli.info.ID) 35 | if cli.remote != nil { 36 | cli.remote.Close() 37 | } 38 | close(cli.chRead) 39 | close(cli.chWrite) 40 | } 41 | 42 | func (cli *Client) remoteAddr() string { 43 | return cli.remote.RemoteAddr().String() 44 | } 45 | 46 | func (cli *Client) read(ctx context.Context, cancel context.CancelFunc) { 47 | defer func() { 48 | utils.Recover(fmt.Sprintf("cli.read(%s)", cli.remoteAddr())) 49 | cancel() 50 | }() 51 | cli.remote.SetReadDeadline(time.Time{}) 52 | send := func(taskID string, ch chan *anet.Msg, msg *anet.Msg) { 53 | defer func() { 54 | if err := recover(); err != nil { 55 | logging.Error("write to channel %s timeout", taskID) 56 | } 57 | }() 58 | select { 59 | case <-ctx.Done(): 60 | return 61 | case ch <- msg: 62 | case <-time.After(10 * time.Second): 63 | return 64 | } 65 | } 66 | for { 67 | _, data, err := cli.remote.ReadMessage() 68 | if err != nil { 69 | logging.Error("cli.read(%s): %v", cli.remoteAddr(), err) 70 | return 71 | } 72 | 73 | cli.parent.stInPackets.Inc() 74 | cli.parent.stInBytes.Add(float64(len(data))) 75 | 76 | var msg anet.Msg 77 | err = json.Unmarshal(data, &msg) 78 | if err != nil { 79 | logging.Error("cli.read.unmarshal(%s): %v", cli.remoteAddr(), err) 80 | return 81 | } 82 | 83 | ch := cli.chRead 84 | if len(msg.TaskID) > 0 { 85 | cli.RLock() 86 | ch = cli.taskRead[msg.TaskID] 87 | cli.RUnlock() 88 | if ch == nil { 89 | // logging.Error("response channel %s not found", msg.TaskID) 90 | continue 91 | } 92 | } 93 | send(msg.TaskID, ch, &msg) 94 | } 95 | } 96 | 97 | func (cli *Client) write(ctx context.Context, cancel context.CancelFunc) { 98 | defer func() { 99 | utils.Recover(fmt.Sprintf("cli.write(%s)", cli.remoteAddr())) 100 | cancel() 101 | }() 102 | send := func(msg *anet.Msg, i int) bool { 103 | data, err := json.Marshal(msg) 104 | if err != nil { 105 | logging.Error("cli.write.marshal(%s) %d times: %v", cli.remoteAddr(), i, err) 106 | return false 107 | } 108 | err = cli.remote.WriteMessage(websocket.TextMessage, data) 109 | if err != nil { 110 | logging.Error("cli.write(%s) %d times: %v", cli.remoteAddr(), i, err) 111 | return false 112 | } 113 | cli.parent.stOutPackets.Inc() 114 | cli.parent.stOutBytes.Add(float64(len(data))) 115 | return true 116 | } 117 | for { 118 | select { 119 | case <-ctx.Done(): 120 | return 121 | case msg := <-cli.chWrite: 122 | if msg == nil { 123 | return 124 | } 125 | if msg.Important { 126 | for i := 0; i < 10; i++ { 127 | if send(msg, i+1) { 128 | break 129 | } 130 | } 131 | continue 132 | } 133 | send(msg, 1) 134 | } 135 | } 136 | } 137 | 138 | func (cli *Client) ID() string { 139 | return cli.info.ID 140 | } 141 | 142 | func (cli *Client) OS() string { 143 | return cli.info.OS 144 | } 145 | 146 | func (cli *Client) Platform() string { 147 | return cli.info.Platform 148 | } 149 | 150 | func (cli *Client) Arch() string { 151 | return cli.info.Arch 152 | } 153 | 154 | func (cli *Client) Version() string { 155 | return cli.info.Version 156 | } 157 | 158 | func (cli *Client) IP() string { 159 | return cli.info.IP.String() 160 | } 161 | 162 | func (cli *Client) Mac() string { 163 | return cli.info.MAC 164 | } 165 | 166 | func (cli *Client) HostName() string { 167 | return cli.info.HostName 168 | } 169 | 170 | func (cli *Client) ChanRead(id string) <-chan *anet.Msg { 171 | cli.RLock() 172 | defer cli.RUnlock() 173 | return cli.taskRead[id] 174 | } 175 | 176 | func (cli *Client) ChanClose(id string) { 177 | cli.Lock() 178 | defer cli.Unlock() 179 | if ch := cli.taskRead[id]; ch != nil { 180 | close(ch) 181 | delete(cli.taskRead, id) 182 | } 183 | } 184 | 185 | func (cli *Client) Unknown() <-chan *anet.Msg { 186 | return cli.chRead 187 | } 188 | 189 | func fillPlugin(p *conf.PluginInfo) *anet.PluginInfo { 190 | return &anet.PluginInfo{ 191 | Name: p.Name, 192 | Version: p.Version.String(), 193 | MD5: p.MD5, 194 | URI: p.URI, 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /code/api/layout/runner.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "fmt" 5 | "server/code/client" 6 | "server/code/conf" 7 | "sync" 8 | "time" 9 | 10 | "github.com/lwch/logging" 11 | "github.com/lwch/runtime" 12 | ) 13 | 14 | const ( 15 | runSeq = iota + 1 16 | runPar 17 | runEvo 18 | ) 19 | 20 | type status struct { 21 | ID string `json:"id"` 22 | Created int64 `json:"created"` 23 | Finished int64 `json:"finished"` 24 | OK bool `json:"ok"` 25 | Msg string `json:"msg"` 26 | } 27 | 28 | type runner struct { 29 | sync.RWMutex 30 | parent *Handler 31 | id string 32 | hosts []string 33 | flags byte 34 | user string 35 | pass string 36 | args []map[string]string 37 | deadline time.Time 38 | tasks []taskHandler 39 | clients *client.Clients 40 | // status 41 | done bool 42 | created int64 43 | finished int64 44 | nodes map[string]*status 45 | } 46 | 47 | func newRunner(parent *Handler, clients *client.Clients, 48 | idx uint32, hosts []string, 49 | mode string, 50 | errContinue bool, 51 | user, pass string) *runner { 52 | rand, err := runtime.UUID(8, "0123456789abcdef") 53 | runtime.Assert(err) 54 | taskID := fmt.Sprintf("yaml-%s-%06d-%s", 55 | time.Now().Format("20060102"), idx, rand) 56 | return &runner{ 57 | parent: parent, 58 | id: taskID, 59 | hosts: hosts, 60 | flags: makeFlags(mode, errContinue), 61 | user: user, 62 | pass: pass, 63 | clients: clients, 64 | nodes: make(map[string]*status), 65 | } 66 | } 67 | 68 | func makeFlags(mode string, errContinue bool) byte { 69 | var h byte 70 | if errContinue { 71 | h = 1 << 7 72 | } 73 | var l byte 74 | switch mode { 75 | case "sequence": 76 | l = runSeq 77 | case "parallel": 78 | l = runPar 79 | case "evenodd": 80 | l = runEvo 81 | } 82 | return h | l 83 | } 84 | 85 | func (r *runner) runMode() byte { 86 | return r.flags & 0x7f 87 | } 88 | 89 | func (r *runner) errContinue() bool { 90 | return (r.flags >> 7) > 0 91 | } 92 | 93 | func (r *runner) runHost(id, dir string, args map[string]string) error { 94 | status := &status{ 95 | ID: id, 96 | Created: time.Now().Unix(), 97 | } 98 | r.Lock() 99 | r.nodes[id] = status 100 | r.Unlock() 101 | for _, t := range r.tasks { 102 | if !t.WantRun(args) { 103 | continue 104 | } 105 | err := t.Run(id, dir, r.user, r.pass, args) 106 | if err != nil { 107 | status.Finished = time.Now().Unix() 108 | status.OK = false 109 | status.Msg = fmt.Sprintf("run task [%s] failed: %v", t.Name(), err.Error()) 110 | logging.Error("run task [%s] on agent [%s] failed: %v", t.Name(), id, err) 111 | return err 112 | } 113 | } 114 | status.Finished = time.Now().Unix() 115 | status.OK = true 116 | return nil 117 | } 118 | 119 | func (r *runner) run(dir string) { 120 | defer func() { 121 | r.done = true 122 | r.finished = time.Now().Unix() 123 | logging.Info("task [%s] run done", r.id) 124 | }() 125 | r.created = time.Now().Unix() 126 | r.args = make([]map[string]string, len(r.hosts)) 127 | for i := 0; i < len(r.hosts); i++ { 128 | r.args[i] = make(map[string]string) 129 | } 130 | switch r.runMode() { 131 | case runSeq: 132 | for i, host := range r.hosts { 133 | args := r.args[i] 134 | initArgs(host, i, args) 135 | err := r.runHost(host, dir, args) 136 | if err != nil && !r.errContinue() { 137 | break 138 | } 139 | } 140 | case runPar: 141 | for i, host := range r.hosts { 142 | args := r.args[i] 143 | initArgs(host, i, args) 144 | go r.runHost(host, dir, args) 145 | } 146 | case runEvo: 147 | var wg sync.WaitGroup 148 | var err error 149 | wg.Add((len(r.hosts) + 1) >> 1) 150 | for i := 0; i < len(r.hosts); i += 2 { 151 | args := r.args[i] 152 | initArgs(r.hosts[i], i, args) 153 | go func(id string, args map[string]string) { 154 | defer wg.Done() 155 | er := r.runHost(id, dir, args) 156 | if er != nil { 157 | err = er 158 | } 159 | }(r.hosts[i], args) 160 | } 161 | wg.Wait() 162 | if err != nil && !r.errContinue() { 163 | break 164 | } 165 | for i := 1; i < len(r.hosts); i += 2 { 166 | args := r.args[i] 167 | initArgs(r.hosts[i], i, args) 168 | go r.runHost(r.hosts[i], dir, args) 169 | } 170 | } 171 | } 172 | 173 | func initArgs(agentID string, idx int, args map[string]string) { 174 | args["ID"] = agentID 175 | args["IDX"] = fmt.Sprintf("%d", idx) 176 | if idx%2 == 0 { 177 | args["EVEN"] = "1" 178 | args["ODD"] = "0" 179 | } else { 180 | args["EVEN"] = "0" 181 | args["ODD"] = "1" 182 | } 183 | } 184 | 185 | func (r *runner) GetClient(id string) *client.Client { 186 | return r.clients.Get(id) 187 | } 188 | 189 | func (r *runner) GetPlugin(name, os, arch string) *conf.PluginInfo { 190 | return r.parent.cfg.GetPlugin(name, os, arch) 191 | } 192 | -------------------------------------------------------------------------------- /code/api/layout/hi_file.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "server/code/client" 10 | "server/code/conf" 11 | "server/code/utils" 12 | "time" 13 | 14 | "github.com/jkstack/anet" 15 | "github.com/lwch/logging" 16 | "github.com/lwch/runtime" 17 | ) 18 | 19 | type fileHandler struct { 20 | taskInfo 21 | push bool 22 | src string 23 | dst string 24 | } 25 | 26 | func (h *fileHandler) Check(t Task) error { 27 | if t.Action != "push" && t.Action != "pull" { 28 | return fmt.Errorf("unexpected action %s for task [%s], supported push or pull", 29 | t.Action, t.Name) 30 | } 31 | if len(t.Src) == 0 { 32 | return fmt.Errorf("missing src for task [%s]", t.Name) 33 | } 34 | if t.Action == "pull" && !filepath.IsAbs(t.Src) { 35 | return fmt.Errorf("unexpected absolute path in src for task [%s]", t.Name) 36 | } 37 | if len(t.Dst) == 0 { 38 | return fmt.Errorf("missing dst for task [%s]", t.Name) 39 | } 40 | if !filepath.IsAbs(t.Dst) { 41 | return fmt.Errorf("unexpected relative path in dst for task [%s]", t.Name) 42 | } 43 | return nil 44 | } 45 | 46 | func (h *fileHandler) Clone(t Task, info taskInfo) taskHandler { 47 | return &fileHandler{ 48 | taskInfo: info, 49 | push: t.Action == "push", 50 | src: t.Src, 51 | dst: t.Dst, 52 | } 53 | } 54 | 55 | func (h *fileHandler) Run(id, dir, user, pass string, args map[string]string) error { 56 | deadline := h.deadline() 57 | args["DEADLINE"] = fmt.Sprintf("%d", deadline.Unix()) 58 | action := "push" 59 | if !h.push { 60 | action = "pull" 61 | } 62 | logging.Info("%s file on agent %s", action, id) 63 | cli := h.parent.GetClient(id) 64 | if cli == nil { 65 | return errClientNotfound(id) 66 | } 67 | p := h.parent.GetPlugin("file", cli.OS(), cli.Arch()) 68 | if p == nil { 69 | return errPluginNotInstalled("file") 70 | } 71 | if h.push { 72 | return h.upload(cli, p, dir, user, pass, deadline) 73 | } 74 | return h.download(cli, p, user, pass, deadline) 75 | } 76 | 77 | func (h *fileHandler) upload(cli *client.Client, p *conf.PluginInfo, 78 | dir, user, pass string, deadline time.Time) error { 79 | src := h.src 80 | if !filepath.IsAbs(src) { 81 | src = filepath.Join(dir, src) 82 | } 83 | token, err := runtime.UUID(16, "0123456789abcdef") 84 | if err != nil { 85 | return fmt.Errorf("generate token: %v", err) 86 | } 87 | taskID, err := utils.TaskID() 88 | if err != nil { 89 | return fmt.Errorf("generate task_id: %v", err) 90 | } 91 | md5, err := utils.MD5Checksum(src) 92 | if err != nil { 93 | return fmt.Errorf("calculate md5 checksum: %v", err) 94 | } 95 | fi, err := os.Stat(src) 96 | if err != nil { 97 | return fmt.Errorf("get file info: %v", err) 98 | } 99 | uri := "/file/upload/" + taskID 100 | h.parent.parent.fh.LogUploadCache(taskID, src, token, deadline, false) 101 | defer h.parent.parent.fh.RemoveUploadCache(taskID) 102 | taskID, err = cli.SendUpload(p, client.UploadContext{ 103 | Dir: filepath.Dir(h.dst), 104 | Name: filepath.Base(h.dst), 105 | Auth: h.auth, 106 | User: user, 107 | Pass: pass, 108 | Mod: int(fi.Mode()), 109 | // OwnUser: ownUser, 110 | // OwnGroup: ownGroup, 111 | Size: uint64(fi.Size()), 112 | Md5: md5, 113 | Uri: uri, 114 | Token: token, 115 | }, taskID) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | h.parent.parent.stFileUsage.Inc() 121 | h.parent.parent.stTotalTasks.Inc() 122 | 123 | var rep *anet.Msg 124 | select { 125 | case rep = <-cli.ChanRead(taskID): 126 | case <-time.After(deadline.Sub(time.Now())): 127 | return errTimeout 128 | } 129 | 130 | switch { 131 | case rep.Type == anet.TypeError: 132 | return errors.New(rep.ErrorMsg) 133 | case rep.Type != anet.TypeUploadRep: 134 | return fmt.Errorf("invalid message type: %d", rep.Type) 135 | } 136 | 137 | if !rep.UploadRep.OK { 138 | return errors.New(rep.UploadRep.ErrMsg) 139 | } 140 | 141 | return nil 142 | } 143 | 144 | func (h *fileHandler) download(cli *client.Client, p *conf.PluginInfo, 145 | user, pass string, deadline time.Time) error { 146 | taskID, err := cli.SendDownload(p, h.src) 147 | if err != nil { 148 | return err 149 | } 150 | defer cli.ChanClose(taskID) 151 | 152 | h.parent.parent.stFileUsage.Inc() 153 | h.parent.parent.stTotalTasks.Inc() 154 | 155 | var rep *anet.Msg 156 | after := time.After(deadline.Sub(time.Now())) 157 | select { 158 | case rep = <-cli.ChanRead(taskID): 159 | case <-after: 160 | return errTimeout 161 | } 162 | 163 | switch { 164 | case rep.Type == anet.TypeError: 165 | return errors.New(rep.ErrorMsg) 166 | case rep.Type != anet.TypeDownloadRep: 167 | return fmt.Errorf("invalid message type: %d", rep.Type) 168 | } 169 | 170 | if !rep.DownloadRep.OK { 171 | return errors.New(rep.DownloadRep.ErrMsg) 172 | } 173 | 174 | f, err := os.Create(h.dst) 175 | if err != nil { 176 | return fmt.Errorf("create destination file: %v", err) 177 | } 178 | defer f.Close() 179 | 180 | left := rep.DownloadRep.Size 181 | for { 182 | var msg *anet.Msg 183 | select { 184 | case msg = <-cli.ChanRead(taskID): 185 | case <-after: 186 | return errTimeout 187 | } 188 | switch msg.Type { 189 | case anet.TypeDownloadData: 190 | n, err := writeFile(f, msg.DownloadData) 191 | if err != nil { 192 | return fmt.Errorf("write data: %v", err) 193 | } 194 | left -= uint64(n) 195 | if left == 0 { 196 | return nil 197 | } 198 | case anet.TypeDownloadError: 199 | return errors.New(msg.DownloadError.Msg) 200 | } 201 | } 202 | } 203 | 204 | func writeFile(f *os.File, data *anet.DownloadData) (int, error) { 205 | _, err := f.Seek(int64(data.Offset), io.SeekStart) 206 | if err != nil { 207 | return 0, err 208 | } 209 | dec, err := utils.DecodeData(data.Data) 210 | if err != nil { 211 | return 0, err 212 | } 213 | return f.Write(dec) 214 | } 215 | -------------------------------------------------------------------------------- /code/api/logging/h_config.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | lapi "server/code/api" 11 | "server/code/client" 12 | "time" 13 | 14 | "github.com/jkstack/anet" 15 | "github.com/lwch/api" 16 | "github.com/lwch/logging" 17 | "github.com/lwch/runtime" 18 | ) 19 | 20 | type configArgs struct { 21 | parent *context 22 | Exclude string `json:"exclude"` 23 | Batch int `json:"batch"` 24 | Buffer int `json:"buffer"` 25 | Interval int `json:"interval"` 26 | K8s *k8sConfig `json:"k8s,omitempty"` 27 | Docker *dockerConfig `json:"docker,omitempty"` 28 | File *fileConfig `json:"file,omitempty"` 29 | } 30 | 31 | type context struct { 32 | parent *Handler 33 | ID int64 `json:"id"` 34 | Args configArgs `json:"args"` 35 | Targets []string `json:"cids"` 36 | Started bool `json:"started"` 37 | } 38 | 39 | func (ctx *context) in(id string) bool { 40 | for _, cid := range ctx.Targets { 41 | if cid == id { 42 | return true 43 | } 44 | } 45 | return false 46 | } 47 | 48 | func (h *Handler) config(clients *client.Clients, ctx *api.Context) { 49 | t := ctx.XStr("type") 50 | var rt context 51 | rt.parent = h 52 | rt.Args.parent = &rt 53 | rt.ID = ctx.XInt64("pid") 54 | rt.Args.Exclude = ctx.OStr("exclude", "") 55 | rt.Args.Batch = ctx.OInt("batch", 1000) 56 | rt.Args.Buffer = ctx.OInt("buffer", 4096) 57 | rt.Args.Interval = ctx.OInt("interval", 30) 58 | 59 | var err error 60 | 61 | if len(rt.Args.Exclude) > 0 { 62 | _, err = regexp.Compile(rt.Args.Exclude) 63 | if err != nil { 64 | lapi.BadParamErr(fmt.Sprintf("exclude: %v", err)) 65 | return 66 | } 67 | } 68 | 69 | switch t { 70 | case "k8s": 71 | rt.Args.K8s = new(k8sConfig) 72 | err = rt.Args.K8s.build(ctx) 73 | case "docker": 74 | rt.Args.Docker = new(dockerConfig) 75 | err = rt.Args.Docker.build(ctx) 76 | case "logtail": 77 | rt.Args.File = new(fileConfig) 78 | err = rt.Args.File.build(ctx) 79 | default: 80 | lapi.BadParamErr("type") 81 | return 82 | } 83 | runtime.Assert(err) 84 | 85 | switch { 86 | case rt.Args.K8s != nil: 87 | var cid string 88 | cid, err = rt.Args.sendK8s(clients, rt.ID, h.cfg.LoggingReport) 89 | if err == errNoCollector { 90 | ctx.ERR(1, err.Error()) 91 | return 92 | } 93 | rt.Targets = []string{cid} 94 | default: 95 | ids := ctx.XCsv("ids") 96 | var clis []*client.Client 97 | for _, id := range ids { 98 | cli := clients.Get(id) 99 | if cli == nil { 100 | ctx.NotFound(fmt.Sprintf("agent: %s", id)) 101 | return 102 | } 103 | clis = append(clis, cli) 104 | } 105 | err = rt.Args.sendTargets(clis, rt.ID, h.cfg.LoggingReport) 106 | rt.Targets = ids 107 | } 108 | runtime.Assert(err) 109 | 110 | dir := filepath.Join(h.cfg.DataDir, "logging", fmt.Sprintf("%d.json", rt.ID)) 111 | err = saveConfig(dir, rt) 112 | runtime.Assert(err) 113 | 114 | h.Lock() 115 | h.data[rt.ID] = &rt 116 | h.Unlock() 117 | 118 | ctx.OK(nil) 119 | } 120 | 121 | func saveConfig(dir string, rt context) error { 122 | os.MkdirAll(filepath.Dir(dir), 0755) 123 | f, err := os.Create(dir) 124 | if err != nil { 125 | return err 126 | } 127 | defer f.Close() 128 | return json.NewEncoder(f).Encode(rt) 129 | } 130 | 131 | func (ctx *context) reSend(cli *client.Client, report string) { 132 | err := ctx.Args.sendTo(cli, ctx.ID, report) 133 | if err != nil { 134 | logging.Error("send logging config of project %d to client [%s]: %v", 135 | ctx.ID, cli.ID(), err) 136 | return 137 | } 138 | if ctx.Started { 139 | taskID, err := cli.SendLoggingStart(ctx.ID) 140 | if err != nil { 141 | logging.Error("send logging start of project %d(%s): %v", 142 | ctx.ID, cli.ID(), err) 143 | return 144 | } 145 | defer cli.ChanClose(taskID) 146 | var msg *anet.Msg 147 | select { 148 | case msg = <-cli.ChanRead(taskID): 149 | case <-time.After(api.RequestTimeout): 150 | logging.Error("wait logging start status of project %d(%s): timeout", 151 | ctx.ID, cli.ID()) 152 | return 153 | } 154 | 155 | switch { 156 | case msg.Type == anet.TypeError: 157 | logging.Error("get logging start status of project %d(%s): type error", 158 | ctx.ID, cli.ID()) 159 | return 160 | case msg.Type != anet.TypeLoggingStatusRep: 161 | logging.Error("get logging start status of project %d(%s): type is not logging_rep", 162 | ctx.ID, cli.ID()) 163 | return 164 | } 165 | 166 | if !msg.LoggingStatusRep.OK { 167 | logging.Error("get logging start status of project %d(%s): %s", 168 | ctx.ID, cli.ID(), msg.LoggingStatusRep.Msg) 169 | return 170 | } 171 | } 172 | } 173 | 174 | func (args *configArgs) sendTo(cli *client.Client, pid int64, report string) error { 175 | switch { 176 | case args.K8s != nil: 177 | _, err := cli.SendLoggingConfigK8s(pid, args.Exclude, 178 | args.Batch, args.Buffer, args.Interval, report, 179 | args.K8s.Namespace, args.K8s.Names, args.K8s.Dir, args.K8s.Api, args.K8s.Token) 180 | args.parent.parent.stTotalTasks.Inc() 181 | return err 182 | case args.Docker != nil: 183 | _, err := cli.SendLoggingConfigDocker(pid, args.Exclude, 184 | args.Batch, args.Buffer, args.Interval, report, 185 | args.Docker.ContainerName, args.Docker.ContainerTag, args.Docker.Dir) 186 | args.parent.parent.stTotalTasks.Inc() 187 | return err 188 | case args.File != nil: 189 | _, err := cli.SendLoggingConfigFile(pid, args.Exclude, 190 | args.Batch, args.Buffer, args.Interval, report, 191 | args.File.Dir) 192 | args.parent.parent.stTotalTasks.Inc() 193 | return err 194 | default: 195 | return errors.New("unsupported") 196 | } 197 | } 198 | 199 | func (args *configArgs) sendK8s(clients *client.Clients, pid int64, report string) (string, error) { 200 | var cli *client.Client 201 | clis := clients.Prefix(args.K8s.Namespace + "-k8s-") 202 | if len(clis) == 0 { 203 | clis = clients.Prefix("k8s-") 204 | if len(clis) == 0 { 205 | return "", errNoCollector 206 | } 207 | } 208 | cli = clis[int(pid)%len(clis)] 209 | err := args.sendTo(cli, pid, report) 210 | if err != nil { 211 | return "", err 212 | } 213 | return cli.ID(), nil 214 | } 215 | 216 | func (args *configArgs) sendTargets(targets []*client.Client, pid int64, report string) error { 217 | for _, cli := range targets { 218 | err := args.sendTo(cli, pid, report) 219 | if err != nil { 220 | logging.Error("send logging config to %s: %v", cli.ID(), err) 221 | return err 222 | } 223 | } 224 | return nil 225 | } 226 | --------------------------------------------------------------------------------