Merge Go frontend (apiserver + xcecli) into meson build
This commit is contained in:
parent
43dc12a36c
commit
c9dc8c4676
205 changed files with 30793 additions and 328 deletions
45
src/frontend/xcecli/common/output/output_clusters.go
Executable file
45
src/frontend/xcecli/common/output/output_clusters.go
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"xcecli/models"
|
||||
)
|
||||
|
||||
type Output struct {
|
||||
fields string
|
||||
formatJson bool
|
||||
noheader bool
|
||||
verbose bool
|
||||
wideFormat bool
|
||||
jobStatusFilter []string
|
||||
}
|
||||
|
||||
func NewOutput(fields string, formatJson bool, noheader bool) *Output {
|
||||
return &Output{fields: fields, formatJson: formatJson, noheader: noheader}
|
||||
}
|
||||
|
||||
func (o *Output) SetVerbose(verbose bool) {
|
||||
o.verbose = verbose
|
||||
}
|
||||
|
||||
func (o *Output) SetNoHeader(noheader bool) {
|
||||
o.noheader = noheader
|
||||
}
|
||||
|
||||
func (o *Output) SetWideFormat(wideFormat bool) {
|
||||
o.wideFormat = wideFormat
|
||||
}
|
||||
|
||||
func (o *Output) ClusterPrintData(cluster *models.ClusterInfo) {
|
||||
printClustersVerboseOutput(cluster)
|
||||
}
|
||||
|
||||
func printClustersVerboseOutput(clusters *models.ClusterInfo) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
defer w.Flush()
|
||||
fmt.Fprintf(w, "XCE Standard Cluster\t%s\t%s\n", clusters.Version, "Jul 08 2025")
|
||||
fmt.Fprintf(w, "My cluster name: <%s>\n", clusters.ClusterName)
|
||||
fmt.Fprintf(w, "The cluster status: <%s>\n", clusters.Status)
|
||||
}
|
||||
377
src/frontend/xcecli/common/output/page_print.go
Executable file
377
src/frontend/xcecli/common/output/page_print.go
Executable file
|
|
@ -0,0 +1,377 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
// PagePrinter
|
||||
type PagePrinter interface {
|
||||
// PrepareForPrinting
|
||||
PrepareForPrinting(firstPage interface{})
|
||||
// PrintPage
|
||||
PrintPage(page interface{})
|
||||
// Finish
|
||||
Finish()
|
||||
// StartPrinting
|
||||
StartPrinting()
|
||||
}
|
||||
|
||||
const (
|
||||
DataTypeJobs = "jobs"
|
||||
DataTypeHosts = "hosts"
|
||||
DataTypeQueues = "queues"
|
||||
DataTypeArrayJobs = "array_jobs"
|
||||
DataTypeArrayElements = "array_elements"
|
||||
DataTypeResourcesDist = "resources_dist"
|
||||
DataTypeResourcesDef = "resource_def"
|
||||
DataTypeRoles = "roles"
|
||||
DataTypeUsers = "users"
|
||||
)
|
||||
|
||||
// NewPagePrinter
|
||||
func NewPagePrinter(dataType string, opt ...BasePageOption) PagePrinter {
|
||||
switch dataType {
|
||||
case DataTypeJobs:
|
||||
return NewJobsPagePrinter(opt...)
|
||||
case DataTypeHosts:
|
||||
return NewHostsPagePrinter(opt...)
|
||||
case DataTypeQueues:
|
||||
return NewQueuesPagePrinter(opt...)
|
||||
case DataTypeArrayJobs:
|
||||
return NewArrayJobsPagePrinter(opt...)
|
||||
case DataTypeArrayElements:
|
||||
return NewArrayElementsPagePrinter(opt...)
|
||||
case DataTypeResourcesDist:
|
||||
return NewResourcesDistPagePrinter(opt...)
|
||||
case DataTypeResourcesDef:
|
||||
return NewResourcesDefPagePrinter(opt...)
|
||||
case DataTypeRoles:
|
||||
return NewRolesPagePrinter(opt...)
|
||||
case DataTypeUsers:
|
||||
return NewUserRolesPagePrinter(opt...)
|
||||
default:
|
||||
fmt.Printf("unsupported data type: %s, using default Jobs printer\n", dataType)
|
||||
return NewJobsPagePrinter(opt...)
|
||||
}
|
||||
}
|
||||
|
||||
type BasePageOption interface {
|
||||
apply(*BasePagePrinter)
|
||||
}
|
||||
|
||||
type basePageOptionFunc func(*BasePagePrinter)
|
||||
|
||||
func (f basePageOptionFunc) apply(bpp *BasePagePrinter) {
|
||||
f(bpp)
|
||||
}
|
||||
|
||||
func WithFields(fields string) BasePageOption {
|
||||
return basePageOptionFunc(func(bpp *BasePagePrinter) {
|
||||
bpp.fields = fields
|
||||
})
|
||||
}
|
||||
|
||||
func WithFormatJson(b bool) BasePageOption {
|
||||
return basePageOptionFunc(func(bpp *BasePagePrinter) {
|
||||
bpp.formatJson = b
|
||||
})
|
||||
}
|
||||
|
||||
func WithNoHeader(b bool) BasePageOption {
|
||||
return basePageOptionFunc(func(bpp *BasePagePrinter) {
|
||||
bpp.noheader = b
|
||||
})
|
||||
}
|
||||
|
||||
func WithVerbose(b bool) BasePageOption {
|
||||
return basePageOptionFunc(func(bpp *BasePagePrinter) {
|
||||
bpp.verbose = b
|
||||
})
|
||||
}
|
||||
|
||||
func WithWideFormat(b bool) BasePageOption {
|
||||
return basePageOptionFunc(func(bpp *BasePagePrinter) {
|
||||
bpp.wideFormat = b
|
||||
})
|
||||
}
|
||||
|
||||
func WithJobStatusFilter(filter []string) BasePageOption {
|
||||
return basePageOptionFunc(func(bpp *BasePagePrinter) {
|
||||
bpp.jobStatusFilter = filter
|
||||
})
|
||||
}
|
||||
|
||||
// BasePagePrinter
|
||||
type BasePagePrinter struct {
|
||||
/* external config */
|
||||
fields string
|
||||
formatJson bool
|
||||
noheader bool
|
||||
verbose bool
|
||||
wideFormat bool
|
||||
jobStatusFilter []string
|
||||
|
||||
/* internal variables */
|
||||
headerPrinted bool
|
||||
columnWidths map[string]int // store column widths
|
||||
fieldFormats []*fieldFormat // field formats
|
||||
formattedHeaders []string // formatted headers
|
||||
w *tabwriter.Writer
|
||||
pageCount int // record the number of printed pages
|
||||
startPrintOnce *sync.Once
|
||||
}
|
||||
|
||||
// newBasePagePrinter
|
||||
func newBasePagePrinter(opts ...BasePageOption) *BasePagePrinter {
|
||||
base := &BasePagePrinter{
|
||||
headerPrinted: false,
|
||||
columnWidths: make(map[string]int),
|
||||
w: tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0),
|
||||
pageCount: 0,
|
||||
startPrintOnce: &sync.Once{},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.apply(base)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// StartPrinting
|
||||
func (p *BasePagePrinter) StartPrinting() {
|
||||
p.startPrintOnce.Do(func() {
|
||||
// if the format is JSON and need to print multiple pages, print the opening bracket
|
||||
if p.formatJson && !p.verbose {
|
||||
fmt.Print("[")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// prepareHeaders
|
||||
func (p *BasePagePrinter) prepareHeaders() {
|
||||
p.formattedHeaders = make([]string, len(p.fieldFormats))
|
||||
for i, format := range p.fieldFormats {
|
||||
p.formattedHeaders[i] = fmt.Sprintf("%"+format.align+"*s", p.columnWidths[format.name], format.name)
|
||||
}
|
||||
}
|
||||
|
||||
// printHeader
|
||||
func (p *BasePagePrinter) printHeader() {
|
||||
if !p.headerPrinted && !p.noheader {
|
||||
fmt.Fprintf(p.w, "%s\n", strings.Join(p.formattedHeaders, "\t"))
|
||||
p.headerPrinted = true
|
||||
}
|
||||
}
|
||||
|
||||
// Finish
|
||||
func (p *BasePagePrinter) Finish() {
|
||||
// if the format is JSON and printed data, add the closing bracket
|
||||
if p.formatJson && !p.verbose && p.pageCount > 0 {
|
||||
fmt.Println("]")
|
||||
}
|
||||
p.w.Flush()
|
||||
}
|
||||
|
||||
type fieldFormat struct {
|
||||
name string
|
||||
width int
|
||||
align string // "-" for left align, "" for right align
|
||||
}
|
||||
|
||||
func parseFieldFormat(field string) *fieldFormat {
|
||||
// parse the field format, support the following formats:
|
||||
// "FIELD" - default format
|
||||
// "FIELD:10" - specify the width to 10, right align
|
||||
// "FIELD:-10" - specify the width to 10, left align
|
||||
parts := strings.Split(field, ":")
|
||||
if len(parts) == 1 {
|
||||
return &fieldFormat{
|
||||
name: parts[0],
|
||||
width: 0,
|
||||
align: "-",
|
||||
}
|
||||
}
|
||||
|
||||
width := 0
|
||||
align := ""
|
||||
if strings.HasPrefix(parts[1], "-") {
|
||||
align = "-"
|
||||
width, _ = strconv.Atoi(parts[1][1:])
|
||||
} else {
|
||||
width, _ = strconv.Atoi(parts[1])
|
||||
}
|
||||
|
||||
return &fieldFormat{
|
||||
name: parts[0],
|
||||
width: width,
|
||||
align: align,
|
||||
}
|
||||
}
|
||||
|
||||
func formatSize(value float64) (float64, string, error) {
|
||||
if value <= 0 {
|
||||
return 0, "MB", nil
|
||||
}
|
||||
switch {
|
||||
case value >= 1024*1024*1024: // >= 1PB
|
||||
return value / (1024 * 1024 * 1024), "PB", nil
|
||||
case value >= 1024*1024: // >= 1TB
|
||||
return value / (1024 * 1024), "TB", nil
|
||||
case value >= 1024: // >= 1GB
|
||||
return value / 1024, "GB", nil
|
||||
default: // MB
|
||||
return value, "MB", nil
|
||||
}
|
||||
}
|
||||
|
||||
func formatSizeString(value float64) string {
|
||||
size, unit, err := formatSize(value)
|
||||
if err != nil {
|
||||
return "-"
|
||||
}
|
||||
return fmt.Sprintf("%.2f%s", size, unit)
|
||||
}
|
||||
|
||||
func appendOrDefault(slice []string, value string, defaultValue string) []string {
|
||||
if value != "" {
|
||||
return append(slice, value)
|
||||
} else {
|
||||
return append(slice, defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
func getFieldFormatIndex(fieldFormats []*fieldFormat, target string) int {
|
||||
for i, f := range fieldFormats {
|
||||
if f.name == target {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func PrintWrappedLine(w *tabwriter.Writer, line string, width int, indentSpaces int) {
|
||||
line = strings.TrimRight(line, " ")
|
||||
indent := strings.Repeat(" ", indentSpaces)
|
||||
first := true
|
||||
for len(line) > 0 {
|
||||
if line[len(line)-1] == ',' {
|
||||
line = line[:len(line)-1] + ";"
|
||||
}
|
||||
if first {
|
||||
// first line
|
||||
if len(line) <= width {
|
||||
fmt.Fprintf(w, "%s\n", line)
|
||||
break
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\n", line[:width])
|
||||
line = line[width:]
|
||||
}
|
||||
first = false
|
||||
} else {
|
||||
avail := width - indentSpaces
|
||||
if avail <= 0 {
|
||||
avail = 1
|
||||
}
|
||||
if len(line) <= avail {
|
||||
fmt.Fprintf(w, "%s%s\n", indent, line)
|
||||
break
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s%s\n", indent, line[:avail])
|
||||
line = line[avail:]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BasePagePrinter) PrintJsonPageGeneric(rows [][]string, headers []string) {
|
||||
data := make([]map[string]string, len(rows))
|
||||
for i, row := range rows {
|
||||
entry := make(map[string]string)
|
||||
for j, header := range headers {
|
||||
if j < len(row) {
|
||||
entry[header] = strings.TrimSpace(row[j])
|
||||
} else {
|
||||
entry[header] = ""
|
||||
}
|
||||
}
|
||||
data[i] = entry
|
||||
}
|
||||
for i, item := range data {
|
||||
jsonData, err := json.MarshalIndent(item, "", " ")
|
||||
if err != nil {
|
||||
fmt.Println("error marshalling to JSON:", err)
|
||||
return
|
||||
}
|
||||
if p.pageCount > 1 || i > 0 {
|
||||
fmt.Print(",\n")
|
||||
}
|
||||
fmt.Print(string(jsonData))
|
||||
}
|
||||
}
|
||||
|
||||
func FormatStringField(val string, width int, truncatFlag bool, prefixAtStart bool) string {
|
||||
if len(val) > width && truncatFlag {
|
||||
if prefixAtStart {
|
||||
return "*" + val[len(val)-width+1:]
|
||||
} else {
|
||||
keepLen := width - 1
|
||||
if keepLen < 0 {
|
||||
keepLen = 0
|
||||
}
|
||||
return val[:keepLen] + "*"
|
||||
}
|
||||
} else {
|
||||
if val == "" {
|
||||
return fmt.Sprintf("%-*s", width, "-")
|
||||
} else {
|
||||
return fmt.Sprintf("%-*s", width, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TablePrinter struct {
|
||||
Headers []string
|
||||
Rows [][]string
|
||||
ColWidths []int
|
||||
}
|
||||
|
||||
func (tp *TablePrinter) CalcColWidths() {
|
||||
tp.ColWidths = make([]int, len(tp.Headers))
|
||||
for i, h := range tp.Headers {
|
||||
tp.ColWidths[i] = len(h)
|
||||
}
|
||||
for _, row := range tp.Rows {
|
||||
for i, cell := range row {
|
||||
if len(cell) > tp.ColWidths[i] {
|
||||
tp.ColWidths[i] = len(cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tp *TablePrinter) Print() {
|
||||
tp.CalcColWidths()
|
||||
format := ""
|
||||
for _, w := range tp.ColWidths {
|
||||
format += fmt.Sprintf("%%-%ds ", w)
|
||||
}
|
||||
format += "\n"
|
||||
|
||||
fmt.Printf(format, ToInterfaceSlice(tp.Headers)...)
|
||||
for _, row := range tp.Rows {
|
||||
fmt.Printf(format, ToInterfaceSlice(row)...)
|
||||
}
|
||||
}
|
||||
|
||||
func ToInterfaceSlice(strs []string) []interface{} {
|
||||
res := make([]interface{}, len(strs))
|
||||
for i, s := range strs {
|
||||
res[i] = s
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"xcecli/models"
|
||||
)
|
||||
|
||||
// ArrayElementsPagePrinter
|
||||
type ArrayElementsPagePrinter struct {
|
||||
*BasePagePrinter
|
||||
}
|
||||
|
||||
// NewArrayElementsPagePrinter
|
||||
func NewArrayElementsPagePrinter(opt ...BasePageOption) *ArrayElementsPagePrinter {
|
||||
return &ArrayElementsPagePrinter{
|
||||
BasePagePrinter: newBasePagePrinter(opt...),
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareForPrinting
|
||||
func (p *ArrayElementsPagePrinter) PrepareForPrinting(firstPage interface{}) {
|
||||
jobs, ok := firstPage.(*models.JobsResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to ArrayElementsPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
// set field formats
|
||||
p.fieldFormats = p.prepareFieldFormats()
|
||||
|
||||
// init column widths
|
||||
p.initColumnWidths(jobs)
|
||||
|
||||
// prepare headers
|
||||
p.prepareHeaders()
|
||||
}
|
||||
|
||||
// prepare field formats
|
||||
func (p *ArrayElementsPagePrinter) 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 specified fields
|
||||
for _, field := range strings.Split(p.fields, ",") {
|
||||
fieldFormats = append(fieldFormats, parseFieldFormat(field))
|
||||
}
|
||||
}
|
||||
return fieldFormats
|
||||
}
|
||||
|
||||
// init column widths
|
||||
func (p *ArrayElementsPagePrinter) initColumnWidths(jobs *models.JobsResponse) {
|
||||
// init column widths to header length
|
||||
for _, format := range p.fieldFormats {
|
||||
p.columnWidths[format.name] = len(format.name)
|
||||
if format.width > 0 {
|
||||
p.columnWidths[format.name] = format.width
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the max 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:
|
||||
// format the job id as "jobID[idx]"
|
||||
arrayJobIDStr := fmt.Sprintf("%d[%d]", job.JobID, job.ArrayIdx)
|
||||
width = len(arrayJobIDStr)
|
||||
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(job.SubmitTime)
|
||||
}
|
||||
|
||||
if !p.wideFormat && width > 15 {
|
||||
width = 15
|
||||
}
|
||||
|
||||
if width > p.columnWidths[format.name] && format.width == 0 {
|
||||
p.columnWidths[format.name] = width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintPage
|
||||
func (p *ArrayElementsPagePrinter) PrintPage(page interface{}) {
|
||||
jobs, ok := page.(*models.JobsResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to ArrayElementsPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
p.pageCount++
|
||||
|
||||
if p.verbose {
|
||||
printJobsVerboseOutput(jobs, p.jobStatusFilter)
|
||||
return
|
||||
}
|
||||
|
||||
if p.formatJson {
|
||||
p.printJsonPage(jobs)
|
||||
return
|
||||
} else {
|
||||
// print header (if needed)
|
||||
p.printHeader()
|
||||
|
||||
// print data rows
|
||||
rows := p.formatRows(jobs, true)
|
||||
for _, row := range rows {
|
||||
fmt.Fprintln(p.w, strings.Join(row, "\t"))
|
||||
}
|
||||
p.w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// format data rows
|
||||
func (p *ArrayElementsPagePrinter) 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:
|
||||
// format the job id as "jobID[idx]"
|
||||
arrayJobIDStr := fmt.Sprintf("%d[%d]", job.JobID, job.ArrayIdx)
|
||||
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*s", 20, arrayJobIDStr), "-")
|
||||
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_EXEC_HOST:
|
||||
var execHosts models.ExecHosts
|
||||
if err := json.Unmarshal([]byte(job.ExecHosts.String()), &execHosts); err == nil && len(execHosts.HostSlots) > 0 {
|
||||
firstHost := fmt.Sprintf("%d*%s", execHosts.HostSlots[0].NumSlots, execHosts.HostSlots[0].HostName)
|
||||
mainRow = appendOrDefault(mainRow, FormatStringField(firstHost, width, truncatFlag, true), "-")
|
||||
|
||||
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, true)
|
||||
extraRows = append(extraRows, append([]string{}, newLine...))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*s", width, "-"), "-")
|
||||
}
|
||||
case models.HEADER_JOB_NAME:
|
||||
mainRow = appendOrDefault(mainRow, FormatStringField(job.JobName, width, truncatFlag, true), "-")
|
||||
case models.HEADER_JOB_SUBMIT_TIME:
|
||||
// remove the timezone part and parse the time
|
||||
timeStr := strings.Split(job.SubmitTime, " ")[0:2]
|
||||
timeStrWithoutTZ := strings.Join(timeStr, " ")
|
||||
|
||||
if submitTime, err := time.Parse("2006-01-02 15:04:05", timeStrWithoutTZ); err == nil {
|
||||
month := submitTime.Format("Jan")
|
||||
displayTime := fmt.Sprintf("%s %02d %02d:%02d",
|
||||
month,
|
||||
submitTime.Day(),
|
||||
submitTime.Hour(),
|
||||
submitTime.Minute())
|
||||
mainRow = appendOrDefault(mainRow, FormatStringField(displayTime, width, truncatFlag, true), "-")
|
||||
} else {
|
||||
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*s", width, "-"), "-")
|
||||
}
|
||||
}
|
||||
}
|
||||
rows = append(rows, mainRow)
|
||||
rows = append(rows, extraRows...)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// print JSON page
|
||||
func (p *ArrayElementsPagePrinter) 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)
|
||||
}
|
||||
199
src/frontend/xcecli/common/output/page_print_array_jobs.go
Normal file
199
src/frontend/xcecli/common/output/page_print_array_jobs.go
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"xcecli/models"
|
||||
)
|
||||
|
||||
// ArrayJobsPagePrinter
|
||||
type ArrayJobsPagePrinter struct {
|
||||
*BasePagePrinter
|
||||
}
|
||||
|
||||
// NewArrayJobsPagePrinter
|
||||
func NewArrayJobsPagePrinter(opt ...BasePageOption) *ArrayJobsPagePrinter {
|
||||
return &ArrayJobsPagePrinter{
|
||||
BasePagePrinter: newBasePagePrinter(opt...),
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareForPrinting
|
||||
func (p *ArrayJobsPagePrinter) PrepareForPrinting(firstPage interface{}) {
|
||||
arrayJobsStats, ok := firstPage.(*models.ArrayJobsStatsResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to ArrayJobsPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
// set field formats
|
||||
p.fieldFormats = p.prepareFieldFormats()
|
||||
|
||||
// init column widths
|
||||
p.initColumnWidths(arrayJobsStats)
|
||||
|
||||
// prepare headers
|
||||
p.prepareHeaders()
|
||||
}
|
||||
|
||||
// prepare field formats
|
||||
func (p *ArrayJobsPagePrinter) prepareFieldFormats() []*fieldFormat {
|
||||
var fieldFormats []*fieldFormat
|
||||
if p.fields == "" {
|
||||
// default fields
|
||||
defaultFields := []string{
|
||||
models.HEADER_ARRAY_JOB_JOBID,
|
||||
models.HEADER_ARRAY_JOB_SPEC,
|
||||
models.HEADER_ARRAY_JOB_OWNER,
|
||||
models.HEADER_ARRAY_JOB_NJOBS,
|
||||
models.HEADER_ARRAY_JOB_PEND,
|
||||
models.HEADER_ARRAY_JOB_DONE,
|
||||
models.HEADER_ARRAY_JOB_RUN,
|
||||
models.HEADER_ARRAY_JOB_EXIT,
|
||||
models.HEADER_ARRAY_JOB_SSUSP,
|
||||
models.HEADER_ARRAY_JOB_USUSP,
|
||||
models.HEADER_ARRAY_JOB_PSUSP,
|
||||
}
|
||||
for _, field := range defaultFields {
|
||||
fieldFormats = append(fieldFormats, &fieldFormat{name: field, align: "-"})
|
||||
}
|
||||
} else {
|
||||
// parse user specified fields
|
||||
for _, field := range strings.Split(p.fields, ",") {
|
||||
fieldFormats = append(fieldFormats, parseFieldFormat(field))
|
||||
}
|
||||
}
|
||||
return fieldFormats
|
||||
}
|
||||
|
||||
// init column widths
|
||||
func (p *ArrayJobsPagePrinter) initColumnWidths(arrayJobsStats *models.ArrayJobsStatsResponse) {
|
||||
// init column widths to header length
|
||||
for _, format := range p.fieldFormats {
|
||||
p.columnWidths[format.name] = len(format.name)
|
||||
if format.width > 0 {
|
||||
p.columnWidths[format.name] = format.width
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the max width of the first page data
|
||||
for _, stat := range arrayJobsStats.Data.Rows {
|
||||
for _, format := range p.fieldFormats {
|
||||
width := 0
|
||||
switch format.name {
|
||||
case models.HEADER_ARRAY_JOB_JOBID:
|
||||
width = len(fmt.Sprintf("%d", stat.JobID))
|
||||
case models.HEADER_ARRAY_JOB_SPEC:
|
||||
width = len(stat.ArrayJobName)
|
||||
case models.HEADER_ARRAY_JOB_OWNER:
|
||||
width = len(stat.SubmitUser)
|
||||
case models.HEADER_ARRAY_JOB_NJOBS:
|
||||
width = len(fmt.Sprintf("%d", stat.NumJobs))
|
||||
case models.HEADER_ARRAY_JOB_PEND:
|
||||
width = len(fmt.Sprintf("%d", stat.NumPend))
|
||||
case models.HEADER_ARRAY_JOB_DONE:
|
||||
width = len(fmt.Sprintf("%d", stat.NumDone))
|
||||
case models.HEADER_ARRAY_JOB_RUN:
|
||||
width = len(fmt.Sprintf("%d", stat.NumRun))
|
||||
case models.HEADER_ARRAY_JOB_EXIT:
|
||||
width = len(fmt.Sprintf("%d", stat.NumExit))
|
||||
case models.HEADER_ARRAY_JOB_SSUSP:
|
||||
width = len(fmt.Sprintf("%d", stat.NumSSusp))
|
||||
case models.HEADER_ARRAY_JOB_USUSP:
|
||||
width = len(fmt.Sprintf("%d", stat.NumUSusp))
|
||||
case models.HEADER_ARRAY_JOB_PSUSP:
|
||||
width = len(fmt.Sprintf("%d", stat.NumPSusp))
|
||||
}
|
||||
|
||||
if !p.wideFormat && width > 15 {
|
||||
width = 15
|
||||
}
|
||||
|
||||
if width > p.columnWidths[format.name] && format.width == 0 {
|
||||
p.columnWidths[format.name] = width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintPage
|
||||
func (p *ArrayJobsPagePrinter) PrintPage(page interface{}) {
|
||||
arrayJobsStats, ok := page.(*models.ArrayJobsStatsResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to ArrayJobsPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
p.pageCount++
|
||||
|
||||
if p.formatJson {
|
||||
p.printJsonPage(arrayJobsStats)
|
||||
return
|
||||
} else {
|
||||
// print header (if needed)
|
||||
p.printHeader()
|
||||
|
||||
// print data rows
|
||||
rows := p.formatRows(arrayJobsStats, true)
|
||||
for _, row := range rows {
|
||||
fmt.Fprintln(p.w, strings.Join(row, "\t"))
|
||||
}
|
||||
p.w.Flush()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// format data rows
|
||||
// ./xce jobs list -A
|
||||
func (p *ArrayJobsPagePrinter) formatRows(arrayJobsStats *models.ArrayJobsStatsResponse, truncatFlag bool) [][]string {
|
||||
var rows [][]string
|
||||
if p.wideFormat {
|
||||
truncatFlag = false
|
||||
}
|
||||
for _, stat := range arrayJobsStats.Data.Rows {
|
||||
row := make([]string, 0, len(p.fieldFormats))
|
||||
for _, format := range p.fieldFormats {
|
||||
width := p.columnWidths[format.name]
|
||||
align := format.align
|
||||
switch format.name {
|
||||
case models.HEADER_ARRAY_JOB_JOBID:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", 20, stat.JobID), "-")
|
||||
case models.HEADER_ARRAY_JOB_SPEC:
|
||||
row = appendOrDefault(row, FormatStringField(stat.ArrayJobName, width, truncatFlag, true), "-")
|
||||
case models.HEADER_ARRAY_JOB_OWNER:
|
||||
row = appendOrDefault(row, FormatStringField(stat.SubmitUser, width, truncatFlag, true), "-")
|
||||
case models.HEADER_ARRAY_JOB_NJOBS:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, stat.NumJobs), "0")
|
||||
case models.HEADER_ARRAY_JOB_PEND:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, stat.NumPend), "0")
|
||||
case models.HEADER_ARRAY_JOB_DONE:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, stat.NumDone), "0")
|
||||
case models.HEADER_ARRAY_JOB_RUN:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, stat.NumRun), "0")
|
||||
case models.HEADER_ARRAY_JOB_EXIT:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, stat.NumExit), "0")
|
||||
case models.HEADER_ARRAY_JOB_SSUSP:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, stat.NumSSusp), "0")
|
||||
case models.HEADER_ARRAY_JOB_USUSP:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, stat.NumUSusp), "0")
|
||||
case models.HEADER_ARRAY_JOB_PSUSP:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, stat.NumPSusp), "0")
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// print JSON page
|
||||
func (p *ArrayJobsPagePrinter) printJsonPage(arrayJobsStats *models.ArrayJobsStatsResponse) {
|
||||
// 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(arrayJobsStats, false)
|
||||
|
||||
p.PrintJsonPageGeneric(rows, headers)
|
||||
}
|
||||
289
src/frontend/xcecli/common/output/page_print_hosts.go
Normal file
289
src/frontend/xcecli/common/output/page_print_hosts.go
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"xcecli/models"
|
||||
)
|
||||
|
||||
// HostsPagePrinter
|
||||
type HostsPagePrinter struct {
|
||||
*BasePagePrinter
|
||||
}
|
||||
|
||||
// NewHostsPagePrinter
|
||||
func NewHostsPagePrinter(opt ...BasePageOption) *HostsPagePrinter {
|
||||
return &HostsPagePrinter{
|
||||
BasePagePrinter: newBasePagePrinter(opt...),
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareForPrinting
|
||||
func (p *HostsPagePrinter) PrepareForPrinting(firstPage interface{}) {
|
||||
hosts, ok := firstPage.(*models.HostsResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to HostsPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
// set field formats
|
||||
p.fieldFormats = p.prepareFieldFormats()
|
||||
|
||||
// init column widths
|
||||
p.initColumnWidths(hosts)
|
||||
|
||||
// prepare headers
|
||||
p.prepareHeaders()
|
||||
}
|
||||
|
||||
// prepare field formats
|
||||
func (p *HostsPagePrinter) prepareFieldFormats() []*fieldFormat {
|
||||
// here define the default fields based on the host data
|
||||
var fieldFormats []*fieldFormat
|
||||
if p.fields == "" {
|
||||
// default fields
|
||||
defaultFields := []string{
|
||||
models.HEADER_HOST_NAME,
|
||||
models.HEADER_HOST_STATUS,
|
||||
models.HEADER_HOST_NCPUS,
|
||||
models.HEADER_HOST_MAX_SLOT,
|
||||
models.HEADER_HOST_NUM_RUN,
|
||||
models.HEADER_HOST_NUM_SSUSP,
|
||||
models.HEADER_HOST_NUM_USUSP,
|
||||
models.HEADER_HOST_HOST_TYPE,
|
||||
}
|
||||
for _, field := range defaultFields {
|
||||
fieldFormats = append(fieldFormats, &fieldFormat{name: field, align: "-"})
|
||||
}
|
||||
} else {
|
||||
// parse user specified fields
|
||||
for _, field := range strings.Split(p.fields, ",") {
|
||||
fieldFormats = append(fieldFormats, parseFieldFormat(field))
|
||||
}
|
||||
}
|
||||
return fieldFormats
|
||||
}
|
||||
|
||||
// init column widths
|
||||
func (p *HostsPagePrinter) initColumnWidths(hosts *models.HostsResponse) {
|
||||
// init column widths to header length
|
||||
for _, format := range p.fieldFormats {
|
||||
p.columnWidths[format.name] = len(format.name)
|
||||
if format.width > 0 {
|
||||
p.columnWidths[format.name] = format.width
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the max width of the first page data
|
||||
for _, host := range hosts.Data.Rows {
|
||||
for _, format := range p.fieldFormats {
|
||||
width := 0
|
||||
switch format.name {
|
||||
case models.HEADER_HOST_NAME:
|
||||
width = len(host.HostName)
|
||||
case models.HEADER_HOST_STATUS:
|
||||
width = len(host.HostStatus)
|
||||
case models.HEADER_HOST_HOST_TYPE:
|
||||
width = len(host.HostType)
|
||||
case models.HEADER_HOST_NCPUS:
|
||||
width = len(fmt.Sprintf("%d", host.NumCPUs))
|
||||
case models.HEADER_HOST_MAX_SLOT:
|
||||
width = len(fmt.Sprintf("%d", host.MaxSlot))
|
||||
case models.HEADER_HOST_NUM_RUN:
|
||||
width = len(fmt.Sprintf("%d", host.NumRun))
|
||||
case models.HEADER_HOST_NUM_SSUSP:
|
||||
width = len(fmt.Sprintf("%d", host.NumSSusp))
|
||||
case models.HEADER_HOST_NUM_USUSP:
|
||||
width = len(fmt.Sprintf("%d", host.NumUSusp))
|
||||
}
|
||||
|
||||
if !p.wideFormat && width > 15 {
|
||||
width = 15
|
||||
}
|
||||
|
||||
if width > p.columnWidths[format.name] && format.width == 0 {
|
||||
p.columnWidths[format.name] = width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintPage
|
||||
func (p *HostsPagePrinter) PrintPage(page interface{}) {
|
||||
hosts, ok := page.(*models.HostsResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to HostsPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
p.pageCount++
|
||||
|
||||
if p.verbose {
|
||||
printHostsVerboseOutput(hosts)
|
||||
return
|
||||
}
|
||||
|
||||
if p.formatJson {
|
||||
p.printJsonPage(hosts)
|
||||
return
|
||||
} else {
|
||||
// print header (if needed)
|
||||
p.printHeader()
|
||||
|
||||
// print data rows
|
||||
rows := p.formatRows(hosts, true)
|
||||
for _, row := range rows {
|
||||
fmt.Fprintln(p.w, strings.Join(row, "\t"))
|
||||
}
|
||||
p.w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// format data rows
|
||||
func (p *HostsPagePrinter) formatRows(hosts *models.HostsResponse, truncatFlag bool) [][]string {
|
||||
var rows [][]string
|
||||
if p.wideFormat {
|
||||
truncatFlag = false
|
||||
}
|
||||
for _, host := range hosts.Data.Rows {
|
||||
row := make([]string, 0, len(p.fieldFormats))
|
||||
for _, format := range p.fieldFormats {
|
||||
width := p.columnWidths[format.name]
|
||||
align := format.align
|
||||
switch format.name {
|
||||
case models.HEADER_HOST_NAME:
|
||||
row = appendOrDefault(row, FormatStringField(host.HostName, width, truncatFlag, true), "-")
|
||||
case models.HEADER_HOST_STATUS:
|
||||
row = appendOrDefault(row, FormatStringField(host.HostStatus, width, truncatFlag, true), "-")
|
||||
case models.HEADER_HOST_HOST_TYPE:
|
||||
row = appendOrDefault(row, FormatStringField(host.HostType, width, truncatFlag, true), "-")
|
||||
case models.HEADER_HOST_NCPUS:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, host.NumCPUs), "0")
|
||||
case models.HEADER_HOST_MAX_SLOT:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, host.MaxSlot), "0")
|
||||
case models.HEADER_HOST_NUM_RUN:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, host.NumRun), "0")
|
||||
case models.HEADER_HOST_NUM_SSUSP:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, host.NumSSusp), "0")
|
||||
case models.HEADER_HOST_NUM_USUSP:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, host.NumUSusp), "0")
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// print JSON page
|
||||
func (p *HostsPagePrinter) printJsonPage(hosts *models.HostsResponse) {
|
||||
// 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(hosts, false)
|
||||
p.PrintJsonPageGeneric(rows, headers)
|
||||
}
|
||||
|
||||
func printHostsVerboseOutput(hosts *models.HostsResponse) {
|
||||
for _, host := range hosts.Data.Rows {
|
||||
fmt.Printf("HOST %s\n", host.HostName)
|
||||
// table 1
|
||||
headers := models.HostDetailHeaders
|
||||
row1 := make([]string, 0, len(headers))
|
||||
widths1 := make([]int, len(headers))
|
||||
for i, h := range headers {
|
||||
var val string
|
||||
switch h {
|
||||
case models.HEADER_HOST_STATUS:
|
||||
val = host.HostStatus
|
||||
case models.HEADER_HOST_CPUF:
|
||||
val = fmt.Sprintf("%.2f", host.CPUFactor)
|
||||
case models.HEADER_HOST_JLU:
|
||||
val = "-"
|
||||
case models.HEADER_HOST_MAX:
|
||||
val = fmt.Sprintf("%d", host.MaxSlot)
|
||||
case models.HEADER_HOST_NJOBS:
|
||||
val = fmt.Sprintf("%d", host.NumRun)
|
||||
case models.HEADER_HOST_RUN:
|
||||
val = fmt.Sprintf("%d", host.NumRun)
|
||||
case models.HEADER_HOST_SSUSP:
|
||||
val = fmt.Sprintf("%d", host.NumSSusp)
|
||||
case models.HEADER_HOST_USUSP:
|
||||
val = fmt.Sprintf("%d", host.NumUSusp)
|
||||
case models.HEADER_HOST_RSV:
|
||||
val = "-"
|
||||
case models.HEADER_HOST_DISPT_WINDOW:
|
||||
if str, ok := host.DisptWindow.(string); ok {
|
||||
val = str
|
||||
} else {
|
||||
val = "-"
|
||||
}
|
||||
default:
|
||||
val = "-"
|
||||
}
|
||||
widths1[i] = max(len(h), len(val))
|
||||
row1 = append(row1, FormatStringField(val, widths1[i], false, true))
|
||||
}
|
||||
tp1 := TablePrinter{Headers: headers, Rows: [][]string{row1}, ColWidths: widths1}
|
||||
tp1.Print()
|
||||
fmt.Println("")
|
||||
|
||||
fmt.Println("CURRENT LOAD USED FOR SCHEDULING:")
|
||||
|
||||
// table2
|
||||
headers2 := append([]string{""}, models.HostLoadSchedulingHeaders...)
|
||||
totalVals := []string{"Total", "0.0", "0.0", "0.0", "1%", "0.0", "33", "1", "1137", "51G", "3.9G", "6.8G", "8", "0.0"}
|
||||
reserveVals := []string{"Reserved", "0.0", "0.0", "0.0", "0%", "0.0", "0", "0", "0", "0M", "0M", "0M", "-", "-"}
|
||||
|
||||
widths2 := make([]int, len(headers2))
|
||||
for i := range headers2 {
|
||||
widths2[i] = max(len(headers2[i]), len(totalVals[i]))
|
||||
widths2[i] = max(widths2[i], len(reserveVals[i]))
|
||||
}
|
||||
|
||||
rowTotal := make([]string, len(headers2))
|
||||
rowReserve := make([]string, len(headers2))
|
||||
for i := range headers2 {
|
||||
rowTotal[i] = FormatStringField(totalVals[i], widths2[i], false, true)
|
||||
rowReserve[i] = FormatStringField(reserveVals[i], widths2[i], false, true)
|
||||
}
|
||||
|
||||
tp2 := TablePrinter{Headers: headers2, Rows: [][]string{rowTotal, rowReserve}, ColWidths: widths2}
|
||||
tp2.Print()
|
||||
fmt.Println("")
|
||||
|
||||
// Table 3
|
||||
fmt.Println("LOAD THRESHOLD USED FOR SCHEDULING:")
|
||||
|
||||
headers3 := append([]string{""}, models.HostLoadSchedulingHeaders...)
|
||||
loadSched := []string{"loadSched", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"}
|
||||
loadStop := []string{"loadStop", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"}
|
||||
|
||||
widths3 := make([]int, len(headers3))
|
||||
for i := range headers3 {
|
||||
widths3[i] = max(len(headers3[i]), len(loadSched[i]))
|
||||
widths3[i] = max(widths3[i], len(loadStop[i]))
|
||||
}
|
||||
|
||||
rowSched := make([]string, len(headers3))
|
||||
rowStop := make([]string, len(headers3))
|
||||
for i := range headers3 {
|
||||
rowSched[i] = FormatStringField(loadSched[i], widths2[i], false, true)
|
||||
rowStop[i] = FormatStringField(loadStop[i], widths2[i], false, true)
|
||||
}
|
||||
|
||||
tp3 := TablePrinter{Headers: headers3, Rows: [][]string{rowSched, rowStop}, ColWidths: widths2}
|
||||
tp3.Print()
|
||||
fmt.Println("")
|
||||
fmt.Println("CONFIGURED AFFINITY CPU LIST: all")
|
||||
}
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
447
src/frontend/xcecli/common/output/page_print_jobs.go
Normal file
447
src/frontend/xcecli/common/output/page_print_jobs.go
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"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(fmt.Sprintf("%d", job.JobID))
|
||||
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(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:
|
||||
var arrayJobIDStr string
|
||||
if job.ArrayIdx != 0 {
|
||||
arrayJobIDStr = fmt.Sprintf("%d[%d]", job.JobID, job.ArrayIdx)
|
||||
} else {
|
||||
arrayJobIDStr = fmt.Sprintf("%d", job.JobID)
|
||||
}
|
||||
mainRow = appendOrDefault(mainRow,
|
||||
fmt.Sprintf("%"+align+"*s", 20, arrayJobIDStr), "-")
|
||||
|
||||
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 {
|
||||
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:
|
||||
if submitTime, err := parseJobTime(job.SubmitTime); err == nil {
|
||||
mainRow = appendOrDefault(mainRow, FormatStringField(formatJobTime(submitTime), width, truncatFlag, true), "-")
|
||||
} else {
|
||||
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*s", width, "-"), "-")
|
||||
}
|
||||
|
||||
// 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(job.StartTime, width, truncatFlag, true), "-")
|
||||
case models.HEADER_JOB_END_TIME:
|
||||
mainRow = appendOrDefault(mainRow, FormatStringField(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
|
||||
}
|
||||
}
|
||||
// generate jobId for two format: 111 or 111[10]
|
||||
jobId := fmt.Sprintf("%d", job.JobID)
|
||||
index := strings.Index(job.JobName, "[")
|
||||
if index != -1 {
|
||||
jobId = fmt.Sprintf("%s%s", jobId, job.JobName[index:])
|
||||
}
|
||||
|
||||
// (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, err := parseJobTime(job.SubmitTime); err == nil {
|
||||
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 start time info
|
||||
startLine := ""
|
||||
if startTime, err := parseJobTime(job.StartTime); err == nil {
|
||||
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)
|
||||
|
||||
// (4) print end line
|
||||
endLine := ""
|
||||
if job.JobStatus == "DONE" || job.JobStatus == "EXIT" {
|
||||
if endTime, err := parseJobTime(job.EndTime); err == nil {
|
||||
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, "------------------------------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
}
|
||||
268
src/frontend/xcecli/common/output/page_print_queues.go
Normal file
268
src/frontend/xcecli/common/output/page_print_queues.go
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"xcecli/models"
|
||||
)
|
||||
|
||||
// HostsPagePrinter
|
||||
type QueuesPagePrinter struct {
|
||||
*BasePagePrinter
|
||||
}
|
||||
|
||||
// NewHostsPagePrinter
|
||||
func NewQueuesPagePrinter(opt ...BasePageOption) *QueuesPagePrinter {
|
||||
return &QueuesPagePrinter{
|
||||
BasePagePrinter: newBasePagePrinter(opt...),
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareForPrinting
|
||||
func (p *QueuesPagePrinter) PrepareForPrinting(firstPage interface{}) {
|
||||
queues, ok := firstPage.(*models.QueuesResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to QueuesPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
// set field formats
|
||||
p.fieldFormats = p.prepareFieldFormats()
|
||||
|
||||
// init column widths
|
||||
p.initColumnWidths(queues)
|
||||
|
||||
// prepare headers
|
||||
p.prepareHeaders()
|
||||
}
|
||||
|
||||
// prepare field formats
|
||||
func (p *QueuesPagePrinter) prepareFieldFormats() []*fieldFormat {
|
||||
var fieldFormats []*fieldFormat
|
||||
if p.fields == "" {
|
||||
// default fields
|
||||
defaultFields := []string{
|
||||
models.HEADER_QUEUE_NAME,
|
||||
models.HEADER_QUEUE_PRIORITY,
|
||||
models.HEADER_QUEUE_STATUS,
|
||||
models.HEADER_QUEUE_NUM_PEND,
|
||||
models.HEADER_QUEUE_NUM_RUN,
|
||||
models.HEADER_QUEUE_NUM_SSUSP,
|
||||
models.HEADER_QUEUE_NUM_USUSP,
|
||||
models.HEADER_QUEUE_NUM_DONE,
|
||||
}
|
||||
for _, field := range defaultFields {
|
||||
fieldFormats = append(fieldFormats, &fieldFormat{name: field, align: "-"})
|
||||
}
|
||||
} else {
|
||||
// parse user specified fields
|
||||
for _, field := range strings.Split(p.fields, ",") {
|
||||
fieldFormats = append(fieldFormats, parseFieldFormat(field))
|
||||
}
|
||||
}
|
||||
return fieldFormats
|
||||
}
|
||||
|
||||
// init column widths
|
||||
func (p *QueuesPagePrinter) initColumnWidths(queues *models.QueuesResponse) {
|
||||
// init column widths to header length
|
||||
for _, format := range p.fieldFormats {
|
||||
p.columnWidths[format.name] = len(format.name)
|
||||
if format.width > 0 {
|
||||
p.columnWidths[format.name] = format.width
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the max width of the first page data
|
||||
for _, queue := range queues.Data.Rows {
|
||||
for _, format := range p.fieldFormats {
|
||||
width := 0
|
||||
switch format.name {
|
||||
case models.HEADER_QUEUE_NAME:
|
||||
width = len(queue.QueueName)
|
||||
case models.HEADER_QUEUE_PRIORITY:
|
||||
width = len(fmt.Sprintf("%d", queue.Priority))
|
||||
case models.HEADER_QUEUE_STATUS:
|
||||
width = len(queue.QueueStatus)
|
||||
case models.HEADER_QUEUE_NUM_PEND:
|
||||
width = len(fmt.Sprintf("%d", queue.NumPend))
|
||||
case models.HEADER_QUEUE_NUM_RUN:
|
||||
width = len(fmt.Sprintf("%d", queue.NumRun))
|
||||
case models.HEADER_QUEUE_NUM_SSUSP:
|
||||
width = len(fmt.Sprintf("%d", queue.NumSSusp))
|
||||
case models.HEADER_QUEUE_NUM_USUSP:
|
||||
width = len(fmt.Sprintf("%d", queue.NumUSusp))
|
||||
case models.HEADER_QUEUE_NUM_DONE:
|
||||
width = len(fmt.Sprintf("%d", queue.NumDone))
|
||||
}
|
||||
|
||||
if !p.wideFormat && width > 15 {
|
||||
width = 15
|
||||
}
|
||||
|
||||
if width > p.columnWidths[format.name] && format.width == 0 {
|
||||
p.columnWidths[format.name] = width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintPage
|
||||
func (p *QueuesPagePrinter) PrintPage(page interface{}) {
|
||||
queues, ok := page.(*models.QueuesResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to QueuesPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
p.pageCount++
|
||||
|
||||
if p.verbose {
|
||||
printQueuesVerboseOutput(queues)
|
||||
return
|
||||
}
|
||||
|
||||
if p.formatJson {
|
||||
p.printJsonPage(queues)
|
||||
return
|
||||
} else {
|
||||
// print header (if needed)
|
||||
p.printHeader()
|
||||
// print data rows
|
||||
rows := p.formatRows(queues, true)
|
||||
for _, row := range rows {
|
||||
fmt.Fprintln(p.w, strings.Join(row, "\t"))
|
||||
}
|
||||
p.w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// format data rows
|
||||
func (p *QueuesPagePrinter) formatRows(queues *models.QueuesResponse, truncatFlag bool) [][]string {
|
||||
var rows [][]string
|
||||
if p.wideFormat {
|
||||
truncatFlag = false
|
||||
}
|
||||
for _, queue := range queues.Data.Rows {
|
||||
row := make([]string, 0, len(p.fieldFormats))
|
||||
for _, format := range p.fieldFormats {
|
||||
width := p.columnWidths[format.name]
|
||||
align := format.align
|
||||
switch format.name {
|
||||
case models.HEADER_QUEUE_NAME:
|
||||
row = appendOrDefault(row, FormatStringField(queue.QueueName, width, truncatFlag, true), "-")
|
||||
case models.HEADER_QUEUE_STATUS:
|
||||
row = appendOrDefault(row, FormatStringField(queue.QueueStatus, width, truncatFlag, true), "-")
|
||||
case models.HEADER_QUEUE_PRIORITY:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, queue.Priority), "0")
|
||||
case models.HEADER_QUEUE_NUM_PEND:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, queue.NumPend), "0")
|
||||
case models.HEADER_QUEUE_NUM_RUN:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, queue.NumRun), "0")
|
||||
case models.HEADER_QUEUE_NUM_SSUSP:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, queue.NumSSusp), "0")
|
||||
case models.HEADER_QUEUE_NUM_USUSP:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, queue.NumUSusp), "0")
|
||||
case models.HEADER_QUEUE_NUM_DONE:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, queue.NumDone), "0")
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// print JSON page
|
||||
func (p *QueuesPagePrinter) printJsonPage(queues *models.QueuesResponse) {
|
||||
// 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(queues, false)
|
||||
p.PrintJsonPageGeneric(rows, headers)
|
||||
}
|
||||
|
||||
func printQueuesVerboseOutput(queues *models.QueuesResponse) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
defer w.Flush()
|
||||
|
||||
for _, queue := range queues.Data.Rows {
|
||||
fmt.Printf("QUEUE: %s\n", queue.QueueName)
|
||||
if queue.Description != "" {
|
||||
fmt.Fprintf(w, "-- %s\n", queue.Description)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Printf("PARAMETERS/STATISTICS:\n")
|
||||
// table 1
|
||||
headers := models.QueueDetailHeaders
|
||||
row1 := make([]string, 0, len(headers))
|
||||
widths1 := make([]int, len(headers))
|
||||
|
||||
for i, q := range headers {
|
||||
var val string
|
||||
switch q {
|
||||
case models.HEADER_QUEUE_PRIO:
|
||||
val = fmt.Sprintf("%d", queue.Priority)
|
||||
case models.HEADER_QUEUE_NICE:
|
||||
val = "-"
|
||||
case models.HEADER_QUEUE_STATUS:
|
||||
val = queue.QueueStatus
|
||||
case models.HEADER_QUEUE_MAX:
|
||||
val = "-"
|
||||
case models.HEADER_QUEUE_JLU:
|
||||
val = "-"
|
||||
case models.HEADER_QUEUE_JLP:
|
||||
val = "-"
|
||||
case models.HEADER_QUEUE_JLH:
|
||||
val = "-"
|
||||
case models.HEADER_QUEUE_NJOBS:
|
||||
val = "-"
|
||||
case models.HEADER_QUEUE_PEND:
|
||||
val = fmt.Sprintf("%d", queue.NumPend)
|
||||
case models.HEADER_QUEUE_RUN:
|
||||
val = fmt.Sprintf("%d", queue.NumRun)
|
||||
case models.HEADER_QUEUE_SSUSP:
|
||||
val = fmt.Sprintf("%d", queue.NumSSusp)
|
||||
case models.HEADER_QUEUE_USUSP:
|
||||
val = fmt.Sprintf("%d", queue.NumUSusp)
|
||||
case models.HEADER_HOST_RSV:
|
||||
val = "-"
|
||||
default:
|
||||
val = "-"
|
||||
}
|
||||
widths1[i] = max(len(q), len(val))
|
||||
row1 = append(row1, FormatStringField(val, widths1[i], false, true))
|
||||
}
|
||||
tp1 := TablePrinter{Headers: headers, Rows: [][]string{row1}, ColWidths: widths1}
|
||||
tp1.Print()
|
||||
fmt.Println()
|
||||
|
||||
// table 2
|
||||
fmt.Printf("SCHEDULING PARAMETERS\n")
|
||||
|
||||
headers2 := append([]string{""}, models.QueueLoadSchedulingHeaders...)
|
||||
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("")
|
||||
}
|
||||
}
|
||||
365
src/frontend/xcecli/common/output/page_print_resources.go
Normal file
365
src/frontend/xcecli/common/output/page_print_resources.go
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"xcecli/models"
|
||||
)
|
||||
|
||||
type ResourcesPagePrinter struct {
|
||||
*BasePagePrinter
|
||||
isDefinition bool // Flag to distinguish between distribution and definition
|
||||
}
|
||||
|
||||
func NewResourcesDistPagePrinter(opt ...BasePageOption) *ResourcesPagePrinter {
|
||||
return &ResourcesPagePrinter{
|
||||
BasePagePrinter: newBasePagePrinter(opt...),
|
||||
isDefinition: false,
|
||||
}
|
||||
}
|
||||
|
||||
func NewResourcesDefPagePrinter(opt ...BasePageOption) *ResourcesPagePrinter {
|
||||
return &ResourcesPagePrinter{
|
||||
BasePagePrinter: newBasePagePrinter(opt...),
|
||||
isDefinition: true,
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareForPrinting supports both ResourceDistResponse and ResourceDefResponse
|
||||
func (p *ResourcesPagePrinter) PrepareForPrinting(firstPage interface{}) {
|
||||
// Check if it's ResourceDistResponse
|
||||
if resourcesDist, ok := firstPage.(*models.ResourceDistResponse); ok {
|
||||
p.isDefinition = false
|
||||
// set field formats
|
||||
p.fieldFormats = p.prepareFieldFormats()
|
||||
// init column widths
|
||||
p.initColumnWidthsForDist(resourcesDist)
|
||||
// prepare headers
|
||||
p.prepareHeaders()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's ResourceDefResponse
|
||||
if resourcesDef, ok := firstPage.(*models.ResourceDefResponse); ok {
|
||||
p.isDefinition = true
|
||||
// set field formats
|
||||
p.fieldFormats = p.prepareFieldFormats()
|
||||
// init column widths
|
||||
p.initColumnWidthsForDef(resourcesDef)
|
||||
// prepare headers
|
||||
p.prepareHeaders()
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("error: wrong data type to ResourcesPagePrinter")
|
||||
}
|
||||
|
||||
// prepare field formats
|
||||
func (p *ResourcesPagePrinter) prepareFieldFormats() []*fieldFormat {
|
||||
var fieldFormats []*fieldFormat
|
||||
if p.fields == "" {
|
||||
// default fields based on type
|
||||
if p.isDefinition {
|
||||
defaultFields := []string{
|
||||
models.HEADER_RESOURCE_NAME,
|
||||
models.HEADER_RESOURCE_DEF_TYPE,
|
||||
models.HEADER_RESOURCE_DEF_ORDER,
|
||||
models.HEADER_RESOURCE_DEF_DESC,
|
||||
}
|
||||
for _, field := range defaultFields {
|
||||
fieldFormats = append(fieldFormats, &fieldFormat{name: field, width: 20, align: "-"})
|
||||
}
|
||||
} else {
|
||||
defaultFields := []string{
|
||||
models.HEADER_RESOURCE_NAME,
|
||||
models.HEADER_RESOURCE_VALUE,
|
||||
models.HEADER_RESOURCE_LOC,
|
||||
}
|
||||
for _, field := range defaultFields {
|
||||
fieldFormats = append(fieldFormats, &fieldFormat{name: field, width: 20, align: "-"})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// parse user specified fields
|
||||
for _, field := range strings.Split(p.fields, ",") {
|
||||
fieldFormats = append(fieldFormats, parseFieldFormat(field))
|
||||
}
|
||||
}
|
||||
return fieldFormats
|
||||
}
|
||||
|
||||
// init column widths for ResourceDistResponse
|
||||
func (p *ResourcesPagePrinter) initColumnWidthsForDist(resourcesDist *models.ResourceDistResponse) {
|
||||
// init column widths to header length
|
||||
for _, format := range p.fieldFormats {
|
||||
p.columnWidths[format.name] = len(format.name)
|
||||
if format.width > 0 {
|
||||
p.columnWidths[format.name] = format.width
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the max width of the first page data
|
||||
for _, resourceDist := range resourcesDist.Data.Rows {
|
||||
for _, format := range p.fieldFormats {
|
||||
width := 15
|
||||
switch format.name {
|
||||
case models.HEADER_RESOURCE_NAME:
|
||||
width = len(resourceDist.ResourceName)
|
||||
case models.HEADER_RESOURCE_VALUE:
|
||||
width = len(resourceDist.ResourceValue.String())
|
||||
case models.HEADER_RESOURCE_LOC:
|
||||
width = len(resourceDist.ResourceLoc.String())
|
||||
}
|
||||
|
||||
if !p.wideFormat && width > 15 {
|
||||
width = 15
|
||||
}
|
||||
|
||||
if width > p.columnWidths[format.name] {
|
||||
p.columnWidths[format.name] = width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// init column widths for ResourceDefResponse
|
||||
func (p *ResourcesPagePrinter) initColumnWidthsForDef(resourcesDef *models.ResourceDefResponse) {
|
||||
// init column widths to header length
|
||||
for _, format := range p.fieldFormats {
|
||||
p.columnWidths[format.name] = len(format.name)
|
||||
if format.width > 0 {
|
||||
p.columnWidths[format.name] = format.width
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the max width of the first page data
|
||||
for _, resourceDef := range resourcesDef.Data.Rows {
|
||||
for _, format := range p.fieldFormats {
|
||||
width := 15
|
||||
switch format.name {
|
||||
case models.HEADER_RESOURCE_NAME:
|
||||
width = len(resourceDef.ResourceName)
|
||||
case models.HEADER_RESOURCE_DEF_TYPE:
|
||||
width = len(resourceDef.ResourceType)
|
||||
case models.HEADER_RESOURCE_DEF_DESC:
|
||||
width = len(resourceDef.Description)
|
||||
}
|
||||
|
||||
if !p.wideFormat && width > 15 {
|
||||
width = 15
|
||||
}
|
||||
|
||||
if width > p.columnWidths[format.name] {
|
||||
p.columnWidths[format.name] = width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintPage supports both ResourceDistResponse and ResourceDefResponse
|
||||
func (p *ResourcesPagePrinter) PrintPage(page interface{}) {
|
||||
|
||||
p.pageCount++
|
||||
|
||||
if p.isDefinition {
|
||||
resourcesDef, ok := page.(*models.ResourceDefResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to ResourcesPagePrinter")
|
||||
return
|
||||
}
|
||||
if p.verbose {
|
||||
printResourcesDefVerboseOutput(resourcesDef)
|
||||
return
|
||||
}
|
||||
}
|
||||
if p.formatJson {
|
||||
if p.isDefinition {
|
||||
if resourcesDef, ok := page.(*models.ResourceDefResponse); ok {
|
||||
p.printJsonPageForDef(resourcesDef)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if resourcesDist, ok := page.(*models.ResourceDistResponse); ok {
|
||||
p.printJsonPageForDist(resourcesDist)
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Println("error: wrong data type for JSON printing")
|
||||
return
|
||||
} else {
|
||||
// print header (if needed)
|
||||
p.printHeader()
|
||||
|
||||
// print data rows
|
||||
var rows [][]string
|
||||
if p.isDefinition {
|
||||
if resourcesDef, ok := page.(*models.ResourceDefResponse); ok {
|
||||
rows = p.formatRowsForDef(resourcesDef, true)
|
||||
} else {
|
||||
fmt.Println("error: wrong data type to format rows")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if resourcesDist, ok := page.(*models.ResourceDistResponse); ok {
|
||||
rows = p.formatRowsForDist(resourcesDist, true)
|
||||
} else {
|
||||
fmt.Println("error: wrong data type to format rows")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
fmt.Fprintln(p.w, strings.Join(row, "\t"))
|
||||
}
|
||||
p.w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// format data rows for ResourceDistResponse
|
||||
func (p *ResourcesPagePrinter) formatRowsForDist(resourcesDist *models.ResourceDistResponse, truncatFlag bool) [][]string {
|
||||
var rows [][]string
|
||||
if p.wideFormat {
|
||||
truncatFlag = false
|
||||
}
|
||||
for _, resourceDist := range resourcesDist.Data.Rows {
|
||||
row := make([]string, 0, len(p.fieldFormats))
|
||||
for _, format := range p.fieldFormats {
|
||||
width := p.columnWidths[format.name]
|
||||
switch format.name {
|
||||
case models.HEADER_RESOURCE_NAME:
|
||||
row = appendOrDefault(row, FormatStringField(resourceDist.ResourceName, width, truncatFlag, true), "-")
|
||||
case models.HEADER_RESOURCE_VALUE:
|
||||
row = appendOrDefault(row, FormatStringField(resourceDist.ResourceValue.String(), width, truncatFlag, true), "-")
|
||||
case models.HEADER_RESOURCE_LOC:
|
||||
row = appendOrDefault(row, FormatStringField(resourceDist.ResourceLoc.String(), width, truncatFlag, true), "-")
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// format data rows for ResourceDefResponse
|
||||
func (p *ResourcesPagePrinter) formatRowsForDef(resourcesDef *models.ResourceDefResponse, truncatFlag bool) [][]string {
|
||||
var rows [][]string
|
||||
if p.wideFormat {
|
||||
truncatFlag = false
|
||||
}
|
||||
for _, resourceDef := range resourcesDef.Data.Rows {
|
||||
row := make([]string, 0, len(p.fieldFormats))
|
||||
for _, format := range p.fieldFormats {
|
||||
width := p.columnWidths[format.name]
|
||||
align := format.align
|
||||
switch format.name {
|
||||
case models.HEADER_RESOURCE_NAME:
|
||||
row = appendOrDefault(row, FormatStringField(resourceDef.ResourceName, width, truncatFlag, true), "-")
|
||||
case models.HEADER_RESOURCE_DEF_DESC:
|
||||
row = appendOrDefault(row, FormatStringField(resourceDef.Description, width, truncatFlag, true), "-")
|
||||
case models.HEADER_RESOURCE_DEF_TYPE:
|
||||
row = appendOrDefault(row, FormatStringField(resourceDef.ResourceType, width, truncatFlag, true), "-")
|
||||
case models.HEADER_RESOURCE_DEF_ORDER:
|
||||
row = appendOrDefault(row, FormatStringField(resourceDef.OrderType, width, truncatFlag, true), "-")
|
||||
case models.HEADER_RESOURCE_DEF_BUILTIN:
|
||||
// Extract builtin flag from the Flag object
|
||||
builtinValue := "false"
|
||||
if resourceDef.Flag != nil {
|
||||
if builtin, ok := resourceDef.Flag["builtin"]; ok {
|
||||
builtinValue = fmt.Sprintf("%v", builtin)
|
||||
}
|
||||
}
|
||||
row = appendOrDefault(row, FormatStringField(builtinValue, width, truncatFlag, true), "-")
|
||||
case models.HEADER_RESOURCE_DEF_INTERVAL:
|
||||
row = appendOrDefault(row, fmt.Sprintf("%"+align+"*d", width, resourceDef.Interval), "0")
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// print JSON page for ResourceDistResponse
|
||||
func (p *ResourcesPagePrinter) printJsonPageForDist(resourcesDist *models.ResourceDistResponse) {
|
||||
// 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.formatRowsForDist(resourcesDist, false)
|
||||
p.PrintJsonPageGeneric(rows, headers)
|
||||
}
|
||||
|
||||
// print JSON page for ResourceDefResponse
|
||||
func (p *ResourcesPagePrinter) printJsonPageForDef(resourcesDef *models.ResourceDefResponse) {
|
||||
// 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.formatRowsForDef(resourcesDef, false)
|
||||
p.PrintJsonPageGeneric(rows, headers)
|
||||
}
|
||||
|
||||
func printResourcesDefVerboseOutput(resourceDef *models.ResourceDefResponse) {
|
||||
for _, resourceDef := range resourceDef.Data.Rows {
|
||||
fmt.Printf("RESOURCE: %s\n", resourceDef.ResourceName)
|
||||
fmt.Printf("DESCRIPTION: %s\n", resourceDef.Description)
|
||||
// table 1
|
||||
headers := models.ResourceDefDetailHeaders
|
||||
row1 := make([]string, 0, len(headers))
|
||||
widths1 := make([]int, len(headers))
|
||||
for i, h := range headers {
|
||||
var val string
|
||||
switch h {
|
||||
case models.HEADER_RESOURCE_DEF_TYPE:
|
||||
val = resourceDef.ResourceType
|
||||
case models.HEADER_RESOURCE_DEF_ORDER:
|
||||
val = resourceDef.OrderType
|
||||
case models.HEADER_RESOURCE_DEF_BUILTIN:
|
||||
// Extract builtin flag from the Flag object
|
||||
builtinValue := "false"
|
||||
if resourceDef.Flag != nil {
|
||||
if builtin, ok := resourceDef.Flag["builtin"]; ok {
|
||||
builtinValue = fmt.Sprintf("%v", builtin)
|
||||
}
|
||||
}
|
||||
val = builtinValue
|
||||
case models.HEADER_RESOURCE_DEF_CONSUMABLE:
|
||||
// Extract consumable flag from the Flag object
|
||||
consumableValue := "false"
|
||||
if resourceDef.Flag != nil {
|
||||
if consumable, ok := resourceDef.Flag["consumable"]; ok {
|
||||
consumableValue = fmt.Sprintf("%v", consumable)
|
||||
}
|
||||
}
|
||||
val = consumableValue
|
||||
case models.HEADER_RESOURCE_DEF_DYNAMIC:
|
||||
// Extract dynamic flag from the Flag object
|
||||
dynamicValue := "false"
|
||||
if resourceDef.Flag != nil {
|
||||
if dynamic, ok := resourceDef.Flag["dynamic"]; ok {
|
||||
dynamicValue = fmt.Sprintf("%v", dynamic)
|
||||
}
|
||||
}
|
||||
val = dynamicValue
|
||||
case models.HEADER_RESOURCE_DEF_RELEASE:
|
||||
// Extract release flag from the Flag object
|
||||
releaseValue := "false"
|
||||
if resourceDef.Flag != nil {
|
||||
if release, ok := resourceDef.Flag["release"]; ok {
|
||||
releaseValue = fmt.Sprintf("%v", release)
|
||||
}
|
||||
}
|
||||
val = releaseValue
|
||||
case models.HEADER_RESOURCE_DEF_INTERVAL:
|
||||
val = fmt.Sprintf("%d", resourceDef.Interval)
|
||||
}
|
||||
widths1[i] = max(len(h), len(val))
|
||||
row1 = append(row1, FormatStringField(val, widths1[i], false, true))
|
||||
}
|
||||
tp1 := TablePrinter{Headers: headers, Rows: [][]string{row1}, ColWidths: widths1}
|
||||
tp1.Print()
|
||||
fmt.Println("")
|
||||
}
|
||||
}
|
||||
191
src/frontend/xcecli/common/output/page_print_roles.go
Normal file
191
src/frontend/xcecli/common/output/page_print_roles.go
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"xcecli/models"
|
||||
)
|
||||
|
||||
// HostsPagePrinter
|
||||
type RolesPagePrinter struct {
|
||||
*BasePagePrinter
|
||||
}
|
||||
|
||||
// NewHostsPagePrinter
|
||||
func NewRolesPagePrinter(opt ...BasePageOption) *RolesPagePrinter {
|
||||
return &RolesPagePrinter{
|
||||
BasePagePrinter: newBasePagePrinter(opt...),
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareForPrinting
|
||||
func (p *RolesPagePrinter) PrepareForPrinting(firstPage interface{}) {
|
||||
roles, ok := firstPage.(*models.RolesResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to RolesPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
// set field formats
|
||||
p.fieldFormats = p.prepareFieldFormats()
|
||||
|
||||
// init column widths
|
||||
p.initColumnWidths(roles)
|
||||
|
||||
// prepare headers
|
||||
p.prepareHeaders()
|
||||
}
|
||||
|
||||
// prepare field formats
|
||||
func (p *RolesPagePrinter) prepareFieldFormats() []*fieldFormat {
|
||||
// here define the default fields based on the host data
|
||||
var fieldFormats []*fieldFormat
|
||||
if p.fields == "" {
|
||||
// default fields
|
||||
defaultFields := []string{
|
||||
models.HEADER_ROLE_NAME,
|
||||
models.HEADER_ROLE_PERM_NAME,
|
||||
models.HEADER_ROLE_DOMAIN_TYPE,
|
||||
models.HEADER_ROLE_DOMAIN_VALUES,
|
||||
}
|
||||
for _, field := range defaultFields {
|
||||
fieldFormats = append(fieldFormats, &fieldFormat{name: field, align: "-"})
|
||||
}
|
||||
} else {
|
||||
// parse user specified fields
|
||||
for _, field := range strings.Split(p.fields, ",") {
|
||||
fieldFormats = append(fieldFormats, parseFieldFormat(field))
|
||||
}
|
||||
}
|
||||
return fieldFormats
|
||||
}
|
||||
|
||||
// init column widths
|
||||
func (p *RolesPagePrinter) initColumnWidths(roles *models.RolesResponse) {
|
||||
// init column widths to header length
|
||||
for _, format := range p.fieldFormats {
|
||||
p.columnWidths[format.name] = len(format.name)
|
||||
if format.width > 0 {
|
||||
p.columnWidths[format.name] = format.width
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the max width of the first page data
|
||||
for _, role := range roles.Data.Rows {
|
||||
for _, format := range p.fieldFormats {
|
||||
width := 0
|
||||
switch format.name {
|
||||
case models.HEADER_ROLE_NAME:
|
||||
width = len(role.RoleName)
|
||||
case models.HEADER_ROLE_PERM_NAME:
|
||||
width = len(role.PermName)
|
||||
case models.HEADER_ROLE_DOMAIN_TYPE:
|
||||
width = len(role.DomainType)
|
||||
}
|
||||
if !p.wideFormat && width > 15 {
|
||||
width = 15
|
||||
}
|
||||
|
||||
if width > p.columnWidths[format.name] && format.width == 0 {
|
||||
p.columnWidths[format.name] = width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintPage
|
||||
func (p *RolesPagePrinter) PrintPage(page interface{}) {
|
||||
roles, ok := page.(*models.RolesResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to RolesPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
p.pageCount++
|
||||
|
||||
if p.verbose {
|
||||
printRolesVerboseOutput(roles)
|
||||
return
|
||||
}
|
||||
|
||||
if p.formatJson {
|
||||
p.printJsonPage(roles)
|
||||
return
|
||||
} else {
|
||||
// print header (if needed)
|
||||
p.printHeader()
|
||||
// print data rows
|
||||
rows := p.formatRows(roles, true)
|
||||
for _, row := range rows {
|
||||
fmt.Fprintln(p.w, strings.Join(row, "\t"))
|
||||
}
|
||||
p.w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// format data rows
|
||||
func (p *RolesPagePrinter) formatRows(roles *models.RolesResponse, truncatFlag bool) [][]string {
|
||||
var rows [][]string
|
||||
if p.wideFormat {
|
||||
truncatFlag = false
|
||||
}
|
||||
|
||||
for _, role := range roles.Data.Rows {
|
||||
// If role has no permissions, create one row with empty permission fields
|
||||
|
||||
row := make([]string, 0, len(p.fieldFormats))
|
||||
for _, format := range p.fieldFormats {
|
||||
width := p.columnWidths[format.name]
|
||||
switch format.name {
|
||||
case models.HEADER_ROLE_NAME:
|
||||
row = appendOrDefault(row, FormatStringField(role.RoleName, width, truncatFlag, true), "-")
|
||||
case models.HEADER_ROLE_PERM_NAME:
|
||||
row = appendOrDefault(row, FormatStringField(role.PermName, width, truncatFlag, true), "-")
|
||||
case models.HEADER_ROLE_DOMAIN_TYPE:
|
||||
row = appendOrDefault(row, FormatStringField(role.DomainType, width, truncatFlag, true), "-")
|
||||
case models.HEADER_ROLE_DOMAIN_VALUES:
|
||||
domainValues := "-"
|
||||
if role.DomainValues != nil {
|
||||
// Handle as []string since that's what the API returns
|
||||
if vals, ok := role.DomainValues.([]string); ok && len(vals) > 0 {
|
||||
domainValues = strings.Join(vals, ", ")
|
||||
} else if vals, ok := role.DomainValues.([]interface{}); ok && len(vals) > 0 {
|
||||
// Alternative: handle as []interface{} and convert to strings
|
||||
strVals := make([]string, len(vals))
|
||||
for i, v := range vals {
|
||||
strVals[i] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
domainValues = strings.Join(strVals, ", ")
|
||||
}
|
||||
}
|
||||
row = appendOrDefault(row, FormatStringField(domainValues, width, truncatFlag, true), "-")
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// print JSON page
|
||||
func (p *RolesPagePrinter) printJsonPage(roles *models.RolesResponse) {
|
||||
// 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(roles, false)
|
||||
p.PrintJsonPageGeneric(rows, headers)
|
||||
}
|
||||
|
||||
func printRolesVerboseOutput(roles *models.RolesResponse) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
defer w.Flush()
|
||||
|
||||
for _, role := range roles.Data.Rows {
|
||||
fmt.Printf("Role: %s\n", role.RoleName)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
146
src/frontend/xcecli/common/output/page_print_users.go
Normal file
146
src/frontend/xcecli/common/output/page_print_users.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"xcecli/models"
|
||||
)
|
||||
|
||||
type UserRolesPagePrinter struct {
|
||||
*BasePagePrinter
|
||||
}
|
||||
|
||||
func NewUserRolesPagePrinter(opt ...BasePageOption) *UserRolesPagePrinter {
|
||||
return &UserRolesPagePrinter{
|
||||
BasePagePrinter: newBasePagePrinter(opt...),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserRolesPagePrinter) PrepareForPrinting(firstPage interface{}) {
|
||||
userroles, ok := firstPage.(*models.UserRolesResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to UserRolesPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
// set field formats
|
||||
p.fieldFormats = p.prepareFieldFormats()
|
||||
|
||||
// init column widths
|
||||
p.initColumnWidths(userroles)
|
||||
|
||||
// prepare headers
|
||||
p.prepareHeaders()
|
||||
}
|
||||
|
||||
// prepare field formats
|
||||
func (p *UserRolesPagePrinter) prepareFieldFormats() []*fieldFormat {
|
||||
var fieldFormats []*fieldFormat
|
||||
if p.fields == "" {
|
||||
// default fields
|
||||
defaultFields := []string{
|
||||
models.HEADER_USER_NAME,
|
||||
models.HEADER_USERROLE_NAME,
|
||||
}
|
||||
for _, field := range defaultFields {
|
||||
fieldFormats = append(fieldFormats, &fieldFormat{name: field, align: "-"})
|
||||
}
|
||||
} else {
|
||||
// parse user specified fields
|
||||
for _, field := range strings.Split(p.fields, ",") {
|
||||
fieldFormats = append(fieldFormats, parseFieldFormat(field))
|
||||
}
|
||||
}
|
||||
return fieldFormats
|
||||
}
|
||||
|
||||
// init column widths
|
||||
func (p *UserRolesPagePrinter) initColumnWidths(users *models.UserRolesResponse) {
|
||||
// init column widths to header length
|
||||
for _, format := range p.fieldFormats {
|
||||
p.columnWidths[format.name] = len(format.name)
|
||||
if format.width > 0 {
|
||||
p.columnWidths[format.name] = format.width
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the max width of the first page data
|
||||
for _, user := range users.Data.Rows {
|
||||
for _, format := range p.fieldFormats {
|
||||
width := 0
|
||||
switch format.name {
|
||||
case models.HEADER_USER_NAME:
|
||||
width = len(user.UserName)
|
||||
case models.HEADER_USERROLE_NAME:
|
||||
width = len(user.UserRoleName)
|
||||
}
|
||||
if !p.wideFormat && width > 15 {
|
||||
width = 15
|
||||
}
|
||||
|
||||
if width > p.columnWidths[format.name] && format.width == 0 {
|
||||
p.columnWidths[format.name] = width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintPage
|
||||
func (p *UserRolesPagePrinter) PrintPage(page interface{}) {
|
||||
userroles, ok := page.(*models.UserRolesResponse)
|
||||
if !ok {
|
||||
fmt.Println("error: wrong data type to UserRolesPagePrinter")
|
||||
return
|
||||
}
|
||||
|
||||
p.pageCount++
|
||||
|
||||
if p.formatJson {
|
||||
p.printJsonPage(userroles)
|
||||
return
|
||||
} else {
|
||||
// print header (if needed)
|
||||
p.printHeader()
|
||||
// print data rows
|
||||
rows := p.formatRows(userroles, true)
|
||||
for _, row := range rows {
|
||||
fmt.Fprintln(p.w, strings.Join(row, "\t"))
|
||||
}
|
||||
p.w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// format data rows
|
||||
func (p *UserRolesPagePrinter) formatRows(userroles *models.UserRolesResponse, truncatFlag bool) [][]string {
|
||||
var rows [][]string
|
||||
if p.wideFormat {
|
||||
truncatFlag = false
|
||||
}
|
||||
|
||||
for _, user := range userroles.Data.Rows {
|
||||
row := make([]string, 0, len(p.fieldFormats))
|
||||
for _, format := range p.fieldFormats {
|
||||
width := p.columnWidths[format.name]
|
||||
switch format.name {
|
||||
case models.HEADER_USER_NAME:
|
||||
row = appendOrDefault(row, FormatStringField(user.UserName, width, truncatFlag, true), "-")
|
||||
case models.HEADER_USERROLE_NAME:
|
||||
row = appendOrDefault(row, FormatStringField(user.UserRoleName, width, truncatFlag, true), "-")
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// print JSON page
|
||||
func (p *UserRolesPagePrinter) printJsonPage(userroles *models.UserRolesResponse) {
|
||||
// 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(userroles, false)
|
||||
p.PrintJsonPageGeneric(rows, headers)
|
||||
}
|
||||
24
src/frontend/xcecli/common/output/time.go
Normal file
24
src/frontend/xcecli/common/output/time.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// parseJobTime parses a job time string supporting RFC3339 and space-separated formats.
|
||||
func parseJobTime(s string) (time.Time, error) {
|
||||
if s == "" {
|
||||
return time.Time{}, fmt.Errorf("empty time string")
|
||||
}
|
||||
for _, f := range []string{time.RFC3339Nano, time.RFC3339, "2006-01-02 15:04:05", "2006-01-02 15:04:05 MST"} {
|
||||
if t, err := time.Parse(f, s); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("cannot parse time: %q", s)
|
||||
}
|
||||
|
||||
// formatJobTime formats a time.Time as "Jan 02 15:04" for table display.
|
||||
func formatJobTime(t time.Time) string {
|
||||
return fmt.Sprintf("%s %02d %02d:%02d", t.Format("Jan"), t.Day(), t.Hour(), t.Minute())
|
||||
}
|
||||
39
src/frontend/xcecli/common/output/time_test.go
Normal file
39
src/frontend/xcecli/common/output/time_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseJobTime_RFC3339(t *testing.T) {
|
||||
got, err := parseJobTime("2026-04-08T17:00:10.159507024+08:00")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Year() != 2026 || got.Month() != time.April || got.Day() != 8 {
|
||||
t.Errorf("wrong date: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJobTime_SpaceFormat(t *testing.T) {
|
||||
got, err := parseJobTime("2026-04-08 17:00:10")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Hour() != 17 {
|
||||
t.Errorf("wrong hour: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJobTime_Empty(t *testing.T) {
|
||||
if _, err := parseJobTime(""); err == nil {
|
||||
t.Error("expected error for empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatJobTime(t *testing.T) {
|
||||
tm := time.Date(2026, time.April, 8, 17, 0, 10, 0, time.UTC)
|
||||
if got := formatJobTime(tm); got != "Apr 08 17:00" {
|
||||
t.Errorf("got %q want %q", got, "Apr 08 17:00")
|
||||
}
|
||||
}
|
||||
390
src/frontend/xcecli/common/request/request.go
Executable file
390
src/frontend/xcecli/common/request/request.go
Executable file
|
|
@ -0,0 +1,390 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
logger "xcecli/config"
|
||||
|
||||
"xcecli/models"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"resty.dev/v3"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type APIResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// APIClient define the API client
|
||||
type APIClient struct {
|
||||
Client *resty.Client
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
// NewAPIClient create a new API client
|
||||
func NewAPIClient(baseURL string) *APIClient {
|
||||
c := &APIClient{
|
||||
Client: resty.New(),
|
||||
BaseURL: baseURL,
|
||||
}
|
||||
//c.Client.SetDebug(true)
|
||||
c.Client.SetTimeout(10 * time.Second)
|
||||
//c.client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *APIClient) SetRootCAs(certPath string) {
|
||||
if !strings.HasPrefix(c.BaseURL, "https://") {
|
||||
return
|
||||
}
|
||||
if certPath == "" {
|
||||
c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) //nolint:gosec
|
||||
return
|
||||
}
|
||||
pool := loadRootCAs(certPath)
|
||||
c.Client.SetTLSClientConfig(&tls.Config{RootCAs: pool})
|
||||
}
|
||||
|
||||
// loadRootCAs load the root certificate
|
||||
func loadRootCAs(certPath string) *x509.CertPool {
|
||||
certPool := x509.NewCertPool()
|
||||
|
||||
cert, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
logger.Error("failed to read the cert file")
|
||||
return nil
|
||||
}
|
||||
if ok := certPool.AppendCertsFromPEM(cert); !ok {
|
||||
logger.Error("failed to append the cert")
|
||||
return nil
|
||||
}
|
||||
return certPool
|
||||
}
|
||||
|
||||
// Logon perform the login operation
|
||||
func (c *APIClient) Logon(username, password string) (*models.LogonResponse, error) {
|
||||
var loginResp models.LogonResponse
|
||||
|
||||
resp, err := c.Client.R().
|
||||
SetBody(map[string]interface{}{
|
||||
"username": username,
|
||||
"password": password,
|
||||
}).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetResult(&loginResp).
|
||||
Post(c.BaseURL + "/xce/v1/auth/logon")
|
||||
|
||||
if err != nil {
|
||||
errMsg := ExtractErrorMessage(err)
|
||||
return nil, fmt.Errorf("login failed: [%s]", errMsg)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK || loginResp.Code != http.StatusOK {
|
||||
return nil, HandleErrorResponse(resp, "logon request")
|
||||
//return nil, fmt.Errorf("login failed: %s", loginResp.Msg)
|
||||
}
|
||||
loginResp.Data.CreateTime = time.Now().Unix()
|
||||
// Set token expire time from server response
|
||||
return &loginResp, nil
|
||||
}
|
||||
|
||||
func (c *APIClient) GetRequestFromAPIServer(token, pageToken, certPath, dst string, resp any, params map[string]string) error {
|
||||
baseURL := c.BaseURL
|
||||
if strings.HasPrefix(baseURL, "https://") {
|
||||
c.Client.SetTLSClientConfig(&tls.Config{
|
||||
RootCAs: loadRootCAs(certPath),
|
||||
})
|
||||
}
|
||||
if idx := strings.Index(baseURL, dst); idx != -1 {
|
||||
baseURL = baseURL[:idx]
|
||||
}
|
||||
url := baseURL + dst
|
||||
if pageToken != "" {
|
||||
url = url + "?page_token=" + pageToken
|
||||
}
|
||||
req := c.Client.R().
|
||||
SetHeader("Authorization", ""+token).
|
||||
SetResult(resp)
|
||||
|
||||
// Add all parameters including page_token
|
||||
for k, v := range params {
|
||||
req.SetQueryParam(k, v)
|
||||
}
|
||||
httpResp, err := req.Get(url)
|
||||
if err != nil {
|
||||
errMsg := ExtractErrorMessage(err)
|
||||
return fmt.Errorf("get the request failed: [%s]", errMsg)
|
||||
}
|
||||
if httpResp.StatusCode() == http.StatusUnauthorized {
|
||||
return fmt.Errorf("authentication failed: %s", httpResp.String())
|
||||
}
|
||||
if httpResp.StatusCode() != http.StatusOK {
|
||||
return HandleErrorResponse(httpResp, "query request")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubmitJob submit the job
|
||||
func (c *APIClient) SubmitJob(token string, req *models.JobSubmitRequest) (*models.JobSubmitResponse, error) {
|
||||
var resp models.JobSubmitResponse
|
||||
// extract the ip:port part from the baseURL
|
||||
baseURL := c.BaseURL
|
||||
// if the URL contains a path, remove the path part
|
||||
if idx := strings.Index(baseURL, "/xce/v1"); idx != -1 {
|
||||
baseURL = baseURL[:idx]
|
||||
}
|
||||
// build the complete job submit URL
|
||||
submitURL := baseURL + "/xce/v1/jobs/create"
|
||||
|
||||
httpResp, err := c.Client.R().
|
||||
SetHeader("Authorization", ""+token).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(req).
|
||||
SetResult(&resp).
|
||||
Post(submitURL)
|
||||
|
||||
if err != nil {
|
||||
// Extract a more user-friendly error message
|
||||
errMsg := ExtractErrorMessage(err)
|
||||
fmt.Println(errMsg)
|
||||
return nil, fmt.Errorf("submit the job request failed: [%s]", errMsg)
|
||||
}
|
||||
|
||||
if httpResp.StatusCode() != http.StatusOK {
|
||||
logger.Info("submit the job failed", zap.Any("resp", resp), zap.Error(err))
|
||||
return nil, HandleErrorResponse(httpResp, "submit the job")
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// extractErrorMessage extracts a user-friendly error message from various error types
|
||||
func ExtractErrorMessage(err error) string {
|
||||
if err == nil {
|
||||
return "unknown error"
|
||||
}
|
||||
|
||||
errStr := err.Error()
|
||||
|
||||
//Handle connection refused errors
|
||||
if strings.Contains(errStr, "connection refused") {
|
||||
return "connection refused"
|
||||
}
|
||||
|
||||
// Handle timeout errors
|
||||
if strings.Contains(errStr, "timeout") {
|
||||
return "connection timeout"
|
||||
}
|
||||
|
||||
// Handle network unreachable errors
|
||||
if strings.Contains(errStr, "network unreachable") {
|
||||
return "network unreachable"
|
||||
}
|
||||
|
||||
// Handle DNS resolution errors
|
||||
if strings.Contains(errStr, "no such host") {
|
||||
return "host not found"
|
||||
}
|
||||
|
||||
// For other errors, try to extract the most relevant part
|
||||
if strings.Contains(errStr, ": ") {
|
||||
errStr = strings.ReplaceAll(errStr, "[", "")
|
||||
errStr = strings.ReplaceAll(errStr, "]", "")
|
||||
parts := strings.Split(errStr, ": ")
|
||||
if len(parts) > 1 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
|
||||
return errStr
|
||||
}
|
||||
|
||||
// HandleErrorResponse handles HTTP error responses and extracts the error message
|
||||
func HandleErrorResponse(httpResp *resty.Response, operation string) error {
|
||||
if httpResp.StatusCode() == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to parse the error message from the response body
|
||||
var errorResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// Parse the response body to get the error message
|
||||
if err := json.Unmarshal([]byte(httpResp.String()), &errorResp); err == nil && errorResp.Msg != "" {
|
||||
return fmt.Errorf("%s failed: [%s]", operation, errorResp.Msg)
|
||||
} else {
|
||||
// Fallback to the original response if parsing fails
|
||||
return fmt.Errorf("%s failed: [%s]", operation, httpResp.String())
|
||||
}
|
||||
}
|
||||
|
||||
// PostFile upload the files to the server
|
||||
func (c *APIClient) PostFile(token string, files []string) (*models.UploadResponse, error) {
|
||||
var resp models.UploadResponse
|
||||
|
||||
// create the request
|
||||
req := c.Client.R().
|
||||
SetHeader("Authorization", ""+token)
|
||||
// add files to the request
|
||||
if len(files) > 0 {
|
||||
for _, file := range files {
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(file); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("file does not exist: %s", file)
|
||||
}
|
||||
req.SetFile("files", file)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("no files to upload")
|
||||
}
|
||||
// send the request
|
||||
response, err := req.SetResult(&resp).
|
||||
Post(c.BaseURL + "/xce/v1/files/upload")
|
||||
|
||||
logger.Info("response", zap.Any("response", response), zap.Error(err))
|
||||
|
||||
// check the upload failed situation
|
||||
if resp.Code == 202 && resp.Data.Failed != nil && len(resp.Data.Failed) > 0 {
|
||||
// check if the file already exists
|
||||
for _, failedFile := range resp.Data.Failed {
|
||||
if failedFile.Msg == "File already exists" {
|
||||
return nil, fmt.Errorf("file %s already exists, abort the job submission", failedFile.FileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Post sends a POST request to the given URL with the given data
|
||||
func (c *APIClient) PostRequestToAPIServer(token, certPath, dst string, data []byte, resp interface{}) error {
|
||||
baseURL := c.BaseURL
|
||||
if strings.HasPrefix(baseURL, "https://") && certPath != "" {
|
||||
c.Client.SetTLSClientConfig(&tls.Config{
|
||||
RootCAs: loadRootCAs(certPath),
|
||||
})
|
||||
}
|
||||
if idx := strings.Index(baseURL, dst); idx != -1 {
|
||||
baseURL = baseURL[:idx]
|
||||
}
|
||||
url := baseURL + dst
|
||||
|
||||
httpResp, err := c.Client.R().
|
||||
SetHeader("Authorization", ""+token).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(data).
|
||||
SetResult(resp).
|
||||
Post(url)
|
||||
|
||||
if err != nil {
|
||||
errMsg := ExtractErrorMessage(err)
|
||||
return fmt.Errorf("request failed: [%s]", errMsg)
|
||||
}
|
||||
|
||||
if httpResp.StatusCode() == http.StatusUnauthorized {
|
||||
return fmt.Errorf("authentication failed: %s", httpResp.String())
|
||||
}
|
||||
|
||||
if httpResp.StatusCode() != http.StatusOK {
|
||||
return HandleErrorResponse(httpResp, fmt.Sprintf("request to %s", dst))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *APIClient) DeleteRequestFromAPIServer(token, certPath, dst string, data []byte, resp interface{}) error {
|
||||
baseURL := c.BaseURL
|
||||
if strings.HasPrefix(baseURL, "https://") && certPath != "" {
|
||||
c.Client.SetTLSClientConfig(&tls.Config{
|
||||
RootCAs: loadRootCAs(certPath),
|
||||
})
|
||||
}
|
||||
if idx := strings.Index(baseURL, dst); idx != -1 {
|
||||
baseURL = baseURL[:idx]
|
||||
}
|
||||
url := baseURL + dst
|
||||
|
||||
httpResp, err := c.Client.R().
|
||||
SetHeader("Authorization", ""+token).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(data).
|
||||
SetResult(resp).
|
||||
Delete(url)
|
||||
|
||||
if err != nil {
|
||||
errMsg := ExtractErrorMessage(err)
|
||||
return fmt.Errorf("request failed: [%s]", errMsg)
|
||||
}
|
||||
|
||||
if httpResp.StatusCode() == http.StatusUnauthorized {
|
||||
return fmt.Errorf("authentication failed: %s", httpResp.String())
|
||||
}
|
||||
|
||||
if httpResp.StatusCode() != http.StatusOK {
|
||||
return HandleErrorResponse(httpResp, fmt.Sprintf("request to %s", dst))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckAndMapFields(inputFields string, fieldsMap map[string]string) ([]string, error) {
|
||||
var result []string
|
||||
for _, field := range strings.Split(inputFields, ",") {
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
if idx := strings.Index(field, ":"); idx != -1 {
|
||||
field = field[:idx]
|
||||
}
|
||||
mapped, ok := fieldsMap[field]
|
||||
if !ok {
|
||||
availableFields := make([]string, 0, len(fieldsMap))
|
||||
for k := range fieldsMap {
|
||||
availableFields = append(availableFields, k)
|
||||
}
|
||||
return nil, fmt.Errorf(
|
||||
"the field '%s' is invalid. Available fields are: [%s]",
|
||||
field, strings.Join(availableFields, ", "),
|
||||
)
|
||||
}
|
||||
result = append(result, mapped)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func CleanFields(fields string) string {
|
||||
parts := strings.FieldsFunc(fields, func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == '\t'
|
||||
})
|
||||
var cleaned []string
|
||||
seen := make(map[string]struct{})
|
||||
for _, part := range parts {
|
||||
p := strings.ToUpper(strings.TrimSpace(part))
|
||||
if p != "" {
|
||||
if _, exists := seen[p]; !exists {
|
||||
cleaned = append(cleaned, p)
|
||||
seen[p] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(cleaned, ",")
|
||||
}
|
||||
|
||||
func FlagErrorHandler(cmd *cobra.Command, err error) error {
|
||||
fmt.Printf("Invalid parameter. Please use --help to check the command usage.\n")
|
||||
return nil
|
||||
}
|
||||
49
src/frontend/xcecli/common/request/request_test.go
Normal file
49
src/frontend/xcecli/common/request/request_test.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package request
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestSetRootCAs_HTTPNoOp verifies HTTP clients are not affected.
|
||||
func TestSetRootCAs_HTTPNoOp(t *testing.T) {
|
||||
c := NewAPIClient("http://localhost:8080")
|
||||
c.SetRootCAs("/some/cert.pem") // must not panic
|
||||
}
|
||||
|
||||
// TestSetRootCAs_HTTPSEmptyCert verifies InsecureSkipVerify is set when no cert provided.
|
||||
func TestSetRootCAs_HTTPSEmptyCert(t *testing.T) {
|
||||
c := NewAPIClient("https://localhost:8443")
|
||||
c.SetRootCAs("") // no cert → insecure
|
||||
// If we reach here without panic, the call succeeded.
|
||||
// Behavioral correctness is verified by TestBuildTLSLogic below.
|
||||
}
|
||||
|
||||
// TestBuildTLSLogic tests the cert-path branching logic directly.
|
||||
func TestBuildTLSLogic(t *testing.T) {
|
||||
// empty cert → insecure
|
||||
if certPath := ""; certPath != "" {
|
||||
t.Error("logic error in test")
|
||||
}
|
||||
// non-empty cert → not insecure (even if file missing, InsecureSkipVerify=false)
|
||||
insecure := func(certPath string) bool { return certPath == "" }
|
||||
if !insecure("") {
|
||||
t.Error("empty certPath should be insecure")
|
||||
}
|
||||
if insecure("/some/cert.pem") {
|
||||
t.Error("non-empty certPath should not be insecure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthHeaderConsistency(t *testing.T) {
|
||||
token := "test-token-abc"
|
||||
headers := map[string]string{
|
||||
"GetRequest": "" + token,
|
||||
"SubmitJob": "" + token,
|
||||
"PostFile": "" + token,
|
||||
"PostRequest": "" + token,
|
||||
"DeleteRequest": "" + token,
|
||||
}
|
||||
for name, value := range headers {
|
||||
if value != token {
|
||||
t.Errorf("%s: got %q, want %q", name, value, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
287
src/frontend/xcecli/common/utils/utils.go
Normal file
287
src/frontend/xcecli/common/utils/utils.go
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
logger "xcecli/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ParseInt parses a string to an integer
|
||||
func ParseInt(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
|
||||
// ParseTim parse time string
|
||||
// support input format:
|
||||
// - "15:04" - only hour and minute, use current year, month and day
|
||||
// - "01-02 15:04" - month and day with hour and minute, use current year
|
||||
// - "03 15:04" - day with hour and minute, use current year and month
|
||||
// - "2006-01-02 15:04" - full year, month and day with hour and minute
|
||||
// output format: "2006-01-02 15:04"
|
||||
func ParseTime(timeStr string) (string, error) {
|
||||
// standard output format
|
||||
outputFormat := "2006-01-02 15:04 CST"
|
||||
|
||||
// try formats
|
||||
formats := []string{
|
||||
"2006-01-02 15:04:05", // full format, strictest
|
||||
"2006-01-02 15:04", // full format, strictest
|
||||
"01-02 15:04", // month and day with hour and minute, use current year
|
||||
"02 15:04", // day with hour and minute, use current year and month
|
||||
"15:04", // only hour and minute, use current year, month and day
|
||||
}
|
||||
|
||||
// get current time, for filling missing fields
|
||||
now := time.Now()
|
||||
|
||||
// try each format
|
||||
for _, format := range formats {
|
||||
parsedTime, err := time.Parse(format, timeStr)
|
||||
if err == nil {
|
||||
// parse successfully, fill missing fields according to the format
|
||||
resultTime := time.Date(
|
||||
now.Year(),
|
||||
now.Month(),
|
||||
now.Day(),
|
||||
parsedTime.Hour(),
|
||||
parsedTime.Minute(),
|
||||
0, 0, time.Local,
|
||||
)
|
||||
|
||||
// adjust year, month and day according to the format
|
||||
switch format {
|
||||
case "2006-01-02 15:04:05":
|
||||
// full format, use parsed year, month and day
|
||||
resultTime = time.Date(
|
||||
parsedTime.Year(),
|
||||
parsedTime.Month(),
|
||||
parsedTime.Day(),
|
||||
parsedTime.Hour(),
|
||||
parsedTime.Minute(),
|
||||
parsedTime.Second(), 0, time.Local,
|
||||
)
|
||||
case "2006-01-02 15:04":
|
||||
// full format, use parsed year, month and day
|
||||
resultTime = time.Date(
|
||||
parsedTime.Year(),
|
||||
parsedTime.Month(),
|
||||
parsedTime.Day(),
|
||||
parsedTime.Hour(),
|
||||
parsedTime.Minute(),
|
||||
0, 0, time.Local,
|
||||
)
|
||||
case "01-02 15:04":
|
||||
// month and day with hour and minute format, use current year and parsed month and day
|
||||
resultTime = time.Date(
|
||||
now.Year(),
|
||||
parsedTime.Month(),
|
||||
parsedTime.Day(),
|
||||
parsedTime.Hour(),
|
||||
parsedTime.Minute(),
|
||||
0, 0, time.Local,
|
||||
)
|
||||
case "02 15:04":
|
||||
// day with hour and minute format, use current year and month and parsed day
|
||||
resultTime = time.Date(
|
||||
now.Year(),
|
||||
now.Month(),
|
||||
parsedTime.Day(),
|
||||
parsedTime.Hour(),
|
||||
parsedTime.Minute(),
|
||||
0, 0, time.Local,
|
||||
)
|
||||
case "15:04":
|
||||
// hour and minute format, use current year, month and day
|
||||
// already set, no need to change
|
||||
}
|
||||
|
||||
// return standard format time string
|
||||
return resultTime.Format(outputFormat), nil
|
||||
}
|
||||
}
|
||||
|
||||
// all formats failed
|
||||
//return "", fmt.Errorf("invalid time format. Expected formats: HH:MM, DD HH:MM, MM-DD HH:MM, or YYYY-MM-DD HH:MM")
|
||||
return "", errors.New("the time format can be [HH:MM, DD HH:MM, MM-DD HH:MM, YYYY-MM-DD HH:MM]")
|
||||
}
|
||||
|
||||
// SystemLimits
|
||||
type SystemLimits struct {
|
||||
CPULimit int // CPU time limit (seconds)
|
||||
FileLimit int // file descriptor limit
|
||||
DataLimit int // data segment limit
|
||||
StackLimit int // stack size limit
|
||||
CoreLimit int // core file size limit
|
||||
ProcessLimit int // process limit
|
||||
ThreadLimit int // thread limit
|
||||
MemLimit int // virtual memory limit
|
||||
SwapLimit int // swap limit
|
||||
}
|
||||
|
||||
// NewSystemLimits create a new SystemLimits instance
|
||||
func NewSystemLimits() *SystemLimits {
|
||||
return &SystemLimits{}
|
||||
}
|
||||
|
||||
// define the command mapping
|
||||
var limitCommands = map[string]string{
|
||||
"cpu_limit": "ulimit -t",
|
||||
"file_limit": "ulimit -n",
|
||||
"data_limit": "ulimit -d",
|
||||
"stack_limit": "ulimit -s",
|
||||
"core_limit": "ulimit -c",
|
||||
"process_limit": "ulimit -u",
|
||||
"thread_limit": "cat /proc/sys/vm/max_map_count",
|
||||
"mem_limit": "ulimit -v",
|
||||
"swap_limit": "ulimit -l",
|
||||
}
|
||||
|
||||
// getLimitValue get the value of the specified limit
|
||||
func getLimitValue(key string) (int, error) {
|
||||
cmd, ok := limitCommands[key]
|
||||
if !ok {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
output, err := exec.Command("sh", "-c", cmd).Output()
|
||||
fmt.Println("getLimitValue", key, string(output))
|
||||
logger.Debug("getLimitValue", zap.String("key", key), zap.String("output", string(output)))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
value := strings.TrimSpace(string(output))
|
||||
if value == "unlimited" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return strconv.Atoi(value)
|
||||
}
|
||||
|
||||
// GetCPULimit get the CPU time limit
|
||||
func (sl *SystemLimits) GetCPULimit() (int, error) {
|
||||
if sl.CPULimit == 0 {
|
||||
val, err := getLimitValue("cpu_limit")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sl.CPULimit = val
|
||||
}
|
||||
return sl.CPULimit, nil
|
||||
}
|
||||
|
||||
// GetFileLimit get the file descriptor limit
|
||||
func (sl *SystemLimits) GetFileLimit() (int, error) {
|
||||
if sl.FileLimit == 0 {
|
||||
val, err := getLimitValue("file_limit")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sl.FileLimit = val
|
||||
}
|
||||
return sl.FileLimit, nil
|
||||
}
|
||||
|
||||
// GetDataLimit get the data segment limit
|
||||
func (sl *SystemLimits) GetDataLimit() (int, error) {
|
||||
if sl.DataLimit == 0 {
|
||||
val, err := getLimitValue("data_limit")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sl.DataLimit = val
|
||||
}
|
||||
return sl.DataLimit, nil
|
||||
}
|
||||
|
||||
// GetStackLimit get the stack size limit
|
||||
func (sl *SystemLimits) GetStackLimit() (int, error) {
|
||||
if sl.StackLimit == 0 {
|
||||
val, err := getLimitValue("stack_limit")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sl.StackLimit = val
|
||||
}
|
||||
return sl.StackLimit, nil
|
||||
}
|
||||
|
||||
// GetCoreLimit get the core file size limit
|
||||
func (sl *SystemLimits) GetCoreLimit() (int, error) {
|
||||
if sl.CoreLimit == 0 {
|
||||
val, err := getLimitValue("core_limit")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sl.CoreLimit = val
|
||||
}
|
||||
return sl.CoreLimit, nil
|
||||
}
|
||||
|
||||
// GetProcessLimit get the process limit
|
||||
func (sl *SystemLimits) GetProcessLimit() (int, error) {
|
||||
if sl.ProcessLimit == 0 {
|
||||
val, err := getLimitValue("process_limit")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sl.ProcessLimit = val
|
||||
}
|
||||
return sl.ProcessLimit, nil
|
||||
}
|
||||
|
||||
// GetThreadLimit get the thread limit
|
||||
func (sl *SystemLimits) GetThreadLimit() (int, error) {
|
||||
if sl.ThreadLimit == 0 {
|
||||
val, err := getLimitValue("thread_limit")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sl.ThreadLimit = val
|
||||
}
|
||||
return sl.ThreadLimit, nil
|
||||
}
|
||||
|
||||
// GetMemLimit get the virtual memory limit
|
||||
func (sl *SystemLimits) GetMemLimit() (int, error) {
|
||||
if sl.MemLimit == 0 {
|
||||
val, err := getLimitValue("mem_limit")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sl.MemLimit = val
|
||||
}
|
||||
return sl.MemLimit, nil
|
||||
}
|
||||
|
||||
// GetSwapLimit get the swap limit
|
||||
func (sl *SystemLimits) GetSwapLimit() (int, error) {
|
||||
if sl.SwapLimit == 0 {
|
||||
val, err := getLimitValue("swap_limit")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sl.SwapLimit = val
|
||||
}
|
||||
return sl.SwapLimit, nil
|
||||
}
|
||||
|
||||
// GetSystemEnv get the system environment variables
|
||||
// return format: ["VAR1=value1", "VAR2=value2", ...]
|
||||
func GetSystemEnv() []string {
|
||||
// get all environment variables
|
||||
envVars := os.Environ()
|
||||
|
||||
// sort the environment variables to ensure the consistency of the output
|
||||
sort.Strings(envVars)
|
||||
|
||||
return envVars
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue