diff --git a/cmd/cloudServerResizeExpectation.go b/cmd/cloudServerResizeExpectation.go new file mode 100644 index 0000000..3b0c5b1 --- /dev/null +++ b/cmd/cloudServerResizeExpectation.go @@ -0,0 +1,179 @@ +/* +Copyright © LiquidWeb + +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 ( + "errors" + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/types/api" + "github.com/liquidweb/liquidweb-cli/utils" + "github.com/liquidweb/liquidweb-cli/validate" +) + +var cloudServerResizeExpectationCmd = &cobra.Command{ + Use: "resize-expectation", + Short: "Determine if a Cloud Server can be resized without downtime", + Long: `This command can be used to determine if a Cloud Server can be resized to the requested +config-id without downtime. + +Depending on inventory and desired config-id (configuration) the resize could either +require a reboot to complete, or be performed entirely live. The intention of this +command is to provide the user with a sane expectation ahead of making the resize +request. + +If there is no inventory available, an exception will be raised. + +Its important to note, this command will *not* make any changes to your Cloud Server. +This command is purely for information gathering. +`, + Run: func(cmd *cobra.Command, args []string) { + uniqIdFlag, _ := cmd.Flags().GetString("uniq-id") + privateParentFlag, _ := cmd.Flags().GetString("private-parent") + diskFlag, _ := cmd.Flags().GetInt64("diskspace") + memoryFlag, _ := cmd.Flags().GetInt64("memory") + vcpuFlag, _ := cmd.Flags().GetInt64("vcpu") + configIdFlag, _ := cmd.Flags().GetInt64("config-id") + + validateFields := map[interface{}]interface{}{ + uniqIdFlag: "UniqId", + } + + if err := validate.Validate(validateFields); err != nil { + lwCliInst.Die(err) + } + + if privateParentFlag != "" && configIdFlag != -1 { + lwCliInst.Die(errors.New("cant pass both --config-id and --private-parent flags")) + } + if privateParentFlag == "" && configIdFlag == -1 { + lwCliInst.Die(errors.New("must pass --config-id or --private-parent")) + } + + apiArgs := map[string]interface{}{ + "uniq_id": uniqIdFlag, + "config_id": configIdFlag, + } + + // if private parent, add args + if privateParentFlag != "" { + if memoryFlag <= 0 && diskFlag <= 0 && vcpuFlag <= 0 { + lwCliInst.Die(errors.New("when --private-parent , at least one of --memory --disk --vcpu are required")) + } + + privateParentUniqId, _, err := lwCliInst.DerivePrivateParentUniqId(privateParentFlag) + if err != nil { + lwCliInst.Die(err) + } + + var cloudServerDetails apiTypes.CloudServerDetails + if err = lwCliInst.CallLwApiInto( + "bleed/storm/server/details", + map[string]interface{}{ + "uniq_id": uniqIdFlag, + }, &cloudServerDetails); err != nil { + lwCliInst.Die(err) + } + + apiArgs["config_id"] = 0 + apiArgs["private_parent"] = privateParentUniqId + apiArgs["disk"] = cloudServerDetails.DiskSpace + apiArgs["memory"] = cloudServerDetails.Memory + apiArgs["vcpu"] = cloudServerDetails.Vcpu + + if diskFlag > 0 { + apiArgs["disk"] = diskFlag + } + if vcpuFlag > 0 { + apiArgs["vcpu"] = vcpuFlag + } + if memoryFlag > 0 { + apiArgs["memory"] = memoryFlag + } + } + + var expectation apiTypes.CloudServerResizeExpectation + err := lwCliInst.CallLwApiInto("bleed/storm/server/resizePlan", apiArgs, &expectation) + if err != nil { + utils.PrintRed("Configuration Not Available\n\n") + fmt.Printf("%s\n", err) + os.Exit(1) + } + + utils.PrintGreen("Configuration Available\n\n") + + fmt.Print("Resource Changes: Disk [") + if expectation.DiskDifference == 0 { + fmt.Printf("%d] ", expectation.DiskDifference) + } else if expectation.DiskDifference >= 0 { + utils.PrintGreen("%d", expectation.DiskDifference) + fmt.Print("] ") + } else { + utils.PrintRed("%d", expectation.DiskDifference) + fmt.Print("] ") + } + + fmt.Print("Memory [") + if expectation.MemoryDifference == 0 { + fmt.Printf("%d] ", expectation.MemoryDifference) + } else if expectation.MemoryDifference >= 0 { + utils.PrintGreen("%d", expectation.MemoryDifference) + fmt.Print("] ") + } else { + utils.PrintRed("%d", expectation.MemoryDifference) + fmt.Print("] ") + } + + fmt.Print("Vcpu [") + if expectation.VcpuDifference == 0 { + fmt.Printf("%d]\n", expectation.VcpuDifference) + } else if expectation.VcpuDifference >= 0 { + utils.PrintGreen("%d", expectation.VcpuDifference) + fmt.Print("]\n") + } else { + utils.PrintRed("%d", expectation.VcpuDifference) + fmt.Print("]\n") + } + + if expectation.RebootRequired { + utils.PrintYellow("\nReboot required.\n") + } else { + utils.PrintGreen("\nNo reboot required.\n") + } + }, +} + +func init() { + cloudServerCmd.AddCommand(cloudServerResizeExpectationCmd) + + cloudServerResizeExpectationCmd.Flags().String("uniq-id", "", "uniq-id of Cloud Server") + + cloudServerResizeExpectationCmd.Flags().String("private-parent", "", + "name or uniq-id of the Private Parent (see: 'cloud private-parent list')") + cloudServerResizeExpectationCmd.Flags().Int64("diskspace", -1, "diskspace for the Cloud Server (when private-parent)") + cloudServerResizeExpectationCmd.Flags().Int64("memory", -1, "memory for the Cloud Server (when private-parent)") + cloudServerResizeExpectationCmd.Flags().Int64("vcpu", -1, "vcpus for the Cloud Server (when private-parent)") + + cloudServerResizeExpectationCmd.Flags().Int64("config-id", -1, + "config-id to check availability for (when !private-parent) (see: 'cloud server options --configs')") + + if err := cloudServerResizeExpectationCmd.MarkFlagRequired("uniq-id"); err != nil { + lwCliInst.Die(err) + } +} diff --git a/instance/cloudServerResize.go b/instance/cloudServerResize.go index 8fe4403..dc6e0f7 100644 --- a/instance/cloudServerResize.go +++ b/instance/cloudServerResize.go @@ -73,6 +73,10 @@ func (self *Client) CloudServerResize(params *CloudServerResizeParams) (result s return } + resizePlanArgs := map[string]interface{}{ + "uniq_id": params.UniqId, + } + resizeArgs := map[string]interface{}{ "uniq_id": params.UniqId, "skip_fs_resize": skipFsResizeInt, @@ -89,10 +93,6 @@ func (self *Client) CloudServerResize(params *CloudServerResizeParams) (result s return } - var ( - liveResize bool - twoRebootResize bool - ) if params.PrivateParent == "" { // non private parent resize if params.Memory != -1 || params.DiskSpace != -1 || params.Vcpu != -1 { @@ -107,27 +107,8 @@ func (self *Client) CloudServerResize(params *CloudServerResizeParams) (result s } validateFields[params.ConfigId] = "PositiveInt64" - if err = validate.Validate(validateFields); err != nil { - return - } - // determine reboot expectation. - // resize up full: 2 reboot - // resize up quick (skip-fs-resize) 1 reboot - // resize down: 1 reboot - var configDetails apiTypes.CloudConfigDetails - if err = self.CallLwApiInto("bleed/storm/config/details", - map[string]interface{}{"id": params.ConfigId}, &configDetails); err != nil { - return - } - - if configDetails.Disk >= cloudServerDetails.DiskSpace { - // disk space going up.. - if !params.SkipFsResize { - // .. and not skipping fs resize, will be 2 reboots. - twoRebootResize = true - } - } + resizePlanArgs["config_id"] = params.ConfigId } else { // private parent resize specific logic if params.Memory == -1 && params.DiskSpace == -1 && params.Vcpu == -1 { @@ -141,37 +122,6 @@ func (self *Client) CloudServerResize(params *CloudServerResizeParams) (result s return } - var ( - diskspaceChanging bool - vcpuChanging bool - memoryChanging bool - memoryCanLive bool - vcpuCanLive bool - ) - // record what resources are changing - if params.DiskSpace != -1 { - if cloudServerDetails.DiskSpace != params.DiskSpace { - diskspaceChanging = true - } - } - if params.Vcpu != -1 { - if cloudServerDetails.Vcpu != params.Vcpu { - vcpuChanging = true - } - } - if params.Memory != -1 { - if cloudServerDetails.Memory != params.Memory { - memoryChanging = true - } - } - // allow resizes to a private parent even if its old non private parent config had exact same specs - if cloudServerDetails.ConfigId == 0 && cloudServerDetails.PrivateParent != privateParentUniqId { - if !diskspaceChanging && !vcpuChanging && !memoryChanging { - err = errors.New("private parent resize, but passed diskspace, memory, vcpu values match existing values") - return - } - } - resizeArgs["newsize"] = 0 // 0 indicates private parent resize resizeArgs["parent"] = privateParentUniqId // uniq_id of the private parent validateFields[privateParentUniqId] = "UniqId" @@ -194,64 +144,37 @@ func (self *Client) CloudServerResize(params *CloudServerResizeParams) (result s validateFields[params.Vcpu] = "PositiveInt64" } - // determine if this will be a live resize - if _, exists := resizeArgs["memory"]; exists { - if params.Memory >= cloudServerDetails.Memory { - // asking for more RAM - memoryCanLive = true - } - } - if _, exists := resizeArgs["vcpu"]; exists { - if params.Vcpu >= cloudServerDetails.Vcpu { - // asking for more vcpu - vcpuCanLive = true - } - } - - if params.Memory != -1 && params.Vcpu != -1 { - if vcpuCanLive && memoryCanLive { - liveResize = true - } - } else if memoryCanLive { - liveResize = true - } else if vcpuCanLive { - liveResize = true - } - - // if diskspace allocation changes its not currently ever done live regardless of memory, vcpu - if params.DiskSpace != -1 { - if resizeArgs["diskspace"] != cloudServerDetails.DiskSpace { - liveResize = false - } - } + resizePlanArgs["config_id"] = 0 + resizePlanArgs["private_parent"] = privateParentUniqId + resizePlanArgs["memory"] = resizeArgs["memory"] + resizePlanArgs["disk"] = resizeArgs["diskspace"] + resizePlanArgs["vcpu"] = resizeArgs["vcpu"] } if err = validate.Validate(validateFields); err != nil { return } + var expectation apiTypes.CloudServerResizeExpectation + if err = self.CallLwApiInto("bleed/storm/server/resizePlan", resizePlanArgs, &expectation); err != nil { + err = fmt.Errorf("Configuration Not Available\n\n%s\n", err) + return + } + if _, err = self.LwCliApiClient.Call("bleed/server/resize", resizeArgs); err != nil { return } var b bytes.Buffer - b.WriteString(fmt.Sprintf("server resized started! You can check progress with 'cloud server status --uniq-id %s'\n\n", params.UniqId)) - if liveResize { - b.WriteString(fmt.Sprintf("\nthis resize will be performed live without downtime.\n")) - } else { - rebootExpectation := "one reboot" - if twoRebootResize { - rebootExpectation = "two reboots" - } - b.WriteString(fmt.Sprintf( - "\nexpect %s during this process. Your server will be online as the disk is copied to the destination.\n", - rebootExpectation)) + b.WriteString(fmt.Sprintf("Server resized started! You can check progress with 'cloud server status --uniq-id %s'\n\n", params.UniqId)) + b.WriteString(fmt.Sprintf("Resource changes: Memory [%d] Disk [%d] Vcpu [%d]\n", expectation.MemoryDifference, + expectation.DiskDifference, expectation.VcpuDifference)) - if twoRebootResize { - b.WriteString(fmt.Sprintf( - "\tTIP: Avoid the second reboot by passing --skip-fs-resize. See usage for additional details.\n")) - } + if expectation.RebootRequired { + b.WriteString("\nExpect a reboot during this resize.\n") + } else { + b.WriteString("\nThis resize will be performed live without downtime.\n") } result = b.String() diff --git a/types/api/cloud.go b/types/api/cloud.go index 2d219bf..9e24c11 100644 --- a/types/api/cloud.go +++ b/types/api/cloud.go @@ -490,6 +490,29 @@ type CloudBlockStorageVolumeResize struct { UniqId string `json:"uniq_id" mapstructure:"uniq_id"` } +type CloudServerResizeExpectation struct { + DiskDifference int64 `json:"diskDifference" mapstructure:"diskDifference"` + MemoryDifference int64 `json:"memoryDifference" mapstructure:"memoryDifference"` + VcpuDifference int64 `json:"vcpuDifference" mapstructure:"vcpuDifference"` + RebootRequired FlexBool `json:"rebootRequired" mapstructure:"rebootRequired"` +} + +type FlexBool bool + +func (self *FlexBool) UnmarshalJSON(data []byte) error { + str := string(data) + + if str == "1" || str == "true" { + *self = true + } else if str == "0" || str == "false" { + *self = false + } else { + return fmt.Errorf("Boolean unmarshal error: invalid input %s", str) + } + + return nil +} + type CloudObjectStoreDetails struct { Accnt int64 `json:"accnt" mapstructure:"accnt"` Caps []CloudObjectStoreDetailsCapsEntry `json:"caps" mapstructure:"caps"`