blob: b2e355a1be4647d89dd27cfdf5986e38517589f4 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""scontrol hash and hash_file against independent reference digests.
scontrol computes these hashes locally with the plugins named by HashPlugin in
its own slurm.conf: the first one listed is the default, and hash/k12 is always
loaded as well. A client-only slurm.conf that includes the real one and sets
HashPlugin=hash/sha3 therefore exercises hash/sha3 without reconfiguring any
daemon. The same client is rejected by K12-only daemons, which is what shows
that HashPlugin selects the hash used for network communication.
"""
import hashlib
import os
import shlex
import pytest
import atf
# KangarooTwelve (RFC 9861) with an empty customization string, 32 bytes.
K12_DIGESTS = {
"": "1ac2d450fc3b4205d19da7bfca1b37513c0803577ac7167f06fe2ce1f0ef39e5",
"test": "333bd5d6c148a56b3eb4ecd489d07cd13f563a5a2593ece9155ca68772671628",
"Hello, World!": "58650c76f081423ae01f3aea4b301b5682550129be77aac1bd9333d8f1c97563",
"Your text here": "23e9976374d3ce4314a21b28eb780172f0e04188380ebd37eb541a1163e47e9a",
}
DIGESTS = {
"k12": K12_DIGESTS,
"sha3": {s: hashlib.sha3_256(s.encode()).hexdigest() for s in K12_DIGESTS},
}
# RFC 9861 KT128 vector for M=ptn(17**4), 83521 bytes: more than one 8192-byte
# chunk, so K12 hashes it as a tree, and SHA3-256 absorbs it over many blocks.
LONG_INPUT = bytes(i % 251 for i in range(17**4))
LONG_DIGESTS = {
"k12": "8701045e22205345ff4dda05555cbb5c3af1a771c2b89baef37db43d9998b9fe",
"sha3": hashlib.sha3_256(LONG_INPUT).hexdigest(),
}
# (client HashPlugin, hash_type argument, expected algorithm)
HASH_CASES = [
("default", "", "k12"),
("default", "2", "k12"),
("sha3", "", "sha3"),
("sha3", "4", "sha3"),
("sha3", "2", "k12"),
]
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_config_parameter("HashPlugin", [None, "hash/k12"])
atf.require_config_parameter("HashPlugin", [None, "hash/k12"], source="slurmdbd")
atf.require_accounting()
atf.require_slurm_running()
@pytest.fixture(scope="module")
def client_env(module_setup):
"""SLURM_CONF settings selecting each HashPlugin for the client only."""
real_conf = f"{atf.properties['slurm-config-dir']}/slurm.conf"
env = {"default": None}
for plugin in ["k12", "sha3"]:
path = f"{plugin}_slurm.conf"
atf.run_command(
f"printf 'include {real_conf}\\nHashPlugin=hash/{plugin}\\n' > {path}",
fatal=True,
)
env[plugin] = f"SLURM_CONF={os.path.abspath(path)}"
return env
@pytest.fixture(scope="module")
def hash_files(module_setup):
"""A file per non-empty input holding exactly its bytes.
hash_file mmap()s the file, which cannot map an empty one.
"""
files = {}
for i, text in enumerate(s for s in K12_DIGESTS if s):
path = f"input_{i}"
atf.run_command(f"printf '%s' {shlex.quote(text)} > {path}", fatal=True)
files[text] = os.path.abspath(path)
return files
@pytest.fixture(scope="module")
def long_file(module_setup):
"""A file holding LONG_INPUT."""
path = "input_long"
atf.run_command(
"python3 -c 'import sys; "
f"sys.stdout.buffer.write(bytes(i % 251 for i in range(17**4)))' > {path}",
fatal=True,
)
return os.path.abspath(path)
@pytest.mark.parametrize("plugin,hash_type,algorithm", HASH_CASES)
def test_hash(client_env, plugin, hash_type, algorithm):
"""scontrol hash matches the reference digest of the selected algorithm"""
got = {
text: atf.run_command_output(
f"scontrol hash {shlex.quote(text)} {hash_type}",
env_vars=client_env[plugin],
fatal=True,
).strip()
for text in K12_DIGESTS
}
assert got == DIGESTS[algorithm], f"scontrol hash should give {algorithm} digests"
@pytest.mark.parametrize("plugin,hash_type,algorithm", HASH_CASES)
def test_hash_file(client_env, hash_files, plugin, hash_type, algorithm):
"""scontrol hash_file matches the reference digest of the file's bytes"""
got = {
text: atf.run_command_output(
f"scontrol hash_file {path} {hash_type}",
env_vars=client_env[plugin],
fatal=True,
).strip()
for text, path in hash_files.items()
}
assert got == {
text: DIGESTS[algorithm][text] for text in hash_files
}, f"scontrol hash_file should give {algorithm} digests"
@pytest.mark.parametrize("plugin,hash_type,algorithm", HASH_CASES)
def test_hash_file_long(client_env, long_file, plugin, hash_type, algorithm):
"""scontrol hash_file matches the reference digest of a multi-block file"""
got = atf.run_command_output(
f"scontrol hash_file {long_file} {hash_type}",
env_vars=client_env[plugin],
fatal=True,
).strip()
assert (
got == LONG_DIGESTS[algorithm]
), f"scontrol hash_file should give {algorithm} digest"
@pytest.mark.parametrize("command", ["sacctmgr -n show cluster", "sinfo"])
def test_sha3_client_rejected(client_env, command):
"""A hash/sha3 client is rejected by K12-only slurmdbd and slurmctld"""
result = atf.run_command(
command, env_vars=client_env["sha3"], xfail=True, fatal=True
)
assert (
result["exit_code"] != 0
), f"'{command}' should fail when its client hashes with SHA3"
atf.run_command(command, env_vars=client_env["k12"], fatal=True)