Skip to content

Commit

Permalink
Add bosh recover command
Browse files Browse the repository at this point in the history
This command takes the recovery plan generated from the `bosh
create-recovery-plan` command and applies it, including the
`max_in_flight` overrides.

Co-authored-by: Chris Selzo <[email protected]>
Co-authored-by: Long Nguyen <[email protected]>
  • Loading branch information
selzoc and lnguyen committed Jul 11, 2023
1 parent 27cd7c0 commit 97349fa
Show file tree
Hide file tree
Showing 12 changed files with 559 additions and 24 deletions.
2 changes: 1 addition & 1 deletion cmd/cloud_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (c CloudCheckCmd) Run(opts CloudCheckOpts) error {
return err
}

return c.deployment.ResolveProblems(answers)
return c.deployment.ResolveProblems(answers, nil)
}

func (_ CloudCheckCmd) applyResolutions(resolutionsToApply []string, probs []boshdir.Problem) ([]boshdir.ProblemAnswer, error) {
Expand Down
3 changes: 2 additions & 1 deletion cmd/cloud_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,9 @@ var _ = Describe("CloudCheckCmd", func() {

Expect(deployment.ResolveProblemsCallCount()).To(Equal(1))

problemAnswers := deployment.ResolveProblemsArgsForCall(0)
problemAnswers, overrides := deployment.ResolveProblemsArgsForCall(0)
Expect(len(problemAnswers)).To(Equal(2))
Expect(overrides).To(BeNil())

problemAnswer0 := problemAnswers[0]
Expect(problemAnswer0.ProblemID).To(Equal(3))
Expand Down
3 changes: 3 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@ func (c Cmd) Execute() (cmdErr error) {
case *CreateRecoveryPlanOpts:
return NewCreateRecoveryPlanCmd(c.deployment(), deps.UI, deps.FS).Run(*opts)

case *RecoverOpts:
return NewRecoverCmd(c.deployment(), deps.UI, deps.FS).Run(*opts)

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

Expand Down
20 changes: 17 additions & 3 deletions cmd/create_recovery_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ type InstanceGroupPlan struct {
PlannedResolutions map[string]string `yaml:"planned_resolutions"`
}

func (p InstanceGroupPlan) resolutionName(problem boshdir.Problem) string {
return p.PlannedResolutions[problem.Type]
}

func (p InstanceGroupPlan) resolutionPlan(problem boshdir.Problem) string {
for _, r := range problem.Resolutions {
if *r.Name == p.resolutionName(problem) {
return r.Plan
}
}

return "No resolution planned"
}

type RecoveryPlan struct {
InstanceGroupsPlan []InstanceGroupPlan `yaml:"instance_groups_plan"`
}
Expand All @@ -37,7 +51,7 @@ func NewCreateRecoveryPlanCmd(deployment boshdir.Deployment, ui boshui.UI, fs bo
}

func (c CreateRecoveryPlanCmd) Run(opts CreateRecoveryPlanOpts) error {
problemsByInstanceGroup, err := c.getProblemsByInstanceGroup()
problemsByInstanceGroup, err := getProblemsByInstanceGroup(c.deployment)
if err != nil {
return err
}
Expand Down Expand Up @@ -180,8 +194,8 @@ func (c CreateRecoveryPlanCmd) printProblemTable(problemType string, problemsFor
c.ui.PrintTable(table)
}

func (c CreateRecoveryPlanCmd) getProblemsByInstanceGroup() (map[string][]boshdir.Problem, error) {
problems, err := c.deployment.ScanForProblems()
func getProblemsByInstanceGroup(deployment boshdir.Deployment) (map[string][]boshdir.Problem, error) {
problems, err := deployment.ScanForProblems()
if err != nil {
return nil, err
}
Expand Down
10 changes: 10 additions & 0 deletions cmd/opts/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ type BoshOpts struct {
Unignore UnignoreOpts `command:"unignore" description:"Unignore an instance"`
CloudCheck CloudCheckOpts `command:"cloud-check" alias:"cck" alias:"cloudcheck" description:"Cloud consistency check and interactive repair"` //nolint:staticcheck
CreateRecoveryPlan CreateRecoveryPlanOpts `command:"create-recovery-plan" description:"Interactively generate a recovery plan for disaster repair"`
Recover RecoverOpts `command:"recover" description:"Apply a recovery plan for disaster repair"`
OrphanedVMs OrphanedVMsOpts `command:"orphaned-vms" description:"List all the orphaned VMs in all deployments"`

// Instance management
Expand Down Expand Up @@ -826,6 +827,15 @@ type CreateRecoveryPlanArgs struct {
RecoveryPlan FileArg `positional-arg-name:"PATH" description:"Create recovery plan file at path"`
}

type RecoverOpts struct {
Args RecoverArgs `positional-args:"true" required:"true"`
cmd
}

type RecoverArgs struct {
RecoveryPlan FileArg `positional-arg-name:"PATH" description:"Path to a recovery plan file"`
}

type OrphanedVMsOpts struct {
cmd
}
Expand Down
32 changes: 32 additions & 0 deletions cmd/opts/opts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2487,6 +2487,38 @@ var _ = Describe("Opts", func() {
})
})

Describe("RecoverOpts", func() {
var opts *RecoverOpts

BeforeEach(func() {
opts = &RecoverOpts{}
})

Describe("Args", func() {
It("contains desired values", func() {
Expect(getStructTagForName("Args", opts)).To(Equal(
`positional-args:"true" required:"true"`,
))
})
})
})

Describe("RecoverArgs", func() {
var opts *RecoverArgs

BeforeEach(func() {
opts = &RecoverArgs{}
})

Describe("RecoveryPlan", func() {
It("contains desired values", func() {
Expect(getStructTagForName("RecoveryPlan", opts)).To(Equal(
`positional-arg-name:"PATH" description:"Path to a recovery plan file"`,
))
})
})
})

Describe("UpdateResurrectionOpts", func() {
var opts *UpdateResurrectionOpts

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

import (
"fmt"

"gopkg.in/yaml.v2"

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 RecoverCmd struct {
deployment boshdir.Deployment
ui boshui.UI
fs boshsys.FileSystem
}

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

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

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

plan, err := c.readPlan(opts)
if err != nil {
return err
}

c.printPlanSummary(problemsByInstanceGroup, plan)
if err := c.ui.AskForConfirmation(); err != nil {
return err
}

var answers []boshdir.ProblemAnswer
maxInFlightOverrides := make(map[string]string)
for _, instanceGroupPlan := range plan.InstanceGroupsPlan {
if instanceGroupPlan.MaxInFlightOverride != "" {
maxInFlightOverrides[instanceGroupPlan.Name] = instanceGroupPlan.MaxInFlightOverride
}

instanceGroupAnswers := getAnswersFromPlan(problemsByInstanceGroup[instanceGroupPlan.Name], instanceGroupPlan)
answers = append(answers, instanceGroupAnswers...)
}

err = c.deployment.ResolveProblems(answers, maxInFlightOverrides)
if err != nil {
return err
}

return nil
}

func getAnswersFromPlan(problems []boshdir.Problem, instanceGroupPlan InstanceGroupPlan) []boshdir.ProblemAnswer {
var answers []boshdir.ProblemAnswer
for _, p := range problems {
resolutionName := instanceGroupPlan.resolutionName(p)
answers = append(answers, boshdir.ProblemAnswer{
ProblemID: p.ID,
Resolution: boshdir.ProblemResolution{
Name: &resolutionName,
Plan: instanceGroupPlan.resolutionPlan(p),
},
})
}

return answers
}

func (c RecoverCmd) readPlan(opts RecoverOpts) (*RecoveryPlan, error) {
planContents, err := c.fs.ReadFile(opts.Args.RecoveryPlan.ExpandedPath)
if err != nil {
return nil, err
}

var plan RecoveryPlan
err = yaml.Unmarshal(planContents, &plan)
if err != nil {
return nil, err
}

return &plan, nil
}

func (c RecoverCmd) printPlanSummary(problemsByInstanceGroup map[string][]boshdir.Problem, plan *RecoveryPlan) {
for instanceGroup, instanceGroupProblems := range problemsByInstanceGroup {
instanceGroupPlan := getPlanForInstanceGroup(instanceGroup, plan)

title := fmt.Sprintf("Instance Group '%s' plan summary", instanceGroup)
if instanceGroupPlan.MaxInFlightOverride != "" {
title = fmt.Sprintf("%s (max_in_flight override: %s)", title, instanceGroupPlan.MaxInFlightOverride)
}

table := boshtbl.Table{
Title: title,
Header: []boshtbl.Header{
boshtbl.NewHeader("#"),
boshtbl.NewHeader("Planned resolution"),
boshtbl.NewHeader("Description"),
},
SortBy: []boshtbl.ColumnSort{{Column: 0, Asc: true}},
}

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

c.ui.PrintTable(table)
}
}

func getPlanForInstanceGroup(instanceGroup string, plan *RecoveryPlan) InstanceGroupPlan {
for _, p := range plan.InstanceGroupsPlan {
if p.Name == instanceGroup {
return p
}
}

return InstanceGroupPlan{}
}
Loading

0 comments on commit 97349fa

Please sign in to comment.