blob: e3b1525b91d3eedc5ed24d9647f8aef023e9ed69 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Per-node SuspendTime when automatic power save is not configured.
With SuspendProgram and ResumeProgram unset, slurmctld rejects a finite
SuspendTime on node creation and any SuspendTime on a node update, while a
dynamic node registering with 'slurmd -Z' is accepted with a warning.
"""
import pytest
import atf
max_nodes = 5
dyn_node = "node100"
g_suspend_time = 100
n_suspend_time = 109
infinite_option_num = 4294967295
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_config_parameter("MaxNodeCount", max_nodes)
atf.require_config_parameter("SuspendProgram", None)
atf.require_config_parameter("ResumeProgram", None)
atf.require_config_parameter("SuspendTime", g_suspend_time)
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_Core")
atf.require_slurm_running()
@pytest.fixture(autouse=True)
def cleanup():
yield
atf.run_command(
"scontrol delete nodename=d1",
user=atf.properties["slurm-user"],
)
# d2 is left behind if an assertion fails before the test deletes it.
atf.run_command(
"scontrol delete nodename=d2",
user=atf.properties["slurm-user"],
)
atf.stop_slurmd(dyn_node)
atf.run_command(
f"scontrol delete nodename={dyn_node}",
user=atf.properties["slurm-user"],
)
def suspend_time_val(value):
"""Build expected suspend_time no_val_struct dict for node comparison."""
if value == infinite_option_num:
return {"set": False, "infinite": True, "number": 0}
return {"set": True, "infinite": False, "number": value}
@pytest.mark.skipif(
atf.get_version() < (26, 11),
reason=(
"#50873: rejecting node creation with SuspendTime when power save"
" is disabled added in 26.11"
),
)
@pytest.mark.skipif(
atf.get_version("bin/scontrol") < (26, 5),
reason="Ticket 24669: Per-node SuspendTime added in 26.05",
)
def test_create_node_suspend_time_power_save_disabled():
# Creating without SuspendTime must succeed even when power save is disabled
atf.run_command(
"scontrol create nodename=d1 state=cloud",
user=atf.properties["slurm-user"],
fatal=True,
)
# Creating with a finite SuspendTime must fail when power save is disabled
result = atf.run_command(
f"scontrol create nodename=d2 state=cloud SuspendTime={n_suspend_time}",
user=atf.properties["slurm-user"],
xfail=True,
fatal=True,
)
assert "automatic power save mode is disabled" in result["stderr"], (
"Node creation with a finite SuspendTime should be rejected while"
f" power save is disabled, got: {result['stderr']}"
)
assert "d2" not in atf.get_nodes(), "A rejected node creation should not create d2"
# INFINITE, NONE and -1 are accepted on create even when power save is
# disabled
for alias in ("INFINITE", "NONE", "-1"):
atf.run_command(
f"scontrol create nodename=d2 state=cloud SuspendTime={alias}",
user=atf.properties["slurm-user"],
fatal=True,
)
assert atf.get_nodes()["d2"]["suspend_time"] == suspend_time_val(
infinite_option_num
), f"Creating d2 with SuspendTime={alias} should disable suspending it"
atf.run_command(
"scontrol delete nodename=d2",
user=atf.properties["slurm-user"],
fatal=True,
)
@pytest.mark.skipif(
atf.get_version("bin/scontrol") < (26, 11),
reason=(
"#50873: rejecting node SuspendTime updates when power save"
" is disabled added in 26.11"
),
)
def test_update_node_suspend_time_power_save_disabled():
atf.run_command(
"scontrol create nodename=d1 state=cloud",
user=atf.properties["slurm-user"],
fatal=True,
)
nodes = atf.get_nodes()
before = nodes["d1"]["suspend_time"]
assert before == suspend_time_val(
g_suspend_time
), "d1 should inherit the finite global SuspendTime"
# Unlike creation, the update path also rejects INFINITE/NONE/-1
for value in (n_suspend_time, "INFINITE", "NONE", "-1"):
result = atf.run_command(
f"scontrol update nodename=d1 SuspendTime={value}",
user=atf.properties["slurm-user"],
xfail=True,
fatal=True,
)
assert (
"Requested operation is presently disabled" in result["stderr"]
), f"SuspendTime={value} update should fail with ESLURM_DISABLED"
nodes = atf.get_nodes()
assert (
nodes["d1"]["suspend_time"] == before
), f"SuspendTime={value} update should have been rejected"
# A rejected SuspendTime must not apply other fields in the same request
result = atf.run_command(
f"scontrol update nodename=d1 SuspendTime={n_suspend_time}"
" Weight=7 Comment=rejected",
user=atf.properties["slurm-user"],
xfail=True,
fatal=True,
)
assert (
"Requested operation is presently disabled" in result["stderr"]
), "SuspendTime update with other fields should fail with ESLURM_DISABLED"
d1 = atf.get_nodes()["d1"]
assert d1["weight"] == 1, "A rejected update should not change Weight"
assert d1["comment"] == "", "A rejected update should not change Comment"
@pytest.mark.skipif(
atf.get_version("sbin/slurmctld") < (26, 11),
reason=(
"#50873: accepting dynamic node SuspendTime with a warning when power"
" save is disabled added in 26.11"
),
)
@pytest.mark.skipif(
atf.get_version("bin/scontrol") < (26, 5)
or atf.get_version("sbin/slurmd") < (26, 5),
reason="Ticket 24669: Per-node SuspendTime added in 26.05",
)
def test_dynamic_register_suspend_time_power_save_disabled():
"""slurmd -Z with a finite SuspendTime registers when power save is disabled"""
atf.start_slurmd(
dyn_node, f"-Z -b --conf 'SuspendTime={n_suspend_time}'", wait="idle"
)
assert atf.get_nodes()[dyn_node]["suspend_time"] == suspend_time_val(
n_suspend_time
), "The dynamic node should keep the SuspendTime from --conf"
warning = (
f"warning: Dynamic node {dyn_node} registered with"
f" SuspendTime={n_suspend_time},"
" but automatic power save mode is disabled"
)
logfile = atf.get_config_parameter("SlurmctldLogFile", live=False, quiet=True)
assert atf.run_command_output(
f"grep -F -- '{warning}' {logfile}", user=atf.properties["slurm-user"]
), "slurmctld should log a warning for the registered SuspendTime"