blob: 213f3758e594db4aa1e8af6070e39346ebf8989c [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""test_102_20.py - sacctmgr load clean=account and clean=qos (destructive).
Isolated tests for sacctmgr load clean=account and clean=qos against the
configured ClusterName. These operations wipe cluster-wide accounts or
global QOS definitions; each test snapshots accounting state before running
and restores it in teardown so later modules are not affected.
Requires Slurm 25.05+ sacctmgr and auto-config.
"""
import pytest
import atf
test_account1 = "test_acct1"
test_account2 = "test_acct2"
test_qos1 = "test_qos1"
test_qos2 = "test_qos2"
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_auto_config(
"clean=account/qos load wipes cluster-wide accounts and global QOS"
)
atf.require_version(
(25, 5),
component="bin/sacctmgr",
reason="Issue 50241: sacctmgr load clean=account/qos requires 25.05+",
)
atf.require_accounting(modify=True)
atf.require_slurm_running()
@pytest.fixture(scope="function", autouse=True)
def setup_db():
"""Grant slurm user on root before each test; remove test entities after."""
atf.run_command(
f"sacctmgr -i add user {atf.properties['slurm-user']} account=root",
user=atf.properties["slurm-user"],
)
yield
atf.run_command(
f"sacctmgr -i remove account {test_account1},{test_account2}",
user=atf.properties["slurm-user"],
)
atf.run_command(
f"sacctmgr -i remove qos {test_qos1},{test_qos2}",
user=atf.properties["slurm-user"],
)
@pytest.fixture
def accounting_snapshot():
"""Snapshot configured-cluster sacctmgr dump; restore after the test.
The restore is an additive `sacctmgr load` of that dump, so it puts back
every association row (accounts, users, limits) the dump named. It
cannot undo two things a clean load can do that the dump does not
capture: clean=qos removes every global QOS not named in the file, not
only the ones this test created, and an unreferenced QOS left behind by
an earlier module is gone with no record in this dump to restore it
from; clean=account deletes and re-creates the cluster row, and the
additive reload restores the root association's limits but not
registration metadata (ControlHost, RPCVersion, Dimensions), which only
slurmctld sets again on re-register. Full recovery from either relies on
require_accounting(modify=True)'s SQL backup, which conftest.py restores
at module teardown; this fixture only needs to undo one test's clean
load well enough for the next test in the module to start clean.
"""
cluster = atf.get_config_parameter("ClusterName")
snap = "accounting_snapshot.cfg"
atf.run_command(
f"sacctmgr dump {cluster} file={snap}",
user=atf.properties["slurm-user"],
fatal=True,
)
yield
atf.run_command(
f"sacctmgr -i load file={snap}",
user=atf.properties["slurm-user"],
fatal=True,
)
def _accounting_cluster_count():
"""Return number of clusters registered in the accounting database."""
out = atf.run_command_output(
"sacctmgr -n -P list cluster format=Cluster",
user=atf.properties["slurm-user"],
fatal=True,
)
lines = [ln for ln in out.strip().split("\n") if ln.strip()]
return len(lines)
def _require_single_accounting_cluster():
"""clean=account/user/qos is only valid when one cluster exists in the DB."""
count = _accounting_cluster_count()
if count != 1:
pytest.skip(
f"clean=account/user/qos requires one accounting cluster, found {count}"
)
def _qos_name_exists(qos_name):
"""Return whether qos_name appears in sacctmgr list qos output."""
out = atf.run_command_output(
"sacctmgr -n -P list qos format=Name",
user=atf.properties["slurm-user"],
fatal=True,
)
for ln in out.strip().split("\n"):
if ln.strip().split("|")[0].strip() == qos_name:
return True
return False
def write_load_file(path, content):
"""Write a sacctmgr load file as the slurm user (remote-exec safe)."""
atf.run_command(
f"cat > {path}",
input=content,
user=atf.properties["slurm-user"],
fatal=True,
)
def test_load_clean_account_keyword_removes_unlisted(accounting_snapshot):
"""clean=account removes accounts not in the load file.
Uses the configured ClusterName only (no extra cluster row) so the
single-cluster requirement for clean=account is satisfied in ATF.
"""
_require_single_accounting_cluster()
cluster = atf.get_config_parameter("ClusterName")
cfg1 = "clean_acct_kw1.cfg"
cfg2 = "clean_acct_kw2.cfg"
config1 = f"""Cluster - '{cluster}'
Parent - 'root'
Account - '{test_account1}':Fairshare=50
Account - '{test_account2}':Fairshare=30
"""
config2 = f"""Cluster - '{cluster}'
Parent - 'root'
Account - '{test_account1}':Fairshare=60
"""
write_load_file(cfg1, config1)
write_load_file(cfg2, config2)
atf.run_command(
f"sacctmgr -i load {cfg1}",
user=atf.properties["slurm-user"],
fatal=True,
)
atf.run_command(
f"sacctmgr -i load {cfg2} clean=account",
user=atf.properties["slurm-user"],
fatal=True,
)
out = atf.run_command_output(
f"sacctmgr -n -P list assoc account={test_account2} cluster={cluster} format=Account",
fatal=True,
)
assert test_account2 not in out, "clean=account should drop unlisted accounts"
out1 = atf.run_command_output(
f"sacctmgr -n -P list assoc account={test_account1} cluster={cluster} format=Fairshare",
fatal=True,
)
assert out1.strip() == "60", "Listed account should keep updated Fairshare"
def test_load_clean_qos_removes_unlisted_qos_definitions(accounting_snapshot):
"""clean=qos drops QOS definitions not present in the load file.
Uses the configured ClusterName only so clean=qos sees one cluster in the DB.
Keep the cluster default QOS (normal) in both files so clean=qos does not
remove it; the accounting_snapshot restore needs that QOS still present.
"""
_require_single_accounting_cluster()
cluster = atf.get_config_parameter("ClusterName")
cfg1 = "clean_qos_def1.cfg"
cfg2 = "clean_qos_def2.cfg"
config1 = f"""QOS - 'normal':Description='Normal QOS default'
QOS - '{test_qos1}':Priority=10
QOS - '{test_qos2}':Priority=20
Cluster - '{cluster}'
Parent - 'root'
Account - '{test_account1}':Fairshare=50
"""
config2 = f"""QOS - 'normal':Description='Normal QOS default'
QOS - '{test_qos1}':Priority=10
Cluster - '{cluster}'
Parent - 'root'
Account - '{test_account1}':Fairshare=50
"""
write_load_file(cfg1, config1)
write_load_file(cfg2, config2)
atf.run_command(
f"sacctmgr -i load {cfg1}",
user=atf.properties["slurm-user"],
fatal=True,
)
assert _qos_name_exists(test_qos2), "Second QOS should exist before clean=qos"
atf.run_command(
f"sacctmgr -i load {cfg2} clean=qos",
user=atf.properties["slurm-user"],
fatal=True,
)
assert not _qos_name_exists(
test_qos2
), "QOS not in file should be removed with clean=qos"
assert _qos_name_exists(
"normal"
), "Cluster default QOS must remain when listed under clean=qos"