Skip to content

Commit 337be43

Browse files
authored
Merge pull request #5219 from grondo/issue#5143
flux-resource: add `-i, --include` option to filter hosts for `status` and `drain` commands
2 parents a2f121d + 17d704b commit 337be43

File tree

9 files changed

+168
-16
lines changed

9 files changed

+168
-16
lines changed

doc/man1/flux-resource.rst

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ COMMANDS
6868
states as with ``flux resource list``. By default, the *STATE* reported
6969
by ``flux resource info`` is "all".
7070

71-
**status** [-n] [-o FORMAT] [-s STATE,...] [--skip-empty]
71+
**status** [-n] [-o FORMAT] [-s STATE,...] [-i TARGETS] [--skip-empty]
7272
Show system view of resources. This command queries both the resource
7373
service and scheduler to identify resources that are available,
7474
excluded by configuration, or administratively drained or draining.
@@ -85,6 +85,12 @@ COMMANDS
8585
valid states include "avail", "exclude", "draining", "drained", and "all".
8686
The special "drain" state is shorthand for "drained,draining".
8787

88+
With *-i, --include=TARGETS*, the results are filtered to only include
89+
resources matching **TARGETS**, which may be specified either as an idset
90+
of broker ranks or list of hosts in hostlist form. It is not an error to
91+
specify ranks or hosts which do not exist, the result will be filtered
92+
to include only those ranks or hosts that are present in *TARGETS*.
93+
8894
The *-o,--format=FORMAT* option customizes output formatting (See the
8995
OUTPUT FORMAT section below for details).
9096

@@ -94,13 +100,19 @@ COMMANDS
94100
unless the ``-s, --states`` option is used. Suppression of empty lines
95101
can may be forced with the ``--skip-empty`` option.
96102

97-
**drain** [-n] [-o FORMAT] [-f] [-u] [targets] [reason ...]
103+
**drain** [-n] [-o FORMAT] [-i TARGETS] [-f] [-u] [targets] [reason ...]
98104
If specified without arguments, list drained nodes. In this mode,
99105
*-n,--no-header* suppresses header from output and *-o,--format=FORMAT*
100106
customizes output formatting (see below). The *targets* argument is an
101107
IDSET or HOSTLIST specifying nodes to drain. Any remaining arguments
102108
are assumed to be a reason to be recorded with the drain event.
103109

110+
With *-i, --include=TARGETS*, **drain** output is filtered to only include
111+
resources matching **TARGETS**, which may be specified either as an idset
112+
of broker ranks or list of hosts in hostlist form. It is not an error to
113+
specify ranks or hosts which do not exist, the result will be filtered
114+
to include only those ranks or hosts that are present in *TARGETS*.
115+
104116
By default, **flux resource drain** will fail if any of the *targets*
105117
are already drained. To change this behavior, use either of the
106118
*-f, --force* or *-u, --update* options. With *--force*, the *reason* for

src/bindings/python/flux/hostlist.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def __iter__(self):
160160
def __contains__(self, name):
161161
"""Test if a hostname is in a Hostlist"""
162162
try:
163-
self.pimpl.find(name)
163+
self.find(name)
164164
except FileNotFoundError:
165165
return False
166166
return True
@@ -218,6 +218,34 @@ def copy(self):
218218
"""Copy a Hostlist object"""
219219
return Hostlist(handle=self.pimpl.copy())
220220

221+
def find(self, host):
222+
"""Return the position of a host in a Hostlist"""
223+
return self.pimpl.find(host)
224+
225+
def index(self, hosts, ignore_nomatch=False):
226+
"""
227+
Return a list of integers corresponding to the indices of ``hosts``
228+
in the current Hostlist.
229+
Args:
230+
hosts (str, Hostlist): List of hosts to find
231+
ignore_nomatch (bool): Ignore hosts in ``hosts`` that are not
232+
present in Hostlist. Otherwise, FileNotFound error is raised
233+
with the missing hosts.
234+
"""
235+
if not isinstance(hosts, Hostlist):
236+
hosts = Hostlist(hosts)
237+
ids = []
238+
notfound = Hostlist()
239+
for host in hosts:
240+
try:
241+
ids.append(self.find(host))
242+
except FileNotFoundError:
243+
notfound.append(host)
244+
if notfound and not ignore_nomatch:
245+
suffix = "s" if len(notfound) > 1 else ""
246+
raise FileNotFoundError(f"host{suffix} '{notfound}' not found")
247+
return ids
248+
221249

