From 4368c97a5c91bd384fdcaaaae15b606a75c6ab94 Mon Sep 17 00:00:00 2001 From: lixiaojun Date: Wed, 27 Mar 2019 15:09:25 +0800 Subject: [PATCH 1/2] support creating uhost in parallel --- ansi/code.go | 13 ++- base/util.go | 112 ++++++++++++++++++++ cmd/eip.go | 23 +++++ cmd/root_test.go | 6 -- cmd/uhost.go | 243 ++++++++++++++++++++++++++++++++------------ go.mod | 1 + go.sum | 1 + services/project.go | 68 +++++++++++++ services/region.go | 111 ++++++++++++++++++++ ux/document.go | 209 +++++++++++++++++++++++++++++++++++++ ux/spinner.go | 23 +++++ ux/spinnerv2.go | 106 +++++++++++++++++++ ux/terminal.go | 47 +++++++++ 13 files changed, 890 insertions(+), 73 deletions(-) delete mode 100644 cmd/root_test.go create mode 100644 services/project.go create mode 100644 services/region.go create mode 100644 ux/document.go create mode 100644 ux/spinnerv2.go create mode 100644 ux/terminal.go diff --git a/ansi/code.go b/ansi/code.go index a837dd9221..f702a87516 100644 --- a/ansi/code.go +++ b/ansi/code.go @@ -7,8 +7,6 @@ import ( const csi = "\x1b[" -// const OSC = "\x1b]" -// const BEL = "\x07" const sep = ";" //CursorLeft move cursor to the left side @@ -17,11 +15,20 @@ var CursorLeft = fmt.Sprintf("%sG", csi) //EraseDown Erase the screen from the current line down to the bottom of the var EraseDown = fmt.Sprintf("%sJ", csi) +//EraseUp Erase the screen from the current line up to the top of the screen +var EraseUp = fmt.Sprintf("%s1J", csi) + +//CursorUp Move cursor up a specific amount of rows. func CursorUp(count int) string { return fmt.Sprintf("%s%dA", csi, count) } -//CursorTo +//CursorPrevLine Move cursor up a specific amount of rows. +func CursorPrevLine(count int) string { + return fmt.Sprintf("%s%dF", csi, count) +} + +//CursorTo Set the absolute position of the cursor. `x` `y` is the top left of the screen. func CursorTo(x, y int) string { return fmt.Sprintf("%s%d;%dH", csi, y+1, x+1) } diff --git a/base/util.go b/base/util.go index 44e236e09b..bb29cc5cd3 100644 --- a/base/util.go +++ b/base/util.go @@ -11,6 +11,7 @@ import ( "runtime" "strconv" "strings" + "sync" "time" "unicode" @@ -18,6 +19,7 @@ import ( uerr "github.com/ucloud/ucloud-sdk-go/ucloud/error" "github.com/ucloud/ucloud-sdk-go/ucloud/helpers/waiter" "github.com/ucloud/ucloud-sdk-go/ucloud/log" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-sdk-go/ucloud/response" "github.com/ucloud/ucloud-cli/model" @@ -39,6 +41,38 @@ var SdkClient *sdk.Client //BizClient 用于调用业务接口 var BizClient *Client +//Logger 日志 +var Logger = log.New() +var mu sync.Mutex + +func init() { + file, err := os.Create(GetHomePath() + fmt.Sprintf("/%s/cli.log", ConfigPath)) + if err != nil { + return + } + Logger.SetOutput(file) +} + +//Log 记录日志 +func Log(logs []string) { + mu.Lock() + defer mu.Unlock() + Logger.Info("=============================================================") + for _, line := range logs { + Logger.Info(line) + } +} + +//ToQueryMap tranform request to map +func ToQueryMap(req request.Common) map[string]string { + reqMap, err := request.ToQueryMap(req) + if err != nil { + return nil + } + // delete(reqMap, "Password") + return reqMap +} + //GetHomePath 获取家目录 func GetHomePath() string { if runtime.GOOS == "windows" { @@ -129,6 +163,19 @@ func HandleError(err error) { } } +//ParseError 解析错误为字符串 +func ParseError(err error) string { + if uErr, ok := err.(uerr.Error); ok && uErr.Code() != 0 { + format := "Something wrong. RetCode:%d. Message:%s" + message := uErr.Message() + if uErr.Code() == -1 || uErr.Code() == -2 { + message = "request timeout, retry later please" + } + return fmt.Sprintf(format, uErr.Code(), message) + } + return fmt.Sprintf("Error:%v", err) +} + //PrintJSON 以JSON格式打印数据集合 func PrintJSON(dataSet interface{}) error { bytes, err := json.MarshalIndent(dataSet, "", " ") @@ -294,6 +341,7 @@ var RegionLabel = map[string]string{ "cn-bj2": "Beijing2", "cn-sh2": "Shanghai2", "cn-gd": "Guangzhou", + "cn-qz": "Quanzhou", "hk": "Hongkong", "us-ca": "LosAngeles", "us-ws": "Washington", @@ -322,6 +370,70 @@ type Poller struct { SdescribeFunc func(string) (interface{}, error) } +//Sspoll 简化版, 支持并发 +func (p *Poller) Sspoll(resourceID, pollText string, targetStates []string, block *ux.Block) { + w := waiter.StateWaiter{ + Pending: []string{"pending"}, + Target: []string{"avaliable"}, + Refresh: func() (interface{}, string, error) { + inst, err := p.SdescribeFunc(resourceID) + if err != nil { + return nil, "", err + } + + if inst == nil { + return nil, "pending", nil + } + instValue := reflect.ValueOf(inst) + instValue = reflect.Indirect(instValue) + instType := instValue.Type() + if instValue.Kind() != reflect.Struct { + return nil, "", fmt.Errorf("Instance is not struct") + } + state := "" + for i := 0; i < instValue.NumField(); i++ { + for _, sf := range p.stateFields { + if instType.Field(i).Name == sf { + state = instValue.Field(i).String() + } + } + } + if state != "" { + for _, t := range targetStates { + if t == state { + return inst, "avaliable", nil + } + } + } + return nil, "pending", nil + + }, + Timeout: p.Timeout, + } + + done := make(chan bool) + go func() { + if _, err := w.Wait(); err != nil { + log.Error(err) + if _, ok := err.(*waiter.TimeoutError); ok { + done <- false + return + } + } + done <- true + }() + + spin := ux.NewDotSpin(p.Out, pollText) + block.SetSpin(spin) + + ret := <-done + if ret { + spin.Stop() + } else { + spin.Timeout() + } +} + //Spoll 简化版 func (p *Poller) Spoll(resourceID, pollText string, targetStates []string) { w := waiter.StateWaiter{ diff --git a/cmd/eip.go b/cmd/eip.go index 51ea2db4c1..533b0edc6a 100644 --- a/cmd/eip.go +++ b/cmd/eip.go @@ -320,6 +320,29 @@ func bindEIP(resourceID, resourceType, eipID, projectID, region *string) { } } +func sbindEIP(resourceID, resourceType, eipID, projectID, region *string) string { + ip := net.ParseIP(*eipID) + if ip != nil { + eipID, err := getEIPIDbyIP(ip, *projectID, *region) + if err != nil { + base.HandleError(err) + } else { + *resourceID = eipID + } + } + req := base.BizClient.NewBindEIPRequest() + req.ResourceId = resourceID + req.ResourceType = resourceType + req.EIPId = sdk.String(base.PickResourceID(*eipID)) + req.ProjectId = sdk.String(base.PickResourceID(*projectID)) + req.Region = region + _, err := base.BizClient.BindEIP(req) + if err != nil { + return base.ParseError(err) + } + return fmt.Sprintf("bind EIP[%s] with %s[%s]", *req.EIPId, *req.ResourceType, *req.ResourceId) +} + //NewCmdEIPUnbind ucloud eip unbind func NewCmdEIPUnbind() *cobra.Command { eipIDs := []string{} diff --git a/cmd/root_test.go b/cmd/root_test.go deleted file mode 100644 index 91c4071e9d..0000000000 --- a/cmd/root_test.go +++ /dev/null @@ -1,6 +0,0 @@ -package cmd - -// func TestCmdRoot(t *testing.T) { -// root := NewCmdRoot() -// root.Execute() -// } diff --git a/cmd/uhost.go b/cmd/uhost.go index 1dd0464cda..9931be44f1 100644 --- a/cmd/uhost.go +++ b/cmd/uhost.go @@ -19,10 +19,12 @@ import ( "fmt" "io" "strings" + "sync" "github.com/spf13/cobra" "github.com/ucloud/ucloud-sdk-go/services/uhost" + "github.com/ucloud/ucloud-sdk-go/services/unet" sdk "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-cli/base" @@ -31,6 +33,8 @@ import ( "github.com/ucloud/ucloud-cli/ux" ) +var uhostSpoller = base.NewSpoller(sdescribeUHostByID, base.Cxt.GetWriter()) + //NewCmdUHost ucloud uhost func NewCmdUHost() *cobra.Command { cmd := &cobra.Command{ @@ -39,19 +43,19 @@ func NewCmdUHost() *cobra.Command { Long: `List,create,delete,stop,restart,poweroff or resize UHost instance`, Args: cobra.NoArgs, } - writer := base.Cxt.GetWriter() + out := base.Cxt.GetWriter() cmd.AddCommand(NewCmdUHostList()) - cmd.AddCommand(NewCmdUHostCreate(writer)) - cmd.AddCommand(NewCmdUHostDelete(writer)) - cmd.AddCommand(NewCmdUHostStop(writer)) - cmd.AddCommand(NewCmdUHostStart(writer)) - cmd.AddCommand(NewCmdUHostReboot(writer)) + cmd.AddCommand(NewCmdUHostCreate(out)) + cmd.AddCommand(NewCmdUHostDelete(out)) + cmd.AddCommand(NewCmdUHostStop(out)) + cmd.AddCommand(NewCmdUHostStart(out)) + cmd.AddCommand(NewCmdUHostReboot(out)) cmd.AddCommand(NewCmdUHostPoweroff()) - cmd.AddCommand(NewCmdUHostResize(writer)) - cmd.AddCommand(NewCmdUHostClone(writer)) - cmd.AddCommand(NewCmdUhostResetPassword(writer)) - cmd.AddCommand(NewCmdUhostReinstallOS(writer)) - cmd.AddCommand(NewCmdUhostCreateImage(writer)) + cmd.AddCommand(NewCmdUHostResize(out)) + cmd.AddCommand(NewCmdUHostClone(out)) + cmd.AddCommand(NewCmdUhostResetPassword(out)) + cmd.AddCommand(NewCmdUhostReinstallOS(out)) + cmd.AddCommand(NewCmdUhostCreateImage(out)) return cmd } @@ -64,6 +68,7 @@ type UHostRow struct { PrivateIP string PublicIP string Config string + Image string Type string State string CreationTime string @@ -95,10 +100,9 @@ func NewCmdUHostList() *cobra.Command { if ip.Type == "Private" { row.PrivateIP = ip.IP } else { - row.PublicIP += fmt.Sprintf("%s %s", ip.IP, ip.Type) + row.PublicIP += fmt.Sprintf("%s", ip.IP) } } - osName := strings.SplitN(host.OsName, " ", 2) cupCore := host.CPU memorySize := host.Memory / 1024 diskSize := 0 @@ -107,7 +111,8 @@ func NewCmdUHostList() *cobra.Command { diskSize += disk.Size } } - row.Config = fmt.Sprintf("%s cpu:%d memory:%dG disk:%dG", osName[0], cupCore, memorySize, diskSize) + row.Config = fmt.Sprintf("cpu:%d memory:%dG disk:%dG", cupCore, memorySize, diskSize) + row.Image = fmt.Sprintf("%s|%s", host.BasicImageId, host.BasicImageName) row.CreationTime = base.FormatDate(host.CreateTime) row.State = host.State row.Type = host.UHostType + "/" + host.HostType @@ -120,8 +125,7 @@ func NewCmdUHostList() *cobra.Command { req.ProjectId = cmd.Flags().String("project-id", base.ConfigIns.ProjectID, "Optional. Assign project-id") req.Region = cmd.Flags().String("region", base.ConfigIns.Region, "Optional. Assign region") req.Zone = cmd.Flags().String("zone", "", "Optional. Assign availability zone") - cmd.Flags().StringSliceVar(&req.UHostIds, "uhost-id", make([]string, 0), "Optional. UHost Instance ID, multiple values separated by comma(without space)") - // req.Tag = cmd.Flags().String("group", "", "Optional. Business group") + cmd.Flags().StringSliceVar(&req.UHostIds, "uhost-id", make([]string, 0), "Optional. Resource ID of uhost instances, multiple values separated by comma(without space)") req.Offset = cmd.Flags().Int("offset", 0, "Optional. Offset default 0") req.Limit = cmd.Flags().Int("limit", 50, "Optional. Limit default 50, max value 100") bindGroup(req, cmd.Flags()) @@ -131,8 +135,10 @@ func NewCmdUHostList() *cobra.Command { //NewCmdUHostCreate [ucloud uhost create] func NewCmdUHostCreate(out io.Writer) *cobra.Command { - var bindEipID *string - var async *bool + var bindEipIDs []string + var password string + var async bool + var count int req := base.BizClient.NewCreateUHostInstanceRequest() eipReq := base.BizClient.NewAllocateEIPRequest() @@ -141,55 +147,62 @@ func NewCmdUHostCreate(out io.Writer) *cobra.Command { Short: "Create UHost instance", Long: "Create UHost instance", Run: func(cmd *cobra.Command, args []string) { + if count > 100 || count < 1 { + fmt.Fprintln(out, "count should be between 1 and 100") + return + } *req.Memory *= 1024 req.LoginMode = sdk.String("Password") req.ImageId = sdk.String(base.PickResourceID(*req.ImageId)) req.VPCId = sdk.String(base.PickResourceID(*req.VPCId)) req.SubnetId = sdk.String(base.PickResourceID(*req.SubnetId)) req.SecurityGroupId = sdk.String(base.PickResourceID(*req.SecurityGroupId)) - - resp, err := base.BizClient.CreateUHostInstance(req) - if err != nil { - base.HandleError(err) - return - } - - if len(resp.UHostIds) == 1 { - text := fmt.Sprintf("uhost[%s] is initializing", resp.UHostIds[0]) - if *async { - fmt.Fprintln(out, text) - } else { - poller := base.NewPoller(describeUHostByID, out) - poller.Poll(resp.UHostIds[0], *req.ProjectId, *req.Region, *req.Zone, text, []string{status.HOST_RUNNING, status.HOST_FAIL}) + if count <= 5 { + for i := 0; i < count; i++ { + bindEipID := "" + if len(bindEipIDs) > i { + bindEipID = bindEipIDs[i] + } + go createUhost(req, eipReq, bindEipID, password, async, out, make(chan bool, count), nil, nil) } + <-ux.Doc.Done } else { - fmt.Fprintf(out, "expect uhost count 1 , accept %d", len(resp.UHostIds)) - return - } - bindEipID = sdk.String(base.PickResourceID(*bindEipID)) - if *bindEipID != "" && len(resp.UHostIds) == 1 { - bindEIP(sdk.String(resp.UHostIds[0]), sdk.String("uhost"), bindEipID, req.ProjectId, req.Region) - } else if *eipReq.OperatorName != "" && *eipReq.Bandwidth != 0 { - eipReq.ChargeType = req.ChargeType - eipReq.Tag = req.Tag - eipReq.Quantity = req.Quantity - eipReq.Region = req.Region - eipReq.ProjectId = req.ProjectId - eipResp, err := base.BizClient.AllocateEIP(eipReq) + wg := &sync.WaitGroup{} + retCh := make(chan bool, count) + ux.Doc.Disable() - if err != nil { - base.HandleError(err) - } else { - for _, eip := range eipResp.EIPSet { - base.Cxt.Printf("allocate EIP[%s] ", eip.EIPId) - for _, ip := range eip.EIPAddr { - base.Cxt.Printf("IP:%s Line:%s \n", ip.IP, ip.OperatorName) + result := map[string]int{ + "total": count, + "success": 0, + "fail": 0, + } + + refresh := ux.NewRefresh() + wg.Add(count) + + go func() { + tokens := make(chan struct{}, 10) + for i := 0; i < count; i++ { + bindEipID := "" + if len(bindEipIDs) > i { + bindEipID = bindEipIDs[i] } - if len(resp.UHostIds) == 1 { - bindEIP(sdk.String(resp.UHostIds[0]), sdk.String("uhost"), sdk.String(eip.EIPId), req.ProjectId, req.Region) + go createUhost(req, eipReq, bindEipID, password, async, out, retCh, wg, tokens) + } + }() + + go func() { + refresh.Do(fmt.Sprintf("uhost creating, total:%d, success:%d, fail:%d", result["total"], result["success"], result["fail"])) + for ret := range retCh { + if ret { + result["success"]++ + } else { + result["fail"]++ } + refresh.Do(fmt.Sprintf("uhost creating, total:%d, success:%d, fail:%d", result["total"], result["success"], result["fail"])) } - } + }() + wg.Wait() } }, } @@ -211,20 +224,22 @@ func NewCmdUHostCreate(out io.Writer) *cobra.Command { flags := cmd.Flags() flags.SortFlags = false - async = flags.Bool("async", false, "Optional. Do not wait for the long-running operation to finish.") req.CPU = flags.Int("cpu", 4, "Required. The count of CPU cores. Optional parameters: {1, 2, 4, 8, 12, 16, 24, 32}") req.Memory = flags.Int("memory-gb", 8, "Required. Memory size. Unit: GB. Range: [1, 128], multiple of 2") - req.Password = flags.String("password", "", "Required. Password of the uhost user(root/ubuntu)") + flags.StringVar(&password, "password", "", "Required. Password of the uhost user(root/ubuntu)") req.ImageId = flags.String("image-id", "", "Required. The ID of image. see 'ucloud image list'") + flags.BoolVar(&async, "async", false, "Optional. Do not wait for the long-running operation to finish.") + flags.IntVar(&count, "count", 1, "Optional. Number of uhost to create. Range [1,100]") req.VPCId = flags.String("vpc-id", "", "Optional. VPC ID. This field is required under VPC2.0. See 'ucloud vpc list'") req.SubnetId = flags.String("subnet-id", "", "Optional. Subnet ID. This field is required under VPC2.0. See 'ucloud subnet list'") req.Name = flags.String("name", "UHost", "Optional. UHost instance name") - bindEipID = flags.String("bind-eip", "", "Optional. Resource ID or IP Address of eip that will be bound to the new created uhost") - eipReq.OperatorName = flags.String("create-eip-line", "", "Optional. Required if you want to create new EIP. Line of the created eip to be bound with the new created uhost") - eipReq.Bandwidth = cmd.Flags().Int("create-eip-bandwidth-mb", 0, "Optional. Required if you want to create new EIP. Bandwidth(Unit:Mbps).The range of value related to network charge mode. By traffic [1, 300]; by bandwidth [1,800] (Unit: Mbps); it could be 0 if the eip belong to the shared bandwidth") - eipReq.PayMode = cmd.Flags().String("create-eip-traffic-mode", "Bandwidth", "Optional. 'Traffic','Bandwidth' or 'ShareBandwidth'") + flags.StringSliceVar(&bindEipIDs, "bind-eip", nil, "Optional. Resource ID or IP Address of eip that will be bound to the new created uhost") + eipReq.OperatorName = flags.String("create-eip-line", "", "Optional. BGP for regions in the chinese mainland and International for overseas regions") + eipReq.Bandwidth = flags.Int("create-eip-bandwidth-mb", 0, "Optional. Required if you want to create new EIP. Bandwidth(Unit:Mbps).The range of value related to network charge mode. By traffic [1, 300]; by bandwidth [1,800] (Unit: Mbps); it could be 0 if the eip belong to the shared bandwidth") + eipReq.PayMode = flags.String("create-eip-traffic-mode", "Bandwidth", "Optional. 'Traffic','Bandwidth' or 'ShareBandwidth'") + eipReq.ShareBandwidthId = flags.String("shared-bw-id", "", "Optional. Resource ID of shared bandwidth. It takes effect when create-eip-traffic-mode is ShareBandwidth ") eipReq.Name = flags.String("create-eip-name", "", "Optional. Name of created eip to bind with the uhost") - eipReq.Remark = cmd.Flags().String("create-eip-remark", "", "Optional.Remark of your EIP.") + eipReq.Remark = flags.String("create-eip-remark", "", "Optional.Remark of your EIP.") req.ChargeType = flags.String("charge-type", "Month", "Optional.'Year',pay yearly;'Month',pay monthly;'Dynamic', pay hourly") req.Quantity = flags.Int("quantity", 1, "Optional. The duration of the instance. N years/months.") @@ -232,7 +247,7 @@ func NewCmdUHostCreate(out io.Writer) *cobra.Command { bindRegion(req, flags) bindZone(req, flags) - req.UHostType = flags.String("type", defaultUhostType, "Optional. Default is 'N2' of which cpu is V4 and sata disk. also support 'N1' means V3 cpu and sata disk;'I2' means V4 cpu and ssd disk;'D1' means big data model;'G1' means GPU type, model for K80;'G2' model for P40; 'G3' model for V100") + req.UHostType = flags.String("type", defaultUhostType, "Optional. Accept values: N1, N2, N3, G1, G2, G3, I1, I2, C1. Forward to https://docs.ucloud.cn/api/uhost-api/uhost_type for details") req.NetCapability = flags.String("net-capability", "Normal", "Optional. Default is 'Normal', also support 'Super' which will enhance multiple times network capability as before") req.Disks[0].Type = flags.String("os-disk-type", "LOCAL_NORMAL", "Optional. Enumeration value. 'LOCAL_NORMAL', Ordinary local disk; 'CLOUD_NORMAL', Ordinary cloud disk; 'LOCAL_SSD',local ssd disk; 'CLOUD_SSD',cloud ssd disk; 'EXCLUSIVE_LOCAL_DISK',big data. The disk only supports a limited combination.") req.Disks[0].Size = flags.Int("os-disk-size-gb", 20, "Optional. Default 20G. Windows should be bigger than 40G Unit GB") @@ -245,7 +260,7 @@ func NewCmdUHostCreate(out io.Writer) *cobra.Command { flags.SetFlagValues("charge-type", "Month", "Year", "Dynamic", "Trial") flags.SetFlagValues("cpu", "1", "2", "4", "8", "12", "16", "24", "32") - flags.SetFlagValues("type", "N2", "N1", "I2", "D1", "G1", "G2", "G3") + flags.SetFlagValues("type", "N2", "N1", "N3", "I2", "I1", "C1", "G1", "G2", "G3") flags.SetFlagValues("net-capability", "Normal", "Super") flags.SetFlagValues("os-disk-type", "LOCAL_NORMAL", "CLOUD_NORMAL", "LOCAL_SSD", "CLOUD_SSD", "EXCLUSIVE_LOCAL_DISK") flags.SetFlagValues("os-disk-backup-type", "NONE", "DATAARK") @@ -278,6 +293,91 @@ func NewCmdUHostCreate(out io.Writer) *cobra.Command { return cmd } +func createUhost(req *uhost.CreateUHostInstanceRequest, eipReq *unet.AllocateEIPRequest, bindEipID, password string, async bool, out io.Writer, retCh chan<- bool, wg *sync.WaitGroup, tokens chan struct{}) { + //控制并发数量 + if tokens != nil { + tokens <- struct{}{} + defer func() { + <-tokens + }() + } + if wg != nil { + defer wg.Done() + } + req.Password = sdk.String(password) + resp, err := base.BizClient.CreateUHostInstance(req) + block := ux.NewBlock() + ux.Doc.Append(block) + logs := []string{} + defer func() { + base.Log(logs) + }() + logs = append(logs, fmt.Sprintf("request:%v", base.ToQueryMap(req))) + if err != nil { + logs = append(logs, fmt.Sprintf("err:%v", err)) + block.Append(base.ParseError(err)) + block.AppendDone() + retCh <- false + return + } + logs = append(logs, fmt.Sprintf("resp:%#v", resp)) + if len(resp.UHostIds) == 1 { + text := fmt.Sprintf("uhost[%s] is initializing", resp.UHostIds[0]) + if async { + block.Append(text) + } else { + uhostSpoller.Sspoll(resp.UHostIds[0], text, []string{status.HOST_RUNNING, status.HOST_FAIL}, block) + } + retCh <- true + } else { + block.Append(fmt.Sprintf("expect uhost count 1 , accept %d", len(resp.UHostIds))) + block.AppendDone() + retCh <- false + return + } + if bindEipID != "" { + eip := base.PickResourceID(bindEipID) + logs = append(logs, fmt.Sprintf("bind eip: %s", eip)) + info := sbindEIP(sdk.String(resp.UHostIds[0]), sdk.String("uhost"), &eip, req.ProjectId, req.Region) + logs = append(logs, fmt.Sprintf("bind eip result: %s", info)) + block.Append(info) + } else if *eipReq.Bandwidth != 0 { + eipReq.ChargeType = req.ChargeType + eipReq.Tag = req.Tag + eipReq.Quantity = req.Quantity + eipReq.Region = req.Region + eipReq.ProjectId = req.ProjectId + logs = append(logs, fmt.Sprintf("create eip request: %v", base.ToQueryMap(eipReq))) + if *eipReq.OperatorName == "" { + if strings.HasPrefix(*req.Region, "cn") { + *eipReq.OperatorName = "BGP" + } else { + *eipReq.OperatorName = "International" + } + } + eipResp, err := base.BizClient.AllocateEIP(eipReq) + + if err != nil { + logs = append(logs, fmt.Sprintf("create eip error: %#v", err)) + block.Append(base.ParseError(err)) + } else { + logs = append(logs, fmt.Sprintf("create eip resp: %#v", eipResp)) + for _, eip := range eipResp.EIPSet { + block.Append(fmt.Sprintf("allocate EIP[%s] ", eip.EIPId)) + for _, ip := range eip.EIPAddr { + block.Append(fmt.Sprintf("IP:%s Line:%s", ip.IP, ip.OperatorName)) + } + if len(resp.UHostIds) == 1 { + info := sbindEIP(sdk.String(resp.UHostIds[0]), sdk.String("uhost"), sdk.String(eip.EIPId), req.ProjectId, req.Region) + logs = append(logs, fmt.Sprintf("bind eip result: %s", info)) + block.Append(info) + } + } + } + } + block.AppendDone() +} + //NewCmdUHostDelete ucloud uhost delete func NewCmdUHostDelete(out io.Writer) *cobra.Command { var uhostIDs *[]string @@ -641,6 +741,21 @@ func describeUHostByID(uhostID, projectID, region, zone string) (interface{}, er return &resp.UHostSet[0], nil } +func sdescribeUHostByID(uhostID string) (interface{}, error) { + req := base.BizClient.NewDescribeUHostInstanceRequest() + req.UHostIds = []string{uhostID} + + resp, err := base.BizClient.DescribeUHostInstance(req) + if err != nil { + return nil, err + } + if len(resp.UHostSet) < 1 { + return nil, nil + } + + return &resp.UHostSet[0], nil +} + func getUhostList(states []string, project, region, zone string) []string { req := base.BizClient.NewDescribeUHostInstanceRequest() req.ProjectId = sdk.String(project) diff --git a/go.mod b/go.mod index 5bb7ad9455..141f9d9757 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 github.com/ucloud/ucloud-sdk-go v0.8.1-beta1 + golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a ) replace ( diff --git a/go.sum b/go.sum index 46b62d7a9a..46e5ea55dc 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTu golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc h1:4gbWbmmPFp4ySWICouJl6emP0MyS31yy9SrTlAGFT+g= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/services/project.go b/services/project.go new file mode 100644 index 0000000000..c7d8f62e41 --- /dev/null +++ b/services/project.go @@ -0,0 +1,68 @@ +package services + +import ( + "fmt" + + "github.com/ucloud/ucloud-cli/base" + "github.com/ucloud/ucloud-sdk-go/services/uaccount" +) + +//ProjectService is a service of ucloud project +type ProjectService interface { + GetDefaultProject() (string, string, error) + GetProjectList() []string +} + +type projectService struct { + client *base.Client +} + +//NewProjectService create project service +func NewProjectService(client *base.Client) ProjectService { + return &projectService{client} +} + +// func (ps *projectService) ListProject() error { +// req := &uaccount.GetProjectListRequest{} +// resp, err := base.BizClient.GetProjectList(req) +// if err != nil { +// return err +// } +// if resp.RetCode != 0 { +// return base.HandleBizError(resp) +// } +// if global.JSON { +// base.PrintJSON(resp.ProjectSet) +// } else { +// base.PrintTable(resp.ProjectSet, []string{"ProjectId", "ProjectName"}) +// } +// return nil +// } + +func (ps *projectService) GetProjectList() []string { + req := &uaccount.GetProjectListRequest{} + resp, err := base.BizClient.GetProjectList(req) + if err != nil { + return nil + } + list := []string{} + for _, p := range resp.ProjectSet { + list = append(list, p.ProjectId+"/"+p.ProjectName) + } + return list +} + +func (ps *projectService) GetDefaultProject() (string, string, error) { + req := ps.client.NewGetProjectListRequest() + + resp, err := ps.client.GetProjectList(req) + if err != nil { + return "", "", err + } + for _, project := range resp.ProjectSet { + if project.IsDefault { + return project.ProjectId, project.ProjectName, nil + } + } + return "", "", fmt.Errorf("No default project") +} diff --git a/services/region.go b/services/region.go new file mode 100644 index 0000000000..be1c78f79a --- /dev/null +++ b/services/region.go @@ -0,0 +1,111 @@ +package services + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + + "github.com/ucloud/ucloud-cli/base" + + "github.com/ucloud/ucloud-sdk-go/services/uaccount" +) + +//RegionService Region 服务 +type RegionService interface { + GetDefaultRegion() (string, string, error) + FetchRegion() (map[string][]string, error) + GetRegionList() []string + GetZoneList(string) []string + GetUserInfo() (*uaccount.UserInfo, error) +} + +type regionService struct { + client *base.Client +} + +//NewRegionService create regionService +func NewRegionService(client *base.Client) RegionService { + return ®ionService{client} +} + +func (rs *regionService) GetDefaultRegion() (string, string, error) { + req := rs.client.NewGetRegionRequest() + resp, err := rs.client.GetRegion(req) + if err != nil { + return "", "", err + } + for _, region := range resp.Regions { + if region.IsDefault { + return region.Region, region.Zone, nil + } + } + return "", "", errors.New("No default region") +} + +func (rs *regionService) FetchRegion() (map[string][]string, error) { + req := rs.client.NewGetRegionRequest() + resp, err := rs.client.GetRegion(req) + if err != nil { + return nil, err + } + regionMap := make(map[string][]string) + for _, region := range resp.Regions { + regionMap[region.Region] = append(regionMap[region.Region], region.Zone) + } + return regionMap, nil +} + +func (rs *regionService) GetRegionList() []string { + regionMap, err := rs.FetchRegion() + if err != nil { + return nil + } + list := []string{} + for region := range regionMap { + list = append(list, region) + } + return list +} + +func (rs *regionService) GetZoneList(region string) []string { + regionMap, err := rs.FetchRegion() + if err != nil { + return nil + } + list := []string{} + if region == "" { + for _, zones := range regionMap { + list = append(list, zones...) + } + } else { + list = regionMap[region] + } + return list +} + +func (rs *regionService) GetUserInfo() (*uaccount.UserInfo, error) { + req := rs.client.NewGetUserInfoRequest() + var userInfo uaccount.UserInfo + resp, err := rs.client.GetUserInfo(req) + + if err != nil { + return nil, err + } + + if len(resp.DataSet) == 1 { + userInfo = resp.DataSet[0] + bytes, err := json.Marshal(userInfo) + if err != nil { + return nil, err + } + fileFullPath := base.GetConfigPath() + "/user.json" + err = ioutil.WriteFile(fileFullPath, bytes, 0600) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("GetUserInfo DataSet length: %d", len(resp.DataSet)) + } + return &userInfo, nil +} diff --git a/ux/document.go b/ux/document.go new file mode 100644 index 0000000000..14cc3a7625 --- /dev/null +++ b/ux/document.go @@ -0,0 +1,209 @@ +package ux + +import ( + "context" + "fmt" + "io" + "os" + "sync" + "time" + + "github.com/ucloud/ucloud-cli/ansi" +) + +//Document 当前进程在打印的内容 +type document struct { + blocks []*Block + framesPerSecond int + once sync.Once + out io.Writer + ticker *time.Ticker + mux sync.Mutex + ctx context.Context + cancel context.CancelFunc + allBlockFull chan bool + Done chan bool + disable bool +} + +var width, rows, _ = terminalSize() + +func (d *document) reset() { + size := 0 + for _, block := range d.blocks { + size += block.printLineNum + } + if size != 0 { + fmt.Printf(ansi.CursorLeft + ansi.CursorPrevLine(size) + ansi.EraseDown) + } +} + +func (d *document) Disable() { + d.disable = true +} + +func (d *document) Render() { + if d.disable { + return + } + d.once.Do(func() { + go func() { + for range d.ticker.C { + d.reset() + for _, block := range d.blocks { + block.printLineNum = 0 + block.mux.Lock() + for _, line := range block.lines { + fmt.Fprintln(d.out, line) + if width != 0 { + lineNum := len(line)/width + 1 + block.printLineNum += lineNum + } else { + block.printLineNum++ + } + } + block.mux.Unlock() + fmt.Fprintf(d.out, "\n") + block.printLineNum++ + } + } + }() + go d.checkBlockDone() + }) +} + +func (d *document) Append(b *Block) { + d.Render() + d.mux.Lock() + defer d.mux.Unlock() + if d.cancel != nil { + d.cancel() + } + d.ctx, d.cancel = context.WithCancel(context.Background()) + go d.checkBlockFull(d.ctx) + d.blocks = append(d.blocks, b) +} + +func (d *document) checkBlockFull(ctx context.Context) { + allFull := make(chan struct{}) + go func() { + for _, b := range d.blocks { + <-b.full + } + close(allFull) + }() + + select { + case <-ctx.Done(): + return + case <-allFull: + d.allBlockFull <- true + return + } +} + +func (d *document) checkBlockDone() { + <-d.allBlockFull + allStable := make(chan struct{}) + go func() { + for _, b := range d.blocks { + <-b.stable + } + close(allStable) + }() + <-allStable + //等待最后一帧渲染 + <-time.After(time.Millisecond * 200) + close(d.Done) +} + +func newDocument(out io.Writer) *document { + doc := &document{ + out: out, + framesPerSecond: 20, + Done: make(chan bool), + allBlockFull: make(chan bool), + } + doc.ticker = time.NewTicker(time.Second / time.Duration(doc.framesPerSecond)) + return doc +} + +//Doc global document +var Doc = newDocument(os.Stdout) + +//Block in document, including a spinner and some text +type Block struct { + spinner *Spin + spinnerIndex int + printLineNum int //已打印到屏幕上的行数 + mux sync.Mutex + lines []string + stable chan struct{} //标识此块已稳定,不再轮询 + full chan struct{} //标识此块不再添加新的内容 +} + +//Update lines in Block +func (b *Block) Update(text string, index int) { + b.mux.Lock() + b.lines[index] = text + b.mux.Unlock() +} + +//Append text to Block +func (b *Block) Append(text string) { + b.lines = append(b.lines, text) +} + +//AppendDone 表示不再往Block内部添加内容 +func (b *Block) AppendDone() { + close(b.full) +} + +//SetSpin set spin for block +func (b *Block) SetSpin(s *Spin) error { + if b.spinner != nil { + return fmt.Errorf("block has spinner already") + } + b.stable = make(chan struct{}) + b.spinner = s + b.spinnerIndex = len(b.lines) + b.lines = append(b.lines, "loading") + strsCh := b.spinner.renderToString() + go func() { + for text := range strsCh { + if len(b.lines) == 0 { + b.Append(text) + } else { + b.Update(text, b.spinnerIndex) + } + } + close(b.stable) + }() + return nil +} + +//NewSpinBlock create a new Block with spinner +func NewSpinBlock(s *Spin) *Block { + block := &Block{ + lines: []string{}, + stable: make(chan struct{}), + full: make(chan struct{}), + } + if s != nil { + block.SetSpin(s) + } else { + close(block.stable) + } + return block +} + +//NewBlock create a new Block without spinner. block.Stable closed +func NewBlock() *Block { + block := &Block{ + lines: []string{}, + stable: make(chan struct{}), + full: make(chan struct{}), + } + close(block.stable) + return block +} diff --git a/ux/spinner.go b/ux/spinner.go index eefabd0c95..b2dcd4e8ab 100644 --- a/ux/spinner.go +++ b/ux/spinner.go @@ -94,3 +94,26 @@ func NewDotSpinner(out io.Writer) *Spinner { TimeoutText: "timeout", } } + +//Refresh 刷新显示文本 +type Refresh struct { + out io.Writer + reset bool +} + +//Do 刷新显示 +func (r *Refresh) Do(text string) { + if r.reset { + fmt.Fprintf(r.out, ansi.CursorLeft+ansi.CursorUp(1)+ansi.EraseDown) + } else { + r.reset = true + } + fmt.Fprintln(r.out, text) +} + +//NewRefresh create a new Refresh instance +func NewRefresh() *Refresh { + return &Refresh{ + out: os.Stdout, + } +} diff --git a/ux/spinnerv2.go b/ux/spinnerv2.go new file mode 100644 index 0000000000..12c72fd8c5 --- /dev/null +++ b/ux/spinnerv2.go @@ -0,0 +1,106 @@ +//Inspaired by https://github.com/oclif/cli-ux + +package ux + +import ( + "fmt" + "io" + "sync" + "time" + + "github.com/ucloud/ucloud-cli/ansi" +) + +// Spinner type +type Spin struct { + out io.Writer + frames []rune + framesPerSecond int + DoingText string + DoneText string + TimeoutText string + ticker *time.Ticker + output string + textChan chan string + wg sync.WaitGroup +} + +// Stop stop render +func (s *Spin) Stop() { + s.ticker.Stop() + s.reset() + output := fmt.Sprintf("%s...%s", s.DoingText, s.DoneText) + s.textChan <- output + //等待最后一帧渲染 + <-time.After(time.Millisecond * 100) + close(s.textChan) +} + +// Timeout stop render +func (s *Spin) Timeout() { + s.ticker.Stop() + s.reset() + output := fmt.Sprintf("%s...%s", s.DoingText, s.TimeoutText) + s.textChan <- output + //等待最后一帧渲染 + <-time.After(time.Millisecond * 100) + close(s.textChan) +} + +func (s *Spin) reset() { + if s.output == "" { + return + } + fmt.Printf(ansi.CursorLeft + ansi.CursorUp(1) + ansi.EraseDown) + s.output = "" +} + +func (s *Spin) renderToString() chan string { + nextFrame := s.newFrameFactory() + go func() { + for range s.ticker.C { + frame := nextFrame() + s.textChan <- fmt.Sprintf("%s...%c", s.DoingText, frame) + } + }() + return s.textChan +} + +func (s *Spin) renderToScreen() { + nextFrame := s.newFrameFactory() + go func() { + for range s.ticker.C { + frame := nextFrame() + s.reset() + s.output = fmt.Sprintf("%s...%c\n", s.DoingText, frame) + fmt.Printf(s.output) + } + }() +} + +func (s *Spin) newFrameFactory() func() rune { + index := 0 + size := len(s.frames) + return func() rune { + char := s.frames[index%size] + index++ + return char + } +} + +var spinFrames = []rune{'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'} + +//NewDotSpin get new DotSpinner instance +func NewDotSpin(out io.Writer, doingText string) *Spin { + s := &Spin{ + out: out, + frames: spinnerFrames, + framesPerSecond: 12, + DoingText: doingText, + DoneText: "done", + TimeoutText: "timeout", + textChan: make(chan string), + } + s.ticker = time.NewTicker(time.Second / time.Duration(s.framesPerSecond)) + return s +} diff --git a/ux/terminal.go b/ux/terminal.go new file mode 100644 index 0000000000..205922c814 --- /dev/null +++ b/ux/terminal.go @@ -0,0 +1,47 @@ +package ux + +import ( + "errors" + "os" + "sync" + + "golang.org/x/sys/unix" +) + +var ( + echoLockMutex sync.Mutex + origTermStatePtr *unix.Termios + tty *os.File + istty bool +) + +func init() { + echoLockMutex.Lock() + defer echoLockMutex.Unlock() + + var err error + tty, err = os.Open("/dev/tty") + istty = true + if err != nil { + tty = os.Stdin + istty = false + } +} + +// terminalSize returns width and rows of the terminal. +func terminalSize() (int, int, error) { + if !istty { + return 0, 0, errors.New("Not Supported") + } + echoLockMutex.Lock() + defer echoLockMutex.Unlock() + + fd := int(tty.Fd()) + + ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) + if err != nil { + return 0, 0, err + } + + return int(ws.Col), int(ws.Row), nil +} From 0785c3ee7e105ab84bece4b551f44da3e45954cd Mon Sep 17 00:00:00 2001 From: lixiaojun Date: Wed, 27 Mar 2019 18:15:45 +0800 Subject: [PATCH 2/2] add 'ucloud uphost list' --- base/client.go | 3 ++ cmd/root.go | 1 + cmd/uphost.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 cmd/uphost.go diff --git a/base/client.go b/base/client.go index 17d4b5c522..a929af3ea9 100644 --- a/base/client.go +++ b/base/client.go @@ -13,6 +13,7 @@ import ( "github.com/ucloud/ucloud-sdk-go/services/ulb" "github.com/ucloud/ucloud-sdk-go/services/umem" "github.com/ucloud/ucloud-sdk-go/services/unet" + "github.com/ucloud/ucloud-sdk-go/services/uphost" "github.com/ucloud/ucloud-sdk-go/services/vpc" "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" @@ -39,6 +40,7 @@ type Client struct { ulb.ULBClient udb.UDBClient umem.UMemClient + uphost.UPHostClient PrivateUHostClient PrivateUDBClient PrivateUMemClient @@ -57,6 +59,7 @@ func NewClient(config *ucloud.Config, credential *auth.Credential) *Client { *ulb.NewClient(config, credential), *udb.NewClient(config, credential), *umem.NewClient(config, credential), + *uphost.NewClient(config, credential), *puhost.NewClient(config, credential), *pudb.NewClient(config, credential), *pumem.NewClient(config, credential), diff --git a/cmd/root.go b/cmd/root.go index 51555da248..f3d5e4ae48 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -64,6 +64,7 @@ func NewCmdRoot() *cobra.Command { cmd.AddCommand(NewCmdRegion()) cmd.AddCommand(NewCmdProject()) cmd.AddCommand(NewCmdUHost()) + cmd.AddCommand(NewCmdUPHost()) cmd.AddCommand(NewCmdEIP()) cmd.AddCommand(NewCmdGssh()) cmd.AddCommand(NewCmdUImage()) diff --git a/cmd/uphost.go b/cmd/uphost.go new file mode 100644 index 0000000000..2fa4bbb446 --- /dev/null +++ b/cmd/uphost.go @@ -0,0 +1,100 @@ +// Copyright © 2018 NAME HERE tony.li@ucloud.cn +// +// 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. + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/ucloud/ucloud-cli/base" +) + +//NewCmdUPHost ucloud uphost +func NewCmdUPHost() *cobra.Command { + cmd := &cobra.Command{ + Use: "uphost", + Short: "List UPHost instances", + Long: `List UPHost instances`, + Args: cobra.NoArgs, + } + cmd.AddCommand(NewCmdUPHostList()) + + return cmd +} + +type uphostRow struct { + ResourceID string + Name string + PrivateIP string + PublicIP string + Config string + Image string + HostType string + Status string + Group string +} + +//NewCmdUPHostList ucloud uphost list +func NewCmdUPHostList() *cobra.Command { + ids := []string{} + req := base.BizClient.NewDescribePHostRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List UPHost instances", + Long: "List UPHost instances", + Run: func(c *cobra.Command, args []string) { + resp, err := base.BizClient.DescribePHost(req) + if err != nil { + base.HandleError(err) + return + } + list := make([]uphostRow, 0) + for _, ins := range resp.PHostSet { + row := uphostRow{ + ResourceID: ins.PHostId, + Name: ins.Name, + Config: fmt.Sprintf("core:%d memory:%dG", ins.CPUSet.CoreCount, ins.Memory/1024), + Group: ins.Tag, + HostType: ins.PHostType, + Status: ins.PMStatus, + Image: ins.ImageName, + } + for _, ip := range ins.IPSet { + if ip.OperatorName == "Private" { + row.PrivateIP = ip.IPAddr + } else { + row.PublicIP = ip.IPAddr + " " + ip.OperatorName + } + } + for _, disk := range ins.DiskSet { + if disk.Name == "data" { + row.Config += fmt.Sprintf(" data-disk:%dG %s", disk.Space, disk.Type) + } + } + list = append(list, row) + } + base.PrintList(list) + }, + } + flags := cmd.Flags() + bindRegion(req, flags) + bindZoneEmpty(req, flags) + bindProjectID(req, flags) + bindOffset(req, flags) + bindLimit(req, flags) + flags.StringSliceVar(&ids, "uphost-id", nil, "Optional. Resource ID of uphost instances. List those specified uphost instances") + + return cmd +}