blob: da53aaf665e8357b6c80419e57cf2aaf3556675a [file]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
# Tests for cloud node behavior with enable_stepmgr active. Kept separate
# from test_141_1 because enabling stepmgr at the module level would alter
# the behavior of every test in that suite.
import subprocess
import time
import pytest
import atf
pytestmark = pytest.mark.slow
suspend_time = 10
suspend_timeout = 10
# Generous ResumeTimeout: srun launches in the background and the test needs a
# few seconds to detect POWERING_UP and register a slurmd, so the node must not
# be marked failed before that manual slurmd comes up.
resume_timeout = 45
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_auto_config("Runs slurmd on same machine as slurmctld")
atf.require_version(
(26, 5, 3),
"sbin/slurmctld",
reason="Ticket 25564: slurmctld must not set SLURM_STEPMGR before"
" batch_host is known",
)
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_CPU")
atf.require_config_parameter("TreeWidth", 65533)
atf.require_config_parameter("ResumeProgram", "/bin/true")
atf.require_config_parameter("SuspendProgram", "/bin/true")
atf.require_config_parameter("SuspendTime", suspend_time)
atf.require_config_parameter("SuspendTimeout", suspend_timeout)
atf.require_config_parameter("ResumeTimeout", resume_timeout)
atf.require_config_parameter_includes("SlurmctldParameters", "idle_on_node_suspend")
atf.require_config_parameter_includes("SlurmctldParameters", "enable_stepmgr")
atf.require_config_parameter_includes("PrologFlags", "Contain")
atf.require_config_parameter("NodeName", {"node1": {"State": "CLOUD"}})
atf.require_config_parameter(
"PartitionName",
{
"primary": {"Nodes": "ALL", "Default": "YES"},
},
)
# Don't run the usual atf.require_slurm_running() because the test starts
# the slurmd manually
atf.start_slurmctld(clean=True)
yield
# conftest only cancels jobs when it started Slurm itself (it keys off
# properties["slurm-started"], which require_slurm_running() sets and
# start_slurmctld() does not), so cancel them here before killing the
# daemons. Otherwise a failure earlier in the test leaves the allocation
# and the orphaned background srun behind.
atf.cancel_all_jobs()
# conftest doesn't stop daemons it didn't start, so stop them here. This
# also covers the manually started node1 slurmd, which is not one of the
# slurmds conftest knows about.
atf.stop_slurmctld(also_slurmds=True)
# Regression test for bug 25564: srun against a POWERED_DOWN node with
# enable_stepmgr active aborted srun with a glibc "free(): invalid pointer"
# because node_info left the alias_addrs output pointer uninitialized. The
# controller advertised SLURM_STEPMGR as the literal string "(null)" for a job
# whose batch_host wasn't picked yet, so srun looked up that bogus stepmgr and
# then freed the uninitialized pointer in slurm_job_step_create().
def test_srun_on_powered_down_node_with_stepmgr():
assert "POWERED_DOWN" in atf.get_node_parameter(
"node1", "state"
), "Cloud node must start in POWERED_DOWN state to reproduce the bug"
# Launch srun in the background against the powered-down cloud node. srun
# requesting node1 triggers a power-up; ResumeProgram=/bin/true does not
# actually start a slurmd, so we register one manually (as test_141_1 does)
# to let the job run. The job prints its Slurm node name via
# $SLURMD_NODENAME.
output_file = "srun.out"
# Run srun directly instead of atf.submit_job_srun(background=True): that
# helper returns only the job id and drops the Popen object, and the
# symptom of this regression is srun itself aborting, so the test needs
# srun's exit code and stderr.
srun_process = atf.run_command(
f"srun -w node1 --output={output_file} bash -c 'echo $SLURMD_NODENAME'",
background=True,
)["process"]
# srun requesting the node transitions it to POWERING_UP
powering_up = atf.wait_for_node_state("node1", "POWERING_UP")
# An srun that dies at launch never powers the node up, so the wait above
# would just time out and blame power save while srun's own error sat
# unread. fatal= cannot be combined with background=True, so check the
# process explicitly and report its output first.
if srun_process.poll() is not None:
try:
stdout, stderr = srun_process.communicate(timeout=atf.PERIODIC_TIMEOUT)
except subprocess.TimeoutExpired:
stdout = stderr = "<unavailable, srun did not exit>"
pytest.fail(
f"srun exited (rc={srun_process.returncode}) before node1 powered up."
f" stdout: {stdout}, stderr: {stderr}"
)
assert powering_up, "node1 did not reach POWERING_UP after srun requested it"
# Pin both preconditions of Ticket 25564 while they still hold. The bug
# needs STEPMGR_ENABLED on the job AND a NULL batch_host, which only
# happens inside the CONFIGURING window - the node is still powering up
# here, so batch_host has not been picked. Without these assertions the
# test would keep passing if enable_stepmgr silently stopped applying to
# cloud allocations, and would guard nothing.
atf.repeat_until(
lambda: len(atf.get_jobs(quiet=True)),
lambda count: count == 1,
timeout=15,
fatal=True,
)
job_id = list(atf.get_jobs(quiet=True))[0]
assert (
atf.get_job_parameter(job_id, "StepMgrEnabled", default="No", quiet=True)
== "Yes"
), f"Job {job_id} must be stepmgr-enabled to reproduce the bug"
assert (
atf.get_job_parameter(job_id, "JobState", quiet=True) == "CONFIGURING"
), f"Job {job_id} must still be CONFIGURING (batch_host unpicked)"
# TODO: Wait 2 seconds to avoid race condition between slurmd and slurmctld
# Remove once bug 16459 is fixed.
time.sleep(2)
# Register a real slurmd so the cloud node actually resumes
atf.run_command(
f"{atf.properties['slurm-sbin-dir']}/slurmd -b -N node1",
fatal=True,
user="root",
)
# Node finishes powering up and the job completes. Wait for a positive
# state rather than "not POWERING_UP": the inverted form is also satisfied
# by the DOWN+POWERED_DOWN state slurmctld sets at ResumeTimeout, so a
# slurmd that never registers would be misreported as an srun failure.
atf.wait_for_node_state(
"node1",
["ALLOCATED", "MIXED", "IDLE"],
timeout=resume_timeout + 5,
fatal=True,
)
# Cloud node registration/configuration can take up to PERIODIC_TIMEOUT
# seconds (bug 16459), so give the job ample time to finish.
try:
stdout, stderr = srun_process.communicate(timeout=atf.PERIODIC_TIMEOUT + 30)
except subprocess.TimeoutExpired:
srun_process.kill()
# kill() may only reach a sudo wrapper: run_command() runs the command
# under "sudo ... /bin/bash -lc" whenever SlurmTestUser is set, and sudo
# forks rather than execs, so srun survives as an orphan still holding
# the stdout/stderr pipes. Bound this communicate() too - unbounded, it
# blocks on an EOF that never arrives and pytest hangs with no failure
# reported and no teardown.
try:
stdout, stderr = srun_process.communicate(timeout=atf.PERIODIC_TIMEOUT)
except subprocess.TimeoutExpired:
stdout = stderr = "<unavailable, srun did not exit>"
pytest.fail(
"srun did not exit after node1 finished powering up."
f" stdout: {stdout}, stderr: {stderr}"
)
# This is the regression check: before the fix srun died here with a glibc
# "free(): invalid pointer" abort (SIGABRT, so a negative returncode).
assert (
"invalid pointer" not in stderr
), f"srun aborted freeing an uninitialized pointer. stderr: {stderr}"
assert srun_process.returncode == 0, (
f"srun exited with rc={srun_process.returncode}"
f" (a negative value is the signal that killed it)."
f" stdout: {stdout}, stderr: {stderr}"
)
atf.assert_file_contents(
output_file,
"node1",
message="srun failed to run the task on node1",
)