222250
def decode(arg):
223251
"""

src/bindings/python/flux/resource/status.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,52 @@ def __init__(self, rstatus=None, allocated_ranks=None):
5454
if allocated_ranks is None:
5555
allocated_ranks = IDset()
5656

57+
self.rstatus = rstatus
5758
self.rset = ResourceSet(rstatus["R"])
59+
self.nodelist = self.rset.nodelist
60+
self.allocated_ranks = allocated_ranks
61+
62+
self._recalculate()
63+
64+
def filter(self, include):
65+
"""
66+
Filter the reported resources in a ResourceStatus object
67+
Args:
68+
include (str, IDset, Hostlist): restrict the current set of
69+
reported ranks to the given ranks or hosts.
70+
"""
71+
try:
72+
include_ranks = IDset(include)
73+
except ValueError:
74+
include_ranks = self.nodelist.index(include, ignore_nomatch=True)
75+
self._recalculate(include_ranks)
5876

59-
# get idset of all ranks and nodelist:
77+
def _recalculate(self, include_ranks=None):
78+
"""
79+
Recalculate derived idsets and drain_info, only including ranks
80+
in the IDset 'include_ranks' if given.
81+
Args:
82+
include_ranks (IDset): restrict the current set of reported ranks.
83+
"""
84+
# get idset of all ranks:
6085
self.all = self.rset.ranks
61-
self.nodelist = self.rset.nodelist
6286

6387
# offline/online
64-
self.offline = IDset(rstatus["offline"])
65-
self.online = IDset(rstatus["online"])
88+
self.offline = IDset(self.rstatus["offline"])
89+
self.online = IDset(self.rstatus["online"])
6690

6791
# excluded: excluded by configuration
68-
self.exclude = IDset(rstatus["exclude"])
92+
self.exclude = IDset(self.rstatus["exclude"])
6993

7094
# allocated: online and allocated by scheduler
71-
self.allocated = allocated_ranks
95+
self.allocated = self.allocated_ranks
96+
97+
# If include_ranks was provided, filter all idsets to only those
98+
# that intersect the provided idset
99+
if include_ranks is not None:
100+
for name in ("all", "offline", "online", "exclude", "allocated"):
101+
result = getattr(self, name).intersect(include_ranks)
102+
setattr(self, name, result)
72103

73104
# drained: free+drain
74105
self.drained = IDset()
@@ -77,8 +108,10 @@ def __init__(self, rstatus=None, allocated_ranks=None):
77108

78109
# drain_info: ranks, timestamp, reason tuples for all drained resources
79110
self.drain_info = []
80-
for drain_ranks, entry in rstatus["drain"].items():
111+
for drain_ranks, entry in self.rstatus["drain"].items():
81112
ranks = IDset(drain_ranks)
113+
if include_ranks is not None:
114+
ranks = ranks.intersect(include_ranks)
82115
self.drained += ranks - self.allocated
83116
self.draining += ranks - self.drained
84117
info = DrainInfo(ranks, entry["timestamp"], entry["reason"])

src/cmd/flux-resource.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,12 @@ def status(args):
366366
else:
367367
rstatus = resource_status(flux.Flux()).get()
368368

369+
if args.include:
370+
try:
371+
rstatus.filter(include=args.include)
372+
except (ValueError, TypeError) as exc:
373+
raise ValueError(f"--include: {exc}") from None
374+
369375
formatter = flux.util.OutputFormat(fmt, headings=headings)
370376

371377
# Remove any `{color*}` fields if color is off
@@ -582,6 +588,13 @@ def main():
582588
help="Update only. Do not return an error if one or more targets "
583589
+ "are already drained. Do not overwrite any existing drain reason.",
584590
)
591+
drain_parser.add_argument(
592+
"-i",
593+
"--include",
594+
metavar="TARGETS",
595+
help="Include only specified targets in output set. TARGETS may be "
596+
+ "provided as an idset or hostlist.",
597+
)
585598
drain_parser.add_argument(
586599
"-o",
587600
"--format",
@@ -634,6 +647,13 @@ def main():
634647
metavar="STATE,...",
635648
help="Output resources in given states",
636649
)
650+
status_parser.add_argument(
651+
"-i",
652+
"--include",
653+
metavar="TARGETS",
654+
help="Include only specified targets in output set. TARGETS may be "
655+
+ "provided as an idset or hostlist.",
656+
)
637657
status_parser.add_argument(
638658
"-n", "--no-header", action="store_true", help="Suppress header output"
639659
)

src/common/libhostlist/hostlist.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ static int append_range_list_with_suffix (struct hostlist *hl,
456456
unsigned long j;
457457

458458
/* compute max buffer size for this set of hosts */
459-
int size = strlen (pfx) + strlen (sfx) + 20 + rng->width;
459+
int size = strlen (pfx) + strlen (sfx) + 20 + rng->width;
460460

461461

462462
for (i = 0; i < n; i++) {
@@ -854,8 +854,8 @@ static void hostlist_coalesce (struct hostlist *hl)
854854
*/
855855
if (new->hi < hprev->hi)
856856
hnext->hi = hprev->hi;
857-
858-
/*
857+
858+
/*
859859
* The duplicated range will inserted piecemeal below,
860860
* e.g. [5-7,6-8] -> [5-6,6-7,7-8]
861861
* Therefore adjust the end of hprev to new->lo (the

src/common/libhostlist/hostlist.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ void hostlist_sort (struct hostlist * hl);
9292
*/
9393
void hostlist_uniq (struct hostlist *hl);
9494

95-
/*
95+
/*
9696
* Return the host at the head of hostlist 'hl', or NULL if list is empty.
9797
* Leaves internal cursor pointing at the head item.
9898
*
@@ -102,7 +102,7 @@ void hostlist_uniq (struct hostlist *hl);
102102
*/
103103
const char * hostlist_first (struct hostlist *hl);
104104

105-
/*
105+
/*
106106
* Return the host at the tail of hostlist 'hl', or NULL if list is empty.
107107
* Leaves internal cursor pointing at the last item.
108108
*
@@ -112,7 +112,7 @@ const char * hostlist_first (struct hostlist *hl);
112112
*/
113113
const char * hostlist_last (struct hostlist *hl);
114114

115-
/*
115+
/*
116116
* Advance the internal cursor and return the next host in 'hl' or NULL
117117
* if list is empty or the end of the list has been reached.
118118
*

t/python/t0020-hostlist.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,24 @@ def test_copy(self):
160160
hl.delete("foo[0-3]")
161161
self.assertEqual(str(cp), "foo[0-3]")
162162

163+
def test_find(self):
164+
hl = hostlist.decode("foo[0-3]")
165+
self.assertEqual(hl.find("foo1"), 1)
166+
self.assertEqual(hl.find("foo3"), 3)
167+
with self.assertRaises(FileNotFoundError):
168+
hl.find("foo4")
169+
170+
def test_index_method(self):
171+
hl = hostlist.decode("foo[0-10]")
172+
self.assertListEqual(hl.index("foo[1,5,7]"), [1, 5, 7])
173+
self.assertListEqual(hl.index(hostlist.decode("foo1")), [1])
174+
self.assertListEqual(hl.index("foo[9-11]", ignore_nomatch=True), [9, 10])
175+
self.assertListEqual(hl.index("foo11", ignore_nomatch=True), [])
176+
with self.assertRaises(FileNotFoundError):
177+
hl.index("foo[9-11]")
178+
with self.assertRaises(FileNotFoundError):
179+
hl.index("foo11")
180+
163181

164182
if __name__ == "__main__":
165183
unittest.main(testRunner=TAPTestRunner())

t/t2311-resource-drain.t

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,12 @@ test_expect_success 'flux resource drain differentiates drain/draining' '
318318
test $(flux resource status -s drain -no {nnodes}) -eq ${SIZE}
319319
'
320320

321+
test_expect_success 'flux resource drain supports --include' '
322+
flux resource drain -ni 0 >drain-include.output &&
323+
test_debug "cat drain-include.output" &&
324+
test $(wc -l <drain-include.output) -eq 1
325+
'
326+
321327
test_expect_success 'flux resource drain works without scheduler loaded' '
322328
flux module unload sched-simple &&
323329
flux resource drain &&

t/t2351-resource-status-input.t

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ for input in ${INPUTDIR}/*.json; do
2626
'
2727
done
2828

29+
# Ensure all tested inputs can also work with --include
30+
# We simply restrict to rank 0 and then ensure {ranks} returns only 0
31+
for input in ${INPUTDIR}/*.json; do
32+
name=$(basename ${input%%.json})
33+
test_expect_success "flux-resource status input --include check: $name" '
34+
base=${input%%.json} &&
35+
name=$(basename $base)-i &&
36+
flux resource status -o "{ranks} {nodelist}" --include=0 \
37+
--from-stdin < $input > $name.output 2>&1 &&
38+
test_debug "cat $name.output" &&
39+
grep "^0[^,-]" $name.output
40+
'
41+
done
42+
43+
2944
test_expect_success 'flux-resource status: header included with all formats' '
3045
cat <<-EOF >headers.expected &&
3146
state==STATE
@@ -192,4 +207,24 @@ test_expect_success 'flux-resource status: lines are combined based on format' '
192207
EOF
193208
test_cmp ts-detailed2.expected ts-detailed2.out
194209
'
210+
test_expect_success 'flux-resource status: --include works with ranks' '
211+
INPUT=${INPUTDIR}/drain.json &&
212+
flux resource status --include=1,3 --from-stdin \
213+
-no "{nnodes}" <$INPUT >drain-include.out &&
214+
test_debug "cat drain-include.out" &&
215+
test "$(cat drain-include.out)" = "2"
216+
'
217+
test_expect_success 'flux-resource status: --include works with hostnames' '
218+
INPUT=${INPUTDIR}/drain.json &&
219+
flux resource status --include=foo[1,3] --from-stdin \
220+
-no "{nodelist}" <$INPUT >drain-include-host.out &&
221+
test_debug "cat drain-include-host.out" &&
222+
test "$(cat drain-include-host.out)" = "foo[1,3]"
223+
'
224+
test_expect_success 'flux-resource status: --include works with invalid host' '
225+
INPUT=${INPUTDIR}/drain.json &&
226+
flux resource status --include=foo7 --from-stdin \
227+
-no "{nodelist}" <$INPUT >drain-empty.out 2>&1 &&
228+
test_must_be_empty drain-fail.out
229+
'
195230
test_done

0 commit comments

Comments
 (0)