Skip to content

Commit

Permalink
Add create-recovery-plan command (#622)
Browse files Browse the repository at this point in the history
* Add `create-recovery-plan` command

This command will allow users to scan a deployment for problems, then
write out a YAML-formatted recovery plan, to be used later in the
`recover` command.

[#185483613]

Authored-by: Chris Selzo <[email protected]>
  • Loading branch information
selzoc authored Jul 10, 2023
1 parent f3ac369 commit 27cd7c0
Show file tree
Hide file tree
Showing 7 changed files with 573 additions and 4 deletions.
3 changes: 3 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,9 @@ func (c Cmd) Execute() (cmdErr error) {
case *CloudCheckOpts:
return NewCloudCheckCmd(c.deployment(), deps.UI).Run(*opts)

case *CreateRecoveryPlanOpts:
return NewCreateRecoveryPlanCmd(c.deployment(), deps.UI, deps.FS).Run(*opts)

case *CleanUpOpts:
return NewCleanUpCmd(deps.UI, c.director()).Run(*opts)

Expand Down
213 changes: 213 additions & 0 deletions cmd/create_recovery_plan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package cmd

import (
"fmt"
"sort"

"gopkg.in/yaml.v2"

bosherr "github.com/cloudfoundry/bosh-utils/errors"
boshsys "github.com/cloudfoundry/bosh-utils/system"

boshdir "github.com/cloudfoundry/bosh-cli/v7/director"
boshui "github.com/cloudfoundry/bosh-cli/v7/ui"
boshtbl "github.com/cloudfoundry/bosh-cli/v7/ui/table"

. "github.com/cloudfoundry/bosh-cli/v7/cmd/opts"
)

type InstanceGroupPlan struct {
Name string `yaml:"name"`
MaxInFlightOverride string `yaml:"max_in_flight_override,omitempty"`
PlannedResolutions map[string]string `yaml:"planned_resolutions"`
}

type RecoveryPlan struct {
InstanceGroupsPlan []InstanceGroupPlan `yaml:"instance_groups_plan"`
}

type CreateRecoveryPlanCmd struct {
deployment boshdir.Deployment
ui boshui.UI
fs boshsys.FileSystem
}

func NewCreateRecoveryPlanCmd(deployment boshdir.Deployment, ui boshui.UI, fs boshsys.FileSystem) CreateRecoveryPlanCmd {
return CreateRecoveryPlanCmd{deployment: deployment, ui: ui, fs: fs}
}

func (c CreateRecoveryPlanCmd) Run(opts CreateRecoveryPlanOpts) error {
problemsByInstanceGroup, err := c.getProblemsByInstanceGroup()
if err != nil {
return err
}

if len(problemsByInstanceGroup) == 0 {
c.ui.PrintLinef("No problems found\n")
return nil
}

maxInFlightByInstanceGroup, err := c.getMaxInFlightByInstanceGroup()
if err != nil {
return err
}

var plan RecoveryPlan
for _, instanceGroup := range sortedMapKeys(problemsByInstanceGroup) {
c.ui.PrintLinef("Instance Group '%s'\n", instanceGroup)

instanceGroupResolutions, err := c.processProblemsByType(problemsByInstanceGroup[instanceGroup])
if err != nil {
return err
}

instanceGroupCurrentMaxInFlight := maxInFlightByInstanceGroup[instanceGroup]
var instanceGroupMaxInFlightOverride string
if c.ui.AskForConfirmationWithLabel(
fmt.Sprintf("Override current max_in_flight value of '%s'?", instanceGroupCurrentMaxInFlight),
) == nil {
instanceGroupMaxInFlightOverride, err = c.ui.AskForTextWithDefaultValue(
fmt.Sprintf("max_in_flight override for '%s'", instanceGroup),
instanceGroupCurrentMaxInFlight,
)
if err != nil {
return err
}
}

plan.InstanceGroupsPlan = append(plan.InstanceGroupsPlan, InstanceGroupPlan{
Name: instanceGroup,
MaxInFlightOverride: instanceGroupMaxInFlightOverride,
PlannedResolutions: instanceGroupResolutions,
})
}

bytes, err := yaml.Marshal(plan)
if err != nil {
return err
}

return c.fs.WriteFile(opts.Args.RecoveryPlan.ExpandedPath, bytes)
}

type updateInstanceGroup struct {
Name string `yaml:"name"`
Update map[string]interface{} `yaml:"update"`
}

type updateManifest struct {
InstanceGroups []updateInstanceGroup `yaml:"instance_groups"`
Update map[string]interface{} `yaml:"update"`
}

func (c CreateRecoveryPlanCmd) getMaxInFlightByInstanceGroup() (map[string]string, error) {
rawManifest, err := c.deployment.Manifest()
if err != nil {
return nil, err
}

var updateManifest updateManifest
err = yaml.Unmarshal([]byte(rawManifest), &updateManifest)
if err != nil {
return nil, err
}

globalMaxInFlight := updateManifest.Update["max_in_flight"]
flightMap := make(map[string]string)
for _, instanceGroup := range updateManifest.InstanceGroups {
groupMaxInFlight := instanceGroup.Update["max_in_flight"]
if groupMaxInFlight == nil {
groupMaxInFlight = globalMaxInFlight
}
flightMap[instanceGroup.Name] = fmt.Sprintf("%v", groupMaxInFlight)
}

return flightMap, nil
}

func sortedMapKeys(problemMap map[string][]boshdir.Problem) []string {
var keys []string
for k := range problemMap {
keys = append(keys, k)
}
sort.Strings(keys)

return keys
}

func (c CreateRecoveryPlanCmd) processProblemsByType(problems []boshdir.Problem) (map[string]string, error) {
problemsByType := mapProblemsByTrait(problems, func(p boshdir.Problem) string { return p.Type })

resolutions := make(map[string]string)
for _, problemType := range sortedMapKeys(problemsByType) {
problemsForType := problemsByType[problemType]
c.printProblemTable(problemType, problemsForType)

var opts []string
for _, res := range problemsForType[0].Resolutions {
opts = append(opts, res.Plan)
}

chosenIndex, err := c.ui.AskForChoice(problemType, opts)
if err != nil {
return nil, err
}

resolutions[problemType] = *problemsForType[0].Resolutions[chosenIndex].Name
}

return resolutions, nil
}

func (c CreateRecoveryPlanCmd) printProblemTable(problemType string, problemsForType []boshdir.Problem) {
table := boshtbl.Table{
Title: fmt.Sprintf("Problem type: %s", problemType),
Content: fmt.Sprintf("%s problems", problemType),
Header: []boshtbl.Header{
boshtbl.NewHeader("#"),
boshtbl.NewHeader("Description"),
},
SortBy: []boshtbl.ColumnSort{{Column: 0, Asc: true}},
}

for _, p := range problemsForType {
table.Rows = append(table.Rows, []boshtbl.Value{
boshtbl.NewValueInt(p.ID),
boshtbl.NewValueString(p.Description),
})
}

c.ui.PrintTable(table)
}

func (c CreateRecoveryPlanCmd) getProblemsByInstanceGroup() (map[string][]boshdir.Problem, error) {
problems, err := c.deployment.ScanForProblems()
if err != nil {
return nil, err
}

if anyProblemsHaveNoInstanceGroups(problems) {
return nil, bosherr.Error("Director does not support this command. Try 'bosh cloud-check' instead")
}

return mapProblemsByTrait(problems, func(p boshdir.Problem) string { return p.InstanceGroup }), nil
}

func anyProblemsHaveNoInstanceGroups(problems []boshdir.Problem) bool {
for _, p := range problems {
if p.InstanceGroup == "" {
return true
}
}

return false
}

func mapProblemsByTrait(problems []boshdir.Problem, traitFunc func(p boshdir.Problem) string) map[string][]boshdir.Problem {
probMap := make(map[string][]boshdir.Problem)
for _, p := range problems {
probMap[traitFunc(p)] = append(probMap[traitFunc(p)], p)
}

return probMap
}
Loading

0 comments on commit 27cd7c0

Please sign in to comment.