| ############################################################################ |
| # Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| ############################################################################ |
| """Verify slurmdbd rejects clients that are not TLS wrapped. |
| |
| Companion to test_166_1 and test_166_2, which cover the conmgr RPC |
| listeners. slurmdbd is fail-closed as well, but tls.shtml documents it as |
| reporting that differently: it negotiates TLS as soon as a connection is |
| accepted, so a client without TLS fails the negotiation and has its |
| connection closed, rather than being answered with ESLURM_TLS_REQUIRED. |
| |
| That distinction is what these tests pin. Asserting only that the client |
| fails would pass even if slurmdbd had replied on an RPC listener, so the |
| reject leg also asserts the TLS-required error is absent. |
| |
| slurmdbd takes its own TLSType from slurmdbd.conf, so it is required there |
| in addition to the cluster-wide setting the other modules need. |
| """ |
| |
| import os |
| |
| import pytest |
| |
| import atf |
| |
| # slurm_strerror(ESLURM_TLS_REQUIRED). What the RPC listeners answer and what |
| # slurmdbd, closing the connection instead, must not. |
| TLS_REQUIRED_MSG = "TLS missing but required for connection" |
| |
| |
| @pytest.fixture(scope="module", autouse=True) |
| def setup(): |
| atf.require_tls() |
| if not atf.get_config_parameter( |
| "TLSType", default=None, live=False, source="slurmdbd", quiet=True |
| ): |
| pytest.skip( |
| "Issue 51066: this test requires TLSType to be set in" |
| " slurmdbd.conf as well as slurm.conf", |
| allow_module_level=True, |
| ) |
| atf.require_accounting() |
| atf.require_slurm_running() |
| |
| |
| @pytest.fixture(scope="module") |
| def non_tls_conf(module_setup): |
| """Path to a slurm.conf that includes the real config but disables TLS. |
| |
| Absolute because the tests consuming it run in their own directory. Only |
| the process pointed at this via SLURM_CONF speaks plaintext; slurmdbd |
| keeps the real configuration. |
| """ |
| real_conf = f"{atf.properties['slurm-config-dir']}/slurm.conf" |
| path = "non_tls_slurm.conf" |
| atf.run_command( |
| f"printf 'include {real_conf}\\nTLSType=tls/none\\n' > {path}", |
| fatal=True, |
| ) |
| return os.path.abspath(path) |
| |
| |
| def test_slurmdbd_rejects_non_tls(non_tls_conf): |
| """A non-TLS client cannot reach slurmdbd, and is not told why. |
| |
| sacctmgr takes its TLSType from slurm.conf, so pointing it at the |
| non-TLS config makes it connect in plaintext. slurmdbd closes that |
| connection during negotiation, so the failure carries no |
| ESLURM_TLS_REQUIRED to explain itself. |
| """ |
| result = atf.run_command( |
| "sacctmgr -n show cluster", |
| env_vars=f"SLURM_CONF={non_tls_conf}", |
| xfail=True, |
| ) |
| |
| assert ( |
| result["exit_code"] != 0 |
| ), f"Expected sacctmgr to fail without TLS, got:\n{result}" |
| |
| output = result["stdout"] + result["stderr"] |
| assert TLS_REQUIRED_MSG not in output, ( |
| "Expected slurmdbd to close the connection without an" |
| f" ESLURM_TLS_REQUIRED reply, got:\n{output}" |
| ) |
| |
| |
| def test_slurmdbd_accepts_tls(): |
| """Control: the same query under the real (TLS) config succeeds.""" |
| result = atf.run_command( |
| "sacctmgr -n show cluster", |
| fatal=True, |
| ) |
| |
| assert ( |
| result["exit_code"] == 0 |
| ), f"Expected sacctmgr to succeed with TLS, got:\n{result}" |