1package command
2
3import (
4	"fmt"
5	"net"
6	"sort"
7	"strings"
8
9	multierror "github.com/hashicorp/go-multierror"
10	"github.com/hashicorp/nomad/api"
11	"github.com/posener/complete"
12	"github.com/ryanuber/columnize"
13)
14
15type ServerMembersCommand struct {
16	Meta
17}
18
19func (c *ServerMembersCommand) Help() string {
20	helpText := `
21Usage: nomad server members [options]
22
23  Display a list of the known servers and their status. Only Nomad servers are
24  able to service this command.
25
26General Options:
27
28  ` + generalOptionsUsage() + `
29
30Server Members Options:
31
32  -detailed
33    Show detailed information about each member. This dumps
34    a raw set of tags which shows more information than the
35    default output format.
36`
37	return strings.TrimSpace(helpText)
38}
39
40func (c *ServerMembersCommand) AutocompleteFlags() complete.Flags {
41	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
42		complete.Flags{
43			"-detailed": complete.PredictNothing,
44		})
45}
46
47func (c *ServerMembersCommand) AutocompleteArgs() complete.Predictor {
48	return complete.PredictNothing
49}
50
51func (c *ServerMembersCommand) Synopsis() string {
52	return "Display a list of known servers and their status"
53}
54
55func (c *ServerMembersCommand) Name() string { return "server members" }
56
57func (c *ServerMembersCommand) Run(args []string) int {
58	var detailed bool
59
60	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
61	flags.Usage = func() { c.Ui.Output(c.Help()) }
62	flags.BoolVar(&detailed, "detailed", false, "Show detailed output")
63
64	if err := flags.Parse(args); err != nil {
65		return 1
66	}
67
68	// Check for extra arguments
69	args = flags.Args()
70	if len(args) != 0 {
71		c.Ui.Error("This command takes no arguments")
72		c.Ui.Error(commandErrorText(c))
73		return 1
74	}
75
76	// Get the HTTP client
77	client, err := c.Meta.Client()
78	if err != nil {
79		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
80		return 1
81	}
82
83	// Query the members
84	srvMembers, err := client.Agent().Members()
85	if err != nil {
86		c.Ui.Error(fmt.Sprintf("Error querying servers: %s", err))
87		return 1
88	}
89
90	if srvMembers == nil {
91		c.Ui.Error("Agent doesn't know about server members")
92		return 0
93	}
94
95	// Sort the members
96	sort.Sort(api.AgentMembersNameSort(srvMembers.Members))
97
98	// Determine the leaders per region.
99	leaders, leaderErr := regionLeaders(client, srvMembers.Members)
100
101	// Format the list
102	var out []string
103	if detailed {
104		out = detailedOutput(srvMembers.Members)
105	} else {
106		out = standardOutput(srvMembers.Members, leaders)
107	}
108
109	// Dump the list
110	c.Ui.Output(columnize.SimpleFormat(out))
111
112	// If there were leader errors display a warning
113	if leaderErr != nil {
114		c.Ui.Output("")
115		c.Ui.Warn(fmt.Sprintf("Error determining leaders: %s", leaderErr))
116		return 1
117	}
118
119	return 0
120}
121
122func standardOutput(mem []*api.AgentMember, leaders map[string]string) []string {
123	// Format the members list
124	members := make([]string, len(mem)+1)
125	members[0] = "Name|Address|Port|Status|Leader|Protocol|Build|Datacenter|Region"
126	for i, member := range mem {
127		reg := member.Tags["region"]
128		regLeader, ok := leaders[reg]
129		isLeader := false
130		if ok {
131			if regLeader == net.JoinHostPort(member.Addr, member.Tags["port"]) {
132
133				isLeader = true
134			}
135		}
136
137		members[i+1] = fmt.Sprintf("%s|%s|%d|%s|%t|%d|%s|%s|%s",
138			member.Name,
139			member.Addr,
140			member.Port,
141			member.Status,
142			isLeader,
143			member.ProtocolCur,
144			member.Tags["build"],
145			member.Tags["dc"],
146			member.Tags["region"])
147	}
148	return members
149}
150
151func detailedOutput(mem []*api.AgentMember) []string {
152	// Format the members list
153	members := make([]string, len(mem)+1)
154	members[0] = "Name|Address|Port|Tags"
155	for i, member := range mem {
156		// Format the tags
157		tagPairs := make([]string, 0, len(member.Tags))
158		for k, v := range member.Tags {
159			tagPairs = append(tagPairs, fmt.Sprintf("%s=%s", k, v))
160		}
161		tags := strings.Join(tagPairs, ",")
162
163		members[i+1] = fmt.Sprintf("%s|%s|%d|%s",
164			member.Name,
165			member.Addr,
166			member.Port,
167			tags)
168	}
169	return members
170}
171
172// regionLeaders returns a map of regions to the IP of the member that is the
173// leader.
174func regionLeaders(client *api.Client, mem []*api.AgentMember) (map[string]string, error) {
175	// Determine the unique regions.
176	leaders := make(map[string]string)
177	regions := make(map[string]struct{})
178	for _, m := range mem {
179		// Ignore left members
180		// This prevents querying for leader status on regions where all members have left
181		if m.Status == "left" {
182			continue
183		}
184
185		regions[m.Tags["region"]] = struct{}{}
186	}
187
188	if len(regions) == 0 {
189		return leaders, nil
190	}
191
192	var mErr multierror.Error
193	status := client.Status()
194	for reg := range regions {
195		l, err := status.RegionLeader(reg)
196		if err != nil {
197			multierror.Append(&mErr, fmt.Errorf("Region %q: %v", reg, err))
198			continue
199		}
200
201		leaders[reg] = l
202	}
203
204	return leaders, mErr.ErrorOrNil()
205}
206