Skip to content
Merged
1 change: 0 additions & 1 deletion pkg/qhost/v9.0/parsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ func ParseHostFullMetrics(out string) ([]HostFullMetrics, error) {
for i := 0; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])
if line == "" || strings.HasPrefix(line, "HOSTNAME") ||
strings.HasPrefix(line, "global") ||
strings.HasPrefix(line, "----") {
continue
}
Expand Down
62 changes: 58 additions & 4 deletions pkg/qhost/v9.0/parsers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,19 +404,73 @@ sim9 lx-amd64 4 1 4 4 0.60 15.6G 465.8M
hl:np_load_long=0.110000
hf:load_report_host=master`

qhostFOutput2 := `HOSTNAME ARCH NCPU NSOC NCOR NTHR LOAD MEMTOT MEMUSE SWAPTO SWAPUS
----------------------------------------------------------------------------------------------
global - - - - - - - - - -
gc:testc=100000.000000
master lx-amd64 14 1 14 14 1.50 7.7G 2.0G 1024.0M 12.0K
gc:testc=100000.000000
hl:load_avg=1.500000
hl:load_short=1.670000
hl:load_medium=1.500000
hl:load_long=1.100000
hl:arch=lx-amd64
hl:num_proc=14.000000
hl:mem_free=5.621G
hl:swap_free=1023.984M
hl:virtual_free=6.621G
hl:mem_total=7.653G
hl:swap_total=1023.996M
hl:virtual_total=8.653G
hl:mem_used=2.032G
hl:swap_used=12.000K
hl:virtual_used=2.032G
hl:cpu=0.500000
hl:m_topology=SCCCCCCCCCCCCCC
hl:m_topology_inuse=SCCCCCCCCCCCCCC
hl:m_socket=1.000000
hl:m_core=14.000000
hl:m_thread=14.000000
hl:np_load_avg=0.107143
hl:np_load_short=0.119286
hl:np_load_medium=0.107143
hl:np_load_long=0.078571
`

It("should return error if output is invalid", func() {
hosts, err := qhost.ParseHostFullMetrics(sample)
Expect(err).To(BeNil())
Expect(hosts).To(HaveLen(2))
Expect(hosts).To(HaveLen(3))
})

It("should parse host full metrics", func() {
hosts, err := qhost.ParseHostFullMetrics(qhostFOutput1)
Expect(err).To(BeNil())
Expect(hosts).To(HaveLen(13))
Expect(hosts[0].Name).To(Equal("master"))
Expect(hosts[12].Name).To(Equal("sim9"))
Expect(hosts).To(HaveLen(14))
Expect(hosts[0].Name).To(Equal("global"))
Expect(hosts[1].Name).To(Equal("master"))
Expect(hosts[12].Name).To(Equal("sim8"))
})

It("should parse host full metrics with global host values", func() {
hosts, err := qhost.ParseHostFullMetrics(qhostFOutput2)
Expect(err).To(BeNil())
Expect(hosts).To(HaveLen(2))
Expect(hosts[0].Name).To(Equal("global"))
Expect(hosts[1].Name).To(Equal("master"))
Expect(len(hosts[0].Resources)).To(Equal(1))
Expect(hosts[0].Resources["testc"]).To(Equal(
qhost.ResourceAvailability{
Name: "testc",
StringValue: "100000.000000",
FloatValue: 100000.000000,
ResourceAvailabilityLimitedBy: "g",
Source: "c",
FullString: "gc:testc=100000.000000",
},
))
})

})

})
194 changes: 193 additions & 1 deletion pkg/qstat/v9.0/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"strconv"
"strings"
"time"
"unicode"
)

const QstatDateFormat = "2006-01-02 03:04:05"
Expand Down Expand Up @@ -68,7 +69,9 @@ func parseFixedWidthJobs(input string) ([]ParallelJobTask, error) {
return tasks, nil
}

// Correct column positions based on your description
// we have 9.0.0 to 9.0.2 format and 9.0.3 format with 3 more columns
// for the job IDs

columnPositions := []struct {
start int
end int
Expand Down Expand Up @@ -942,3 +945,192 @@ func ParseJobArrayTask(out string) ([]JobArrayTask, error) {
}
return jobArrayTasks, nil
}

/*
qstat -f
queuename qtype resv/used/tot. load_avg arch states
---------------------------------------------------------------------------------
all.q@master BIP 0/9/14 0.69 lx-amd64
2 0.50500 sleep root r 2025-02-15 12:28:22 1
3 0.50500 sleep root r 2025-02-15 12:28:23 1
4 0.50500 sleep root r 2025-02-15 12:28:23 1
5 0.50500 sleep root r 2025-02-15 12:28:24 1
6 0.50500 sleep root r 2025-02-15 12:28:24 1
7 0.50500 sleep root r 2025-02-15 12:28:25 1
8 0.50500 sleep root r 2025-02-15 12:28:25 1
12 0.60500 sleep root r 2025-02-15 12:29:31 2
---------------------------------------------------------------------------------
test.q@master BIP 0/6/10 0.69 lx-amd64
9 0.50500 sleep root r 2025-02-15 12:28:34 1
10 0.50500 sleep root r 2025-02-15 12:28:38 1
11 0.50500 sleep root r 2025-02-15 12:29:03 1 1
11 0.50500 sleep root r 2025-02-15 12:29:03 1 2
13 0.60500 sleep root r 2025-02-15 12:29:35 2
*/

// ParseQstatFullOutput parses the output of the "qstat -f" command and returns
// a slice of FullQueueInfo containing queue details and associated job information.
//
// It expects an output with queue header lines (non-indented) followed by one or more
// job lines (indented) until a separator (a line full of "-" characters) is encountered.
func ParseQstatFullOutput(out string) ([]FullQueueInfo, error) {
lines := strings.Split(out, "\n")
var results []FullQueueInfo
var currentQueue *FullQueueInfo

for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if strings.HasPrefix(trimmed, "####") {
break
}

// Skip any known header lines.
lower := strings.ToLower(trimmed)
if strings.HasPrefix(lower, "queuename") {
continue
}

// If this is a separator line, then finish the current block.
if isSeparatorLine(trimmed) {
if currentQueue != nil {
results = append(results, *currentQueue)
currentQueue = nil
}
continue
}

// If the line does not start with whitespace, it is a queue header.
if !startsWithWhitespace(line) {
// If an active queue exists, push it into results before starting a new block.
if currentQueue != nil {
results = append(results, *currentQueue)
}

fields := strings.Fields(line)
if len(fields) < 5 {
return nil, fmt.Errorf("invalid queue header format: %q", line)
}
queueName := fields[0]
qtype := fields[1]
resvUsedTot := fields[2] // Expected format: "resv/used/tot"
loadAvgStr := fields[3]
arch := fields[4]

parts := strings.Split(resvUsedTot, "/")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid resv/used/tot format in queue header: %q", line)
}
reserved, err := strconv.Atoi(parts[0])
if err != nil {
return nil, fmt.Errorf("invalid reserved value in queue header: %v", err)
}
used, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid used value in queue header: %v", err)
}
total, err := strconv.Atoi(parts[2])
if err != nil {
return nil, fmt.Errorf("invalid total value in queue header: %v", err)
}
loadAvg, err := strconv.ParseFloat(loadAvgStr, 64)
if err != nil {
return nil, fmt.Errorf("invalid load_avg value in queue header: %v", err)
}
currentQueue = &FullQueueInfo{
QueueName: queueName,
QueueType: qtype,
Reserved: reserved,
Used: used,
Total: total,
LoadAvg: loadAvg,
Arch: arch,
Jobs: []JobInfo{},
}
} else {
// This is a job line. It must belong to an already parsed queue header.
if currentQueue == nil {
return nil, fmt.Errorf("job info found without preceding queue header: %q", line)
}
fields := strings.Fields(line)
if len(fields) < 8 {
return nil, fmt.Errorf("invalid job line format: %q", line)
}
jobID, err := strconv.Atoi(fields[0])
if err != nil {
return nil, fmt.Errorf("invalid job id in job line %q: %v", line, err)
}
score, err := strconv.ParseFloat(fields[1], 64)
if err != nil {
return nil, fmt.Errorf("invalid score in job line %q: %v", line, err)
}
taskName := fields[2]
owner := fields[3]
state := fields[4]
datetimeStr := fields[5] + " " + fields[6]
startTime, err := time.Parse("2006-01-02 15:04:05", datetimeStr)
if err != nil {
return nil, fmt.Errorf(
"failed to parse datetime '%s' in job line %q: %v",
datetimeStr, line, err)
}
var submitTime time.Time
if strings.Contains(state, "q") {
submitTime = startTime
startTime = time.Time{}
}
slots, err := strconv.Atoi(fields[7])
if err != nil {
return nil, fmt.Errorf("invalid slots in job line %q: %v", line, err)
}
// optional tasks
var taskIDs []int64
if len(fields) > 8 {
taskID, err := strconv.Atoi(fields[8])
if err != nil {
return nil, fmt.Errorf("invalid task id in job line %q: %v", line, err)
}
taskIDs = []int64{int64(taskID)}
}
job := JobInfo{
JobID: jobID,
Priority: score,
Name: taskName,
User: owner,
State: state,
StartTime: startTime,
SubmitTime: submitTime,
Queue: currentQueue.QueueName,
Slots: slots,
JaTaskIDs: taskIDs,
}
currentQueue.Jobs = append(currentQueue.Jobs, job)
}
}

// Append the last queue block if it exists.
if currentQueue != nil {
results = append(results, *currentQueue)
}
return results, nil
}

// startsWithWhitespace returns true if the first rune of the string is a whitespace.
func startsWithWhitespace(s string) bool {
for _, r := range s {
return unicode.IsSpace(r)
}
return false
}

// isSeparatorLine checks if the provided line is made up entirely of '-' characters.
func isSeparatorLine(s string) bool {
for _, r := range s {
if r != '-' {
return false
}
}
return true
}
Loading
Loading