blob: 889b900f601a674819bb16c17c660abcb5752d42 [file]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Ticket 24934.
With Flags=IGNORE_JOBS a NodeCnt= reservation over a PartitionName may be
created even when every node in the partition is busy; without IGNORE_JOBS
the same request is denied. Uses a dedicated partition where a running job
occupies all partition nodes so placement relies on IGNORE_JOBS rather than
idle nodes only. The empty Features= field is the reproduction trigger from
bug 3214, not a documented requirement.
The regression exists only on 25.11.0 through 25.11.4 (before the fix in
25.11.5). Slurm 25.05 passes without the fix because _resv_select() still
uses the older min_nodes WILL_RUN path; do not xfail releases before 25.11.
"""
import os
import pytest
import atf
test_name = os.path.splitext(os.path.basename(__file__))[0]
partition = f"{test_name}_part"
testuser = atf.properties["test-user"]
# Setup
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_nodes(4, [("CPUs", 2), ("RealMemory", 50)])
atf.require_slurm_running()
@pytest.fixture(scope="module")
def part_nodes(setup):
node_names = sorted(n for n in atf.get_nodes().keys() if n != "DEFAULT")
four_nodes = node_names[:4]
nodelist = ",".join(four_nodes)
atf.run_command(
f"scontrol create PartitionName={partition} Nodes={nodelist} "
f"State=UP Default=NO",
user=atf.properties["slurm-user"],
fatal=True,
)
yield four_nodes
atf.run_command(
f"scontrol delete PartitionName={partition}",
user=atf.properties["slurm-user"],
quiet=True,
)
@pytest.fixture(scope="function")
def reservation():
"""Delete any reservation registered during the test at teardown."""
created = []
yield created
for name in created:
atf.run_command(
f"scontrol delete ReservationName={name}",
user=atf.properties["slurm-user"],
quiet=True,
)
@pytest.fixture(scope="function")
def busy_partition(part_nodes):
"""Occupy every node in the test partition so placement needs IGNORE_JOBS."""
job_id = atf.submit_job_sbatch(
f"-p {partition} -N4 --wrap='sleep infinity'",
fatal=True,
)
atf.wait_for_job_state(job_id, "RUNNING", fatal=True)
return part_nodes
@pytest.mark.xfail(
(25, 11) <= atf.get_version() < (25, 11, 5),
reason="Ticket 24934: reservation create with NodeCnt and Flags=IGNORE_JOBS fails when partition nodes are occupied",
)
def test_reservation_nodecnt_ignore_jobs_busy_partition(busy_partition, reservation):
"""NodeCnt + partition + IGNORE_JOBS succeeds when jobs use all nodes."""
resv_auto = f"{test_name}_auto"
resv_exp = f"{test_name}_exp"
# Baseline: an explicit Nodes= IGNORE_JOBS reservation over the busy nodes
# must succeed first, isolating the NodeCnt path below from the IGNORE_JOBS
# behavior itself.
two_nodes = ",".join(busy_partition[:2])
cmd_exp = (
f"scontrol create reservation ReservationName={resv_exp} "
f"PartitionName={partition} Nodes={two_nodes} StartTime=now "
f"Duration=30 Users={testuser} Flags=IGNORE_JOBS"
)
out_exp = atf.run_command(
cmd_exp,
user=atf.properties["slurm-user"],
fatal=True,
)
assert (
out_exp["exit_code"] == 0
), f"Explicit Nodes= should succeed as baseline: {out_exp['stderr']}"
atf.run_command(
f"scontrol delete ReservationName={resv_exp}",
user=atf.properties["slurm-user"],
fatal=True,
)
cmd_auto = (
f"scontrol create reservation ReservationName={resv_auto} "
f"PartitionName={partition} NodeCnt=2 StartTime=now Duration=30 "
f"Users={testuser} Flags=IGNORE_JOBS Features="
)
reservation.append(resv_auto)
out_auto = atf.run_command(
cmd_auto,
user=atf.properties["slurm-user"],
fatal=True,
)
assert (
out_auto["exit_code"] == 0
), f"Expected NodeCnt automatic selection to succeed: {out_auto['stderr']}"
# The reservation must actually hold 2 of the (busy) partition nodes,
# confirming IGNORE_JOBS selected occupied nodes rather than nothing.
assert (
atf.get_reservation_parameter(resv_auto, "NodeCnt") == 2
), "NodeCnt reservation did not reserve 2 nodes"
reserved = set(
atf.node_range_to_list(atf.get_reservation_parameter(resv_auto, "Nodes"))
)
assert reserved <= set(busy_partition), (
f"Reserved nodes {sorted(reserved)} are not a subset of the busy "
f"partition nodes {sorted(busy_partition)}"
)
def test_reservation_nodecnt_without_ignore_jobs_fails_busy_partition(
busy_partition, reservation
):
"""NodeCnt + partition + empty Features fails when all nodes busy, no IGNORE_JOBS."""
resv_neg = f"{test_name}_neg"
cmd = (
f"scontrol create reservation ReservationName={resv_neg} "
f"PartitionName={partition} NodeCnt=2 StartTime=now Duration=30 "
f"Users={testuser} Features="
)
reservation.append(resv_neg)
out = atf.run_command(
cmd,
user=atf.properties["slurm-user"],
fatal=True,
xfail=True,
)
assert "nodes are busy" in out["stderr"].lower(), (
"Expected reservation without IGNORE_JOBS to fail due to no idle "
f"nodes in the partition: {out['stderr']}"
)