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