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