blob: 0d5649ec97e6c0825e1d1c0f8967198f4bf82880 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""A GRES device keeps the controller's device number past an unusable entry.
An explicit gres.conf with no AutoDetect lists three gpu File records in
order: /dev/tty0, a regular file, and /dev/tty1, on a node running
TaskPlugin=task/cgroup with ConstrainDevices=yes. The controller counts
every configured record, so /dev/tty1 owns device number 2 and slurmd
must number it 2 as well (gres.c gres_node_config_load) even though a
regular file fails the character or block device check (gres.c
_set_gres_device_desc) and initializes no device. An older slurmd
numbered only the devices it could initialize, so everything behind the
unusable entry sat one number low and the job holding the unusable
record was handed the device behind it. The step environment, the device
cgroup, the prolog, epilog, and batch environment, and the share an mps
job gets of its sharing devices all resolve a device through that
number, and a job allocated the unusable record alone gets no device at
all. The same load must also refuse a gres.conf listing that File name
twice, as it always refused a duplicated usable one. The devices are
plain tty character devices, so no GPU hardware is required.
"""
import os
import re
import stat
import pytest
import atf
# The two usable records, in gres.conf order. The unusable regular file
# sits between them, so the second tty is the device that proves the
# numbering.
CONFIGURED_TTYS = ["/dev/tty0", "/dev/tty1"]
# The vendor GPU variables a gpu record without Flags sets beside the Slurm
# ones (gres.c, the GRES_CONF_ENV_SET default).
VENDOR_VARIABLES = (
"CUDA_VISIBLE_DEVICES",
"ROCR_VISIBLE_DEVICES",
"GPU_DEVICE_ORDINAL",
"ZE_AFFINITY_MASK",
)
# The mps count of the node, which the gres/mps plugin spreads over the
# three configured gpu records, the share an mps job on one device asks
# for, and the share that makes an mps job span two devices. A share is
# reported as the percentage of the count on the job's device, and the
# count is chosen so that percentage differs from the one computed from
# the count of 100 the plugin falls back to when it finds no count for a
# device (gres_mps.c _get_dev_count). The all_sharing flag lets an mps job
# use every sharing device of the node, which the select parameter below
# lets it span.
MPS_COUNT = 600
MPS_SHARE = 50
MPS_SPANNING_SHARE = 300
MPS_RECORD = f"Name=mps Count={MPS_COUNT} Flags=all_sharing"
# The regular file configured as the middle gpu File record, the module's
# healthy four record gres.conf content, and the directory where the
# Prolog and Epilog leave the GRES environment of every job. Assigned in
# setup, the file and the directory in the module fixture working
# directory.
_unusable_file = None
_gres_conf = None
_prep_env_dir = None
@pytest.fixture(scope="module", autouse=True)
def setup():
global _unusable_file, _gres_conf, _prep_env_dir
atf.require_version(
(26, 11),
"sbin/slurmd",
reason=(
"Issue 50982: slurmd numbers GRES devices as slurmctld counts them "
"since 26.11"
),
)
atf.require_config_parameter("SelectType", "select/cons_tres")
# Strict on purpose: the CR_* allocation units are mutually exclusive
# (read_config.c _parse_select_type_param rejects more than one), so an
# additive include on a variant value would leave slurm.conf unloadable.
# MULTIPLE_SHARING_GRES_PJ lets one mps job take shares on more than
# one sharing device, which the spanning test needs.
atf.require_config_parameter(
"SelectTypeParameters", "CR_CPU,MULTIPLE_SHARING_GRES_PJ"
)
atf.require_config_parameter_includes("GresTypes", "gpu")
atf.require_config_parameter_includes("GresTypes", "mps")
atf.require_config_parameter_includes("TaskPlugin", "task/cgroup")
atf.require_config_parameter("ConstrainDevices", "yes", source="cgroup")
atf.require_tty(0)
atf.require_tty(1)
# A regular file stats fine yet is neither a character nor a block
# device, so slurmd counts its record and initializes no device for
# it. Written in the module fixture working directory, which
# conftest.py update_tmp_path_exec_permissions already opened up, and
# named in gres.conf by its absolute path so the root-run slurmd can
# stat it there. Its name ends in a digit no tty uses: the gres/mps
# plugin keys a device's count by that trailing number, and -1, the
# key a name without one gets, is also its "no device" value, which
# would hand a job that resolved no device the unusable record's
# count instead of the fallback.
_unusable_file = os.path.abspath("unusable_gpu_file9")
atf.run_command(
f"cat > {_unusable_file}", input="not a device\n", fatal=True, quiet=True
)
_gres_conf = (
f"Name=gpu File={CONFIGURED_TTYS[0]}\n"
f"Name=gpu File={_unusable_file}\n"
f"Name=gpu File={CONFIGURED_TTYS[1]}\n"
f"{MPS_RECORD}\n"
)
atf.require_config_file("gres.conf", _gres_conf)
# The Prolog and the Epilog record the GRES variables slurmd hands them,
# one file per job and hook, in the same opened up directory. They exit
# 0 whatever they find, so a missing variable fails a test and never
# drains the node.
_prep_env_dir = os.getcwd()
recorded = "|".join(
("SLURM_JOB_GPUS", "CUDA_MPS_ACTIVE_THREAD_PERCENTAGE") + VENDOR_VARIABLES
)
for hook in ("Prolog", "Epilog"):
script = os.path.join(_prep_env_dir, f"{hook.lower()}.sh")
out = f"{_prep_env_dir}/{hook.lower()}.$SLURM_JOB_ID"
# Written aside and renamed, so the file exists only once complete
atf.make_bash_script(
script,
f"env | grep -E '^({recorded})=' > {out}.tmp\n"
f"mv {out}.tmp {out}\n"
"exit 0\n",
)
atf.require_config_parameter(hook, script)
atf.require_nodes(1, [("Gres", f"gpu:3,mps:{MPS_COUNT}"), ("CPUs", 2)])
atf.require_slurm_running()
def _install_gres_conf(content):
"""Write the module's gres.conf as the slurm user that owns it.
slurmd reads the file the config dir holds, so a test driving a load
against a different content installs it here and puts the healthy one
back the same way.
"""
atf.run_command(
f"cat > {atf.properties['slurm-config-dir']}/gres.conf",
input=content,
user=atf.properties["slurm-user"],
fatal=True,
quiet=True,
)
def _read_prep_env(hook, job_id):
"""The variables the Prolog or Epilog recorded for a job, as a dict.
The hook writes its file as the job starts or ends on the node, so the
file is waited for rather than expected at once.
"""
path = os.path.join(_prep_env_dir, f"{hook}.{job_id}")
assert atf.wait_for_file(
path
), f"the {hook} must leave its GRES environment for job {job_id} in {path}"
recorded = atf.run_command_output(f"cat {path}", fatal=True, quiet=True)
return dict(line.split("=", 1) for line in recorded.splitlines() if "=" in line)
def _hold_lowest_devices(gres_node, count):
"""Run a job on the lowest device numbers for the rest of the test.
The controller picks free devices in ascending order
(gres_select_filter.c _pick_gres_topo), so a job asking count GPUs
takes numbers 0 to count-1 and leaves the next number to the test's own
jobs: number 2, the record behind the unusable entry, behind a two-GPU
job, and number 1, the unusable record itself, behind a one-GPU job.
The allocation is checked rather than assumed, as a precondition, and
the job goes away with the test's other jobs when the test ends.
"""
job_id = atf.submit_job_sbatch(
f"-w {gres_node} -N1 -n1 -t5 --gres=gpu:{count} -J hold_low_devices "
'--wrap "sleep infinity"',
fatal=True,
)
atf.wait_for_job_state(job_id, "RUNNING", fatal=True)
expected = "gpu:1(IDX:0)" if count == 1 else f"gpu:{count}(IDX:0-{count - 1})"
job_gres = atf.get_job_parameter(job_id, "GRES", fatal=True, quiet=True)
if not job_gres or expected not in job_gres:
pytest.fail(
f"the holding job must be allocated device numbers 0 to {count - 1} "
f"for the test to mean anything, got GRES={job_gres!r}"
)
return job_id
def _probe_script(name):
"""Write the step script that reports its GRES numbers and tty access.
The step reports the device numbers the controller allocated its job
while the job still exists, its Slurm and vendor variables, then opens
each configured tty in a subshell, so a refusal is a nonzero status
rather than a failed redirection in the probe itself, and exits 0
whatever the outcome so the step's own status stays clean.
"""
scontrol = f"{atf.properties['slurm-bin-dir']}/scontrol"
atf.make_bash_script(
name,
f'echo "JOB_IDX=$({scontrol} show job -dd $SLURM_JOB_ID | '
"grep -oE 'gpu:1\\(IDX:[0-9,-]+\\)' | head -1)\"\n"
'echo "STEP_GPUS=$SLURM_STEP_GPUS"\n'
'echo "GPUS_ON_NODE=$SLURM_GPUS_ON_NODE"\n'
+ "".join(f'echo "{var}=${var}"\n' for var in VENDOR_VARIABLES)
+ f"for f in {' '.join(CONFIGURED_TTYS)}\n"
"do\n"
' if (exec 3< "$f") 2>/dev/null\n'
" then\n"
' echo "OPEN $f"\n'
" else\n"
' echo "DENIED $f"\n'
" fi\n"
"done\n"
"exit 0\n",
)
return name
def _parse_probe(result):
"""The probe's variables as a dict and its tty verdicts as a dict."""
report = dict(re.findall(r"^([A-Z_]+)=(\S*)$", result["stdout"], re.MULTILINE))
probed = {
path: state
for state, path in re.findall(
r"^(OPEN|DENIED) (\S+)$", result["stdout"], re.MULTILINE
)
}
assert set(probed) == set(CONFIGURED_TTYS), (
"the step must probe both configured ttys, its report reads "
f"{result['stdout']!r}"
)
return report, probed
@pytest.fixture()
def healthy_gres_conf():
"""Leave the module's healthy gres.conf installed however a test ends."""
yield
_install_gres_conf(_gres_conf)
@pytest.fixture()
def readable_ttys():
"""Grant both ttys world read for a test and put their modes back.
A file mode refuses an open just as flatly as the device cgroup, so the
cgroup is left as the only refuser and openability rather than the
listing is the answer the probes read.
"""
starting_modes = {
path: stat.S_IMODE(os.stat(path).st_mode) for path in CONFIGURED_TTYS
}
for path in starting_modes:
atf.run_command(f"chmod o+r {path}", user="root", quiet=True, fatal=True)
yield
for path, mode in starting_modes.items():
atf.run_command(f"chmod {mode:04o} {path}", user="root", quiet=True)
restored = {path: stat.S_IMODE(os.stat(path).st_mode) for path in starting_modes}
assert restored == starting_modes, (
"device node modes were not restored, expected "
f"{starting_modes!r} and read {restored!r}"
)
@pytest.fixture(scope="module")
def gres_node():
"""The node hosting the module's gpu gres, read from a step on it."""
output = atf.run_job_output(
"-N1 -n1 -t1 --gres=gpu:1 --mpi=none -J gpu_node_probe "
"printenv SLURMD_NODENAME",
fatal=True,
)
node = output.strip()
assert node, "a step holding the gpu gres must report the node it ran on"
return node
def test_step_env_numbers_devices_as_the_controller_counts(gres_node):
"""SLURM_STEP_GPUS reads 0,2 for a step holding all three records.
slurmstepd builds SLURM_STEP_GPUS from each allocated device's
shared number (gres_common.c common_gres_set_env), so the record
behind the unusable middle entry keeps the controller's number 2
while SLURM_GPUS_ON_NODE still counts all three allocated records. A
slurmd numbering only the usable devices read 0,1 here. The vendor
GPU variables renumber the two devices the task can see from zero
under ConstrainDevices=yes (gres_common.c common_gres_set_env with
gres_use_local_device_index()), so each of them reads 0,1 and the
Slurm variable is the one that carries the shared number.
"""
result = atf.run_job(
f"-w {gres_node} -N1 -n1 -t1 --gres=gpu:3 --mpi=none "
"-J step_env_numbers bash -c "
"'echo STEP_GPUS=$SLURM_STEP_GPUS && "
"echo GPUS_ON_NODE=$SLURM_GPUS_ON_NODE && "
+ " && ".join(f"echo {var}=${var}" for var in VENDOR_VARIABLES)
+ "'",
fatal=True,
)
report = dict(re.findall(r"^([A-Z_]+)=(\S*)$", result["stdout"], re.MULTILINE))
assert "STEP_GPUS" in report, (
"the step must report its SLURM_STEP_GPUS, its report reads "
f"{result['stdout']!r}"
)
assert report["STEP_GPUS"] == "0,2", (
"a step holding all three configured records must read the "
"controller numbers 0,2 in SLURM_STEP_GPUS, with the device "
"behind the unusable entry keeping number 2, got "
f"{report['STEP_GPUS']!r}"
)
assert report.get("GPUS_ON_NODE") == "3", (
"the step must count all three allocated records in "
f"SLURM_GPUS_ON_NODE, its report reads {result['stdout']!r}"
)
for name in VENDOR_VARIABLES:
assert report.get(name) == "0,1", (
f"{name} must renumber the step's two devices to 0,1 under "
f"ConstrainDevices=yes, its report reads {result['stdout']!r}"
)
def test_device_cgroup_admits_the_controller_numbered_device(gres_node, readable_ttys):
"""A step holding only device number 2 opens /dev/tty1 and no other.
With the two lowest device numbers held by another job the following
one gpu step gets number 2, the record behind the unusable entry.
task/cgroup admits it by the major and minor slurmd resolved from the
File path it numbered and denies the other configured ones
(task_cgroup_devices.c _handle_device_access). Under the one-low
numbering that number matched no device, the step environment came up
empty, and the granted device stayed denied. The vendor GPU variables
renumber the devices the task can see from zero under
ConstrainDevices=yes (gres_common.c common_gres_set_env with
gres_use_local_device_index()), so each of them reads 0 for the single
device the step holds.
"""
_hold_lowest_devices(gres_node, 2)
result = atf.run_job(
f"-w {gres_node} -N1 -n1 -t1 --gres=gpu:1 --mpi=none "
f"-J probe_device_cgroup {_probe_script('./probe_number_2.sh')}",
fatal=True,
)
report, probed = _parse_probe(result)
assert report.get("JOB_IDX") == "gpu:1(IDX:2)", (
"with the two lowest device numbers held by another job, the one "
"gpu job must be allocated the controller number 2, its report "
f"reads {result['stdout']!r}"
)
assert report.get("STEP_GPUS") == "2", (
"the step must read its allocated controller number 2 in "
f"SLURM_STEP_GPUS, its report reads {result['stdout']!r}"
)
for name in VENDOR_VARIABLES:
assert report.get(name) == "0", (
f"{name} must renumber the step's single device to 0 under "
f"ConstrainDevices=yes, its report reads {result['stdout']!r}"
)
assert probed[CONFIGURED_TTYS[1]] == "OPEN", (
"the step holding device number 2 must be able to open its own "
f"{CONFIGURED_TTYS[1]}, the probe reads {probed!r}"
)
assert probed[CONFIGURED_TTYS[0]] == "DENIED", (
"the step holding only device number 2 must be refused the "
f"unallocated {CONFIGURED_TTYS[0]}, the probe reads {probed!r}"
)
def test_step_allocated_the_unusable_record_alone_gets_no_device(
gres_node, readable_ttys
):
"""A step holding only device number 1, the unusable record, gets nothing.
With number 0 held by another job the following one gpu step gets
number 1, the record whose File initialized no device. The controller
counts it as one GPU, so SLURM_GPUS_ON_NODE reads 1, but no device
answers to its number: the Slurm and vendor variables come up empty
and the device cgroup admits neither tty. Under the one-low numbering
that number named /dev/tty1, the device allocated to number 2, and the
step was handed it.
"""
_hold_lowest_devices(gres_node, 1)
result = atf.run_job(
f"-w {gres_node} -N1 -n1 -t1 --gres=gpu:1 --mpi=none "
f"-J probe_unusable_record {_probe_script('./probe_number_1.sh')}",
fatal=True,
)
report, probed = _parse_probe(result)
assert report.get("JOB_IDX") == "gpu:1(IDX:1)", (
"with device number 0 held by another job, the one gpu job must be "
f"allocated the controller number 1, its report reads {result['stdout']!r}"
)
assert report.get("GPUS_ON_NODE") == "1", (
"the step must still count its one allocated record in "
f"SLURM_GPUS_ON_NODE, its report reads {result['stdout']!r}"
)
for name in ("STEP_GPUS",) + VENDOR_VARIABLES:
assert report.get(name) == "", (
f"{name} must be empty for the step allocated the unusable record "
f"alone, its report reads {result['stdout']!r}"
)
for path in CONFIGURED_TTYS:
assert probed[path] == "DENIED", (
"the step allocated the unusable record alone must be refused "
f"{path}, the probe reads {probed!r}"
)
def test_prolog_epilog_and_batch_env_number_devices_as_the_controller_counts(
gres_node,
):
"""The prolog, epilog, and batch environment carry the controller's number.
slurmd builds the prolog and epilog GRES environment from the job's
allocation bitmap, looking each allocated bit up by device number
(gres_common.c gres_common_prep_set_env), and the batch step gets
SLURM_JOB_GPUS the same way (gres_common.c common_gres_set_env). With
the two lowest numbers held, a one gpu batch job gets number 2 and all
three must report it, where the one-low numbering found no device for
that bit and left every one of them empty. The holding job, allocated
numbers 0 and 1, owns the single device 0, where the one-low numbering
handed it /dev/tty1 as its second one. The prolog and epilog carry the
shared number in the vendor variables too, while the batch step
renumbers its single device to 0 under ConstrainDevices=yes. The
gres/mps plugin resolves the sharing device of an mps job through its
own copy of the lookup before it computes the job's share of that
device (gres_mps.c gres_p_prep_set_env), so an mps job landing behind
the held devices must get its share in the prolog and the epilog, where
the one-low numbering walked the device list by position, found nothing
for bit 2, and set no share.
"""
hold_job = _hold_lowest_devices(gres_node, 2)
recorded = _read_prep_env("prolog", hold_job)
for name in ("SLURM_JOB_GPUS",) + VENDOR_VARIABLES:
assert recorded.get(name) == "0", (
"the prolog of the job holding numbers 0 and 1 must name only "
f"device 0 in {name}, the unusable record owning none, it "
f"recorded {recorded!r}"
)
gpu_job = atf.submit_job_sbatch(
f"-w {gres_node} -N1 -n1 -t1 --gres=gpu:1 -J batch_number_2 "
"--output batch_gpu.out --wrap 'echo JOB_GPUS=$SLURM_JOB_GPUS'",
fatal=True,
)
atf.wait_for_job_state(gpu_job, "DONE", fatal=True)
atf.assert_file_contents(
"batch_gpu.out",
"JOB_GPUS=2\n",
contains=True,
strip=False,
message=(
"the batch step allocated the controller number 2 must read it in "
"SLURM_JOB_GPUS"
),
)
for hook in ("prolog", "epilog"):
recorded = _read_prep_env(hook, gpu_job)
for name in ("SLURM_JOB_GPUS",) + VENDOR_VARIABLES:
assert recorded.get(name) == "2", (
f"the {hook} of the job allocated the controller number 2 must "
f"read it in {name}, it recorded {recorded!r}"
)
expected_share = str(MPS_SHARE * 100 // (MPS_COUNT // 3))
mps_job = atf.submit_job_sbatch(
f"-w {gres_node} -N1 -n1 -t1 --gres=mps:{MPS_SHARE} -J mps_behind_unusable "
"--output batch_mps.out --wrap 'echo JOB_GPUS=$SLURM_JOB_GPUS "
"PCT=$CUDA_MPS_ACTIVE_THREAD_PERCENTAGE CVD=$CUDA_VISIBLE_DEVICES'",
fatal=True,
)
atf.wait_for_job_state(mps_job, "DONE", fatal=True)
atf.assert_file_contents(
"batch_mps.out",
f"JOB_GPUS=2 PCT={expected_share} CVD=0\n",
contains=True,
strip=False,
message=(
"the batch step of the mps job sharing the device behind the "
f"unusable entry must read number 2, its {expected_share} percent "
"share, and the device renumbered to 0"
),
)
for hook in ("prolog", "epilog"):
recorded = _read_prep_env(hook, mps_job)
assert recorded.get("SLURM_JOB_GPUS") == "2", (
f"the {hook} of the mps job sharing the device behind the unusable "
f"entry must read number 2 in SLURM_JOB_GPUS, it recorded {recorded!r}"
)
assert recorded.get("CUDA_VISIBLE_DEVICES") == "2", (
f"the {hook} of the mps job must name its sharing device 2 in "
f"CUDA_VISIBLE_DEVICES, it recorded {recorded!r}"
)
assert recorded.get("CUDA_MPS_ACTIVE_THREAD_PERCENTAGE") == expected_share, (
f"the {hook} of the mps job must read its {MPS_SHARE} of the "
f"{MPS_COUNT // 3} on its device as {expected_share} percent in "
f"CUDA_MPS_ACTIVE_THREAD_PERCENTAGE, it recorded {recorded!r}"
)
def test_mps_job_spanning_the_unusable_record_gets_its_share(gres_node):
"""An mps job spanning the unusable record and /dev/tty1 gets its share.
With device 0 held by a gpu job, an mps job asking more than one
device's count spans the two remaining records, numbers 1 and 2, the
lower of which initialized no device. The gres/mps plugin computes the
job's share from the first device in list order whose number the job
holds (gres_mps.c gres_p_prep_set_env, as gres_common.c
common_gres_set_env picks it), /dev/tty1 here, so the prolog and the
epilog carry the share and name device 2. A lookup through the job's
lowest number alone found no device for number 1 and set no share. The
share of a spanning job is the plugin's own arithmetic over the count
of that one device, so the test pins the computed form.
"""
_hold_lowest_devices(gres_node, 1)
expected_share = str(MPS_SPANNING_SHARE * 100 // (MPS_COUNT // 3))
mps_job = atf.submit_job_sbatch(
f"-w {gres_node} -N1 -n1 -t1 --gres=mps:{MPS_SPANNING_SHARE} "
"-J mps_spanning_unusable --output batch_mps_spanning.out "
"--wrap 'echo JOB_GPUS=$SLURM_JOB_GPUS GPUS_ON_NODE=$SLURM_GPUS_ON_NODE'",
fatal=True,
)
atf.wait_for_job_state(mps_job, "DONE", fatal=True)
atf.assert_file_contents(
"batch_mps_spanning.out",
"JOB_GPUS=2 GPUS_ON_NODE=2\n",
contains=True,
strip=False,
message=(
"the batch step of the spanning mps job must name device 2 alone "
"in SLURM_JOB_GPUS while SLURM_GPUS_ON_NODE counts both records"
),
)
for hook in ("prolog", "epilog"):
recorded = _read_prep_env(hook, mps_job)
assert recorded.get("SLURM_JOB_GPUS") == "2", (
f"the {hook} of the spanning mps job must name device 2 alone in "
f"SLURM_JOB_GPUS, it recorded {recorded!r}"
)
assert recorded.get("CUDA_VISIBLE_DEVICES") == "2", (
f"the {hook} of the spanning mps job must name device 2 in "
f"CUDA_VISIBLE_DEVICES, it recorded {recorded!r}"
)
assert recorded.get("CUDA_MPS_ACTIVE_THREAD_PERCENTAGE") == expected_share, (
f"the {hook} of the spanning mps job must read its share over the "
f"{MPS_COUNT // 3} on device 2 as {expected_share} percent in "
f"CUDA_MPS_ACTIVE_THREAD_PERCENTAGE, it recorded {recorded!r}"
)
def test_duplicate_unusable_file_refuses_config_load(gres_node, healthy_gres_conf):
"""slurmd refuses its gres config when the unusable File repeats.
The gres config load records every configured File name whether or
not its device initialized and fails on a repeated one (gres.c
gres_node_config_load), which the gpu plugin turns into a fatal
error (gres_gpu.c gres_p_node_config_load). slurmd -G runs that same
load (slurmd.c _load_gres) in a foreground process, so the refusal
is driven without stopping the node's running slurmd. The repeat
replaces the third record to keep the count at the configured three,
since the load trims records beyond the slurm.conf count before the
duplicate check walks the list. An older slurmd dropped an unusable
entry before that check saw its name and accepted the repeat.
"""
slurmd_load = f"{atf.properties['slurm-sbin-dir']}/slurmd -N {gres_node} -G"
healthy = atf.run_command(slurmd_load, user="root", quiet=True)
assert healthy["exit_code"] == 0, (
"the gres config load must accept the healthy gres.conf with one "
"unusable File entry, it reads "
f"{healthy['stdout'] + healthy['stderr']!r}"
)
broken_conf = (
f"Name=gpu File={CONFIGURED_TTYS[0]}\n"
f"Name=gpu File={_unusable_file}\n"
f"Name=gpu File={_unusable_file}\n"
f"{MPS_RECORD}\n"
)
_install_gres_conf(broken_conf)
refused = atf.run_command(slurmd_load, user="root", quiet=True)
assert refused["exit_code"] != 0, (
"the gres config load must refuse a gres.conf listing the "
"unusable File name twice, it exited 0 with "
f"{refused['stdout'] + refused['stderr']!r}"
)
assert (
f"duplicate device file name ({_unusable_file})"
in refused["stdout"] + refused["stderr"]
), (
"the refusal must name the duplicated device file, it reads "
f"{refused['stdout'] + refused['stderr']!r}"
)
# The healthy file goes back before the clean reload is asserted, and
# the healthy_gres_conf fixture covers the endings that never get here.
_install_gres_conf(_gres_conf)
restored = atf.run_command(slurmd_load, user="root", quiet=True)
assert restored["exit_code"] == 0, (
"the gres config load must accept the restored healthy gres.conf, "
f"it reads {restored['stdout'] + restored['stderr']!r}"
)