Skip to content

Commit

Permalink
Merge pull request #53 from liquidweb/resize-update-reboot-expection
Browse files Browse the repository at this point in the history
Use new resizePlan method to determine resize reboot expectation
  • Loading branch information
sgsullivan committed Oct 16, 2020
2 parents fba5b62 + dc3f4ab commit 830c61d
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 100 deletions.
179 changes: 179 additions & 0 deletions cmd/cloudServerResizeExpectation.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
123 changes: 23 additions & 100 deletions instance/cloudServerResize.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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"
Expand All @@ -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()
Expand Down
23 changes: 23 additions & 0 deletions types/api/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down

0 comments on commit 830c61d

Please sign in to comment.