// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 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. -json Output the latest information about each member in a JSON format. -t Format and display latest information about each member using a Go template. ` return strings.TrimSpace(helpText) } func (c *ServerMembersCommand) AutocompleteFlags() complete.Flags { return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), complete.Flags{ "-detailed": complete.PredictNothing, "-verbose": complete.PredictNothing, "-json": complete.PredictNothing, "-t": complete.PredictAnything, }) } 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, json bool var tmpl string 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") flags.BoolVar(&json, "json", false, "") flags.StringVar(&tmpl, "t", "", "") 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) if json || len(tmpl) > 0 { for _, member := range srvMembers.Members { member.Tags["Leader"] = fmt.Sprintf("%t", isLeader(member, leaders)) } out, err := Format(json, tmpl, srvMembers.Members) if err != nil { c.Ui.Error(err.Error()) return 1 } c.Ui.Output(out) return 0 } // 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 }