blob: c13a00c15b834389449f028974261f06114780bb [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Per-node config_overrides applies on the registration that reports it.
slurmd always registers physical memory. The controller compares that with
the configured RealMemory and marks the node INVAL when the host is short,
unless config_overrides applies to that node. The flag may arrive only in
this registration (slurmd --parameters), and the memory check has to see it
then. A global SlurmdParameters flag is not set here, and slurmctld is not
given the flag in slurm.conf, so the previous registration cannot already
have it.
The node is first brought up with a tiny RealMemory so startup can see it
IDLE. The test then raises RealMemory far above the host and reconfigures.
One slurmd restart carries config_overrides. A second restart would store
the flag on the first pass and then pass for the wrong reason.
"""
import re
import pytest
import atf
pytestmark = pytest.mark.slow
NODE = "node1" # require_nodes(1, ...) yields node1
# Far above any host this suite runs on, so reported physical memory is
# below 100% of configured RealMemory.
HUGE_MEMORY = 100000000
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_CPU")
# ATF enables config_overrides by default. Drop it so the global flag
# cannot satisfy the memory check. The test supplies it per node.
atf.require_config_parameter_excludes("SlurmdParameters", "config_overrides")
# Small enough that the host's physical memory passes the first registration.
atf.require_nodes(1, [("CPUs", 2), ("RealMemory", 1)])
atf.require_version(
(26, 11),
component="sbin/slurmctld",
reason="Ticket 25180: per-node config_overrides is applied at registration in 26.11",
)
atf.require_version(
(25, 11),
component="bin/scontrol",
reason="Ticket 22862: per-node Parameters= in slurm.conf and scontrol show node added in 25.11",
)
atf.require_slurm_running()
@pytest.fixture
def restore_node_line():
"""Put RealMemory and Parameters back on the node line after the test."""
yield
atf.set_node_parameter(NODE, "RealMemory", 1)
atf.set_node_parameter(NODE, "Parameters", None)
def _restart_slurmd(parameters):
"""Restart this node's slurmd once with ``--parameters``.
Returns when the controller has the registration of the new slurmd.
"""
atf.stop_slurmd(NODE)
atf.start_slurmd(NODE, args=f"--parameters={parameters}", fatal=True)
def _set_real_memory(memory):
"""Write RealMemory on this node's slurm.conf line. Does not reconfigure."""
path = f"{atf.properties['slurm-config-dir']}/slurm.conf"
text = atf.run_command_output(
f"cat {path}", user=atf.properties["slurm-user"], quiet=True
)
found = False
lines = []
for line in text.splitlines():
if re.match(rf"NodeName={NODE}(\s|$)", line):
if re.search(r"(^|\s)RealMemory=\S+", line):
line = re.sub(
r"(^|\s)RealMemory=\S+",
lambda m: f"{m.group(1)}RealMemory={memory}",
line,
)
else:
line += f" RealMemory={memory}"
found = True
lines.append(line)
if not found:
pytest.fail(f"NodeName={NODE} was not found in {path}")
atf.run_command(
f"cat > {path}",
input="\n".join(lines) + "\n",
user=atf.properties["slurm-user"],
fatal=True,
)
def test_config_overrides_applies_on_that_registration():
"""Raising RealMemory above the host does not leave the node INVAL when
the same registration reports config_overrides."""
for _ in atf.timer(timeout=60, fatal=True):
state = atf.get_node_field(NODE, "State")
if state is not None and "DOWN" not in state and "INVAL" not in state:
break
parameters = atf.get_node_field(NODE, "Parameters") or ""
assert "config_override" not in parameters, atf.get_node_detail(NODE)
_set_real_memory(HUGE_MEMORY)
atf.run_command(
"scontrol reconfigure", user=atf.properties["slurm-user"], fatal=True
)
for _ in atf.timer(timeout=60, fatal=True):
if atf.get_node_field(NODE, "RealMemory") == str(HUGE_MEMORY):
break
# Keep whatever this node already reports and add the flag.
# --parameters replaces the daemon's global list.
reported = parameters
if reported and not reported.endswith(","):
reported += ","
reported += "config_overrides"
_restart_slurmd(reported)
# The registration that reported config_overrides must not leave the
# node invalid.
for _ in atf.timer(timeout=60, fatal=True):
state = atf.get_node_field(NODE, "State")
if state is not None and "INVAL" not in state and "DOWN" not in state:
break
memory = atf.get_node_field(NODE, "RealMemory")
assert memory == str(HUGE_MEMORY), atf.get_node_detail(NODE)
parameters = atf.get_node_field(NODE, "Parameters") or ""
assert "config_override" in parameters, atf.get_node_detail(NODE)
def test_node_line_config_overrides_applies(restore_node_line):
"""config_overrides on the node's slurm.conf line also applies.
A node's Parameters= are added to SlurmdParameters for that node, so with
config_overrides there a RealMemory above the host does not make the node
INVAL either.
"""
atf.set_node_parameter(NODE, "Parameters", "config_overrides")
atf.set_node_parameter(NODE, "RealMemory", HUGE_MEMORY)
for _ in atf.timer(timeout=60, fatal=True):
state = atf.get_node_field(NODE, "State")
if state is not None and "INVAL" not in state and "DOWN" not in state:
break
memory = atf.get_node_field(NODE, "RealMemory")
assert memory == str(HUGE_MEMORY), atf.get_node_detail(NODE)
parameters = atf.get_node_field(NODE, "Parameters") or ""
assert "config_override" in parameters, atf.get_node_detail(NODE)