Skip to content

Commit 9542e63

Browse files
authored
Merge pull request #6185 from grondo/issue#6145
enhance `flux queue list` with `-q, --queue=` option and abillity to report queue enabled/started status
2 parents e925dff + 64d7bd3 commit 9542e63

File tree

6 files changed

+185
-35
lines changed

6 files changed

+185
-35
lines changed

doc/man1/flux-queue.rst

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ list
4242

4343
.. program:: flux queue list
4444

45-
List queue defaults and limits.
45+
List queue status, defaults, and limits.
46+
47+
.. option:: -q, --queue=QUEUE,...
48+
49+
Limit output to specified queues
4650

4751
.. option:: -n, --no-header
4852

@@ -219,6 +223,24 @@ The following field names can be specified:
219223
**queuem**
220224
queue name, but default queue is marked up with an asterisk
221225

226+
**submission**
227+
Description of queue submission status: ``enabled`` or ``disabled``
228+
229+
**scheduling**
230+
Description of queue scheduling status: ``started`` or ``stopped``
231+
232+
**enabled**
233+
Single character submission status: ```` if enabled, ```` if disabled.
234+
235+
**started**
236+
Single character scheduling status: ```` if started, ```` if stopped.
237+
238+
**enabled.ascii**
239+
Single character submission status: ``y`` if enabled, ``n`` if disabled.
240+
241+
**started.ascii**
242+
Single character scheduling status: ``y`` if started, ``n`` if stopped.
243+
222244
**defaults.timelimit**
223245
default timelimit for jobs submitted to the queue
224246

etc/completions/flux.pre

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,7 @@ _flux_queue()
880880
-t --timeout= \
881881
"
882882
local list_OPTS="\
883+
-q --queue= \
883884
-o --format= \
884885
-n --no-header \
885886
"

src/bindings/python/flux/util.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,24 @@ def format_field(self, value, spec):
535535
return retval
536536

537537

