forked from lei.chen/myxce
apiserver now returns pending_reason as an object ({main_reason,
main_reason_name, host_reasons, res_reasons}) since the PEND-reason work.
xcecli's QueryJob.PendingReason was a plain string, so every job list
failed with "cannot unmarshal object into string". Two fixes:
- xcecli/models/jobs.go: PendingReason is now a string-backed type with a
custom UnmarshalJSON that accepts null / empty string / plain string
(older API) / the object form (current API, rendered as readable text —
LSF bjobs -p style: "match_fail; host t1: match_fail; ..."). Unknown
objects fall back to the raw bytes instead of erroring, so a job list
can never fail on an unexpected pending_reason shape.
- apiserver/models/jobs.go: JobAdditionalFields gains PendingReason so the
fields= query (xcecli `jobs list -l`) no longer rejects pending_reason
with "unsupported query field".
- xcecli/common/output/page_print_jobs.go: adapt the verbose printer to the
new type.
Tests: xcecli/models/jobs_test.go — 6 cases (null / empty / plain string /
object / within QueryJob / unknown object). xcecli 9 packages + apiserver
models/controllers/utils all pass.
Verified on the test cluster: `xcecli jobs list` (no unmarshal error) and
`xcecli jobs list -l` show "PENDING REASONS: host_unavail; host t1:
host_unavail; host t2: match_fail; host t3: match_fail".
Co-Authored-By: Claude <noreply@anthropic.com>
445 lines
15 KiB
Go
445 lines
15 KiB
Go
package output
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"text/tabwriter"
|
|
jobfmt "xcecli/common/format"
|
|
"xcecli/models"
|
|
)
|
|
|
|
// JobsPagePrinter is the printer for jobs
|
|
type JobsPagePrinter struct {
|
|
*BasePagePrinter
|
|
}
|
|
|
|
// NewJobsPagePrinter creates a new jobs page printer
|
|
func NewJobsPagePrinter(opt ...BasePageOption) *JobsPagePrinter {
|
|
return &JobsPagePrinter{
|
|
BasePagePrinter: newBasePagePrinter(opt...),
|
|
}
|
|
}
|
|
|
|
// PrepareForPrinting prepares for printing, initializes field formats and column widths
|
|
func (p *JobsPagePrinter) PrepareForPrinting(firstPage interface{}) {
|
|
jobs, ok := firstPage.(*models.JobsResponse)
|
|
if !ok {
|
|
fmt.Println("error: wrong type of data passed to JobsPagePrinter")
|
|
return
|
|
}
|
|
|
|
// set the field formats
|
|
p.fieldFormats = p.prepareFieldFormats()
|
|
|
|
// initialize the column widths
|
|
p.initColumnWidths(jobs)
|
|
|
|
// prepare the headers
|
|
p.prepareHeaders()
|
|
}
|
|
|
|
// prepare the field formats
|
|
func (p *JobsPagePrinter) prepareFieldFormats() []*fieldFormat {
|
|
var fieldFormats []*fieldFormat
|
|
if p.fields == "" {
|
|
// default fields
|
|
defaultFields := []string{
|
|
models.HEADER_JOB_JOBID,
|
|
models.HEADER_JOB_USER,
|
|
models.HEADER_JOB_STATUS,
|
|
models.HEADER_JOB_QUEUE,
|
|
models.HEADER_JOB_FROM_HOST,
|
|
models.HEADER_JOB_EXEC_HOST,
|
|
models.HEADER_JOB_NAME,
|
|
models.HEADER_JOB_SUBMIT_TIME,
|
|
}
|
|
for _, field := range defaultFields {
|
|
fieldFormats = append(fieldFormats, &fieldFormat{name: field, align: "-"})
|
|
}
|
|
} else {
|
|
// parse user defined fields
|
|
for _, field := range strings.Split(p.fields, ",") {
|
|
fieldFormats = append(fieldFormats, parseFieldFormat(field))
|
|
}
|
|
}
|
|
return fieldFormats
|
|
}
|
|
|
|
// initialize the column widths
|
|
func (p *JobsPagePrinter) initColumnWidths(jobs *models.JobsResponse) {
|
|
// initialize the column widths to the length of the headers
|
|
for _, format := range p.fieldFormats {
|
|
p.columnWidths[format.name] = len(format.name)
|
|
if format.width > 0 {
|
|
p.columnWidths[format.name] = format.width
|
|
}
|
|
}
|
|
|
|
// calculate the maximum width of the first page data
|
|
for _, job := range jobs.Data.Rows {
|
|
for _, format := range p.fieldFormats {
|
|
width := 0
|
|
switch format.name {
|
|
case models.HEADER_JOB_JOBID:
|
|
width = len(jobfmt.FormatJobID(job.UniqueJobID))
|
|
case models.HEADER_JOB_USER:
|
|
width = len(job.SubmitUser)
|
|
case models.HEADER_JOB_STATUS:
|
|
width = len(job.JobStatus)
|
|
case models.HEADER_JOB_QUEUE:
|
|
width = len(job.QueueName)
|
|
case models.HEADER_JOB_FROM_HOST:
|
|
width = len(job.FromHost)
|
|
case models.HEADER_JOB_EXEC_HOST:
|
|
continue
|
|
case models.HEADER_JOB_NAME:
|
|
width = len(job.JobName)
|
|
case models.HEADER_JOB_SUBMIT_TIME:
|
|
width = len(fmtUnixTime(job.SubmitTime))
|
|
case models.HEADER_JOB_CORE_LIMIT:
|
|
width = len(fmt.Sprintf("%f", job.CoreLimit))
|
|
case models.HEADER_JOB_MEM_LIMIT:
|
|
width = len(fmt.Sprintf("%f", job.MemLimit))
|
|
}
|
|
|
|
if !p.wideFormat && width > 15 {
|
|
width = 15
|
|
}
|
|
|
|
if width > p.columnWidths[format.name] && format.width == 0 {
|
|
p.columnWidths[format.name] = width
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// PrintPage prints a page of data
|
|
func (p *JobsPagePrinter) PrintPage(page interface{}) {
|
|
jobs, ok := page.(*models.JobsResponse)
|
|
if !ok {
|
|
fmt.Println("error: wrong type of data passed to JobsPagePrinter")
|
|
return
|
|
}
|
|
|
|
p.pageCount++
|
|
|
|
if p.verbose {
|
|
printJobsVerboseOutput(jobs, p.jobStatusFilter)
|
|
return
|
|
}
|
|
|
|
if p.formatJson {
|
|
p.printJsonPage(jobs)
|
|
return
|
|
} else {
|
|
// print the header (if needed)
|
|
p.printHeader()
|
|
|
|
// print the data rows
|
|
rows := p.formatRows(jobs, true)
|
|
for _, row := range rows {
|
|
fmt.Fprintln(p.w, strings.Join(row, "\t"))
|
|
}
|
|
p.w.Flush()
|
|
}
|
|
}
|
|
|
|
func (p *JobsPagePrinter) printJsonPage(jobs *models.JobsResponse) {
|
|
// if the format is JSON, directly convert the current page to JSON
|
|
headers := make([]string, len(p.fieldFormats))
|
|
for i, format := range p.fieldFormats {
|
|
headers[i] = format.name
|
|
}
|
|
rows := p.formatRows(jobs, false)
|
|
p.PrintJsonPageGeneric(rows, headers)
|
|
|
|
}
|
|
|
|
// format the data rows
|
|
func (p *JobsPagePrinter) formatRows(jobs *models.JobsResponse, truncatFlag bool) [][]string {
|
|
var rows [][]string
|
|
if p.wideFormat {
|
|
truncatFlag = false
|
|
}
|
|
for _, job := range jobs.Data.Rows {
|
|
mainRow := make([]string, 0, len(p.fieldFormats))
|
|
var extraRows [][]string
|
|
for _, format := range p.fieldFormats {
|
|
width := p.columnWidths[format.name]
|
|
|
|
align := format.align
|
|
|
|
switch format.name {
|
|
case models.HEADER_JOB_JOBID:
|
|
mainRow = appendOrDefault(mainRow,
|
|
fmt.Sprintf("%"+align+"*s", 20, jobfmt.FormatJobID(job.UniqueJobID)), "-")
|
|
|
|
case models.HEADER_JOB_EXEC_HOST:
|
|
var execHosts models.ExecHosts
|
|
if err := json.Unmarshal([]byte(job.ExecHosts.String()), &execHosts); err == nil && len(execHosts.HostSlots) > 0 && execHosts.HostSlots[0].HostName != "" {
|
|
firstHost := fmt.Sprintf("%d*%s", execHosts.HostSlots[0].NumSlots, execHosts.HostSlots[0].HostName)
|
|
mainRow = appendOrDefault(mainRow,
|
|
FormatStringField(firstHost, width, truncatFlag, false), "-")
|
|
if len(execHosts.HostSlots) > 1 {
|
|
for _, slot := range execHosts.HostSlots[1:] {
|
|
newLine := make([]string, len(p.fieldFormats))
|
|
for i, f := range p.fieldFormats {
|
|
if f.name == "EXEC_HOST" {
|
|
continue
|
|
}
|
|
newLine[i] = strings.Repeat(" ", p.columnWidths[f.name])
|
|
}
|
|
hostText := fmt.Sprintf("%d*%s", slot.NumSlots, slot.HostName)
|
|
newLine[getFieldFormatIndex(p.fieldFormats, "EXEC_HOST")] = FormatStringField(hostText, width, truncatFlag, false)
|
|
extraRows = append(extraRows, append([]string{}, newLine...))
|
|
}
|
|
}
|
|
} else {
|
|
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*s", width, "-"), "-")
|
|
}
|
|
case models.HEADER_JOB_SUBMIT_TIME:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(fmtUnixTime(job.SubmitTime), width, truncatFlag, true), "-")
|
|
|
|
// normal fields
|
|
case models.HEADER_JOB_NAME:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(job.JobName, width, truncatFlag, true), "-")
|
|
case models.HEADER_JOB_USER:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(job.SubmitUser, width, truncatFlag, true), "-")
|
|
case models.HEADER_JOB_STATUS:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(job.JobStatus, width, truncatFlag, true), "-")
|
|
case models.HEADER_JOB_QUEUE:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(job.QueueName, width, truncatFlag, true), "-")
|
|
case models.HEADER_JOB_FROM_HOST:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(job.FromHost, width, truncatFlag, true), "-")
|
|
case models.HEADER_JOB_COMMAND:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(job.Command, width, truncatFlag, true), "-")
|
|
case models.HEADER_JOB_PROJECT_NAME:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(job.ProjectName, width, truncatFlag, true), "-")
|
|
case models.HEADER_JOB_START_TIME:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(fmtUnixTime(job.StartTime), width, truncatFlag, true), "-")
|
|
case models.HEADER_JOB_END_TIME:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(fmtUnixTime(job.EndTime), width, truncatFlag, true), "-")
|
|
case models.HEADER_JOB_CWD:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(job.CWD, width, truncatFlag, true), "-")
|
|
case models.HEADER_JOB_LAUNCH_TYPE:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(job.LaunchType, width, truncatFlag, true), "-")
|
|
case models.HEADER_JOB_INTERACTIVE_URL:
|
|
mainRow = appendOrDefault(mainRow, FormatStringField(job.InteractiveJobURL, width, truncatFlag, true), "-")
|
|
|
|
// not string field
|
|
case models.HEADER_JOB_NUM_PROCESSORS:
|
|
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*d", width, job.NumProcessors), "-")
|
|
case models.HEADER_JOB_CORE_LIMIT:
|
|
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*s", width, formatSizeString(job.CoreLimit)), "-")
|
|
case models.HEADER_JOB_MEM_LIMIT:
|
|
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*s", width, formatSizeString(job.MemLimit)), "-")
|
|
default:
|
|
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*v", width, "-"), "-")
|
|
}
|
|
}
|
|
rows = append(rows, mainRow)
|
|
rows = append(rows, extraRows...)
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func printJobsVerboseOutput(jobs *models.JobsResponse, filter []string) {
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
|
defer w.Flush()
|
|
for jobIndex, job := range jobs.Data.Rows {
|
|
// if the filter is not empty, only show the jobs that match the filter
|
|
if len(filter) > 0 {
|
|
found := false
|
|
for _, status := range filter {
|
|
if job.JobStatus == status {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
continue
|
|
}
|
|
}
|
|
jobId := jobfmt.FormatJobID(job.UniqueJobID)
|
|
|
|
// (1) print the job baisc info when sending "jobs list -l" command
|
|
firstLine := "Job <" + jobId + ">, "
|
|
if job.JobName != "" {
|
|
firstLine = firstLine + "Job Name <" + job.JobName + ">, "
|
|
}
|
|
if job.SubmitUser != "" {
|
|
firstLine = firstLine + "User <" + job.SubmitUser + ">, "
|
|
}
|
|
if job.ProjectName != "" {
|
|
firstLine = firstLine + "Project <" + job.ProjectName + ">, "
|
|
}
|
|
if job.JobStatus != "" {
|
|
firstLine = firstLine + "Status <" + job.JobStatus + ">, "
|
|
}
|
|
|
|
if job.QueueName != "" {
|
|
firstLine = firstLine + "Queue <" + job.QueueName + ">, "
|
|
}
|
|
if job.JobPriority != 0 {
|
|
firstLine = firstLine + "Priority <" + strconv.Itoa(job.JobPriority) + ">, "
|
|
}
|
|
if job.LaunchType != "" {
|
|
firstLine = firstLine + "Launch Type <" + job.LaunchType + ">, "
|
|
}
|
|
if job.Command != "" {
|
|
firstLine = firstLine + "Command <" + job.Command + ">, "
|
|
}
|
|
if job.JobDescription != "" {
|
|
firstLine = firstLine + "Job Description <" + job.JobDescription + "> "
|
|
}
|
|
PrintWrappedLine(w, firstLine, 80, 18)
|
|
|
|
// (2) print the submit time info
|
|
submitLine := ""
|
|
if submitTime, ok := unixToTime(job.SubmitTime); ok {
|
|
submitLine = submitLine + fmt.Sprintf("%s %s %02d:%02d:%02d: ",
|
|
submitTime.Format("Jan"), submitTime.Format("02"),
|
|
submitTime.Hour(), submitTime.Minute(), submitTime.Second())
|
|
if job.FromHost != "" {
|
|
submitLine = submitLine + "Submitted from host <" + job.FromHost + ">, "
|
|
}
|
|
if job.CWD != "" {
|
|
submitLine = submitLine + "CWD <" + job.CWD + ">, "
|
|
}
|
|
if job.NumProcessors != 0 {
|
|
submitLine = submitLine + strconv.Itoa(job.NumProcessors) + " Task(s), "
|
|
}
|
|
if job.Resreq != "" {
|
|
submitLine = submitLine + "Requested Resources <" + job.Resreq + ">, "
|
|
}
|
|
}
|
|
PrintWrappedLine(w, submitLine, 80, 18)
|
|
|
|
// (3) print the pending reason for jobs waiting to be dispatched
|
|
if job.JobStatus == "PEND" || job.JobStatus == "PSUSP" {
|
|
if string(job.PendingReason) != "" {
|
|
fmt.Fprintf(w, " PENDING REASONS:\n")
|
|
fmt.Fprintf(w, " %s\n", string(job.PendingReason))
|
|
}
|
|
}
|
|
|
|
// (4) print the start time info — skip for jobs that haven't started yet
|
|
startLine := ""
|
|
if job.JobStatus == "PEND" || job.JobStatus == "PSUSP" {
|
|
goto printEndLine
|
|
}
|
|
if startTime, ok := unixToTime(job.StartTime); ok {
|
|
month := startTime.Format("Jan")
|
|
day := startTime.Format("02")
|
|
exec_hostname := ""
|
|
started_slots := 0
|
|
if len(job.ExecHosts.HostSlots) > 0 {
|
|
for _, host := range job.ExecHosts.HostSlots {
|
|
started_slots += host.NumSlots
|
|
if exec_hostname == "" {
|
|
exec_hostname = exec_hostname + fmt.Sprintf("<%d*%s>", host.NumSlots, host.HostName)
|
|
} else {
|
|
exec_hostname = exec_hostname + " " + fmt.Sprintf("<%d*%s>", host.NumSlots, host.HostName)
|
|
}
|
|
}
|
|
}
|
|
startLine = startLine + fmt.Sprintf("%s %s %02d:%02d:%02d: ",
|
|
month, day, startTime.Hour(), startTime.Minute(), startTime.Second())
|
|
|
|
if started_slots > 0 {
|
|
startSlotMsg := fmt.Sprintf("%d", started_slots)
|
|
startLine = startLine + "Started " + startSlotMsg + " Task(s) "
|
|
}
|
|
if exec_hostname != "" {
|
|
startLine = startLine + "on Host(s) " + exec_hostname + ", "
|
|
}
|
|
if job.ExecHome != "" {
|
|
startLine = startLine + "Execution Home <" + job.ExecHome + ">, "
|
|
}
|
|
if job.ExecCWD != "" {
|
|
startLine = startLine + "Execution CWD <" + job.ExecCWD + ">, "
|
|
}
|
|
if job.InteractiveJobURL != "" {
|
|
startLine = startLine + "Interactive Job URL <" + job.InteractiveJobURL + ">, "
|
|
}
|
|
|
|
//fmt.Fprintf(w, "%s %s %02d:%02d:%02d: Resource usage collected.\n",
|
|
// month, day, submitTime.Hour(), submitTime.Minute(), submitTime.Second())
|
|
//fmt.Fprintf(w, " MEM: Mbytes; SWAP: Mbytes; NTHREAD: \n")
|
|
pgids := make([]string, len(job.PGIDs))
|
|
pids := make([]string, len(job.PID))
|
|
for i, pgid := range job.PGIDs {
|
|
pgids[i] = fmt.Sprintf("%d", pgid)
|
|
}
|
|
for i, pid := range job.PID {
|
|
pids[i] = fmt.Sprintf("%d", pid)
|
|
}
|
|
|
|
if len(pgids) > 0 {
|
|
startLine = startLine + "PGID: " + strings.Join(pgids, ",") + ", "
|
|
} else if len(pids) > 0 {
|
|
// only show pids
|
|
startLine = startLine + "PIDs: " + strings.Join(pids, ",") + ", "
|
|
}
|
|
}
|
|
PrintWrappedLine(w, startLine, 80, 18)
|
|
|
|
printEndLine:
|
|
// (4) print end line
|
|
endLine := ""
|
|
if job.JobStatus == "DONE" || job.JobStatus == "EXIT" {
|
|
if endTime, ok := unixToTime(job.EndTime); ok {
|
|
endLine = endLine + fmt.Sprintf("%s %s %02d:%02d:%02d: ",
|
|
endTime.Format("Jan"), endTime.Format("02"),
|
|
endTime.Hour(), endTime.Minute(), endTime.Second())
|
|
}
|
|
if job.JobStatus == "EXIT" {
|
|
endLine = endLine + "Completed <" + strings.ToLower(job.JobStatus) + ">. "
|
|
} else {
|
|
endLine = endLine + "Done successfully. "
|
|
}
|
|
|
|
if job.CPUTime != 0 {
|
|
endLine = endLine + "The CPU time used is <" + strconv.Itoa(job.CPUTime) + "> seconds."
|
|
|
|
}
|
|
|
|
}
|
|
PrintWrappedLine(w, endLine, 80, 18)
|
|
fmt.Println("")
|
|
// print the scheduling parameters table
|
|
fmt.Printf("SCHEDULING PARAMETERS\n")
|
|
|
|
headers2 := append([]string{""}, models.JobLoadSchedulingHeaders...)
|
|
loadSched := []string{"loadSched", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"}
|
|
loadStop := []string{"loadStop", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"}
|
|
|
|
widths2 := make([]int, len(headers2))
|
|
for i := range headers2 {
|
|
widths2[i] = max(len(headers2[i]), len(loadSched[i]))
|
|
widths2[i] = max(widths2[i], len(loadStop[i]))
|
|
}
|
|
|
|
rowSched := make([]string, len(headers2))
|
|
rowStop := make([]string, len(headers2))
|
|
for i := range headers2 {
|
|
rowSched[i] = FormatStringField(loadSched[i], widths2[i], false, true)
|
|
rowStop[i] = FormatStringField(loadStop[i], widths2[i], false, true)
|
|
}
|
|
|
|
tp2 := TablePrinter{Headers: headers2, Rows: [][]string{rowSched, rowStop}, ColWidths: widths2}
|
|
tp2.Print()
|
|
fmt.Println("")
|
|
|
|
// print the resource requirement details
|
|
fmt.Printf("RESOURCE REQUIREMENT DETAILS:\n")
|
|
fmt.Fprintln(w, " Combined: select[type == local] order[r15s:pg]")
|
|
fmt.Fprintln(w, " Effective: select[type == local] order[r15s:pg]")
|
|
if jobIndex != len(jobs.Data.Rows)-1 {
|
|
fmt.Fprintln(w, "------------------------------------------------------------------------------")
|
|
}
|
|
}
|
|
}
|