diff --git a/example/cmd/microctl/cluster_members.go b/example/cmd/microctl/cluster_members.go index 1594d837..12278aa7 100644 --- a/example/cmd/microctl/cluster_members.go +++ b/example/cmd/microctl/cluster_members.go @@ -1,15 +1,46 @@ package main import ( + "bufio" + "fmt" + "io" + "os" "sort" + "strings" + "github.com/canonical/lxd/shared" cli "github.com/canonical/lxd/shared/cmd" + "github.com/canonical/lxd/shared/termios" "github.com/spf13/cobra" + "golang.org/x/sys/unix" + "gopkg.in/yaml.v2" "github.com/canonical/microcluster/client" + "github.com/canonical/microcluster/cluster" "github.com/canonical/microcluster/microcluster" ) +const recoveryConfirmation = `You should only run this command if: + - A quorum of cluster members is permanently lost + - You are *absolutely* sure all microd instances are stopped + - This instance has the most up to date database + +Do you want to proceed? (yes/no): ` + +const recoveryYamlComment = `# Member roles can be modified. Unrecoverable nodes should be given the role "spare". +# +# "voter" - Voting member of the database. A majority of voters is a quorum. +# "stand-by" - Non-voting member of the database; can be promoted to voter. +# "spare" - Not a member of the database. +# +# The edit is aborted if: +# - the number of members changes +# - the name of any member changes +# - the ID of any member changes +# - the address of any member changes +# - no changes are made +` + type cmdClusterMembers struct { common *CmdControl } @@ -27,6 +58,9 @@ func (c *cmdClusterMembers) command() *cobra.Command { var cmdList = cmdClusterMembersList{common: c.common} cmd.AddCommand(cmdList.command()) + var cmdRestore = cmdClusterEdit{common: c.common} + cmd.AddCommand(cmdRestore.command()) + return cmd } @@ -130,3 +164,73 @@ func (c *cmdClusterMemberRemove) run(cmd *cobra.Command, args []string) error { return nil } + +type cmdClusterEdit struct { + common *CmdControl +} + +func (c *cmdClusterEdit) command() *cobra.Command { + cmd := &cobra.Command{ + Use: "edit", + Short: "Recover the cluster from this node if quorum is lost", + RunE: c.run, + } + + return cmd +} + +func (c *cmdClusterEdit) run(cmd *cobra.Command, args []string) error { + m, err := microcluster.App(microcluster.Args{StateDir: c.common.FlagStateDir, Verbose: c.common.FlagLogVerbose, Debug: c.common.FlagLogDebug}) + if err != nil { + return err + } + + members, err := m.GetLocalClusterMembers() + if err != nil { + return err + } + + membersYaml, err := yaml.Marshal(members) + if err != nil { + return err + } + + var content []byte + if !termios.IsTerminal(unix.Stdin) { + content, err = io.ReadAll(os.Stdin) + if err != nil { + return err + } + } else { + reader := bufio.NewReader(os.Stdin) + fmt.Print(recoveryConfirmation) + + input, _ := reader.ReadString('\n') + input = strings.TrimSuffix(input, "\n") + + if strings.ToLower(input) != "yes" { + fmt.Println("Cluster edit aborted; no changes made") + return nil + } + + content, err = shared.TextEditor("", append([]byte(recoveryYamlComment), membersYaml...)) + if err != nil { + return err + } + } + + newMembers := []cluster.LocalMember{} + err = yaml.Unmarshal(content, &newMembers) + if err != nil { + return err + } + + err = m.RecoverFromQuorumLoss(newMembers) + if err != nil { + return fmt.Errorf("cluster edit: %w", err) + } + + fmt.Println("Cluster reconfigured successfully") + + return nil +}