1package command
2
3import (
4	"fmt"
5	"sort"
6	"strings"
7	"time"
8
9	"github.com/hashicorp/nomad/api"
10	"github.com/hashicorp/nomad/api/contexts"
11	"github.com/posener/complete"
12)
13
14type EvalStatusCommand struct {
15	Meta
16}
17
18func (c *EvalStatusCommand) Help() string {
19	helpText := `
20Usage: nomad eval status [options] <evaluation>
21
22  Display information about evaluations. This command can be used to inspect the
23  current status of an evaluation as well as determine the reason an evaluation
24  did not place all allocations.
25
26General Options:
27
28  ` + generalOptionsUsage() + `
29
30Eval Status Options:
31
32  -monitor
33    Monitor an outstanding evaluation
34
35  -verbose
36    Show full information.
37
38  -json
39    Output the evaluation in its JSON format.
40
41  -t
42    Format and display evaluation using a Go template.
43`
44
45	return strings.TrimSpace(helpText)
46}
47
48func (c *EvalStatusCommand) Synopsis() string {
49	return "Display evaluation status and placement failure reasons"
50}
51
52func (c *EvalStatusCommand) AutocompleteFlags() complete.Flags {
53	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
54		complete.Flags{
55			"-json":    complete.PredictNothing,
56			"-monitor": complete.PredictNothing,
57			"-t":       complete.PredictAnything,
58			"-verbose": complete.PredictNothing,
59		})
60}
61
62func (c *EvalStatusCommand) AutocompleteArgs() complete.Predictor {
63	return complete.PredictFunc(func(a complete.Args) []string {
64		client, err := c.Meta.Client()
65		if err != nil {
66			return nil
67		}
68
69		if err != nil {
70			return nil
71		}
72
73		resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Evals, nil)
74		if err != nil {
75			return []string{}
76		}
77		return resp.Matches[contexts.Evals]
78	})
79}
80
81func (c *EvalStatusCommand) Name() string { return "eval status" }
82
83func (c *EvalStatusCommand) Run(args []string) int {
84	var monitor, verbose, json bool
85	var tmpl string
86
87	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
88	flags.Usage = func() { c.Ui.Output(c.Help()) }
89	flags.BoolVar(&monitor, "monitor", false, "")
90	flags.BoolVar(&verbose, "verbose", false, "")
91	flags.BoolVar(&json, "json", false, "")
92	flags.StringVar(&tmpl, "t", "", "")
93
94	if err := flags.Parse(args); err != nil {
95		return 1
96	}
97
98	// Check that we got exactly one evaluation ID
99	args = flags.Args()
100
101	// Get the HTTP client
102	client, err := c.Meta.Client()
103	if err != nil {
104		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
105		return 1
106	}
107
108	// If args not specified but output format is specified, format and output the evaluations data list
109	if len(args) == 0 && json || len(tmpl) > 0 {
110		evals, _, err := client.Evaluations().List(nil)
111		if err != nil {
112			c.Ui.Error(fmt.Sprintf("Error querying evaluations: %v", err))
113			return 1
114		}
115
116		out, err := Format(json, tmpl, evals)
117		if err != nil {
118			c.Ui.Error(err.Error())
119			return 1
120		}
121
122		c.Ui.Output(out)
123		return 0
124	}
125
126	if len(args) != 1 {
127		c.Ui.Error("This command takes one argument")
128		c.Ui.Error(commandErrorText(c))
129		return 1
130	}
131
132	evalID := args[0]
133
134	// Truncate the id unless full length is requested
135	length := shortId
136	if verbose {
137		length = fullId
138	}
139
140	// Query the allocation info
141	if len(evalID) == 1 {
142		c.Ui.Error(fmt.Sprintf("Identifier must contain at least two characters."))
143		return 1
144	}
145
146	evalID = sanitizeUUIDPrefix(evalID)
147	evals, _, err := client.Evaluations().PrefixList(evalID)
148	if err != nil {
149		c.Ui.Error(fmt.Sprintf("Error querying evaluation: %v", err))
150		return 1
151	}
152	if len(evals) == 0 {
153		c.Ui.Error(fmt.Sprintf("No evaluation(s) with prefix or id %q found", evalID))
154		return 1
155	}
156
157	if len(evals) > 1 {
158		// Format the evals
159		out := make([]string, len(evals)+1)
160		out[0] = "ID|Priority|Triggered By|Status|Placement Failures"
161		for i, eval := range evals {
162			failures, _ := evalFailureStatus(eval)
163			out[i+1] = fmt.Sprintf("%s|%d|%s|%s|%s",
164				limit(eval.ID, length),
165				eval.Priority,
166				eval.TriggeredBy,
167				eval.Status,
168				failures,
169			)
170		}
171		c.Ui.Error(fmt.Sprintf("Prefix matched multiple evaluations\n\n%s", formatList(out)))
172		return 1
173	}
174
175	// If we are in monitor mode, monitor and exit
176	if monitor {
177		mon := newMonitor(c.Ui, client, length)
178		return mon.monitor(evals[0].ID, true)
179	}
180
181	// Prefix lookup matched a single evaluation
182	eval, _, err := client.Evaluations().Info(evals[0].ID, nil)
183	if err != nil {
184		c.Ui.Error(fmt.Sprintf("Error querying evaluation: %s", err))
185		return 1
186	}
187
188	// If output format is specified, format and output the data
189	if json || len(tmpl) > 0 {
190		out, err := Format(json, tmpl, eval)
191		if err != nil {
192			c.Ui.Error(err.Error())
193			return 1
194		}
195
196		c.Ui.Output(out)
197		return 0
198	}
199
200	failureString, failures := evalFailureStatus(eval)
201	triggerNoun, triggerSubj := getTriggerDetails(eval)
202	statusDesc := eval.StatusDescription
203	if statusDesc == "" {
204		statusDesc = eval.Status
205	}
206
207	// Format eval timestamps
208	var formattedCreateTime, formattedModifyTime string
209	if verbose {
210		formattedCreateTime = formatUnixNanoTime(eval.CreateTime)
211		formattedModifyTime = formatUnixNanoTime(eval.ModifyTime)
212	} else {
213		formattedCreateTime = prettyTimeDiff(time.Unix(0, eval.CreateTime), time.Now())
214		formattedModifyTime = prettyTimeDiff(time.Unix(0, eval.ModifyTime), time.Now())
215	}
216
217	// Format the evaluation data
218	basic := []string{
219		fmt.Sprintf("ID|%s", limit(eval.ID, length)),
220		fmt.Sprintf("Create Time|%s", formattedCreateTime),
221		fmt.Sprintf("Modify Time|%s", formattedModifyTime),
222		fmt.Sprintf("Status|%s", eval.Status),
223		fmt.Sprintf("Status Description|%s", statusDesc),
224		fmt.Sprintf("Type|%s", eval.Type),
225		fmt.Sprintf("TriggeredBy|%s", eval.TriggeredBy),
226	}
227
228	if triggerNoun != "" && triggerSubj != "" {
229		basic = append(basic, fmt.Sprintf("%s|%s", triggerNoun, triggerSubj))
230	}
231
232	basic = append(basic,
233		fmt.Sprintf("Priority|%d", eval.Priority),
234		fmt.Sprintf("Placement Failures|%s", failureString))
235
236	if !eval.WaitUntil.IsZero() {
237		basic = append(basic,
238			fmt.Sprintf("Wait Until|%s", formatTime(eval.WaitUntil)))
239	}
240
241	if verbose {
242		// NextEval, PreviousEval, BlockedEval
243		basic = append(basic,
244			fmt.Sprintf("Previous Eval|%s", eval.PreviousEval),
245			fmt.Sprintf("Next Eval|%s", eval.NextEval),
246			fmt.Sprintf("Blocked Eval|%s", eval.BlockedEval))
247	}
248	c.Ui.Output(formatKV(basic))
249
250	if failures {
251		c.Ui.Output(c.Colorize().Color("\n[bold]Failed Placements[reset]"))
252		sorted := sortedTaskGroupFromMetrics(eval.FailedTGAllocs)
253		for _, tg := range sorted {
254			metrics := eval.FailedTGAllocs[tg]
255
256			noun := "allocation"
257			if metrics.CoalescedFailures > 0 {
258				noun += "s"
259			}
260			c.Ui.Output(fmt.Sprintf("Task Group %q (failed to place %d %s):", tg, metrics.CoalescedFailures+1, noun))
261			c.Ui.Output(formatAllocMetrics(metrics, false, "  "))
262			c.Ui.Output("")
263		}
264
265		if eval.BlockedEval != "" {
266			c.Ui.Output(fmt.Sprintf("Evaluation %q waiting for additional capacity to place remainder",
267				limit(eval.BlockedEval, length)))
268		}
269	}
270
271	return 0
272}
273
274func sortedTaskGroupFromMetrics(groups map[string]*api.AllocationMetric) []string {
275	tgs := make([]string, 0, len(groups))
276	for tg := range groups {
277		tgs = append(tgs, tg)
278	}
279	sort.Strings(tgs)
280	return tgs
281}
282
283func getTriggerDetails(eval *api.Evaluation) (noun, subject string) {
284	switch eval.TriggeredBy {
285	case "job-register", "job-deregister", "periodic-job", "rolling-update", "deployment-watcher":
286		return "Job ID", eval.JobID
287	case "node-update":
288		return "Node ID", eval.NodeID
289	case "max-plan-attempts":
290		return "Previous Eval", eval.PreviousEval
291	default:
292		return "", ""
293	}
294}
295