github.com/zhizhiboom/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/command/job_status.go (about)

     1  package command
     2  
     3  import (
     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/hashicorp/nomad/nomad/structs"
    12  	"github.com/posener/complete"
    13  )
    14  
    15  const (
    16  	// maxFailedTGs is the maximum number of task groups we show failure reasons
    17  	// for before deferring to eval-status
    18  	maxFailedTGs = 5
    19  )
    20  
    21  type JobStatusCommand struct {
    22  	Meta
    23  	length    int
    24  	evals     bool
    25  	allAllocs bool
    26  	verbose   bool
    27  }
    28  
    29  func (c *JobStatusCommand) Help() string {
    30  	helpText := `
    31  Usage: nomad status [options] <job>
    32  
    33    Display status information about a job. If no job ID is given, a list of all
    34    known jobs will be displayed.
    35  
    36  General Options:
    37  
    38    ` + generalOptionsUsage() + `
    39  
    40  Status Options:
    41  
    42    -short
    43      Display short output. Used only when a single job is being
    44      queried, and drops verbose information about allocations.
    45  
    46    -evals
    47      Display the evaluations associated with the job.
    48  
    49    -all-allocs
    50      Display all allocations matching the job ID, including those from an older
    51      instance of the job.
    52  
    53    -verbose
    54      Display full information.
    55  `
    56  	return strings.TrimSpace(helpText)
    57  }
    58  
    59  func (c *JobStatusCommand) Synopsis() string {
    60  	return "Display status information about a job"
    61  }
    62  
    63  func (c *JobStatusCommand) AutocompleteFlags() complete.Flags {
    64  	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
    65  		complete.Flags{
    66  			"-all-allocs": complete.PredictNothing,
    67  			"-evals":      complete.PredictNothing,
    68  			"-short":      complete.PredictNothing,
    69  			"-verbose":    complete.PredictNothing,
    70  		})
    71  }
    72  
    73  func (c *JobStatusCommand) AutocompleteArgs() complete.Predictor {
    74  	return complete.PredictFunc(func(a complete.Args) []string {
    75  		client, err := c.Meta.Client()
    76  		if err != nil {
    77  			return nil
    78  		}
    79  
    80  		resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Jobs, nil)
    81  		if err != nil {
    82  			return []string{}
    83  		}
    84  		return resp.Matches[contexts.Jobs]
    85  	})
    86  }
    87  
    88  func (c *JobStatusCommand) Name() string { return "status" }
    89  
    90  func (c *JobStatusCommand) Run(args []string) int {
    91  	var short bool
    92  
    93  	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
    94  	flags.Usage = func() { c.Ui.Output(c.Help()) }
    95  	flags.BoolVar(&short, "short", false, "")
    96  	flags.BoolVar(&c.evals, "evals", false, "")
    97  	flags.BoolVar(&c.allAllocs, "all-allocs", false, "")
    98  	flags.BoolVar(&c.verbose, "verbose", false, "")
    99  
   100  	if err := flags.Parse(args); err != nil {
   101  		return 1
   102  	}
   103  
   104  	// Check that we either got no jobs or exactly one.
   105  	args = flags.Args()
   106  	if len(args) > 1 {
   107  		c.Ui.Error("This command takes either no arguments or one: <job>")
   108  		c.Ui.Error(commandErrorText(c))
   109  		return 1
   110  	}
   111  
   112  	// Truncate the id unless full length is requested
   113  	c.length = shortId
   114  	if c.verbose {
   115  		c.length = fullId
   116  	}
   117  
   118  	// Get the HTTP client
   119  	client, err := c.Meta.Client()
   120  	if err != nil {
   121  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   122  		return 1
   123  	}
   124  
   125  	// Invoke list mode if no job ID.
   126  	if len(args) == 0 {
   127  		jobs, _, err := client.Jobs().List(nil)
   128  		if err != nil {
   129  			c.Ui.Error(fmt.Sprintf("Error querying jobs: %s", err))
   130  			return 1
   131  		}
   132  
   133  		if len(jobs) == 0 {
   134  			// No output if we have no jobs
   135  			c.Ui.Output("No running jobs")
   136  		} else {
   137  			c.Ui.Output(createStatusListOutput(jobs))
   138  		}
   139  		return 0
   140  	}
   141  
   142  	// Try querying the job
   143  	jobID := args[0]
   144  
   145  	jobs, _, err := client.Jobs().PrefixList(jobID)
   146  	if err != nil {
   147  		c.Ui.Error(fmt.Sprintf("Error querying job: %s", err))
   148  		return 1
   149  	}
   150  	if len(jobs) == 0 {
   151  		c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
   152  		return 1
   153  	}
   154  	if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
   155  		c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
   156  		return 1
   157  	}
   158  	// Prefix lookup matched a single job
   159  	job, _, err := client.Jobs().Info(jobs[0].ID, nil)
   160  	if err != nil {
   161  		c.Ui.Error(fmt.Sprintf("Error querying job: %s", err))
   162  		return 1
   163  	}
   164  
   165  	periodic := job.IsPeriodic()
   166  	parameterized := job.IsParameterized()
   167  
   168  	// Format the job info
   169  	basic := []string{
   170  		fmt.Sprintf("ID|%s", *job.ID),
   171  		fmt.Sprintf("Name|%s", *job.Name),
   172  		fmt.Sprintf("Submit Date|%s", formatTime(getSubmitTime(job))),
   173  		fmt.Sprintf("Type|%s", *job.Type),
   174  		fmt.Sprintf("Priority|%d", *job.Priority),
   175  		fmt.Sprintf("Datacenters|%s", strings.Join(job.Datacenters, ",")),
   176  		fmt.Sprintf("Status|%s", getStatusString(*job.Status, job.Stop)),
   177  		fmt.Sprintf("Periodic|%v", periodic),
   178  		fmt.Sprintf("Parameterized|%v", parameterized),
   179  	}
   180  
   181  	if periodic && !parameterized {
   182  		if *job.Stop {
   183  			basic = append(basic, fmt.Sprintf("Next Periodic Launch|none (job stopped)"))
   184  		} else {
   185  			location, err := job.Periodic.GetLocation()
   186  			if err == nil {
   187  				now := time.Now().In(location)
   188  				next, err := job.Periodic.Next(now)
   189  				if err == nil {
   190  					basic = append(basic, fmt.Sprintf("Next Periodic Launch|%s",
   191  						fmt.Sprintf("%s (%s from now)",
   192  							formatTime(next), formatTimeDifference(now, next, time.Second))))
   193  				}
   194  			}
   195  		}
   196  	}
   197  
   198  	c.Ui.Output(formatKV(basic))
   199  
   200  	// Exit early
   201  	if short {
   202  		return 0
   203  	}
   204  
   205  	// Print periodic job information
   206  	if periodic && !parameterized {
   207  		if err := c.outputPeriodicInfo(client, job); err != nil {
   208  			c.Ui.Error(err.Error())
   209  			return 1
   210  		}
   211  	} else if parameterized {
   212  		if err := c.outputParameterizedInfo(client, job); err != nil {
   213  			c.Ui.Error(err.Error())
   214  			return 1
   215  		}
   216  	} else {
   217  		if err := c.outputJobInfo(client, job); err != nil {
   218  			c.Ui.Error(err.Error())
   219  			return 1
   220  		}
   221  	}
   222  
   223  	return 0
   224  }
   225  
   226  // outputPeriodicInfo prints information about the passed periodic job. If a
   227  // request fails, an error is returned.
   228  func (c *JobStatusCommand) outputPeriodicInfo(client *api.Client, job *api.Job) error {
   229  	// Output the summary
   230  	if err := c.outputJobSummary(client, job); err != nil {
   231  		return err
   232  	}
   233  
   234  	// Generate the prefix that matches launched jobs from the periodic job.
   235  	prefix := fmt.Sprintf("%s%s", *job.ID, structs.PeriodicLaunchSuffix)
   236  	children, _, err := client.Jobs().PrefixList(prefix)
   237  	if err != nil {
   238  		return fmt.Errorf("Error querying job: %s", err)
   239  	}
   240  
   241  	if len(children) == 0 {
   242  		c.Ui.Output("\nNo instances of periodic job found")
   243  		return nil
   244  	}
   245  
   246  	out := make([]string, 1)
   247  	out[0] = "ID|Status"
   248  	for _, child := range children {
   249  		// Ensure that we are only showing jobs whose parent is the requested
   250  		// job.
   251  		if child.ParentID != *job.ID {
   252  			continue
   253  		}
   254  
   255  		out = append(out, fmt.Sprintf("%s|%s",
   256  			child.ID,
   257  			child.Status))
   258  	}
   259  
   260  	c.Ui.Output(c.Colorize().Color("\n[bold]Previously Launched Jobs[reset]"))
   261  	c.Ui.Output(formatList(out))
   262  	return nil
   263  }
   264  
   265  // outputParameterizedInfo prints information about a parameterized job. If a
   266  // request fails, an error is returned.
   267  func (c *JobStatusCommand) outputParameterizedInfo(client *api.Client, job *api.Job) error {
   268  	// Output parameterized job details
   269  	c.Ui.Output(c.Colorize().Color("\n[bold]Parameterized Job[reset]"))
   270  	parameterizedJob := make([]string, 3)
   271  	parameterizedJob[0] = fmt.Sprintf("Payload|%s", job.ParameterizedJob.Payload)
   272  	parameterizedJob[1] = fmt.Sprintf("Required Metadata|%v", strings.Join(job.ParameterizedJob.MetaRequired, ", "))
   273  	parameterizedJob[2] = fmt.Sprintf("Optional Metadata|%v", strings.Join(job.ParameterizedJob.MetaOptional, ", "))
   274  	c.Ui.Output(formatKV(parameterizedJob))
   275  
   276  	// Output the summary
   277  	if err := c.outputJobSummary(client, job); err != nil {
   278  		return err
   279  	}
   280  
   281  	// Generate the prefix that matches launched jobs from the parameterized job.
   282  	prefix := fmt.Sprintf("%s%s", *job.ID, structs.DispatchLaunchSuffix)
   283  	children, _, err := client.Jobs().PrefixList(prefix)
   284  	if err != nil {
   285  		return fmt.Errorf("Error querying job: %s", err)
   286  	}
   287  
   288  	if len(children) == 0 {
   289  		c.Ui.Output("\nNo dispatched instances of parameterized job found")
   290  		return nil
   291  	}
   292  
   293  	out := make([]string, 1)
   294  	out[0] = "ID|Status"
   295  	for _, child := range children {
   296  		// Ensure that we are only showing jobs whose parent is the requested
   297  		// job.
   298  		if child.ParentID != *job.ID {
   299  			continue
   300  		}
   301  
   302  		out = append(out, fmt.Sprintf("%s|%s",
   303  			child.ID,
   304  			child.Status))
   305  	}
   306  
   307  	c.Ui.Output(c.Colorize().Color("\n[bold]Dispatched Jobs[reset]"))
   308  	c.Ui.Output(formatList(out))
   309  	return nil
   310  }
   311  
   312  // outputJobInfo prints information about the passed non-periodic job. If a
   313  // request fails, an error is returned.
   314  func (c *JobStatusCommand) outputJobInfo(client *api.Client, job *api.Job) error {
   315  
   316  	// Query the allocations
   317  	jobAllocs, _, err := client.Jobs().Allocations(*job.ID, c.allAllocs, nil)
   318  	if err != nil {
   319  		return fmt.Errorf("Error querying job allocations: %s", err)
   320  	}
   321  
   322  	// Query the evaluations
   323  	jobEvals, _, err := client.Jobs().Evaluations(*job.ID, nil)
   324  	if err != nil {
   325  		return fmt.Errorf("Error querying job evaluations: %s", err)
   326  	}
   327  
   328  	latestDeployment, _, err := client.Jobs().LatestDeployment(*job.ID, nil)
   329  	if err != nil {
   330  		return fmt.Errorf("Error querying latest job deployment: %s", err)
   331  	}
   332  
   333  	// Output the summary
   334  	if err := c.outputJobSummary(client, job); err != nil {
   335  		return err
   336  	}
   337  
   338  	// Determine latest evaluation with failures whose follow up hasn't
   339  	// completed, this is done while formatting
   340  	var latestFailedPlacement *api.Evaluation
   341  	blockedEval := false
   342  
   343  	// Format the evals
   344  	evals := make([]string, len(jobEvals)+1)
   345  	evals[0] = "ID|Priority|Triggered By|Status|Placement Failures"
   346  	for i, eval := range jobEvals {
   347  		failures, _ := evalFailureStatus(eval)
   348  		evals[i+1] = fmt.Sprintf("%s|%d|%s|%s|%s",
   349  			limit(eval.ID, c.length),
   350  			eval.Priority,
   351  			eval.TriggeredBy,
   352  			eval.Status,
   353  			failures,
   354  		)
   355  
   356  		if eval.Status == "blocked" {
   357  			blockedEval = true
   358  		}
   359  
   360  		if len(eval.FailedTGAllocs) == 0 {
   361  			// Skip evals without failures
   362  			continue
   363  		}
   364  
   365  		if latestFailedPlacement == nil || latestFailedPlacement.CreateIndex < eval.CreateIndex {
   366  			latestFailedPlacement = eval
   367  		}
   368  	}
   369  
   370  	if c.verbose || c.evals {
   371  		c.Ui.Output(c.Colorize().Color("\n[bold]Evaluations[reset]"))
   372  		c.Ui.Output(formatList(evals))
   373  	}
   374  
   375  	if blockedEval && latestFailedPlacement != nil {
   376  		c.outputFailedPlacements(latestFailedPlacement)
   377  	}
   378  
   379  	c.outputReschedulingEvals(client, job, jobAllocs, c.length)
   380  
   381  	if latestDeployment != nil {
   382  		c.Ui.Output(c.Colorize().Color("\n[bold]Latest Deployment[reset]"))
   383  		c.Ui.Output(c.Colorize().Color(c.formatDeployment(latestDeployment)))
   384  	}
   385  
   386  	// Format the allocs
   387  	c.Ui.Output(c.Colorize().Color("\n[bold]Allocations[reset]"))
   388  	c.Ui.Output(formatAllocListStubs(jobAllocs, c.verbose, c.length))
   389  	return nil
   390  }
   391  
   392  func (c *JobStatusCommand) formatDeployment(d *api.Deployment) string {
   393  	// Format the high-level elements
   394  	high := []string{
   395  		fmt.Sprintf("ID|%s", limit(d.ID, c.length)),
   396  		fmt.Sprintf("Status|%s", d.Status),
   397  		fmt.Sprintf("Description|%s", d.StatusDescription),
   398  	}
   399  
   400  	base := formatKV(high)
   401  	if len(d.TaskGroups) == 0 {
   402  		return base
   403  	}
   404  	base += "\n\n[bold]Deployed[reset]\n"
   405  	base += formatDeploymentGroups(d, c.length)
   406  	return base
   407  }
   408  
   409  func formatAllocListStubs(stubs []*api.AllocationListStub, verbose bool, uuidLength int) string {
   410  	if len(stubs) == 0 {
   411  		return "No allocations placed"
   412  	}
   413  
   414  	allocs := make([]string, len(stubs)+1)
   415  	if verbose {
   416  		allocs[0] = "ID|Eval ID|Node ID|Task Group|Version|Desired|Status|Created|Modified"
   417  		for i, alloc := range stubs {
   418  			allocs[i+1] = fmt.Sprintf("%s|%s|%s|%s|%d|%s|%s|%s|%s",
   419  				limit(alloc.ID, uuidLength),
   420  				limit(alloc.EvalID, uuidLength),
   421  				limit(alloc.NodeID, uuidLength),
   422  				alloc.TaskGroup,
   423  				alloc.JobVersion,
   424  				alloc.DesiredStatus,
   425  				alloc.ClientStatus,
   426  				formatUnixNanoTime(alloc.CreateTime),
   427  				formatUnixNanoTime(alloc.ModifyTime))
   428  		}
   429  	} else {
   430  		allocs[0] = "ID|Node ID|Task Group|Version|Desired|Status|Created|Modified"
   431  		for i, alloc := range stubs {
   432  			now := time.Now()
   433  			createTimePretty := prettyTimeDiff(time.Unix(0, alloc.CreateTime), now)
   434  			modTimePretty := prettyTimeDiff(time.Unix(0, alloc.ModifyTime), now)
   435  			allocs[i+1] = fmt.Sprintf("%s|%s|%s|%d|%s|%s|%s|%s",
   436  				limit(alloc.ID, uuidLength),
   437  				limit(alloc.NodeID, uuidLength),
   438  				alloc.TaskGroup,
   439  				alloc.JobVersion,
   440  				alloc.DesiredStatus,
   441  				alloc.ClientStatus,
   442  				createTimePretty,
   443  				modTimePretty)
   444  		}
   445  	}
   446  
   447  	return formatList(allocs)
   448  }
   449  
   450  func formatAllocList(allocations []*api.Allocation, verbose bool, uuidLength int) string {
   451  	if len(allocations) == 0 {
   452  		return "No allocations placed"
   453  	}
   454  
   455  	allocs := make([]string, len(allocations)+1)
   456  	if verbose {
   457  		allocs[0] = "ID|Eval ID|Node ID|Task Group|Version|Desired|Status|Created|Modified"
   458  		for i, alloc := range allocations {
   459  			allocs[i+1] = fmt.Sprintf("%s|%s|%s|%s|%d|%s|%s|%s|%s",
   460  				limit(alloc.ID, uuidLength),
   461  				limit(alloc.EvalID, uuidLength),
   462  				limit(alloc.NodeID, uuidLength),
   463  				alloc.TaskGroup,
   464  				getVersion(alloc.Job),
   465  				alloc.DesiredStatus,
   466  				alloc.ClientStatus,
   467  				formatUnixNanoTime(alloc.CreateTime),
   468  				formatUnixNanoTime(alloc.ModifyTime))
   469  		}
   470  	} else {
   471  		allocs[0] = "ID|Node ID|Task Group|Version|Desired|Status|Created|Modified"
   472  		for i, alloc := range allocations {
   473  			now := time.Now()
   474  			createTimePretty := prettyTimeDiff(time.Unix(0, alloc.CreateTime), now)
   475  			modTimePretty := prettyTimeDiff(time.Unix(0, alloc.ModifyTime), now)
   476  			allocs[i+1] = fmt.Sprintf("%s|%s|%s|%d|%s|%s|%s|%s",
   477  				limit(alloc.ID, uuidLength),
   478  				limit(alloc.NodeID, uuidLength),
   479  				alloc.TaskGroup,
   480  				getVersion(alloc.Job),
   481  				alloc.DesiredStatus,
   482  				alloc.ClientStatus,
   483  				createTimePretty,
   484  				modTimePretty)
   485  		}
   486  	}
   487  
   488  	return formatList(allocs)
   489  }
   490  
   491  // outputJobSummary displays the given jobs summary and children job summary
   492  // where appropriate
   493  func (c *JobStatusCommand) outputJobSummary(client *api.Client, job *api.Job) error {
   494  	// Query the summary
   495  	summary, _, err := client.Jobs().Summary(*job.ID, nil)
   496  	if err != nil {
   497  		return fmt.Errorf("Error querying job summary: %s", err)
   498  	}
   499  
   500  	if summary == nil {
   501  		return nil
   502  	}
   503  
   504  	periodic := job.IsPeriodic()
   505  	parameterizedJob := job.IsParameterized()
   506  
   507  	// Print the summary
   508  	if !periodic && !parameterizedJob {
   509  		c.Ui.Output(c.Colorize().Color("\n[bold]Summary[reset]"))
   510  		summaries := make([]string, len(summary.Summary)+1)
   511  		summaries[0] = "Task Group|Queued|Starting|Running|Failed|Complete|Lost"
   512  		taskGroups := make([]string, 0, len(summary.Summary))
   513  		for taskGroup := range summary.Summary {
   514  			taskGroups = append(taskGroups, taskGroup)
   515  		}
   516  		sort.Strings(taskGroups)
   517  		for idx, taskGroup := range taskGroups {
   518  			tgs := summary.Summary[taskGroup]
   519  			summaries[idx+1] = fmt.Sprintf("%s|%d|%d|%d|%d|%d|%d",
   520  				taskGroup, tgs.Queued, tgs.Starting,
   521  				tgs.Running, tgs.Failed,
   522  				tgs.Complete, tgs.Lost,
   523  			)
   524  		}
   525  		c.Ui.Output(formatList(summaries))
   526  	}
   527  
   528  	// Always display the summary if we are periodic or parameterized, but
   529  	// only display if the summary is non-zero on normal jobs
   530  	if summary.Children != nil && (parameterizedJob || periodic || summary.Children.Sum() > 0) {
   531  		if parameterizedJob {
   532  			c.Ui.Output(c.Colorize().Color("\n[bold]Parameterized Job Summary[reset]"))
   533  		} else {
   534  			c.Ui.Output(c.Colorize().Color("\n[bold]Children Job Summary[reset]"))
   535  		}
   536  		summaries := make([]string, 2)
   537  		summaries[0] = "Pending|Running|Dead"
   538  		summaries[1] = fmt.Sprintf("%d|%d|%d",
   539  			summary.Children.Pending, summary.Children.Running, summary.Children.Dead)
   540  		c.Ui.Output(formatList(summaries))
   541  	}
   542  
   543  	return nil
   544  }
   545  
   546  // outputReschedulingEvals displays eval IDs and time for any
   547  // delayed evaluations by task group
   548  func (c *JobStatusCommand) outputReschedulingEvals(client *api.Client, job *api.Job, allocListStubs []*api.AllocationListStub, uuidLength int) error {
   549  	// Get the most recent alloc ID by task group
   550  
   551  	mostRecentAllocs := make(map[string]*api.AllocationListStub)
   552  	for _, alloc := range allocListStubs {
   553  		a, ok := mostRecentAllocs[alloc.TaskGroup]
   554  		if !ok || alloc.ModifyTime > a.ModifyTime {
   555  			mostRecentAllocs[alloc.TaskGroup] = alloc
   556  		}
   557  	}
   558  
   559  	followUpEvalIds := make(map[string]string)
   560  	for tg, alloc := range mostRecentAllocs {
   561  		if alloc.FollowupEvalID != "" {
   562  			followUpEvalIds[tg] = alloc.FollowupEvalID
   563  		}
   564  	}
   565  
   566  	if len(followUpEvalIds) == 0 {
   567  		return nil
   568  	}
   569  	// Print the reschedule info section
   570  	var delayedEvalInfos []string
   571  
   572  	taskGroups := make([]string, 0, len(followUpEvalIds))
   573  	for taskGroup := range followUpEvalIds {
   574  		taskGroups = append(taskGroups, taskGroup)
   575  	}
   576  	sort.Strings(taskGroups)
   577  	var evalDetails []string
   578  	for _, taskGroup := range taskGroups {
   579  		evalID := followUpEvalIds[taskGroup]
   580  		evaluation, _, err := client.Evaluations().Info(evalID, nil)
   581  		// Eval time is not critical output,
   582  		// so don't return it on errors, if its not set, or its already in the past
   583  		if err != nil || evaluation.WaitUntil.IsZero() || time.Now().After(evaluation.WaitUntil) {
   584  			continue
   585  		}
   586  		evalTime := prettyTimeDiff(evaluation.WaitUntil, time.Now())
   587  		if c.verbose {
   588  			delayedEvalInfos = append(delayedEvalInfos, "Task Group|Reschedule Policy|Eval ID|Eval Time")
   589  			rp := job.LookupTaskGroup(taskGroup).ReschedulePolicy
   590  			evalDetails = append(evalDetails, fmt.Sprintf("%s|%s|%s|%s", taskGroup, rp.String(), limit(evalID, uuidLength), evalTime))
   591  		} else {
   592  			delayedEvalInfos = append(delayedEvalInfos, "Task Group|Eval ID|Eval Time")
   593  			evalDetails = append(evalDetails, fmt.Sprintf("%s|%s|%s", taskGroup, limit(evalID, uuidLength), evalTime))
   594  		}
   595  	}
   596  	if len(evalDetails) == 0 {
   597  		return nil
   598  	}
   599  	// Only show this section if there is pending evals
   600  	delayedEvalInfos = append(delayedEvalInfos, evalDetails...)
   601  	c.Ui.Output(c.Colorize().Color("\n[bold]Future Rescheduling Attempts[reset]"))
   602  	c.Ui.Output(formatList(delayedEvalInfos))
   603  	return nil
   604  }
   605  
   606  func (c *JobStatusCommand) outputFailedPlacements(failedEval *api.Evaluation) {
   607  	if failedEval == nil || len(failedEval.FailedTGAllocs) == 0 {
   608  		return
   609  	}
   610  
   611  	c.Ui.Output(c.Colorize().Color("\n[bold]Placement Failure[reset]"))
   612  
   613  	sorted := sortedTaskGroupFromMetrics(failedEval.FailedTGAllocs)
   614  	for i, tg := range sorted {
   615  		if i >= maxFailedTGs {
   616  			break
   617  		}
   618  
   619  		c.Ui.Output(fmt.Sprintf("Task Group %q:", tg))
   620  		metrics := failedEval.FailedTGAllocs[tg]
   621  		c.Ui.Output(formatAllocMetrics(metrics, false, "  "))
   622  		if i != len(sorted)-1 {
   623  			c.Ui.Output("")
   624  		}
   625  	}
   626  
   627  	if len(sorted) > maxFailedTGs {
   628  		trunc := fmt.Sprintf("\nPlacement failures truncated. To see remainder run:\nnomad eval-status %s", failedEval.ID)
   629  		c.Ui.Output(trunc)
   630  	}
   631  }
   632  
   633  // list general information about a list of jobs
   634  func createStatusListOutput(jobs []*api.JobListStub) string {
   635  	out := make([]string, len(jobs)+1)
   636  	out[0] = "ID|Type|Priority|Status|Submit Date"
   637  	for i, job := range jobs {
   638  		out[i+1] = fmt.Sprintf("%s|%s|%d|%s|%s",
   639  			job.ID,
   640  			getTypeString(job),
   641  			job.Priority,
   642  			getStatusString(job.Status, &job.Stop),
   643  			formatTime(time.Unix(0, job.SubmitTime)))
   644  	}
   645  	return formatList(out)
   646  }
   647  
   648  func getTypeString(job *api.JobListStub) string {
   649  	t := job.Type
   650  
   651  	if job.Periodic {
   652  		t += "/periodic"
   653  	}
   654  
   655  	if job.ParameterizedJob {
   656  		t += "/parameterized"
   657  	}
   658  
   659  	return t
   660  }
   661  
   662  func getStatusString(status string, stop *bool) string {
   663  	if stop != nil && *stop {
   664  		return fmt.Sprintf("%s (stopped)", status)
   665  	}
   666  	return status
   667  }