538+
class AltField:
539+
"""
540+
Convenient wrapper for fields that have an ascii and non-ascii
541+
representation. Allows the ascii representation to be selected with
542+
{field.ascii}.
543+
"""
544+
545+
def __init__(self, default, ascii):
546+
self.default = default
547+
self.ascii = ascii
548+
549+
def __str__(self):
550+
return self.default
551+
552+
def __format__(self, fmt):
553+
return str(self).__format__(fmt)
554+
555+
538556
class OutputFormat:
539557
"""
540558
Store a parsed version of the program's output format,

src/cmd/flux-queue.py

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import sys
1717

1818
import flux
19-
from flux.util import UtilConfig, parse_fsd
19+
from flux.util import AltField, FilterActionSetUpdate, UtilConfig, parse_fsd
2020

2121

2222
def print_enable_status(name, status):
@@ -110,8 +110,14 @@ class FluxQueueConfig(UtilConfig):
110110
"default": {
111111
"description": "Default flux-queue list format string",
112112
"format": (
113-
"?:{queuem:<8.8} {defaults.timelimit!F:>11} {limits.timelimit!F:>10} {limits.range.nnodes:>10} "
114-
"{limits.range.ncores:>10} {limits.range.ngpus:>10}"
113+
"?:{queuem:<8.8} "
114+
"{color_enabled}{enabled:>2}{color_off} "
115+
"{color_started}{started:>2}{color_off} "
116+
"{defaults.timelimit!F:>8} "
117+
"{limits.timelimit!F:>8} "
118+
"{limits.range.nnodes:>10} "
119+
"{limits.range.ncores:>10} "
120+
"{limits.range.ngpus:>10}"
115121
),
116122
},
117123
}
@@ -234,11 +240,15 @@ def timelimit(self):
234240

235241

236242
class QueueInfo:
237-
def __init__(self, name, config):
243+
def __init__(self, name, config, enabled, started):
238244
self.name = name
239245
self.config = config
240246
self.limits = QueueLimitsInfo(name, config)
241247
self.defaults = QueueDefaultsInfo(name, config)
248+
self.scheduling = "started" if started else "stopped"
249+
self.submission = "enabled" if enabled else "disabled"
250+
self._enabled = enabled
251+
self._started = started
242252

243253
def __getattr__(self, attr):
244254
try:
@@ -259,13 +269,53 @@ def queuem(self):
259269
q = self.queue + ("*" if defaultq and self.queue == defaultq else "")
260270
return q
261271

272+
@property
273+
def color_enabled(self):
274+
return "\033[01;32m" if self._enabled else "\033[01;31m"
275+
276+
@property
277+
def color_off(self):
278+
return "\033[0;0m"
279+
280+
@property
281+
def enabled(self):
282+
return AltField("✔", "y") if self._enabled else AltField("✗", "n")
283+
284+
@property
285+
def color_started(self):
286+
return "\033[01;32m" if self._started else "\033[01;31m"
287+
288+
@property
289+
def started(self):
290+
return AltField("✔", "y") if self._started else AltField("✗", "n")
291+
292+
293+
def fetch_all_queue_status(handle, queues=None):
294+
if handle is None:
295+
# Return fake payload if handle is not open (e.g. during testing)
296+
return {"enable": True, "start": True}
297+
topic = "job-manager.queue-status"
298+
if queues is None:
299+
return handle.rpc(topic, {}).get()
300+
rpcs = {x: handle.rpc(topic, {"name": x}) for x in queues}
301+
return {x: rpcs[x].get() for x in rpcs}
302+
262303

263304
def list(args):
264305
headings = {
265306
"queue": "QUEUE",
266307
"queuem": "QUEUE",
267-
"defaults.timelimit": "DEFAULTTIME",
268-
"limits.timelimit": "TIMELIMIT",
308+
"submission": "SUBMIT",
309+
"scheduling": "SCHED",
310+
"enabled": "EN",
311+
"started": "ST",
312+
"enabled.ascii": "EN",
313+
"started.ascii": "ST",
314+
"color_enabled": "",
315+
"color_started": "",
316+
"color_off": "",
317+
"defaults.timelimit": "TDEFAULT",
318+
"limits.timelimit": "TLIMIT",
269319
"limits.range.nnodes": "NNODES",
270320
"limits.range.ncores": "NCORES",
271321
"limits.range.ngpus": "NGPUS",
@@ -277,6 +327,7 @@ def list(args):
277327
"limits.max.ngpus": "MAXGPUS",
278328
}
279329
config = None
330+
handle = None
280331

281332
if args.from_stdin:
282333
config = json.loads(sys.stdin.read())
@@ -291,13 +342,29 @@ def list(args):
291342
fmt = FluxQueueConfig("list").load().get_format_string(args.format)
292343
formatter = flux.util.OutputFormat(fmt, headings=headings)
293344

345+
# Build queue_config from args.queue, or config["queue"] if --queue
346+
# was unused:
347+
queue_config = {}
348+
if args.queue:
349+
for queue in args.queue:
350+
try:
351+
queue_config[queue] = config["queues"][queue]
352+
except KeyError:
353+
raise ValueError(f"No such queue: {queue}")
354+
elif config and "queues" in config:
355+
queue_config = config["queues"]
356+
294357
queues = []
295358
if config and "queues" in config:
296-
for key, value in config["queues"].items():
297-
queues.append(QueueInfo(key, config))
359+
status = fetch_all_queue_status(handle, queue_config.keys())
360+
for key, value in queue_config.items():
361+
queues.append(
362+
QueueInfo(key, config, status[key]["enable"], status[key]["start"])
363+
)
298364
else:
299365
# single anonymous queue
300-
queues.append(QueueInfo(None, config))
366+
status = fetch_all_queue_status(handle)
367+
queues.append(QueueInfo(None, config, status["enable"], status["start"]))
301368

302369
formatter.print_items(queues, no_header=args.no_header)
303370

@@ -425,6 +492,10 @@ def __call__(self, parser, namespace, values, option_string=None):
425492

426493
@flux.util.CLIMain(LOGGER)
427494
def main():
495+
sys.stdout = open(
496+
sys.stdout.fileno(), "w", encoding="utf8", errors="surrogateescape"
497+
)
498+
428499
parser = argparse.ArgumentParser(prog="flux-queue")
429500
subparsers = parser.add_subparsers(
430501
title="subcommands", description="", dest="subcommand"
@@ -452,6 +523,14 @@ def main():
452523
list_parser = subparsers.add_parser(
453524
"list", formatter_class=flux.util.help_formatter()
454525
)
526+
list_parser.add_argument(
527+
"-q",
528+
"--queue",
529+
action=FilterActionSetUpdate,
530+
default=set(),
531+
metavar="QUEUE,...",
532+
help="Include only specified queues in output",
533+
)
455534
list_parser.add_argument(
456535
"-o",
457536
"--format",

src/cmd/flux-resource.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
resource_status,
2828
)
2929
from flux.rpc import RPC
30-
from flux.util import Deduplicator, FilterActionSetUpdate, UtilConfig
30+
from flux.util import AltField, Deduplicator, FilterActionSetUpdate, UtilConfig
3131

3232

3333
class FluxResourceConfig(UtilConfig):
@@ -203,24 +203,6 @@ def ranks_by_queue(resource_set, config, queues):
203203
return ranks
204204

205205

206-
class AltField:
207-
"""
208-
Convenient wrapper for fields that have an ascii and non-ascii
209-
representation. Allows the ascii representation to be selected with
210-
{field.ascii}.
211-
"""
212-
213-
def __init__(self, default, ascii):
214-
self.default = default
215-
self.ascii = ascii
216-
217-
def __str__(self):
218-
return self.default
219-
220-
def __format__(self, fmt):
221-
return str(self).__format__(fmt)
222-
223-
224206
class ResourceStatusLine:
225207
"""Information specific to a given flux resource status line"""
226208

t/t2241-queue-cmd-list.t

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ test_under_flux 1 full
99

1010
test_expect_success 'flux-queue: default lists expected fields' '
1111
flux queue list > default.out &&
12-
grep DEFAULTTIME default.out &&
13-
grep TIMELIMIT default.out &&
12+
grep TDEFAULT default.out &&
13+
grep TLIMIT default.out &&
1414
grep NNODES default.out &&
1515
grep NCORES default.out &&
16+
grep ST default.out &&
17+
grep EN default.out &&
1618
grep NGPUS default.out
1719
'
1820

@@ -25,10 +27,12 @@ test_expect_success 'flux-queue: FLUX_QUEUE_LIST_FORMAT_DEFAULT works' '
2527

2628
test_expect_success 'flux-queue: --no-header works' '
2729
flux queue list --no-header > default_no_header.out &&
28-
test_must_fail grep DEFAULTTIME default_no_header.out &&
29-
test_must_fail grep TIMELIMIT default_no_header.out &&
30+
test_must_fail grep TDEFAULT default_no_header.out &&
31+
test_must_fail grep TLIMIT default_no_header.out &&
3032
test_must_fail grep NNODES default_no_header.out &&
3133
test_must_fail grep NCORES default_no_header.out &&
34+
test_must_fail grep ST default_no_header.out &&
35+
test_must_fail grep EN default_no_header.out &&
3236
test_must_fail grep NGPUS default_no_header.out
3337
'
3438

@@ -41,8 +45,8 @@ ALL_LIMITS_FMT="\
4145

4246
test_expect_success 'flux-queue: expected headers with non-default fields output' '
4347
flux queue list -o "${ALL_LIMITS_FMT}" > non_default.out &&
44-
grep DEFAULTTIME non_default.out &&
45-
grep TIMELIMIT non_default.out &&
48+
grep TDEFAULT non_default.out &&
49+
grep TLIMIT non_default.out &&
4650
grep NNODES non_default.out &&
4751
grep NCORES non_default.out &&
4852
grep NGPUS non_default.out &&
@@ -66,6 +70,41 @@ test_expect_success 'flux-queue: empty config limits are 0/infinity' '
6670
echo "inf,inf,0-inf,0-inf,0-inf,0,0,0,inf,inf,inf" > empty_config_all.exp &&
6771
test_cmp empty_config_all.exp empty_config_all.out
6872
'
73+
test_expect_success 'flux-queue: empty config: queue is enabled/started' '
74+
test_debug "flux queue list" &&
75+
test "$(flux queue list -no {submission})" = "enabled" &&
76+
test "$(flux queue list -no {scheduling})" = "started" &&
77+
test "$(flux queue list -no {enabled})" = "✔" &&
78+
test "$(flux queue list -no {started})" = "✔"
79+
'
80+
test_expect_success 'flux-queue: stop anonymous queue' '
81+
flux queue stop
82+
'
83+
test_expect_success 'flux-queue: queue is enabled/stopped' '
84+
test_debug "flux queue list" &&
85+
test "$(flux queue list -no {submission})" = "enabled" &&
86+
test "$(flux queue list -no {scheduling})" = "stopped" &&
87+
test "$(flux queue list -no {enabled})" = "✔" &&
88+
test "$(flux queue list -no {started})" = "✗"
89+
'
90+
test_expect_success 'flux-queue: disable anonymous queue' '
91+
flux queue disable testing
92+
'
93+
test_expect_success 'flux-queue: queue is disabled/stopped' '
94+
test_debug "flux queue list" &&
95+
test "$(flux queue list -no {submission})" = "disabled" &&
96+
test "$(flux queue list -no {scheduling})" = "stopped" &&
97+
test "$(flux queue list -no {enabled})" = "✗" &&
98+
test "$(flux queue list -no {started})" = "✗"
99+
'
100+
test_expect_success 'flux-queue: enable and start anonymous queue' '
101+
flux queue enable &&
102+
flux queue start
103+
'
104+
test_expect_success 'flux-queue: --queue option fails with anonymouns queue' '
105+
test_expect_code 1 flux queue list --queue=batch
106+
'
107+
69108
test_expect_success 'flux-queue: fsd of infinity is infinity' '
70109
flux queue list -n \
71110
-o "{defaults.timelimit!F},{limits.timelimit!F}" \
@@ -135,6 +174,15 @@ test_expect_success 'flux-queue: queue config, no default marked' '
135174
test $(grep "^batch,batch$" queues_no_default.out | wc -l) -eq 1 &&
136175
test $(grep ^"debug,debug$" queues_no_default.out | wc -l) -eq 1
137176
'
177+
test_expect_success 'flux-queue: --queue option works' '
178+
test "$(flux queue list -q batch -no "{queue}")" = "batch" &&
179+
test "$(flux queue list -q debug -no "{queue}")" = "debug" &&
180+
test_debug "flux queue list -q batch,debug -n" &&
181+
test $(flux queue list -q batch,debug -n | wc -l) -eq 2
182+
'
183+
test_expect_success 'flux-queue: invalid queue is detected with --queue' '
184+
test_expect_code 1 flux queue list --queue=foo
185+
'
138186

139187
# N.B. job-size.max.ngpus left out of [policy.limits] to test default
140188
# of infinity

0 commit comments

Comments
 (0)