blob: 9b3c5126a5d59200f46e48eba33b6e26cf430dc6 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""
Verify a partition's QOS cannot be changed or removed while jobs are running
or suspended in it, and that pending jobs do not block the change.
Older versions let the partition QOS change under running jobs, leaking usage.
"""
import re
import pytest
import atf
part = "pq"
acct = "acct_pq"
qos_old = "qos_pq_old"
qos_new = "qos_pq_new"
in_use_err = "Partition is in use"
pytestmark = pytest.mark.skipif(
atf.get_version() < (26, 11),
reason="Ticket 25710: Refusing partition QOS changes with active jobs was added in 26.11",
)
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_accounting(modify=True)
atf.require_config_parameter_includes("AccountingStorageEnforce", "limits")
atf.require_config_parameter(
"PartitionName",
{
"debug": {"Nodes": "ALL", "Default": "YES"},
part: {"Nodes": "ALL"},
},
)
atf.require_slurm_running()
@pytest.fixture(scope="module", autouse=True)
def setup_db(setup):
def _sacctmgr_add(opts):
atf.run_command(
f"sacctmgr -i add {opts}",
user=atf.properties["slurm-user"],
fatal=True,
)
_sacctmgr_add(f"qos {qos_old} GrpSubmitJobs=100")
_sacctmgr_add(f"qos {qos_new} GrpSubmitJobs=100")
_sacctmgr_add(f"account {acct}")
user = atf.get_user_name()
_sacctmgr_add(f"user {user} Account={acct}")
yield
for cmd in [
f"user {user} where account={acct}",
f"account {acct}",
f"qos {qos_old},{qos_new}",
]:
atf.run_command(
f"sacctmgr -i remove {cmd}", user=atf.properties["slurm-user"], quiet=True
)
@pytest.fixture(scope="function")
def job_ids():
"""Jobs submitted by the test, cancelled once it is done"""
ids = []
yield ids
atf.cancel_jobs(ids, fatal=True)
@pytest.fixture(scope="function", autouse=True)
def part_qos(job_ids):
"""Start every test with the partition using qos_old"""
atf.run_command(
f"scontrol update partition={part} qos={qos_old}",
user=atf.properties["slurm-user"],
fatal=True,
)
yield
atf.cancel_jobs(job_ids, fatal=True)
job_ids.clear()
atf.run_command(
f"scontrol update partition={part} qos=",
user=atf.properties["slurm-user"],
fatal=True,
)
def _partition_qos():
"""Name of the partition's QOS, or None if it has none"""
output = atf.run_command_output(f"scontrol show partition {part}", fatal=True)
qos = re.search(r"\bQoS=(\S+)", output).group(1)
return None if qos == "N/A" else qos
def _qos_submit_jobs(qos):
"""Number of submitted jobs currently counted against a QOS"""
output = atf.run_command_output(
f"scontrol show assoc_mgr flags=qos qos={qos}", fatal=True
)
match = re.search(r"GrpSubmitJobs=\S+\((\d+)\)", output)
assert match, f"No GrpSubmitJobs usage reported for QOS {qos}"
return int(match.group(1))
def _update_partition(qos):
return atf.run_command(
f"scontrol update partition={part} qos={qos}",
user=atf.properties["slurm-user"],
)
def _submit_running_job(job_ids):
job_id = atf.submit_job_sbatch(
f"-p {part} -A {acct} -o /dev/null --wrap 'sleep infinity'", fatal=True
)
job_ids.append(job_id)
atf.wait_for_job_state(job_id, "RUNNING", fatal=True)
return job_id
def _submit_pending_job(job_ids):
job_id = atf.submit_job_sbatch(
f"-p {part} -A {acct} -H -o /dev/null --wrap 'sleep infinity'", fatal=True
)
job_ids.append(job_id)
atf.wait_for_job_state(job_id, "PENDING", fatal=True)
return job_id
def test_change_refused_with_running_job(job_ids):
"""Changing a partition's QOS is refused while a job is running in it"""
_submit_running_job(job_ids)
result = _update_partition(qos_new)
assert result["exit_code"] != 0, "QOS change should be refused"
assert in_use_err in result["stderr"]
assert _partition_qos() == qos_old
def test_remove_refused_with_running_job(job_ids):
"""Removing a partition's QOS is refused while a job is running in it"""
_submit_running_job(job_ids)
result = _update_partition("")
assert result["exit_code"] != 0, "QOS removal should be refused"
assert in_use_err in result["stderr"]
assert _partition_qos() == qos_old
def test_change_refused_with_suspended_job(job_ids):
"""Changing a partition's QOS is refused while a job is suspended in it"""
job_id = _submit_running_job(job_ids)
atf.run_command(
f"scontrol suspend {job_id}", user=atf.properties["slurm-user"], fatal=True
)
atf.wait_for_job_state(job_id, "SUSPENDED", fatal=True)
result = _update_partition(qos_new)
assert result["exit_code"] != 0, "QOS change should be refused"
assert in_use_err in result["stderr"]
assert _partition_qos() == qos_old
def test_same_qos_allowed_with_running_job(job_ids):
"""Setting the QOS the partition already has is not a change"""
_submit_running_job(job_ids)
result = _update_partition(qos_old)
assert result["exit_code"] == 0, "Setting the same QOS should be allowed"
assert _partition_qos() == qos_old
def test_change_allowed_after_jobs_end(job_ids):
"""Changing a partition's QOS works once no job is running in it"""
job_id = _submit_running_job(job_ids)
assert _update_partition(qos_new)["exit_code"] != 0
atf.cancel_jobs([job_id], fatal=True)
result = _update_partition(qos_new)
assert result["exit_code"] == 0, "QOS change should work with no active jobs"
assert _partition_qos() == qos_new
def test_pending_job_usage_moves_with_qos(job_ids):
"""Pending jobs do not block a QOS change and their submit usage moves"""
_submit_pending_job(job_ids)
assert _qos_submit_jobs(qos_old) == 1
assert _qos_submit_jobs(qos_new) == 0
result = _update_partition(qos_new)
assert result["exit_code"] == 0, "Pending jobs must not block a QOS change"
assert _partition_qos() == qos_new
assert _qos_submit_jobs(qos_old) == 0
assert _qos_submit_jobs(qos_new) == 1
def test_pending_job_usage_moves_on_qos_removal(job_ids):
"""Removing the partition QOS releases the pending job's submit usage"""
_submit_pending_job(job_ids)
assert _qos_submit_jobs(qos_old) == 1
result = _update_partition("")
assert result["exit_code"] == 0, "Pending jobs must not block QOS removal"
assert _partition_qos() is None
assert _qos_submit_jobs(qos_old) == 0