From 1805159ebeb838b27a85268e2fb2ca5b21e2ddb2 Mon Sep 17 00:00:00 2001 From: "zhengtao.jiang" Date: Fri, 10 Jul 2026 15:23:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0sqlserver=E7=9A=84cli?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- products/udb/internal/sqlserver/cmd.go | 18 ++ products/udb/internal/sqlserver/completion.go | 126 ++++++++++ products/udb/internal/sqlserver/create.go | 226 ++++++++++++++++++ products/udb/internal/sqlserver/create_ha.go | 202 ++++++++++++++++ products/udb/internal/sqlserver/db.go | 26 ++ products/udb/internal/sqlserver/delete.go | 77 ++++++ products/udb/internal/sqlserver/list.go | 86 +++++++ products/udb/internal/sqlserver/poll.go | 52 ++++ products/udb/internal/sqlserver/restart.go | 61 +++++ products/udb/internal/sqlserver/rows.go | 26 ++ products/udb/internal/sqlserver/start.go | 62 +++++ products/udb/internal/sqlserver/status.go | 11 + products/udb/internal/sqlserver/stop.go | 56 +++++ products/udb/product.yaml | 8 + 14 files changed, 1037 insertions(+) create mode 100644 products/udb/internal/sqlserver/cmd.go create mode 100644 products/udb/internal/sqlserver/completion.go create mode 100644 products/udb/internal/sqlserver/create.go create mode 100644 products/udb/internal/sqlserver/create_ha.go create mode 100644 products/udb/internal/sqlserver/db.go create mode 100644 products/udb/internal/sqlserver/delete.go create mode 100644 products/udb/internal/sqlserver/list.go create mode 100644 products/udb/internal/sqlserver/poll.go create mode 100644 products/udb/internal/sqlserver/restart.go create mode 100644 products/udb/internal/sqlserver/rows.go create mode 100644 products/udb/internal/sqlserver/start.go create mode 100644 products/udb/internal/sqlserver/status.go create mode 100644 products/udb/internal/sqlserver/stop.go create mode 100644 products/udb/product.yaml diff --git a/products/udb/internal/sqlserver/cmd.go b/products/udb/internal/sqlserver/cmd.go new file mode 100644 index 0000000000..1d58f2c0b0 --- /dev/null +++ b/products/udb/internal/sqlserver/cmd.go @@ -0,0 +1,18 @@ +package sqlserver + +import ( + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-cli/pkg/cli" +) + +// NewCommand builds the `sqlserver` root command and mounts the `db` subtree. +func NewCommand(ctx *cli.Context) *cobra.Command { + cmd := &cobra.Command{ + Use: "sqlserver", + Short: "Manipulate SQL Server on UCloud platform", + Long: "Manipulate SQL Server on UCloud platform", + } + cmd.AddCommand(newSQLServerDB(ctx)) + return cmd +} diff --git a/products/udb/internal/sqlserver/completion.go b/products/udb/internal/sqlserver/completion.go new file mode 100644 index 0000000000..294e46b555 --- /dev/null +++ b/products/udb/internal/sqlserver/completion.go @@ -0,0 +1,126 @@ +package sqlserver + +import ( + "fmt" + + "github.com/ucloud/ucloud-sdk-go/services/udb" + "github.com/ucloud/ucloud-sdk-go/services/vpc" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/pkg/cli" +) + +var dbVersionList = []string{"sqlserver-2017", "sqlserver-2019", "sqlserver-2022"} + +// getAllVPCIns returns VPC list for the given project and region. +func getAllVPCIns(ctx *cli.Context, project, region string) ([]vpc.VPCInfo, error) { + client := cli.NewServiceClient(ctx, vpc.NewClient) + req := client.NewDescribeVPCRequest() + req.ProjectId = &project + req.Region = ®ion + resp, err := client.DescribeVPC(req) + if err != nil { + return nil, err + } + return resp.DataSet, nil +} + +// getAllVPCIdNames returns VPC ID/Name pairs for shell completion. +func getAllVPCIdNames(ctx *cli.Context, project, region string) []string { + vpcInsList, err := getAllVPCIns(ctx, project, region) + list := []string{} + if err != nil { + return nil + } + for _, v := range vpcInsList { + list = append(list, fmt.Sprintf("%s/%s", v.VPCId, v.Name)) + } + return list +} + +// getAllSubnets returns subnet list for the given VPC, project and region. +func getAllSubnets(ctx *cli.Context, vpcID, project, region string) ([]vpc.SubnetInfo, error) { + client := cli.NewServiceClient(ctx, vpc.NewClient) + req := client.NewDescribeSubnetRequest() + req.ProjectId = sdk.String(cli.PickResourceID(project)) + req.Region = sdk.String(region) + if vpcID != "" { + req.VPCId = sdk.String(cli.PickResourceID(vpcID)) + } + subnets := []vpc.SubnetInfo{} + for limit, offset := 50, 0; ; offset += limit { + req.Limit = sdk.Int(limit) + req.Offset = sdk.Int(offset) + resp, err := client.DescribeSubnet(req) + if err != nil { + ctx.HandleError(err) + return nil, err + } + subnets = append(subnets, resp.DataSet...) + if limit+offset >= resp.TotalCount { + break + } + } + return subnets, nil +} + +// getAllSubnetIDNames returns Subnet ID/Name pairs for shell completion. +func getAllSubnetIDNames(ctx *cli.Context, vpcID, project, region string) []string { + subnets, err := getAllSubnets(ctx, vpcID, project, region) + if err != nil { + return nil + } + list := []string{} + for _, s := range subnets { + list = append(list, fmt.Sprintf("%s/%s", s.SubnetId, s.SubnetName)) + } + return list +} + +func getUDBIDList(ctx *cli.Context, states []string, dbType, project, region, zone string) []string { + udbs, err := getUDBList(ctx, states, dbType, project, region, zone) + if err != nil { + return nil + } + list := []string{} + for _, db := range udbs { + list = append(list, fmt.Sprintf("%s/%s", db.DBId, db.Name)) + } + return list +} + +func getUDBList(ctx *cli.Context, states []string, dbType, project, region, zone string) ([]udb.UDBInstanceSet, error) { + client := cli.NewServiceClient(ctx, udb.NewClient) + req := client.NewDescribeUDBInstanceRequest() + if dbType == "" { + dbType = "sqlserver" + } + req.ClassType = &dbType + req.ProjectId = &project + req.Region = ®ion + req.Zone = &zone + list := []udb.UDBInstanceSet{} + for offset, limit := 0, 50; ; offset += limit { + req.Offset = sdk.Int(offset) + req.Limit = sdk.Int(limit) + resp, err := client.DescribeUDBInstance(req) + if err != nil { + return nil, err + } + for _, ins := range resp.DataSet { + if states != nil { + for _, s := range states { + if s == ins.State { + list = append(list, ins) + } + } + } else { + list = append(list, ins) + } + } + if offset+limit >= resp.TotalCount { + break + } + } + return list, nil +} diff --git a/products/udb/internal/sqlserver/create.go b/products/udb/internal/sqlserver/create.go new file mode 100644 index 0000000000..b94e0809aa --- /dev/null +++ b/products/udb/internal/sqlserver/create.go @@ -0,0 +1,226 @@ +package sqlserver + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/ucloud/ucloud-sdk-go/services/uaccount" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +var dbStorageClassList = []string{"CLOUD_RSSD"} +var dbSpecClassList = []string{"O"} +var dbMachineTypeList = []string{ + "o.sqlserver2m.medium", // 2C4G + "o.sqlserver2m.xlarge", // 4C8G + "o.sqlserver2m.2xlarge", // 8C16G + "o.sqlserver2m.4xlarge", // 16C32G + "o.sqlserver2m.8xlarge", // 32C64G + "o.sqlserver4m.medium", // 2C8G + "o.sqlserver4m.xlarge", // 4C16G + "o.sqlserver4m.2xlarge", // 8C32G + "o.sqlserver4m.4xlarge", // 16C64G + "o.sqlserver4m.8xlarge", // 32C128G + "o.sqlserver8m.medium", // 2C16G + "o.sqlserver8m.xlarge", // 4C32G + "o.sqlserver8m.2xlarge", // 8C64G + "o.sqlserver8m.4xlarge", // 16C128G + "o.sqlserver8m.8xlarge", // 32C256G +} + +// newCreate returns the "create" command for SQL Server Normal (single-node) instances. +func newCreate(ctx *cli.Context) *cobra.Command { + var labels []string + var name, password, version, storageClass, specClass string + var cpu, memory, port, diskSpace int + var chargeType string + var quantity int + var vpcID, subnetID string + var backupCount, backupTime, backupDuration int + var tag, alarmTemplateID string + var couponID string + var async bool + var common request.CommonBase + + cmd := &cobra.Command{ + Use: "create", + Short: "Create SQL Server instance (Normal/single-node mode) on UCloud platform", + Long: "Create SQL Server instance (Normal/single-node mode) on UCloud platform", + Run: func(c *cobra.Command, args []string) { + region := common.GetRegion() + zone := common.GetZone() + projectID := common.GetProjectId() + if len(name) < 6 { + ctx.HandleError(fmt.Errorf("name must be at least 6 characters")) + return + } + if diskSpace < 20 || diskSpace > 32000 { + ctx.HandleError(fmt.Errorf("disk-size-gb must be between 20 and 32000")) + return + } + + params := map[string]interface{}{ + "Action": "CreateUDBSQLServerInstance", + "Region": region, + "Zone": zone, + "Name": name, + "AdminPassword": password, + "DBTypeId": version, + "Port": port, + "DiskSpace": diskSpace, + "CPU": cpu, + "MemoryLimit": memory, + "StorageClass": storageClass, + "SpecificationClass": specClass, + "ChargeType": chargeType, + "Quantity": quantity, + "InstanceMode": "Normal", + "BackupCount": backupCount, + "BackupTime": backupTime, + "BackupDuration": backupDuration, + } + if projectID != "" { + params["ProjectId"] = projectID + } + + // Optional params, only send when explicitly set + if c.Flags().Changed("vpc-id") { + params["VPCId"] = vpcID + } + if c.Flags().Changed("subnet-id") { + params["SubnetId"] = subnetID + } + if c.Flags().Changed("tag") { + params["Tag"] = tag + } + if c.Flags().Changed("alarm-template-id") { + params["AlarmTemplateId"] = alarmTemplateID + } + if c.Flags().Changed("coupon-id") { + params["CouponId"] = couponID + } + + idx := 0 + for _, l := range labels { + parts := strings.SplitN(l, "=", 2) + if len(parts) == 2 { + params[fmt.Sprintf("Labels.%d.Key", idx)] = parts[0] + params[fmt.Sprintf("Labels.%d.Value", idx)] = parts[1] + idx++ + } + } + + client := cli.NewServiceClient(ctx, uaccount.NewClient) + req := client.NewGenericRequest() + if err := req.SetPayload(params); err != nil { + ctx.HandleError(fmt.Errorf("set payload: %w", err)) + return + } + resp, err := client.GenericInvoke(req) + if err != nil { + ctx.HandleError(err) + return + } + + dbID, _ := resp.GetPayload()["DBId"].(string) + if dbID == "" { + ctx.HandleError(fmt.Errorf("empty DBId in response")) + return + } + w := ctx.ProgressWriter() + if async { + fmt.Fprintf(w, "udb[%s] is initializing\n", dbID) + } else { + text := fmt.Sprintf("udb[%s] is initializing", dbID) + ctx.PollerTo(w, describeUdbByID(ctx)).Spoll(dbID, text, []string{UDB_RUNNING, UDB_FAIL}) + } + ctx.EmitResult(cli.OpResultRow{ResourceID: dbID, Action: "create", Status: "Initializing"}) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + // Required flags + flags.StringVar(&name, "name", "", "Required. Instance name, at least 6 characters") + flags.StringVar(&password, "password", "", "Required. Admin password") + flags.StringVar(&version, "db-type", "", "Required. SQL Server version. Options: sqlserver-2017, sqlserver-2019, sqlserver-2022") + + // Optional flags with defaults + ctx.BindRegion(cmd, &common) + ctx.BindZone(cmd, &common) + ctx.BindProjectID(cmd, &common) + flags.IntVar(&cpu, "cpu", 2, "Optional. CPU cores. Options: 2/4/8/16/32/64, default 2") + flags.IntVar(&memory, "memory", 4000, "Optional. Memory limit (MB). Options: 2000/4000/6000/8000/12000/16000/24000/32000/48000/64000/96000/128000/192000/256000/320000, default 4000") + flags.IntVar(&port, "port", 1433, "Optional. Port, default 1433") + flags.IntVar(&diskSpace, "disk-size-gb", 50, "Optional. Disk size (GiB), 20-32000, default 50") + flags.StringVar(&storageClass, "storage-class", "CLOUD_RSSD", "Optional. Storage class: CLOUD_RSSD") + flags.StringVar(&specClass, "spec-class", "O", "Optional. Spec class: O(NVMe)") + + flags.StringVar(&chargeType, "charge-type", "Month", "Optional. Year / Month / Dynamic") + flags.IntVar(&quantity, "quantity", 1, "Optional. Purchase duration") + flags.StringVar(&vpcID, "vpc-id", "", "Optional. VPC ID. See 'ucloud vpc list'") + flags.StringVar(&subnetID, "subnet-id", "", "Optional. Subnet ID. See 'ucloud subnet list'") + flags.IntVar(&backupCount, "backup-count", 7, "Optional. Weekly backup count, default 7") + flags.IntVar(&backupTime, "backup-time", 1, "Optional. Backup start hour (0-23), default 1") + flags.IntVar(&backupDuration, "backup-duration", 24, "Optional. Backup interval hours, default 24") + flags.StringVar(&tag, "tag", "", "Optional. Business group name") + flags.StringVar(&alarmTemplateID, "alarm-template-id", "", "Optional. Alarm template ID") + flags.StringSliceVar(&labels, "label", nil, "Optional. Resource label, format: key=value, repeatable") + flags.StringVar(&couponID, "coupon-id", "", "Optional. Coupon ID") + flags.BoolVar(&async, "async", false, "Optional. Do not wait for creation to finish") + + command.SetFlagValues(cmd, "db-type", dbVersionList...) + command.SetFlagValues(cmd, "storage-class", dbStorageClassList...) + command.SetFlagValues(cmd, "spec-class", dbSpecClassList...) + command.SetFlagValues(cmd, "charge-type", "Month", "Dynamic", "Year") + command.SetFlagValues(cmd, "cpu", "2", "4", "8", "16", "32", "64") + + command.SetCompletion(cmd, "vpc-id", func() []string { + return getAllVPCIdNames(ctx, common.GetProjectId(), common.GetRegion()) + }) + command.SetCompletion(cmd, "subnet-id", func() []string { + return getAllSubnetIDNames(ctx, vpcID, common.GetProjectId(), common.GetRegion()) + }) + + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("password") + cmd.MarkFlagRequired("db-type") + + // Custom usage, highlight required flags + requiredFlags := []string{"name", "password", "db-type"} + cmd.SetUsageFunc(func(c *cobra.Command) error { + w := c.OutOrStderr() + fmt.Fprintln(w, "Usage:") + fmt.Fprintf(w, " %s [flags]\n\n", c.CommandPath()) + fmt.Fprintln(w, "★ Required flags (must be provided):") + for _, name := range requiredFlags { + f := c.Flags().Lookup(name) + if f != nil { + fmt.Fprintf(w, " --%-20s %s\n", f.Name, f.Usage) + } + } + fmt.Fprintln(w, "\nOptional flags:") + c.Flags().VisitAll(func(f *pflag.Flag) { + for _, req := range requiredFlags { + if f.Name == req { + return + } + } + defVal := "" + if f.DefValue != "" && f.DefValue != "[]" { + defVal = fmt.Sprintf(" (default %s)", f.DefValue) + } + fmt.Fprintf(w, " --%-20s %s%s\n", f.Name, f.Usage, defVal) + }) + return nil + }) + + return cmd +} diff --git a/products/udb/internal/sqlserver/create_ha.go b/products/udb/internal/sqlserver/create_ha.go new file mode 100644 index 0000000000..ef33796dc1 --- /dev/null +++ b/products/udb/internal/sqlserver/create_ha.go @@ -0,0 +1,202 @@ +package sqlserver + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/ucloud/ucloud-sdk-go/services/uaccount" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +// newCreateHA returns the "create-ha" command for SQL Server HA (AlwaysOn cluster) instances. +func newCreateHA(ctx *cli.Context) *cobra.Command { + var labels []string + var name, password, version, machineType, storageClass, specClass string + var port, diskSpace int + var chargeType string + var quantity int + var vpcID, subnetID string + var backupCount, backupTime, backupDuration int + var tag, alarmTemplateID string + var couponID string + var async bool + var common request.CommonBase + + cmd := &cobra.Command{ + Use: "create-ha", + Short: "Create SQL Server instance (HA/AlwaysOn cluster mode) on UCloud platform", + Long: "Create SQL Server instance (HA/AlwaysOn cluster mode) on UCloud platform. Requires VPC and Subnet.", + Run: func(c *cobra.Command, args []string) { + region := common.GetRegion() + zone := common.GetZone() + projectID := common.GetProjectId() + if len(name) < 6 { + ctx.HandleError(fmt.Errorf("name must be at least 6 characters")) + return + } + if diskSpace < 20 || diskSpace > 32000 { + ctx.HandleError(fmt.Errorf("disk-size-gb must be between 20 and 32000")) + return + } + + params := map[string]interface{}{ + "Action": "CreateUDBSQLServerInstance", + "Region": region, + "Zone": zone, + "Name": name, + "AdminPassword": password, + "DBTypeId": version, + "Port": port, + "DiskSpace": diskSpace, + "MachineType": machineType, + "VPCId": vpcID, + "SubnetId": subnetID, + "StorageClass": storageClass, + "SpecificationClass": specClass, + "ChargeType": chargeType, + "Quantity": quantity, + "InstanceMode": "AlwaysOn", + "BackupCount": backupCount, + "BackupTime": backupTime, + "BackupDuration": backupDuration, + } + if projectID != "" { + params["ProjectId"] = projectID + } + + // Optional params, only send when explicitly set + if c.Flags().Changed("tag") { + params["Tag"] = tag + } + if c.Flags().Changed("alarm-template-id") { + params["AlarmTemplateId"] = alarmTemplateID + } + if c.Flags().Changed("coupon-id") { + params["CouponId"] = couponID + } + + idx := 0 + for _, l := range labels { + parts := strings.SplitN(l, "=", 2) + if len(parts) == 2 { + params[fmt.Sprintf("Labels.%d.Key", idx)] = parts[0] + params[fmt.Sprintf("Labels.%d.Value", idx)] = parts[1] + idx++ + } + } + + client := cli.NewServiceClient(ctx, uaccount.NewClient) + req := client.NewGenericRequest() + if err := req.SetPayload(params); err != nil { + ctx.HandleError(fmt.Errorf("set payload: %w", err)) + return + } + resp, err := client.GenericInvoke(req) + if err != nil { + ctx.HandleError(err) + return + } + + dbID, _ := resp.GetPayload()["DBId"].(string) + if dbID == "" { + ctx.HandleError(fmt.Errorf("empty DBId in response")) + return + } + w := ctx.ProgressWriter() + if async { + fmt.Fprintf(w, "udb[%s] is initializing\n", dbID) + } else { + text := fmt.Sprintf("udb[%s] is initializing", dbID) + ctx.PollerTo(w, describeUdbByID(ctx)).Spoll(dbID, text, []string{UDB_RUNNING, UDB_FAIL}) + } + ctx.EmitResult(cli.OpResultRow{ResourceID: dbID, Action: "create-ha", Status: "Initializing"}) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + // Required flags + flags.StringVar(&name, "name", "", "Required. Instance name, at least 6 characters") + flags.StringVar(&password, "password", "", "Required. Admin password") + flags.StringVar(&version, "db-type", "", "Required. SQL Server version. Options: sqlserver-2017, sqlserver-2019, sqlserver-2022") + flags.StringVar(&vpcID, "vpc-id", "", "Required. VPC ID. See 'ucloud vpc list'") + flags.StringVar(&subnetID, "subnet-id", "", "Required. Subnet ID. See 'ucloud subnet list'") + + // Optional flags with defaults + ctx.BindRegion(cmd, &common) + ctx.BindZone(cmd, &common) + ctx.BindProjectID(cmd, &common) + flags.StringVar(&machineType, "machine-type", "o.sqlserver2m.medium", "Optional. Machine type ID, e.g. o.sqlserver2m.medium for 2C4G. Use ListUDBMachineType API with InstanceMode=AlwaysOn to get available types") + flags.IntVar(&port, "port", 1433, "Optional. Port, default 1433") + flags.IntVar(&diskSpace, "disk-size-gb", 50, "Optional. Disk size (GiB), 20-32000, default 50") + flags.StringVar(&storageClass, "storage-class", "CLOUD_RSSD", "Optional. Storage class: CLOUD_RSSD") + flags.StringVar(&specClass, "spec-class", "O", "Optional. Spec class: O(NVMe)") + + flags.StringVar(&chargeType, "charge-type", "Month", "Optional. Year / Month / Dynamic") + flags.IntVar(&quantity, "quantity", 1, "Optional. Purchase duration") + flags.IntVar(&backupCount, "backup-count", 7, "Optional. Weekly backup count, default 7") + flags.IntVar(&backupTime, "backup-time", 1, "Optional. Backup start hour (0-23), default 1") + flags.IntVar(&backupDuration, "backup-duration", 24, "Optional. Backup interval hours, default 24") + flags.StringVar(&tag, "tag", "", "Optional. Business group name") + flags.StringVar(&alarmTemplateID, "alarm-template-id", "", "Optional. Alarm template ID") + flags.StringSliceVar(&labels, "label", nil, "Optional. Resource label, format: key=value, repeatable") + flags.StringVar(&couponID, "coupon-id", "", "Optional. Coupon ID") + flags.BoolVar(&async, "async", false, "Optional. Do not wait for creation to finish") + + command.SetFlagValues(cmd, "db-type", dbVersionList...) + command.SetFlagValues(cmd, "storage-class", dbStorageClassList...) + command.SetFlagValues(cmd, "spec-class", dbSpecClassList...) + command.SetFlagValues(cmd, "charge-type", "Month", "Dynamic", "Year") + command.SetFlagValues(cmd, "machine-type", dbMachineTypeList...) + + command.SetCompletion(cmd, "vpc-id", func() []string { + return getAllVPCIdNames(ctx, common.GetProjectId(), common.GetRegion()) + }) + command.SetCompletion(cmd, "subnet-id", func() []string { + return getAllSubnetIDNames(ctx, vpcID, common.GetProjectId(), common.GetRegion()) + }) + + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("password") + cmd.MarkFlagRequired("db-type") + cmd.MarkFlagRequired("vpc-id") + cmd.MarkFlagRequired("subnet-id") + + // Custom usage, highlight required flags + requiredFlags := []string{"name", "password", "db-type", "vpc-id", "subnet-id"} + cmd.SetUsageFunc(func(c *cobra.Command) error { + w := c.OutOrStderr() + fmt.Fprintln(w, "Usage:") + fmt.Fprintf(w, " %s [flags]\n\n", c.CommandPath()) + fmt.Fprintln(w, "★ Required flags (must be provided):") + for _, name := range requiredFlags { + f := c.Flags().Lookup(name) + if f != nil { + fmt.Fprintf(w, " --%-20s %s\n", f.Name, f.Usage) + } + } + fmt.Fprintln(w, "\nOptional flags:") + c.Flags().VisitAll(func(f *pflag.Flag) { + for _, req := range requiredFlags { + if f.Name == req { + return + } + } + defVal := "" + if f.DefValue != "" && f.DefValue != "[]" { + defVal = fmt.Sprintf(" (default %s)", f.DefValue) + } + fmt.Fprintf(w, " --%-20s %s%s\n", f.Name, f.Usage, defVal) + }) + return nil + }) + + return cmd +} diff --git a/products/udb/internal/sqlserver/db.go b/products/udb/internal/sqlserver/db.go new file mode 100644 index 0000000000..6eda94f320 --- /dev/null +++ b/products/udb/internal/sqlserver/db.go @@ -0,0 +1,26 @@ +package sqlserver + +import ( + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-cli/pkg/cli" +) + +// newSQLServerDB builds the "sqlserver db" subcommand group for instance lifecycle operations. +func newSQLServerDB(ctx *cli.Context) *cobra.Command { + cmd := &cobra.Command{ + Use: "db", + Short: "Manage SQL Server instances", + Long: "Manage SQL Server instances", + } + + cmd.AddCommand(newList(ctx)) + cmd.AddCommand(newCreate(ctx)) + cmd.AddCommand(newCreateHA(ctx)) + cmd.AddCommand(newDelete(ctx)) + cmd.AddCommand(newStart(ctx)) + cmd.AddCommand(newStop(ctx)) + cmd.AddCommand(newRestart(ctx)) + + return cmd +} diff --git a/products/udb/internal/sqlserver/delete.go b/products/udb/internal/sqlserver/delete.go new file mode 100644 index 0000000000..0612c46f94 --- /dev/null +++ b/products/udb/internal/sqlserver/delete.go @@ -0,0 +1,77 @@ +package sqlserver + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-sdk-go/services/udb" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +// newDelete returns the "delete" command for SQL Server instances. +func newDelete(ctx *cli.Context) *cobra.Command { + var idNames []string + var yes bool + client := cli.NewServiceClient(ctx, udb.NewClient) + req := client.NewDeleteUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete SQL Server instances by udb-id", + Long: "Delete SQL Server instances by udb-id", + Run: func(c *cobra.Command, args []string) { + ok, err := ctx.Confirm(yes, "Are you sure you want to delete the udb(s)?") + if err != nil { + ctx.HandleError(err) + return + } + if !ok { + return + } + w := ctx.ProgressWriter() + results := []cli.OpResultRow{} + for _, idname := range idNames { + id := ctx.PickResourceID(idname) + any, err := describeUdbByID(ctx)(id, nil) + if err != nil { + ctx.HandleError(err) + continue + } + req.DBId = &id + ins, ok := any.(*udb.UDBInstanceSet) + if ok && ins.State == UDB_RUNNING { + stopReq := client.NewStopUDBInstanceRequest() + stopReq.ProjectId = req.ProjectId + stopReq.Region = req.Region + stopReq.Zone = req.Zone + stopReq.DBId = req.DBId + stopUdbIns(ctx, stopReq, false, w) + } + _, err = client.DeleteUDBInstance(req) + if err != nil { + ctx.HandleError(err) + continue + } + fmt.Fprintf(w, "udb[%s] deleted\n", idname) + results = append(results, cli.OpResultRow{ResourceID: id, Action: "delete", Status: "Deleted"}) + } + ctx.EmitResult(results...) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "udb-id", nil, "Required. Resource ID of UDB instances to delete") + ctx.BindRegion(cmd, req) + ctx.BindZone(cmd, req) + ctx.BindProjectID(cmd, req) + flags.BoolVarP(&yes, "yes", "y", false, "Optional. Do not prompt for confirmation.") + + cmd.MarkFlagRequired("udb-id") + command.SetCompletion(cmd, "udb-id", func() []string { + return getUDBIDList(ctx, nil, "", *req.ProjectId, *req.Region, *req.Zone) + }) + return cmd +} diff --git a/products/udb/internal/sqlserver/list.go b/products/udb/internal/sqlserver/list.go new file mode 100644 index 0000000000..07d7f579d3 --- /dev/null +++ b/products/udb/internal/sqlserver/list.go @@ -0,0 +1,86 @@ +package sqlserver + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-sdk-go/services/udb" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +// newList returns the "list" command for SQL Server instances. +func newList(ctx *cli.Context) *cobra.Command { + client := cli.NewServiceClient(ctx, udb.NewClient) + req := client.NewDescribeUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "list", + Short: "List SQL Server instances", + Long: "List SQL Server instances", + Run: func(c *cobra.Command, args []string) { + if *req.DBId != "" { + *req.DBId = ctx.PickResourceID(*req.DBId) + } + resp, err := client.DescribeUDBInstance(req) + if err != nil { + ctx.HandleError(err) + return + } + list := []UDBSQLServerRow{} + for _, ins := range resp.DataSet { + row := UDBSQLServerRow{} + row.Name = ins.Name + row.Zone = ins.Zone + row.Role = ins.Role + row.ResourceID = ins.DBId + row.Group = ins.Tag + row.VPC = ins.VPCId + row.Subnet = ins.SubnetId + row.IP = ins.VirtualIP + row.Mode = ins.InstanceMode + row.DiskType = ins.InstanceType + row.Status = ins.State + row.Config = fmt.Sprintf("%s|%dG|%dG", ins.DBTypeId, ins.MemoryLimit/1000, ins.DiskSpace) + list = append(list, row) + for _, slave := range ins.DataSet { + row := UDBSQLServerRow{} + row.Name = slave.Name + row.Zone = slave.Zone + row.Role = fmt.Sprintf("⮑ %s", slave.Role) + row.ResourceID = slave.DBId + row.Group = slave.Tag + row.VPC = slave.VPCId + row.Subnet = slave.SubnetId + row.IP = slave.VirtualIP + row.Mode = slave.InstanceMode + row.DiskType = slave.InstanceType + row.Config = fmt.Sprintf("%s|%dG|%dG", slave.DBTypeId, slave.MemoryLimit/1000, slave.DiskSpace) + row.Status = slave.State + list = append(list, row) + } + } + ctx.PrintList(list) + }, + } + flags := cmd.Flags() + flags.SortFlags = false + + req.DBId = flags.String("udb-id", "", "Optional. List the specified SQL Server instance") + ctx.BindProjectID(cmd, req) + ctx.BindRegion(cmd, req) + ctx.BindZone(cmd, req) + ctx.BindLimit(cmd, req) + ctx.BindOffset(cmd, req) + req.IncludeSlaves = flags.Bool("include-slaves", false, "Optional. When specifying the udb-id, whether to display its slaves together. Accept values:true, false") + req.ClassType = sdk.String("sqlserver") + + command.SetFlagValues(cmd, "include-slaves", "true", "false") + command.SetCompletion(cmd, "udb-id", func() []string { + return getUDBIDList(ctx, nil, "sqlserver", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} diff --git a/products/udb/internal/sqlserver/poll.go b/products/udb/internal/sqlserver/poll.go new file mode 100644 index 0000000000..0866f52bb2 --- /dev/null +++ b/products/udb/internal/sqlserver/poll.go @@ -0,0 +1,52 @@ +package sqlserver + +import ( + "fmt" + "io" + + "github.com/ucloud/ucloud-sdk-go/services/udb" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + + "github.com/ucloud/ucloud-cli/pkg/cli" +) + +// stopUdbIns stops the instance and narrates progress to out (the caller passes +// ctx.ProgressWriter(): stdout in table mode, stderr in json/yaml). Returns the +// stop error so callers can decide whether to record a structured result. +func stopUdbIns(ctx *cli.Context, req *udb.StopUDBInstanceRequest, async bool, out io.Writer) error { + client := cli.NewServiceClient(ctx, udb.NewClient) + _, err := client.StopUDBInstance(req) + if err != nil { + ctx.HandleError(err) + return err + } + text := fmt.Sprintf("udb[%s] is stopping", *req.DBId) + if async { + fmt.Fprintln(out, text) + } else { + ctx.PollerTo(out, describeUdbByID(ctx)).Spoll(*req.DBId, text, []string{UDB_SHUTOFF, UDB_FAIL}) + } + return nil +} + +// describeUdbByID returns the poller's describe func, closing over ctx so it +// can build an authed udb client. +func describeUdbByID(ctx *cli.Context) func(udbID string, commonBase *request.CommonBase) (interface{}, error) { + return func(udbID string, commonBase *request.CommonBase) (interface{}, error) { + client := cli.NewServiceClient(ctx, udb.NewClient) + req := client.NewDescribeUDBInstanceRequest() + if commonBase != nil { + req.CommonBase = *commonBase + } + req.DBId = sdk.String(udbID) + resp, err := client.DescribeUDBInstance(req) + if err != nil { + return nil, err + } + if len(resp.DataSet) < 1 { + return nil, fmt.Errorf("udb[%s] may not exist", udbID) + } + return &resp.DataSet[0], nil + } +} diff --git a/products/udb/internal/sqlserver/restart.go b/products/udb/internal/sqlserver/restart.go new file mode 100644 index 0000000000..a36923c932 --- /dev/null +++ b/products/udb/internal/sqlserver/restart.go @@ -0,0 +1,61 @@ +package sqlserver + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-sdk-go/services/udb" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +// newRestart returns the "restart" command for SQL Server instances. +func newRestart(ctx *cli.Context) *cobra.Command { + var async bool + var idNames []string + client := cli.NewServiceClient(ctx, udb.NewClient) + req := client.NewRestartUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "restart", + Short: "Restart SQL Server instances by udb-id", + Long: "Restart SQL Server instances by udb-id", + Run: func(c *cobra.Command, args []string) { + w := ctx.ProgressWriter() + results := []cli.OpResultRow{} + for _, idname := range idNames { + id := ctx.PickResourceID(idname) + req.DBId = &id + _, err := client.RestartUDBInstance(req) + if err != nil { + ctx.HandleError(err) + continue + } + if async { + fmt.Fprintf(w, "udb[%s] is restarting\n", idname) + } else { + text := fmt.Sprintf("udb[%s] is restarting", idname) + ctx.PollerTo(w, describeUdbByID(ctx)).Spoll(*req.DBId, text, []string{UDB_RUNNING, UDB_FAIL}) + } + results = append(results, cli.OpResultRow{ResourceID: id, Action: "restart", Status: "Restarting"}) + } + ctx.EmitResult(results...) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "udb-id", nil, "Required. Resource ID of UDB instances to restart") + ctx.BindRegion(cmd, req) + ctx.BindZone(cmd, req) + ctx.BindProjectID(cmd, req) + flags.BoolVarP(&async, "async", "a", false, "Optional. Do not wait for the long-running operation to finish.") + + cmd.MarkFlagRequired("udb-id") + command.SetCompletion(cmd, "udb-id", func() []string { + return getUDBIDList(ctx, nil, "", *req.ProjectId, *req.Region, *req.Zone) + }) + return cmd +} diff --git a/products/udb/internal/sqlserver/rows.go b/products/udb/internal/sqlserver/rows.go new file mode 100644 index 0000000000..8c0010d7a8 --- /dev/null +++ b/products/udb/internal/sqlserver/rows.go @@ -0,0 +1,26 @@ +package sqlserver + +// MachineTypeRow 计算规格表格行 +type MachineTypeRow struct { + ID string + Description string + Cpu int + Memory int + Group string +} + +// UDBSQLServerRow 表格行 +type UDBSQLServerRow struct { + Name string + ResourceID string + Role string + Status string + Config string + Mode string + DiskType string + IP string + Group string + Zone string + VPC string + Subnet string +} diff --git a/products/udb/internal/sqlserver/start.go b/products/udb/internal/sqlserver/start.go new file mode 100644 index 0000000000..47118abe4d --- /dev/null +++ b/products/udb/internal/sqlserver/start.go @@ -0,0 +1,62 @@ +package sqlserver + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-sdk-go/services/udb" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +// newStart returns the "start" command for SQL Server instances. +func newStart(ctx *cli.Context) *cobra.Command { + var async bool + var idNames []string + client := cli.NewServiceClient(ctx, udb.NewClient) + req := client.NewStartUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "start", + Short: "Start SQL Server instances by udb-id", + Long: "Start SQL Server instances by udb-id", + Run: func(c *cobra.Command, args []string) { + w := ctx.ProgressWriter() + results := []cli.OpResultRow{} + for _, idname := range idNames { + id := ctx.PickResourceID(idname) + req.DBId = &id + _, err := client.StartUDBInstance(req) + if err != nil { + ctx.HandleError(err) + continue + } + if async { + fmt.Fprintf(w, "udb[%s] is starting\n", idname) + } else { + text := fmt.Sprintf("udb[%s] is starting", idname) + ctx.PollerTo(w, describeUdbByID(ctx)).Spoll(*req.DBId, text, []string{UDB_RUNNING, UDB_FAIL}) + } + results = append(results, cli.OpResultRow{ResourceID: id, Action: "start", Status: "Starting"}) + } + ctx.EmitResult(results...) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "udb-id", nil, "Required. Resource ID of UDB instances to start") + ctx.BindRegion(cmd, req) + ctx.BindZone(cmd, req) + ctx.BindProjectID(cmd, req) + flags.BoolVarP(&async, "async", "a", false, "Optional. Do not wait for the long-running operation to finish.") + + cmd.MarkFlagRequired("udb-id") + + command.SetCompletion(cmd, "udb-id", func() []string { + return getUDBIDList(ctx, []string{UDB_SHUTOFF}, "", *req.ProjectId, *req.Region, *req.Zone) + }) + return cmd +} diff --git a/products/udb/internal/sqlserver/status.go b/products/udb/internal/sqlserver/status.go new file mode 100644 index 0000000000..d29a669dbb --- /dev/null +++ b/products/udb/internal/sqlserver/status.go @@ -0,0 +1,11 @@ +package sqlserver + +// UDB-domain state constants, product-owned copies. +const ( + UDB_FAIL = "Fail" + UDB_RUNNING = "Running" + UDB_SHUTOFF = "Shutoff" + UDB_RECOVER_FAIL = "Recover fail" + UDB_UPGRADE_FAIL = "UpgradeFail" + UDB_TOBE_SWITCH = "WaitForSwitch" +) diff --git a/products/udb/internal/sqlserver/stop.go b/products/udb/internal/sqlserver/stop.go new file mode 100644 index 0000000000..bd7071c3bd --- /dev/null +++ b/products/udb/internal/sqlserver/stop.go @@ -0,0 +1,56 @@ +package sqlserver + +import ( + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-sdk-go/services/udb" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +// newStop returns the "stop" command for SQL Server instances. +func newStop(ctx *cli.Context) *cobra.Command { + var idNames []string + var async bool + client := cli.NewServiceClient(ctx, udb.NewClient) + req := client.NewStopUDBInstanceRequest() + cmd := &cobra.Command{ + Use: "stop", + Short: "Stop SQL Server instances by udb-id", + Long: "Stop SQL Server instances by udb-id", + Run: func(c *cobra.Command, args []string) { + w := ctx.ProgressWriter() + results := []cli.OpResultRow{} + for _, idname := range idNames { + id := ctx.PickResourceID(idname) + req.DBId = sdk.String(id) + if err := stopUdbIns(ctx, req, async, w); err != nil { + continue + } + results = append(results, cli.OpResultRow{ResourceID: id, Action: "stop", Status: "Stopping"}) + } + ctx.EmitResult(results...) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&idNames, "udb-id", nil, "Required. Resource ID of UDB instances to stop") + ctx.BindRegion(cmd, req) + ctx.BindZone(cmd, req) + ctx.BindProjectID(cmd, req) + req.ForceToKill = flags.Bool("force", false, "Optional. Stop UDB instances by force or not") + flags.BoolVarP(&async, "async", "a", false, "Optional. Do not wait for the long-running operation to finish.") + + cmd.MarkFlagRequired("udb-id") + + command.SetFlagValues(cmd, "force", "true", "false") + command.SetCompletion(cmd, "udb-id", func() []string { + return getUDBIDList(ctx, []string{UDB_RUNNING}, "", *req.ProjectId, *req.Region, *req.Zone) + }) + + return cmd +} diff --git a/products/udb/product.yaml b/products/udb/product.yaml new file mode 100644 index 0000000000..96a6554641 --- /dev/null +++ b/products/udb/product.yaml @@ -0,0 +1,8 @@ +# products/udb/product.yaml — udb 产品元数据(归属真源,owner 自治维护) +name: udb +owners: + - Episkey-G +commands: + - mysql + - sqlserver +enabled: true