diff --git a/.github/conf/.goreleaser.yml b/.github/conf/.goreleaser.yml new file mode 100644 index 0000000..798c7d2 --- /dev/null +++ b/.github/conf/.goreleaser.yml @@ -0,0 +1,34 @@ +before: + hooks: + - go mod tidy +builds: + - binary: collar + env: + - CGO_ENABLED=0 + main: ./cmd/cli.go + goos: + - linux + - windows + - darwin + goarm: + - 6 + - 7 + ldflags: + - -s -w -X github.com/DVKunion/collar/pkg/config.Version={{.Version}} +archives: + - replacements: + darwin: darwin + linux: linux + windows: windows + 386: i386 + amd64: x86_64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4b78af4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +name: collar + +on: + push: + tags: + - "v*" + pull_request: + branches: [ main ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.18 + + - name: Build + run: go build --ldflags "-s -w -X github.com/DVKunion/SeaMoon/pkg/consts.Version=${{github.ref_name}}" cmd/cli.go + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8ecc744 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: release + +on: + push: + tags: + - "v*" + +jobs: + build: + name: GoReleaser build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.18 + id: go + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + distribution: goreleaser + version: latest + args: -f .github/conf/.goreleaser.yml + workdir: . + env: + GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5330f70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +.DS_Store +.config.yml \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..743ec59 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Collar + +![](https://cdn.dvkunion.cn/collar/collar.png) + +Collar 是长亭牧云主机助手(Collie) 的 CLI 工具,方便使用者在终端管理 Collie。 + +Collar 作为 Collie 牧羊犬的项圈,希望能让用户更加舒适的管理和使用 Collie。 + +会逐步从 Collie 核心功能(文件管理、在线终端)为主,抽空慢慢填齐其余的全部的功能(Docker、资源负载显示、登陆历史、进程清单等) + +在这个基础上,可能还会做一些功能的优化,比如多主机的数据聚合、多主机同步执行命令(类ansible)等。 + +目前已支持的功能模块: + ++ 主机列表 ++ 进程列表 ++ shell终端 + +## 开始使用 + +首先注册百川云平台,并开通 牧云主机助手 应用。 + +然后点击工作台-Token管理-生成Token,勾选所有牧云主机助手相关权限,生成您的Token信息。 + +![](https://cdn.dvkunion.cn/collar/c1d25c402e94487b8e0dcfd18d4c297a.png) + +> 请注意,token 存在有效期。为了方便使用,您可以申请一个时间比较长的 token。 +> 您的 token 十分珍贵,可以获取您所有主机的权限!请妥善保管~ + +然后在github-release页面下载符合自己操作系统的二进制文件,放置在$PATH目录下 + +执行: +`collar auth -t YOUT_TOKEN` + +初始化身份认证成功,即可开始使用。 + +## 使用手册 + +### 主机列表 + +`collar hosts` + +获取主机列表信息。 + +### 进程列表 +`collar top [hostId/host_name/host_ip/host_inner_ip]` + +获取主机进程信息, 每3s更新一次。 + +### 登陆主机 Terminal + +`collar shell [hostId/host_name/host_ip/host_inner_ip]` + +可以通过 主机ID/主机名/主机IP/主机内网IP 进行登录。 + +**使用自动登陆模式**: + +`collar shell -a [hostId/host_name/host_ip/host_inner_ip]` + +这将会使用您配置的自动登录用户名进行登陆(暂不支持通过cli设置自动登录用户名) \ No newline at end of file diff --git a/cmd/cli.go b/cmd/cli.go new file mode 100644 index 0000000..c6aaa12 --- /dev/null +++ b/cmd/cli.go @@ -0,0 +1,142 @@ +package main + +import ( + "errors" + "fmt" + "github.com/DVKunion/collar/pkg/utils" + "os" + "time" + + "github.com/DVKunion/collar/pkg/config" + "github.com/DVKunion/collar/pkg/log" + "github.com/DVKunion/collar/pkg/sdk" + "github.com/spf13/cobra" +) + +var ( + token string + auto bool + rootCmd = &cobra.Command{} + authCmd = &cobra.Command{ + Use: "auth", + Short: "auth rivers.chaitin.cn", + RunE: auth, + } + listCmd = &cobra.Command{ + Use: "hosts", + Short: "list hosts", + PreRunE: auth, + RunE: hosts, + } + + topCmd = &cobra.Command{ + Use: "top", + Short: "list host process && systemInfo", + PreRunE: auth, + RunE: process, + } + + shellCmd = &cobra.Command{ + Use: "shell", + Short: "run a shell at host", + PreRunE: auth, + RunE: terminal, + } + + versionCmd = &cobra.Command{ + Use: "version", + Short: "version", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Collie - Collar: %s\n", config.Version) + }, + } +) + +func auth(cmd *cobra.Command, args []string) error { + err := config.SingleConfig.Load() + if err != nil { + // 说明加载失败了,需要重新认证 + if token == "" { + return errors.New("empty token") + } + config.SingleConfig = &config.Config{ + Token: token, + HostList: make([]config.Host, 0), + UpdateAt: time.Now(), + } + } + + resp, errSDK := sdk.GetHostList() + if errSDK != nil { + return errSDK + } + + if v, ok := resp.Body().([]config.Host); ok { + config.SingleConfig.HostList = v + // 说明加载的情况 + if err != nil { + log.Infof(cmd.Context(), "success auth ! host total: %d", len(config.SingleConfig.HostList)) + log.Infof(cmd.Context(), "online: %d", len(config.SingleConfig.GetOnline())) + log.Infof(cmd.Context(), "offline: %d", len(config.SingleConfig.HostList)-len(config.SingleConfig.GetOnline())) + } + return config.SingleConfig.Save() + } else { + return errors.New("get host list error") + } +} + +func hosts(cmd *cobra.Command, args []string) error { + log.Info(cmd.Context(), config.SingleConfig.HostList) + return nil +} + +func process(cmd *cobra.Command, args []string) error { + resp, err := sdk.GetProcessList(args[0]) + if err != nil { + return err + } + log.Info(cmd.Context(), resp.Body()) + return nil +} + +func terminal(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + if len(args) != 1 { + return errors.New("must have at least one host arg") + } + log.Infof(ctx, "connecting to %s ......", args[0]) + hostId := utils.Trans2HostId(args[0]) + if hostId == "" { + return errors.New("host offline or not exists,use `collar hosts` to check host status。") + } + user := "" + if auto { + resp, err := sdk.GetAutoLoginUser(hostId) + if err != nil { + return err + } + user = resp.Body().(string) + } + tm, err := sdk.GetTerminal(ctx, hostId, user) + if err != nil { + return err + } + if err = tm.Transfer(); err != nil { + return err + } + return nil +} + +func main() { + authCmd.Flags().StringVarP(&token, "token", "t", "", "users token") + shellCmd.Flags().BoolVarP(&auto, "auto", "a", false, "use auto login") + + rootCmd.AddCommand(authCmd) + rootCmd.AddCommand(listCmd) + rootCmd.AddCommand(topCmd) + rootCmd.AddCommand(shellCmd) + rootCmd.AddCommand(versionCmd) + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/cmd/test.go b/cmd/test.go new file mode 100644 index 0000000..81a47fc --- /dev/null +++ b/cmd/test.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + + ui "github.com/gizak/termui/v3" + "github.com/gizak/termui/v3/widgets" +) + +func main() { + if err := ui.Init(); err != nil { + log.Fatalf("failed to initialize termui: %v", err) + } + defer ui.Close() + + p := widgets.NewParagraph() + p.Text = "Hello World!" + p.SetRect(0, 0, 25, 5) + + ui.Render(p) + + for e := range ui.PollEvents() { + if e.Type == ui.KeyboardEvent { + break + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8937da9 --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module github.com/DVKunion/collar + +go 1.18 + +require ( + github.com/containerd/console v1.0.3 + github.com/gizak/termui/v3 v3.1.0 + github.com/go-resty/resty/v2 v2.7.0 + github.com/gogf/gf/v2 v2.3.2 + github.com/gorilla/websocket v1.5.0 + github.com/spf13/cobra v1.6.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/fatih/color v1.13.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/mattn/go-colorable v0.1.9 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.opentelemetry.io/otel v1.7.0 // indirect + go.opentelemetry.io/otel/sdk v1.7.0 // indirect + go.opentelemetry.io/otel/trace v1.7.0 // indirect + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..31b74e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,106 @@ +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E= +github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc= +github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/gogf/gf/v2 v2.3.2 h1:nlJ0zuDWqFb93/faZmr7V+GADx/lzz5Unz/9x6OJ2u8= +github.com/gogf/gf/v2 v2.3.2/go.mod h1:tsbmtwcAl2chcYoq/fP9W2FZf06aw4i89X34nbSHo9Y= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= +github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opentelemetry.io/otel v1.7.0 h1:Z2lA3Tdch0iDcrhJXDIlC94XE+bxok1F9B+4Lz/lGsM= +go.opentelemetry.io/otel v1.7.0/go.mod h1:5BdUoMIz5WEs0vt0CUEMtSSaTSHBBVwrhnz7+nrD5xk= +go.opentelemetry.io/otel/sdk v1.7.0 h1:4OmStpcKVOfvDOgCt7UriAPtKolwIhxpnSNI/yK+1B0= +go.opentelemetry.io/otel/sdk v1.7.0/go.mod h1:uTEOTwaqIVuTGiJN7ii13Ibp75wJmYUDe374q6cZwUU= +go.opentelemetry.io/otel/trace v1.7.0 h1:O37Iogk1lEkMRXewVtZ1BBTVn5JEp8GrJvP92bJqC6o= +go.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 h1:GLw7MR8AfAG2GmGcmVgObFOHXYypgGjnGno25RDwn3Y= +golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..386e33b --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,87 @@ +package config + +import ( + "io" + "os" + "time" + + "gopkg.in/yaml.v3" +) + +const CfgPath = ".config.yml" + +var Version = "" + +var SingleConfig = &Config{} + +type Config struct { + Token string `json:"token" yaml:"token"` + UpdateAt time.Time `json:"update_at" yaml:"update_at"` + HostList []Host `json:"host_list" yaml:"host_list"` +} + +type Host struct { + Id string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + OsType string `json:"os_type" yaml:"os_type"` + OsName string `json:"os_name" yaml:"os_name"` + OrgId string `json:"org_id" yaml:"org_id"` + UserId string `json:"user_id" yaml:"user_id"` + Cpu int `json:"cpu" yaml:"cpu"` + Memory int64 `json:"memory" yaml:"memory"` + InternalIp string `json:"internal_ip" yaml:"internal_ip"` + ExternalIp string `json:"external_ip" yaml:"external_ip"` + Comment interface{} `json:"comment" yaml:"comment"` + Arch string `json:"arch" yaml:"arch"` + Status string `json:"status" yaml:"status"` + Uptime int `json:"uptime" yaml:"uptime"` + Ipinfo struct { + Country string `json:"country" yaml:"country"` + Province string `json:"province" yaml:"province"` + City string `json:"city" yaml:"city"` + } `json:"ipinfo" yaml:"ipinfo"` + LatestTime int `json:"latest_time" yaml:"latest_time"` + EngineVersion string `json:"engine_version" yaml:"engine_version"` +} + +func (c *Config) Load() error { + f, err := os.Open(CfgPath) + if err != nil { + return err + } + t, err := io.ReadAll(f) + if err != nil { + return err + } + err = yaml.Unmarshal(t, &c) + if err != nil { + return err + } + return nil +} + +func (c *Config) GetOnline() []Host { + var online []Host + for _, h := range c.HostList { + if h.Status == "online" { + online = append(online, h) + } + } + return online +} + +func (c *Config) Save() error { + data, err := yaml.Marshal(&c) + + f, err := os.Create(CfgPath) + if err != nil { + return err + } + defer f.Close() + + _, err = f.Write(data) + if err != nil { + return err + } + return nil +} diff --git a/pkg/console/host.go b/pkg/console/host.go new file mode 100644 index 0000000..be9e037 --- /dev/null +++ b/pkg/console/host.go @@ -0,0 +1,3 @@ +package console + +// output \ No newline at end of file diff --git a/pkg/console/top.go b/pkg/console/top.go new file mode 100644 index 0000000..90f153b --- /dev/null +++ b/pkg/console/top.go @@ -0,0 +1 @@ +package console diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 0000000..0ccfd65 --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,174 @@ +package log + +import ( + "context" + + "github.com/gogf/gf/v2/os/glog" +) + +// Log +// a set of chain/setter/logger +type Log interface { + Logger + Chain + Setter +} + +type defaultLog struct { + *glog.Logger +} + +func New() Log { + logger := glog.New() + return &defaultLog{Logger: logger} +} + +var DefaultLog = New() + +func Clone() Log { + return DefaultLog.Clone() +} + +func Skip(skip int) Log { + return DefaultLog.Skip(skip) +} + +func Line(long ...bool) Log { + return DefaultLog.Line(long...) +} + +func Prefix(prefix string) Log { + return DefaultLog.Prefix(prefix) +} + +func Cat(category interface{}) Log { + return DefaultLog.Cat(category) +} + +func Info(ctx context.Context, v ...interface{}) { + DefaultLog.Info(ctx, v...) +} + +func Infof(ctx context.Context, format string, v ...interface{}) { + DefaultLog.Infof(ctx, format, v...) +} + +func Debug(ctx context.Context, v ...interface{}) { + DefaultLog.Debug(ctx, v...) +} + +func Debugf(ctx context.Context, format string, v ...interface{}) { + DefaultLog.Debugf(ctx, format, v...) +} + +func Notice(ctx context.Context, v ...interface{}) { + DefaultLog.Notice(ctx, v...) +} + +func Noticef(ctx context.Context, format string, v ...interface{}) { + DefaultLog.Noticef(ctx, format, v...) +} + +func Warning(ctx context.Context, v ...interface{}) { + DefaultLog.Warning(ctx, v...) +} + +func Warningf(ctx context.Context, format string, v ...interface{}) { + DefaultLog.Warningf(ctx, format, v...) +} + +func Error(ctx context.Context, v ...interface{}) { + DefaultLog.Error(ctx, v...) +} + +func Errorf(ctx context.Context, format string, v ...interface{}) { + DefaultLog.Errorf(ctx, format, v...) +} + +func Critical(ctx context.Context, v ...interface{}) { + DefaultLog.Critical(ctx, v...) +} + +func Criticalf(ctx context.Context, format string, v ...interface{}) { + DefaultLog.Criticalf(ctx, format, v...) +} + +func Panic(ctx context.Context, v ...interface{}) { + DefaultLog.Panic(ctx, v...) +} + +func Panicf(ctx context.Context, format string, v ...interface{}) { + DefaultLog.Panicf(ctx, format, v...) +} + +func Fatal(ctx context.Context, v ...interface{}) { + DefaultLog.Fatal(ctx, v...) +} + +func Fatalf(ctx context.Context, format string, v ...interface{}) { + DefaultLog.Fatalf(ctx, format, v...) +} + +func (l *defaultLog) Info(ctx context.Context, v ...interface{}) { + l.Logger.Info(ctx, v...) +} + +func (l *defaultLog) Infof(ctx context.Context, format string, v ...interface{}) { + l.Logger.Infof(ctx, format, v...) +} + +func (l *defaultLog) Debug(ctx context.Context, v ...interface{}) { + l.Logger.Debug(ctx, v...) +} + +func (l *defaultLog) Debugf(ctx context.Context, format string, v ...interface{}) { + l.Logger.Debugf(ctx, format, v...) +} + +func (l *defaultLog) Notice(ctx context.Context, v ...interface{}) { + l.Logger.Notice(ctx, v...) +} + +func (l *defaultLog) Noticef(ctx context.Context, format string, v ...interface{}) { + l.Logger.Noticef(ctx, format, v...) +} + +func (l *defaultLog) Warning(ctx context.Context, v ...interface{}) { + l.Logger.Warning(ctx, v...) +} + +func (l *defaultLog) Warningf(ctx context.Context, format string, v ...interface{}) { + l.Logger.Warningf(ctx, format, v...) +} + +func (l *defaultLog) Error(ctx context.Context, v ...interface{}) { + l.Logger.Error(ctx, v...) +} + +func (l *defaultLog) Errorf(ctx context.Context, format string, v ...interface{}) { + l.Logger.Errorf(ctx, format, v...) +} + +func (l *defaultLog) Critical(ctx context.Context, v ...interface{}) { + l.Logger.Critical(ctx, v...) +} + +func (l *defaultLog) Criticalf(ctx context.Context, format string, v ...interface{}) { + l.Logger.Criticalf(ctx, format, v...) +} + +func (l *defaultLog) Panic(ctx context.Context, v ...interface{}) { + l.Logger.Panic(ctx, v...) +} + +func (l *defaultLog) Panicf(ctx context.Context, format string, v ...interface{}) { + l.Logger.Panicf(ctx, format, v...) +} + +func (l *defaultLog) Fatal(ctx context.Context, v ...interface{}) { + l.Logger.Fatal(ctx, v...) +} + +func (l *defaultLog) Fatalf(ctx context.Context, format string, v ...interface{}) { + l.Logger.Fatalf(ctx, format, v...) +} diff --git a/pkg/log/log_chain.go b/pkg/log/log_chain.go new file mode 100644 index 0000000..87279c0 --- /dev/null +++ b/pkg/log/log_chain.go @@ -0,0 +1,52 @@ +package log + +// Chain +// log chain call +type Chain interface { + Clone() Log + Skip(skip int) Log + Line(long ...bool) Log + Prefix(prefix string) Log + Cat(category interface{}) Log +} + +func (l *defaultLog) Clone() Log { + log := *l + log.Logger = l.Logger.Clone() + return &log +} + +func (l *defaultLog) Skip(skip int) Log { + log := *l + log.Logger = l.Logger.Skip(skip) + return &log +} + +func (l *defaultLog) Line(long ...bool) Log { + log := *l + log.Logger = l.Logger.Line(long...) + return &log +} + +func (l *defaultLog) Prefix(prefix string) Log { + log := *l + log.Logger = l.Logger.Clone() + log.Logger.SetPrefix(prefix) + return &log +} + +func (l *defaultLog) Cat(category interface{}) Log { + var cat string + switch t := category.(type) { + case string: + cat = t + case interface{ String() string }: + cat = t.String() + default: + cat = "default" + } + + log := *l + log.Logger = l.Logger.Cat(cat) + return &log +} diff --git a/pkg/log/log_level.go b/pkg/log/log_level.go new file mode 100644 index 0000000..2e9de02 --- /dev/null +++ b/pkg/log/log_level.go @@ -0,0 +1,22 @@ +package log + +import ( + "github.com/gogf/gf/v2/os/glog" +) + +type Level int + +const ( + LEVEL_ALL = LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT + LEVEL_DEV = LEVEL_ALL + LEVEL_PROD = LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT + LEVEL_NONE = glog.LEVEL_NONE + LEVEL_DEBU = glog.LEVEL_DEBU + LEVEL_INFO = glog.LEVEL_INFO + LEVEL_NOTI = glog.LEVEL_NOTI + LEVEL_WARN = glog.LEVEL_WARN + LEVEL_ERRO = glog.LEVEL_ERRO + LEVEL_CRIT = glog.LEVEL_CRIT + LEVEL_PANI = glog.LEVEL_PANI + LEVEL_FATA = glog.LEVEL_FATA +) diff --git a/pkg/log/log_logger.go b/pkg/log/log_logger.go new file mode 100644 index 0000000..5e7bdae --- /dev/null +++ b/pkg/log/log_logger.go @@ -0,0 +1,28 @@ +package log + +import ( + "context" +) + +// Logger +// log output handle +type Logger interface { + Print(ctx context.Context, v ...interface{}) + Printf(ctx context.Context, format string, v ...interface{}) + Debug(ctx context.Context, v ...interface{}) + Debugf(ctx context.Context, format string, v ...interface{}) + Info(ctx context.Context, v ...interface{}) + Infof(ctx context.Context, format string, v ...interface{}) + Notice(ctx context.Context, v ...interface{}) + Noticef(ctx context.Context, format string, v ...interface{}) + Warning(ctx context.Context, v ...interface{}) + Warningf(ctx context.Context, format string, v ...interface{}) + Error(ctx context.Context, v ...interface{}) + Errorf(ctx context.Context, format string, v ...interface{}) + Critical(ctx context.Context, v ...interface{}) + Criticalf(ctx context.Context, format string, v ...interface{}) + Panic(ctx context.Context, v ...interface{}) + Panicf(ctx context.Context, format string, v ...interface{}) + Fatal(ctx context.Context, v ...interface{}) + Fatalf(ctx context.Context, format string, v ...interface{}) +} diff --git a/pkg/log/log_setter.go b/pkg/log/log_setter.go new file mode 100644 index 0000000..54bd1cf --- /dev/null +++ b/pkg/log/log_setter.go @@ -0,0 +1,79 @@ +package log + +import ( + "context" + "time" +) + +type Handler func(ctx context.Context) + +// Setter +// set logger config +type Setter interface { + SetPath(path string) error + SetFile(path string) + SetLevel(level Level) + SetPrefix(prefix string) + SetHeaderPrint(enabled bool) + SetStdoutPrint(enabled bool) + + SetRotateSize(size int64) + SetRotateExpire(t time.Duration) + SetRotateBackupLimit(limit int) + SetRotateBackupExpire(t time.Duration) + SetRotateBackupCompress(n int) +} + +func (l *defaultLog) SetPath(path string) error { + return l.Logger.SetPath(path) +} + +func (l *defaultLog) SetFile(pattern string) { + l.Logger.SetFile(pattern) +} + +func (l *defaultLog) SetLevel(level Level) { + l.Logger.SetLevel(int(level)) +} + +func (l *defaultLog) SetPrefix(prefix string) { + l.Logger.SetPrefix(prefix) +} + +func (l *defaultLog) SetHeaderPrint(enabled bool) { + l.Logger.SetHeaderPrint(enabled) +} + +func (l *defaultLog) SetStdoutPrint(enabled bool) { + l.Logger.SetStdoutPrint(enabled) +} + +func (l *defaultLog) SetRotateSize(size int64) { + config := l.Logger.GetConfig() + config.RotateSize = size + l.Logger.SetConfig(config) +} + +func (l *defaultLog) SetRotateExpire(t time.Duration) { + config := l.Logger.GetConfig() + config.RotateExpire = t + l.Logger.SetConfig(config) +} + +func (l *defaultLog) SetRotateBackupLimit(limit int) { + config := l.Logger.GetConfig() + config.RotateBackupLimit = limit + l.Logger.SetConfig(config) +} + +func (l *defaultLog) SetRotateBackupExpire(t time.Duration) { + config := l.Logger.GetConfig() + config.RotateBackupExpire = t + l.Logger.SetConfig(config) +} + +func (l *defaultLog) SetRotateBackupCompress(n int) { + config := l.Logger.GetConfig() + config.RotateBackupCompress = n + l.Logger.SetConfig(config) +} diff --git a/pkg/sdk/collie.go b/pkg/sdk/collie.go new file mode 100644 index 0000000..019c1c1 --- /dev/null +++ b/pkg/sdk/collie.go @@ -0,0 +1,110 @@ +package sdk + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/DVKunion/collar/pkg/terminal" + "github.com/DVKunion/collar/pkg/utils" +) + +const ( + Host = "collie.rivers.chaitin.cn" + HostList = "api/v1/host/list?page=1&size=1000" + ProcessList = "api/v1/host/%s/processes?id=%s&field=cpu_usage&order_by=desc&type=instant" + LoginList = "api/v1/host/%s/login_history?id=%s&type=instant" + ImageList = "api/v1/docker/%s/images?id=%s" + ContainerList = "api/v1/docker/%s/containers?id=%s" + + AutoLoginUser = "api/v1/host/%s/auto_login_user?id=%s" + TerminalWss = "api/v1/ws/terminal?id=%s" + TerminalWssAuto = "api/v1/ws/terminal?id=%s&user=%s" +) + +func GetHostList() (Response, error) { + originResp, err := utils.Get(strings.Join([]string{"https:/", Host, HostList}, "/")) + if err != nil { + return nil, err + } + resp := &HostResponse{} + err = json.Unmarshal(originResp.Body(), &resp) + if err != nil { + return nil, err + } + return resp, nil +} + +func GetProcessList(hostId string) (Response, error) { + originResp, err := utils.Get(strings.Join([]string{"https:/", Host, fmt.Sprintf(ProcessList, hostId, hostId)}, "/")) + if err != nil { + return nil, err + } + resp := &ProcessResponse{} + err = json.Unmarshal(originResp.Body(), &resp) + if err != nil { + return nil, err + } + return resp, nil +} + +func GetLoginList(hostId string) (Response, error) { + originResp, err := utils.Get(strings.Join([]string{"https:/", Host, fmt.Sprintf(LoginList, hostId, hostId)}, "/")) + if err != nil { + return nil, err + } + resp := &HostResponse{} + err = json.Unmarshal(originResp.Body(), &resp) + if err != nil { + return nil, err + } + return resp, nil +} + +func GetImageList(hostId string) (Response, error) { + originResp, err := utils.Get(strings.Join([]string{"https:/", Host, fmt.Sprintf(ImageList, hostId, hostId)}, "/")) + if err != nil { + return nil, err + } + resp := &HostResponse{} + err = json.Unmarshal(originResp.Body(), &resp) + if err != nil { + return nil, err + } + return resp, nil +} + +func GetContainerList(hostId string) (Response, error) { + originResp, err := utils.Get(strings.Join([]string{"https:/", Host, fmt.Sprintf(ContainerList, hostId, hostId)}, "/")) + if err != nil { + return nil, err + } + resp := &HostResponse{} + err = json.Unmarshal(originResp.Body(), &resp) + if err != nil { + return nil, err + } + return resp, nil +} + +func GetAutoLoginUser(hostId string) (Response, error) { + originResp, err := utils.Get(strings.Join([]string{"https:/", Host, fmt.Sprintf(AutoLoginUser, hostId, hostId)}, "/")) + if err != nil { + return nil, err + } + resp := &AutoLoginResponse{} + err = json.Unmarshal(originResp.Body(), &resp) + if err != nil { + return nil, err + } + return resp, nil +} + +func GetTerminal(ctx context.Context, hostId string, user string) (*terminal.Terminal, error) { + tm := terminal.NewTerminal(ctx) + if user == "" { + return tm, tm.Connect(strings.Join([]string{"wss:/", Host, fmt.Sprintf(TerminalWss, hostId)}, "/")) + } + return tm, tm.Connect(strings.Join([]string{"wss:/", Host, fmt.Sprintf(TerminalWssAuto, hostId, user)}, "/")) +} diff --git a/pkg/sdk/response.go b/pkg/sdk/response.go new file mode 100644 index 0000000..6d38864 --- /dev/null +++ b/pkg/sdk/response.go @@ -0,0 +1,82 @@ +package sdk + +import "github.com/DVKunion/collar/pkg/config" + +type Response interface { + Success() bool + Body() interface{} +} + +type HostResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + Edges []config.Host `json:"edges"` + HasNextPage bool `json:"has_next_page"` + TotalCount int `json:"total_count"` + } +} + +func (h *HostResponse) Success() bool { + if h.Code != 200 || h.Message != "ok" { + return false + } + return true +} + +func (h *HostResponse) Body() interface{} { + return h.Data.Edges +} + +type ProcessResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + DataTime int `json:"data_time"` + Processes []Process `json:"processes"` + } +} + +type Process struct { + Name string `json:"name"` + Pid int `json:"pid"` + User string `json:"user"` + Uid int `json:"uid"` + StartTime int `json:"start_time"` + CpuUsage float64 `json:"cpu_usage"` + Memory int `json:"memory"` + MemoryUsage float64 `json:"memory_usage"` + Cmd string `json:"cmd"` +} + +func (p *ProcessResponse) Success() bool { + if p.Code != 200 || p.Message != "ok" { + return false + } + return true +} + +func (p *ProcessResponse) Body() interface{} { + return p.Data.Processes +} + +type AutoLoginResponse struct { + Message string `json:"message"` + Data struct { + Id int `json:"id"` + HostId string `json:"host_id"` + Name string `json:"name"` + } `json:"data"` + Code int `json:"code"` +} + +func (a *AutoLoginResponse) Success() bool { + if a.Code != 200 || a.Message != "ok" { + return false + } + return true +} + +func (a *AutoLoginResponse) Body() interface{} { + return a.Data.Name +} diff --git a/pkg/terminal/terminal.go b/pkg/terminal/terminal.go new file mode 100644 index 0000000..77a2366 --- /dev/null +++ b/pkg/terminal/terminal.go @@ -0,0 +1,177 @@ +package terminal + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "sync" + "time" + + "github.com/DVKunion/collar/pkg/config" + "github.com/DVKunion/collar/pkg/log" + "github.com/containerd/console" + "github.com/gorilla/websocket" +) + +type Terminal struct { + ctx context.Context + conn *websocket.Conn + header http.Header + host config.Host + pool sync.Pool +} + +type Message struct { + MsgType MessageType `json:"type"` + Setting *MessageSetting `json:"setting,omitempty"` + Share *MessageShare `json:"share,omitempty"` + Value string `json:"value,omitempty"` +} + +var ( + HandShakeMsg = &Message{ + MsgType: Control, + Setting: &MessageSetting{Col: "129", Row: "29"}, + } + PingMsg = &Message{ + MsgType: Ping, + } +) + +func (m *Message) Send() []byte { + data, err := json.Marshal(&m) + if err != nil { + return []byte{} + } + return data +} + +type MessageSetting struct { + Col string `json:"col"` + Row string `json:"row"` +} + +type MessageShare struct { + Status int `json:"status,omitempty"` +} + +type MessageType string + +const ( + Data MessageType = "data" + Control MessageType = "ctrl" + Ping MessageType = "ping" +) + +func NewTerminal(ctx context.Context) *Terminal { + return &Terminal{ + ctx: ctx, + header: map[string][]string{ + "X-Ca-Token": {config.SingleConfig.Token}, + }, + pool: sync.Pool{ + New: func() interface{} { + return make([]byte, 64*1024+262) + }, + }, + } +} + +func (t *Terminal) Connect(url string) error { + dialer := websocket.Dialer{ + HandshakeTimeout: 10 * time.Second, + } + wss, resp, err := dialer.Dial(url, t.header) + if err != nil { + return err + } + if resp.StatusCode != http.StatusSwitchingProtocols { + return err + } + t.conn = wss + // first need send control msg + err = t.conn.WriteMessage(websocket.TextMessage, HandShakeMsg.Send()) + if err != nil { + log.Error(nil, err) + } + log.Infof(t.ctx, "connect success") + return err +} + +func (t *Terminal) Transfer() error { + term, err := console.ConsoleFromFile(os.Stdout) + if err != nil { + return err + } + + err = term.SetRaw() + if err != nil { + return err + } + defer term.Reset() + + errC := make(chan error, 1) + go func() { + b := t.pool.Get().([]byte) + defer t.pool.Put(b) + + _, err := io.CopyBuffer(term, t, b) + errC <- err + }() + + go func() { + b := t.pool.Get().([]byte) + defer t.pool.Put(b) + + _, err := io.CopyBuffer(t, term, b) + errC <- err + }() + + if err := <-errC; err != nil && err != io.EOF { + log.Info(nil, "Connect Close") + } + return nil +} + +func (t *Terminal) Read(b []byte) (n int, err error) { + _, message, err := t.conn.ReadMessage() + if err != nil { + if wsErr, ok := err.(*websocket.CloseError); ok && wsErr.Code == websocket.CloseNormalClosure { + return 0, io.EOF + } + return 0, err + } + msg := &Message{} + err = json.Unmarshal(message, &msg) + if err != nil { + return 0, err + } + if msg.MsgType == Ping { + err = t.conn.WriteMessage(websocket.TextMessage, PingMsg.Send()) + if err != nil { + return 0, err + } + } + fmt.Print(msg.Value) + copy(b, msg.Value) + return 0, nil +} + +func (t *Terminal) Write(b []byte) (n int, err error) { + cmd := &Message{ + MsgType: Data, + Value: string(b), + } + err = t.conn.WriteMessage(websocket.TextMessage, cmd.Send()) + if err != nil { + return 0, err + } + return len(b), nil +} + +func (t *Terminal) Login() error { + return nil +} diff --git a/pkg/utils/args.go b/pkg/utils/args.go new file mode 100644 index 0000000..918d35e --- /dev/null +++ b/pkg/utils/args.go @@ -0,0 +1,16 @@ +package utils + +import "github.com/DVKunion/collar/pkg/config" + +// Trans2HostId transfer users input 2 hostId +func Trans2HostId(arg string) string { + // Just Get Online host + hostList := config.SingleConfig.GetOnline() + for _, h := range hostList { + // check id/name/internal_ip/external_ip + if h.Id == arg || h.Name == arg || h.ExternalIp == arg || h.InternalIp == arg { + return h.Id + } + } + return "" +} diff --git a/pkg/utils/http.go b/pkg/utils/http.go new file mode 100644 index 0000000..71cb435 --- /dev/null +++ b/pkg/utils/http.go @@ -0,0 +1,36 @@ +package utils + +import ( + "context" + "crypto/tls" + "errors" + "net/http" + + "github.com/go-resty/resty/v2" + + "github.com/DVKunion/collar/pkg/config" +) + +func Get(url string) (*resty.Response, error) { + client := newRequest(config.SingleConfig.Token) + resp, err := client.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode() != http.StatusOK { + return nil, errors.New("http request error: " + resp.Status()) + } + return resp, nil +} + +func newRequest(token string, ctx ...context.Context) *resty.Request { + client := resty.New() + client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) + client.SetHeader("X-Ca-Token", token) + req := client.R() + if len(ctx) > 0 { + req.SetContext(ctx[0]) + } + + return req +}