open-nomad/command/server_members.go
Luiz Aoqui 68e5b58007
cli: display Raft version in server members (#12317)
The previous output of the `nomad server members` command would output a
column named `Protocol` that displayed the Serf protocol being currently
used by servers.

This is not a configurable option, so it holds very little value to
operators. It is also easy to confuse it with the Raft Protocol version,
which is configurable and highly relevant to operators.

This commit replaces the previous `Protocol` column with the new `Raft
Version`. It also updates the `-detailed` flag to be called `-verbose`
so it matches other commands. The detailed output now also outputs the
same information as the standard output with the addition of the
previous `Protocol` column and `Tags`.
2022-03-17 14:15:10 -04:00

219 lines
5.3 KiB
Go

package command
import (
"fmt"
"net"
"sort"
"strings"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/api"
"github.com/posener/complete"
"github.com/ryanuber/columnize"
)
type ServerMembersCommand struct {
Meta
}
func (c *ServerMembersCommand) Help() string {
helpText := `
Usage: nomad server members [options]
Display a list of the known servers and their status. Only Nomad servers are
able to service this command.
If ACLs are enabled, this option requires a token with the 'node:read'
capability.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
Server Members Options:
-verbose
Show detailed information about each member. This dumps a raw set of tags
which shows more information than the default output format.
`
return strings.TrimSpace(helpText)
}
func (c *ServerMembersCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-detailed": complete.PredictNothing,
})
}
func (c *ServerMembersCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (c *ServerMembersCommand) Synopsis() string {
return "Display a list of known servers and their status"
}
func (c *ServerMembersCommand) Name() string { return "server members" }
func (c *ServerMembersCommand) Run(args []string) int {
var detailed, verbose bool
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&detailed, "detailed", false, "Show detailed output")
flags.BoolVar(&verbose, "verbose", false, "Show detailed output")
if err := flags.Parse(args); err != nil {
return 1
}
// Check for extra arguments
args = flags.Args()
if len(args) != 0 {
c.Ui.Error("This command takes no arguments")
c.Ui.Error(commandErrorText(c))
return 1
}
// Keep support for previous flag name
if detailed {
verbose = true
}
// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
// Query the members
srvMembers, err := client.Agent().Members()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying servers: %s", err))
return 1
}
if srvMembers == nil {
c.Ui.Error("Agent doesn't know about server members")
return 0
}
// Sort the members
sort.Sort(api.AgentMembersNameSort(srvMembers.Members))
// Determine the leaders per region.
leaders, leaderErr := regionLeaders(client, srvMembers.Members)
// Format the list
var out []string
if verbose {
out = verboseOutput(srvMembers.Members, leaders)
} else {
out = standardOutput(srvMembers.Members, leaders)
}
// Dump the list
c.Ui.Output(columnize.SimpleFormat(out))
// If there were leader errors display a warning
if leaderErr != nil {
c.Ui.Output("")
c.Ui.Warn(fmt.Sprintf("Error determining leaders: %s", leaderErr))
return 1
}
return 0
}
func standardOutput(mem []*api.AgentMember, leaders map[string]string) []string {
// Format the members list
members := make([]string, len(mem)+1)
members[0] = "Name|Address|Port|Status|Leader|Raft Version|Build|Datacenter|Region"
for i, member := range mem {
members[i+1] = fmt.Sprintf("%s|%s|%d|%s|%t|%s|%s|%s|%s",
member.Name,
member.Addr,
member.Port,
member.Status,
isLeader(member, leaders),
member.Tags["raft_vsn"],
member.Tags["build"],
member.Tags["dc"],
member.Tags["region"])
}
return members
}
func verboseOutput(mem []*api.AgentMember, leaders map[string]string) []string {
// Format the members list
members := make([]string, len(mem)+1)
members[0] = "Name|Address|Port|Status|Leader|Protocol|Raft Version|Build|Datacenter|Region|Tags"
for i, member := range mem {
// Format the tags
tagPairs := make([]string, 0, len(member.Tags))
for k, v := range member.Tags {
tagPairs = append(tagPairs, fmt.Sprintf("%s=%s", k, v))
}
tags := strings.Join(tagPairs, ",")
members[i+1] = fmt.Sprintf("%s|%s|%d|%s|%t|%d|%s|%s|%s|%s|%s",
member.Name,
member.Addr,
member.Port,
member.Status,
isLeader(member, leaders),
member.ProtocolCur,
member.Tags["raft_vsn"],
member.Tags["build"],
member.Tags["dc"],
member.Tags["region"],
tags,
)
}
return members
}
// regionLeaders returns a map of regions to the IP of the member that is the
// leader.
func regionLeaders(client *api.Client, mem []*api.AgentMember) (map[string]string, error) {
// Determine the unique regions.
leaders := make(map[string]string)
regions := make(map[string]struct{})
for _, m := range mem {
// Ignore left members
// This prevents querying for leader status on regions where all members have left
if m.Status == "left" {
continue
}
regions[m.Tags["region"]] = struct{}{}
}
if len(regions) == 0 {
return leaders, nil
}
var mErr multierror.Error
status := client.Status()
for reg := range regions {
l, err := status.RegionLeader(reg)
if err != nil {
_ = multierror.Append(&mErr, fmt.Errorf("Region %q: %v", reg, err))
continue
}
leaders[reg] = l
}
return leaders, mErr.ErrorOrNil()
}
func isLeader(member *api.AgentMember, leaders map[string]string) bool {
addr := net.JoinHostPort(member.Addr, member.Tags["port"])
reg := member.Tags["region"]
regLeader, ok := leaders[reg]
return ok && regLeader == addr
}