diff --git a/pkg/qhost/v9.0/parsers.go b/pkg/qhost/v9.0/parsers.go index b0dff37..7447394 100644 --- a/pkg/qhost/v9.0/parsers.go +++ b/pkg/qhost/v9.0/parsers.go @@ -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 } diff --git a/pkg/qhost/v9.0/parsers_test.go b/pkg/qhost/v9.0/parsers_test.go index afae316..fa21818 100644 --- a/pkg/qhost/v9.0/parsers_test.go +++ b/pkg/qhost/v9.0/parsers_test.go @@ -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", + }, + )) }) + }) }) diff --git a/pkg/qstat/v9.0/parser.go b/pkg/qstat/v9.0/parser.go index e356e4a..1a0c90e 100644 --- a/pkg/qstat/v9.0/parser.go +++ b/pkg/qstat/v9.0/parser.go @@ -26,6 +26,7 @@ import ( "strconv" "strings" "time" + "unicode" ) const QstatDateFormat = "2006-01-02 03:04:05" @@ -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 @@ -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 +} diff --git a/pkg/qstat/v9.0/parser_test.go b/pkg/qstat/v9.0/parser_test.go index 2af8118..1d97fa2 100644 --- a/pkg/qstat/v9.0/parser_test.go +++ b/pkg/qstat/v9.0/parser_test.go @@ -1,6 +1,6 @@ /*___INFO__MARK_BEGIN__*/ /************************************************************************* -* Copyright 2024 HPC-Gridware GmbH +* Copyright 2024-2025 HPC-Gridware GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -452,68 +452,70 @@ test.q 0.08 0 0 2 2 0 0 }) - Describe("JobArrayTask", func() { - - It("should parse the output of qstat -g d", func() { - input := `job-ID prior name user state submit/start at queue slots ja-task-ID ------------------------------------------------------------------------------------------------------------------ - 33 0.50500 sleep root r 2025-02-10 16:47:18 all.q@master 1 1 - 33 0.50500 sleep root r 2025-02-10 16:47:18 all.q@master 1 3 - 33 0.50500 sleep root r 2025-02-10 16:47:18 all.q@master 1 5 - 33 0.50500 sleep root r 2025-02-10 16:47:18 all.q@master 1 23 - 33 0.50500 sleep root r 2025-02-10 16:47:18 all.q@master 1 25 - 33 0.50500 sleep root r 2025-02-10 16:47:18 all.q@master 1 27 - 36 0.60500 sleep root qw 2025-02-10 16:52:21 2 - 37 0.60500 sleep root qw 2025-02-10 16:52:35 2 - 38 0.60500 sleep root qw 2025-02-10 16:52:49 2 - 39 0.60500 sleep root qw 2025-02-10 16:53:23 2 1 - 39 0.60500 sleep root qw 2025-02-10 16:53:23 2 2 - 33 0.50500 sleep root qw 2025-02-10 16:47:18 1 95 - 33 0.50500 sleep root qw 2025-02-10 16:47:18 1 97 - 33 0.50500 sleep root qw 2025-02-10 16:47:18 1 99 - 34 0.50500 sleep root qw 2025-02-10 16:51:51 1 -` - jobArrayTasks, err := qstat.ParseJobArrayTask(input) + Describe("FullQueueInfo", func() { + + It("should parse the output of qstat -f", func() { + input := `queuename qtype resv/used/tot. load_avg arch states +--------------------------------------------------------------------------------- +all.q@master BIP 0/2/14 0.59 lx-amd64 + 12 0.50500 sleep root r 2025-02-15 07:29:31 2 +--------------------------------------------------------------------------------- +test.q@master BIP 0/2/10 0.59 lx-amd64 + 13 0.50500 sleep root r 2025-02-15 07:29:35 2 + +############################################################################ + - PENDING JOBS - PENDING JOBS - PENDING JOBS - PENDING JOBS - PENDING JOBS +############################################################################ + 14 0.60500 sleep root qw 2025-02-15 07:03:48 111` + full, err := qstat.ParseQstatFullOutput(input) + Expect(err).NotTo(HaveOccurred()) + Expect(len(full)).To(Equal(2)) + + Expect(full[0].QueueName).To(Equal("all.q@master")) + Expect(full[0].QueueType).To(Equal("BIP")) + Expect(full[0].Reserved).To(Equal(0)) + Expect(full[0].Used).To(Equal(2)) + Expect(full[0].Total).To(Equal(14)) + Expect(full[0].Jobs).To(HaveLen(1)) + Expect(full[0].Jobs[0].JobID).To(Equal(12)) + Expect(full[0].Jobs[0].Name).To(Equal("sleep")) + Expect(full[0].Jobs[0].User).To(Equal("root")) + Expect(full[0].Jobs[0].State).To(Equal("r")) + Expect(full[0].Jobs[0].StartTime).To(Equal(time.Date(2025, 2, 15, 7, 29, 31, 0, time.UTC))) + Expect(len(full[0].Jobs[0].JaTaskIDs)).To(Equal(0)) + + Expect(full[1].QueueName).To(Equal("test.q@master")) + Expect(full[1].QueueType).To(Equal("BIP")) + Expect(full[1].Reserved).To(Equal(0)) + Expect(full[1].Used).To(Equal(2)) + Expect(full[1].Total).To(Equal(10)) + Expect(full[1].Jobs).To(HaveLen(1)) + Expect(full[1].Jobs[0].JobID).To(Equal(13)) + Expect(full[1].Jobs[0].Name).To(Equal("sleep")) + Expect(full[1].Jobs[0].User).To(Equal("root")) + Expect(full[1].Jobs[0].State).To(Equal("r")) + Expect(full[1].Jobs[0].StartTime).To(Equal(time.Date(2025, 2, 15, 7, 29, 35, 0, time.UTC))) + Expect(len(full[1].Jobs[0].JaTaskIDs)).To(Equal(0)) + + input2 := `queuename qtype resv/used/tot. load_avg arch states +--------------------------------------------------------------------------------- +all.q@master BIP 0/0/14 0.55 lx-amd64 +--------------------------------------------------------------------------------- +test.q@master BIP 0/0/10 0.55 lx-amd64 + +############################################################################ + - PENDING JOBS - PENDING JOBS - PENDING JOBS - PENDING JOBS - PENDING JOBS +############################################################################ + 14 0.55500 sleep root qw 2025-02-15 07:03:48 111` + full, err = qstat.ParseQstatFullOutput(input2) Expect(err).NotTo(HaveOccurred()) - Expect(len(jobArrayTasks)).To(Equal(15)) - - Expect(jobArrayTasks).To(ContainElement(qstat.JobArrayTask{ - JobInfo: qstat.JobInfo{ - JobID: 33, - Priority: 0.505, - Name: "sleep", - User: "root", - State: "r", - StartTime: time.Date(2025, 2, 10, 16, 47, 18, 0, time.UTC), - SubmitTime: time.Time{}, - Queue: "all.q@master", - Slots: 1, - JaTaskIDs: []int64{1}, - }, - })) - - Expect(jobArrayTasks).To(ContainElement(qstat.JobArrayTask{ - JobInfo: qstat.JobInfo{ - JobID: 36, - Priority: 0.605, - Name: "sleep", - User: "root", - State: "qw", - SubmitTime: time.Date(2025, 2, 10, 16, 52, 21, 0, time.UTC), - StartTime: time.Time{}, - Queue: "", - Slots: 2, - JaTaskIDs: []int64{0}, - }, - })) + Expect(len(full)).To(Equal(2)) - }) + Expect(full[0].QueueName).To(Equal("all.q@master")) + Expect(full[0].Jobs).To(HaveLen(0)) - It("should parse an empty input", func() { - input := "" - jobArrayTasks, err := qstat.ParseJobArrayTask(input) - Expect(err).NotTo(HaveOccurred()) - Expect(len(jobArrayTasks)).To(Equal(0)) + Expect(full[1].QueueName).To(Equal("test.q@master")) + Expect(full[1].Jobs).To(HaveLen(0)) }) diff --git a/pkg/qstat/v9.0/qstat.go b/pkg/qstat/v9.0/qstat.go index 4a97977..d32e301 100644 --- a/pkg/qstat/v9.0/qstat.go +++ b/pkg/qstat/v9.0/qstat.go @@ -40,7 +40,7 @@ type QStat interface { // qstat -explain a|c|A|E ShowQueueExplanation(reason string) ([]QueueExplanation, error) // qstat -f - ShowFullOutput() ([]JobInfo, error) + ShowFullOutput() ([]FullQueueInfo, error) // qstat -F ShowFullOutputWithResources(resourceAttributes string) ([]JobInfo, error) // qstat -g c diff --git a/pkg/qstat/v9.0/qstat_impl.go b/pkg/qstat/v9.0/qstat_impl.go index f1146e8..bb5d2d8 100644 --- a/pkg/qstat/v9.0/qstat_impl.go +++ b/pkg/qstat/v9.0/qstat_impl.go @@ -161,8 +161,13 @@ func (q *QStatImpl) ShowQueueExplanation(reason string) ([]QueueExplanation, err return nil, fmt.Errorf("not implemented") } -func (q *QStatImpl) ShowFullOutput() ([]JobInfo, error) { - return nil, fmt.Errorf("not implemented") +// ShowFullOutput is equivalent to "qstat -f" +func (q *QStatImpl) ShowFullOutput() ([]FullQueueInfo, error) { + out, err := q.NativeSpecification([]string{"-f"}) + if err != nil { + return nil, fmt.Errorf("failed to get output of qstat: %w", err) + } + return ParseQstatFullOutput(out) } func (q *QStatImpl) ShowFullOutputWithResources(resourceAttributes string) ([]JobInfo, error) { diff --git a/pkg/qstat/v9.0/qstat_impl_test.go b/pkg/qstat/v9.0/qstat_impl_test.go index 625443d..23ae731 100644 --- a/pkg/qstat/v9.0/qstat_impl_test.go +++ b/pkg/qstat/v9.0/qstat_impl_test.go @@ -33,6 +33,7 @@ var _ = Describe("QstatImpl", func() { It("should return the command line", func() { var err error var q qstat.QStat + q, err = qstat.NewCommandLineQstat( qstat.CommandLineQStatConfig{ DryRun: true, diff --git a/pkg/qstat/v9.0/types.go b/pkg/qstat/v9.0/types.go index 2e86ddf..0b5fec4 100644 --- a/pkg/qstat/v9.0/types.go +++ b/pkg/qstat/v9.0/types.go @@ -68,13 +68,15 @@ type QueueExplanation struct { } type FullQueueInfo struct { - QueueName string `json:"queuename"` - QueueType string `json:"qtype"` - ResvUsedTot string `json:"resv_used_tot"` - LoadAvg string `json:"load_avg"` - Arch string `json:"arch"` - States string `json:"states"` - Jobs []JobInfo `json:"jobs"` + QueueName string `json:"queuename"` + QueueType string `json:"qtype"` + Reserved int `json:"reserved"` + Used int `json:"used"` + Total int `json:"total"` + LoadAvg float64 `json:"load_avg"` + Arch string `json:"arch"` + States string `json:"states"` + Jobs []JobInfo `json:"jobs"` } type FullQueueInfoWithResources struct {