├── .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 | [](https://github.com/jkstack/smartagent-server/actions/workflows/build.yml)
4 | [](https://github.com/jkstack/smartagent-server)
5 | [](https://www.gnu.org/licenses/agpl-3.0.txt)
6 | [](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 |
--------------------------------------------------------------------------------