curve/tools-v2/pkg/cli/command/base.go

398 lines
9.9 KiB
Go

/*
* Copyright (c) 2022 NetEase Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Project: CurveCli
* Created Date: 2022-05-09
* Author: chengyi (Cyber-SiKu)
*/
package basecmd
import (
"context"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/moby/term"
"github.com/olekukonko/tablewriter"
cmderror "github.com/opencurve/curve/tools-v2/internal/error"
cobrautil "github.com/opencurve/curve/tools-v2/internal/utils"
process "github.com/opencurve/curve/tools-v2/internal/utils/process"
config "github.com/opencurve/curve/tools-v2/pkg/config"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const (
CURL_VERSION = "curl/7.54.0"
)
// FinalCurveCmd is the final executable command,
// it has no subcommands.
// The execution process is Init->RunCommand->Print.
// Error Use to indicate whether the command is wrong
// and the reason for the execution error
type FinalCurveCmd struct {
Use string `json:"-"`
Short string `json:"-"`
Long string `json:"-"`
Example string `json:"-"`
Error *cmderror.CmdError `json:"error"`
Result interface{} `json:"result"`
TableNew *tablewriter.Table `json:"-"`
Header []string `json:"-"`
Cmd *cobra.Command `json:"-"`
}
func (fc *FinalCurveCmd) SetHeader(header []string) {
fc.Header = header
fc.TableNew.SetHeader(header)
width := 80
if ws, err := term.GetWinsize(0); err == nil {
if width < int(ws.Width) {
width = int(ws.Width)
}
}
if len(header) != 0 {
fc.TableNew.SetColWidth(width/len(header) - 1)
}
}
// FinalCurveCmdFunc is the function type for final command
// If there is flag[required] related code should not be placed in init,
// the check for it is placed between PreRun and Run
type FinalCurveCmdFunc interface {
Init(cmd *cobra.Command, args []string) error
RunCommand(cmd *cobra.Command, args []string) error
Print(cmd *cobra.Command, args []string) error
// result in plain format string
ResultPlainOutput() error
AddFlags()
}
// MidCurveCmd is the middle command and has subcommands.
// If you execute this command
// you will be prompted which subcommands are included
type MidCurveCmd struct {
Use string
Short string
Cmd *cobra.Command
}
// Add subcommand for MidCurveCmd
type MidCurveCmdFunc interface {
AddSubCommands()
}
func NewFinalCurveCli(cli *FinalCurveCmd, funcs FinalCurveCmdFunc) *cobra.Command {
cli.Cmd = &cobra.Command{
Use: cli.Use,
Short: cli.Short,
Long: cli.Long,
Example: cli.Example,
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
err := funcs.Init(cmd, args)
show := config.GetFlagBool(cli.Cmd, config.VERBOSE)
process.SetShow(show)
if err != nil {
return err
}
err = funcs.RunCommand(cmd, args)
if err != nil {
return err
}
return funcs.Print(cmd, args)
},
SilenceUsage: false,
}
config.AddFormatFlag(cli.Cmd)
funcs.AddFlags()
cobrautil.SetFlagErrorFunc(cli.Cmd)
// set table
cli.TableNew = tablewriter.NewWriter(os.Stdout)
cli.TableNew.SetRowLine(true)
cli.TableNew.SetAutoFormatHeaders(true)
cli.TableNew.SetAutoWrapText(true)
cli.TableNew.SetRowLine(true)
cli.TableNew.SetAlignment(tablewriter.ALIGN_LEFT)
return cli.Cmd
}
func NewMidCurveCli(cli *MidCurveCmd, add MidCurveCmdFunc) *cobra.Command {
cli.Cmd = &cobra.Command{
Use: cli.Use,
Short: cli.Short,
Args: cobrautil.NoArgs,
}
add.AddSubCommands()
return cli.Cmd
}
type Metric struct {
Addrs []string
SubUri string
timeout time.Duration
}
type MetricResult struct {
Addr string
Key string
Value string
Err *cmderror.CmdError
}
func NewMetric(addrs []string, subUri string, timeout time.Duration) *Metric {
return &Metric{
Addrs: addrs,
SubUri: subUri,
timeout: timeout,
}
}
func QueryMetric(m *Metric) (string, *cmderror.CmdError) {
response := make(chan string, 1)
size := len(m.Addrs)
if size > config.MaxChannelSize() {
size = config.MaxChannelSize()
}
errs := make(chan *cmderror.CmdError, size)
for _, host := range m.Addrs {
url := "http://" + host + m.SubUri
go httpGet(url, m.timeout, response, errs)
}
var retStr string
var vecErrs []*cmderror.CmdError
count := 0
for err := range errs {
if err.Code != cmderror.CODE_SUCCESS {
vecErrs = append(vecErrs, err)
} else {
retStr = <-response
vecErrs = append(vecErrs, cmderror.ErrSuccess())
break
}
count++
if count >= len(m.Addrs) {
// all host failed
break
}
}
retErr := cmderror.MergeCmdError(vecErrs)
return retStr, &retErr
}
func GetMetricValue(metricRet string) (string, *cmderror.CmdError) {
kv := cobrautil.RmWitespaceStr(metricRet)
kvVec := strings.Split(kv, ":")
if len(kvVec) != 2 {
err := cmderror.ErrParseMetric()
err.Format(metricRet)
return "", err
}
kvVec[1] = strings.Replace(kvVec[1], "\"", "", -1)
return kvVec[1], cmderror.ErrSuccess()
}
func GetKeyValueFromJsonMetric(metricRet string, key string) (string, *cmderror.CmdError) {
var data map[string]interface{}
if err := json.Unmarshal([]byte(metricRet), &data); err != nil {
err := cmderror.ErrParseMetric()
err.Format(metricRet)
return "", err
}
return data[key].(string), cmderror.ErrSuccess()
}
func httpGet(url string, timeout time.Duration, response chan string, errs chan *cmderror.CmdError) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
interErr := cmderror.ErrHttpCreateGetRequest()
interErr.Format(err.Error())
errs <- interErr
}
// for get curl url
req.Header.Set("User-Agent", CURL_VERSION)
client := http.Client{
Timeout: timeout,
}
resp, err := client.Do(req)
if err != nil {
interErr := cmderror.ErrHttpClient()
interErr.Format(err.Error())
errs <- interErr
} else if resp.StatusCode != http.StatusOK {
statusErr := cmderror.ErrHttpStatus(resp.StatusCode)
statusErr.Format(url, resp.StatusCode)
errs <- statusErr
} else {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
interErr := cmderror.ErrHttpUnreadableResult()
interErr.Format(url, err.Error())
errs <- interErr
}
// get response
response <- string(body)
errs <- cmderror.ErrSuccess()
}
}
type Rpc struct {
Addrs []string
RpcTimeout time.Duration
RpcRetryTimes int32
RpcFuncName string
}
func NewRpc(addrs []string, timeout time.Duration, retryTimes int32, funcName string) *Rpc {
return &Rpc{
Addrs: addrs,
RpcTimeout: timeout,
RpcRetryTimes: retryTimes,
RpcFuncName: funcName,
}
}
type RpcFunc interface {
NewRpcClient(cc grpc.ClientConnInterface)
Stub_Func(ctx context.Context) (interface{}, error)
}
type Result struct {
addr string
err *cmderror.CmdError
result interface{}
}
func GetRpcResponse(rpc *Rpc, rpcFunc RpcFunc) (interface{}, *cmderror.CmdError) {
size := len(rpc.Addrs)
if size > config.MaxChannelSize() {
size = config.MaxChannelSize()
}
results := make(chan Result, size)
for _, addr := range rpc.Addrs {
go func(address string) {
log.Printf("%s: start to dial", address)
ctx, cancel := context.WithTimeout(context.Background(), rpc.RpcTimeout)
defer cancel()
conn, err := grpc.DialContext(ctx, address, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
if err != nil {
errDial := cmderror.ErrRpcDial()
errDial.Format(address, err.Error())
results <- Result{
addr: address,
err: errDial,
result: nil,
}
log.Printf("%s: fail to dial", address)
} else {
rpcFunc.NewRpcClient(conn)
log.Printf("%s: start to rpc [%s]", address, rpc.RpcFuncName)
res, err := rpcFunc.Stub_Func(ctx)
if err != nil {
errRpc := cmderror.ErrRpcCall()
errRpc.Format(rpc.RpcFuncName, err.Error())
results <- Result{
addr: address,
err: errRpc,
result: nil,
}
log.Printf("%s: fail to get rpc [%s] response", address, rpc.RpcFuncName)
} else {
results <- Result{
addr: address,
err: cmderror.ErrSuccess(),
result: res,
}
log.Printf("%s: get rpc[%s] request successfully", address, rpc.RpcFuncName)
}
}
}(addr)
}
var ret interface{}
var vecErrs []*cmderror.CmdError
count := 0
for res := range results {
if res.err.TypeCode() != cmderror.CODE_SUCCESS {
vecErrs = append(vecErrs, res.err)
} else {
ret = res.result
break
}
count++
if count >= len(rpc.Addrs) {
// all host failed
break
}
}
if len(vecErrs) >= len(rpc.Addrs) {
retErr := cmderror.MergeCmdError(vecErrs)
return ret, &retErr
}
return ret, cmderror.ErrSuccess()
}
type RpcResult struct {
Response interface{}
Error *cmderror.CmdError
}
func GetRpcListResponse(rpcList []*Rpc, rpcFunc []RpcFunc) ([]interface{}, []*cmderror.CmdError) {
chanSize := len(rpcList)
if chanSize > config.MaxChannelSize() {
chanSize = config.MaxChannelSize()
}
results := make(chan RpcResult, chanSize)
size := 0
for i := range rpcList {
size++
go func(rpc *Rpc, rpcFunc RpcFunc) {
res, err := GetRpcResponse(rpc, rpcFunc)
results <- RpcResult{res, err}
}(rpcList[i], rpcFunc[i])
}
count := 0
var retRes []interface{}
var vecErrs []*cmderror.CmdError
for res := range results {
if res.Error.TypeCode() != cmderror.CODE_SUCCESS {
// get fail
vecErrs = append(vecErrs, res.Error)
} else {
retRes = append(retRes, res.Response)
}
count++
if count >= size {
// get all rpc response
break
}
}
return retRes, vecErrs
}