blob: 959fdd143be604bf02705ef94e093274110954a4 [file]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Tests for the implicit max_nodes rollback on a rejected node count update."""
import os
import pytest
import requests
import atf
test_name = os.path.splitext(os.path.basename(__file__))[0]
TASKS = 8
IMPLICIT_MAX_NODES = 8
MIN_NODES = 2
UPDATE_TASKS_PER_NODE = 4
REDERIVED_MAX_NODES = 2
@pytest.fixture(scope="module", autouse=True)
def setup():
# slurmrestd is upgraded with the client commands, so it is never newer
# than slurmctld and this also gates the implicit max_nodes support
atf.require_version(
(26, 5, 3),
"sbin/slurmrestd",
reason="Ticket 25443: data_parser v0.0.45 was added in 26.05 and the implicit max_nodes rederive in 26.05.3",
)
atf.require_config_parameter_includes("AuthAltTypes", "auth/jwt")
atf.require_slurmrestd("slurmctld,util", "v0.0.45")
atf.require_nodes(2, [("CPUs", 8), ("RealMemory", 1024)])
atf.require_slurm_running()
def test_rejected_max_nodes_update_keeps_implicit_bound():
"""Verify a rejected node count update leaves the implicit max_nodes intact"""
job_id = atf.submit_job_sbatch(
f'--hold -n{TASKS} -J {test_name} -t 1 --wrap "hostname" -o /dev/null',
fatal=True,
)
max_nodes = atf.get_jobs(job_id, use_json=True, fatal=True)[job_id]["max_nodes"][
"number"
]
assert (
max_nodes == IMPLICIT_MAX_NODES
), f"max_nodes should start implicitly derived as {IMPLICIT_MAX_NODES}"
# Raising only the minimum leaves max_nodes implicitly derived, which is
# what lets the next update be rejected by slurmctld
response = requests.post(
f"{atf.properties['slurmrestd_url']}/slurm/v0.0.45/job/{job_id}",
headers=atf.properties["slurmrestd-headers"],
json={"minimum_nodes": MIN_NODES},
)
assert (
response.status_code == 200
), f"Raising minimum_nodes should return HTTP 200, got {response.status_code}"
errors = response.json().get("errors")
assert not errors, f"Raising minimum_nodes should succeed: {errors}"
# slurmrestd can set maximum_nodes without minimum_nodes, which scontrol
# cannot, so this is the only way to have slurmctld reject the update
response = requests.post(
f"{atf.properties['slurmrestd_url']}/slurm/v0.0.45/job/{job_id}",
headers=atf.properties["slurmrestd-headers"],
json={"maximum_nodes": MIN_NODES - 1},
)
assert response.headers.get("content-type", "").startswith(
"application/json"
), f"Expected a JSON response, got {response.headers.get('content-type')}"
assert response.json().get(
"errors"
), "A maximum_nodes below the minimum should be rejected"
atf.run_command(
f"scontrol update jobid={job_id} TasksPerNode={UPDATE_TASKS_PER_NODE}",
user=atf.properties["slurm-user"],
fatal=True,
)
max_nodes = atf.get_jobs(job_id, use_json=True, fatal=True)[job_id]["max_nodes"][
"number"
]
assert max_nodes == REDERIVED_MAX_NODES, (
f"max_nodes should be rederived as "
f"ROUNDUP({TASKS}, {UPDATE_TASKS_PER_NODE})={REDERIVED_MAX_NODES}, "
f"but is {max_nodes}"
)