blob: 8e5a7f6f2414577502a0a121deb6c7cbededadc5 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test slurmdbd HTTP endpoint content, authentication, and connection reuse."""
import json
import socket
from http.client import HTTPResponse
import pytest
import requests
import atf
VERSION = "v0.0.46"
ENDPOINTS = ("conf", "diag")
TIMEOUT = 30
PRIVILEGED_TEST_USER = (
atf.properties["test-user-uid"] == 0
or atf.properties["test-user"] == atf.properties["slurm-user"]
)
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 11),
"sbin/slurmdbd",
reason="Ticket 25218: slurmdbd HTTP endpoints require 26.11+",
)
atf.require_accounting()
atf.require_config_parameter_excludes(
"CommunicationParameters", "disable_http", source="slurmdbd"
)
for source in ("slurm", "slurmdbd"):
atf.require_config_parameter_includes("AuthAltTypes", "auth/jwt", source=source)
atf.require_config_parameter(
"HttpAuthParameters",
f"jwt_key={atf.properties['slurm-config-dir']}/jwt_hs256.key",
source="slurmdbd",
)
atf.require_slurm_running()
atf.run_command(
"sacctmgr -nP show cluster", user=atf.properties["slurm-user"], fatal=True
)
@pytest.fixture
def versioned_endpoint():
atf.require_version(
(26, 11),
"sbin/slurmdbd",
max_version=(28, 11),
reason=f"Ticket 25218: data_parser/{VERSION} requires 26.11 through 28.05",
)
@pytest.fixture(scope="module")
def config():
return {key.lower(): value for key, value in atf.get_config(source="dbd").items()}
@pytest.fixture(scope="module")
def target(config):
return (
config.get("dbdaddr") or config.get("dbdhost", "localhost"),
int(config.get("dbdport", 6819)),
)
@pytest.fixture(scope="module")
def url(target):
host, port = target
if ":" in host:
host = f"[{host}]"
return f"http://{host}:{port}"
@pytest.fixture(scope="module")
def headers():
result = {}
for user in {atf.properties["slurm-user"], atf.properties["test-user"]}:
output = atf.run_command_output(
f"scontrol token username={user}",
user=atf.properties["slurm-user"],
fatal=True,
quiet=True,
)
token = output.strip().splitlines()[-1].split("=", 1)[1]
result[user] = {
"X-SLURM-USER-NAME": user,
"X-SLURM-USER-TOKEN": token,
"Accept": "application/json",
}
return result
@pytest.mark.parametrize(
"path, expected",
[("/readyz", "READY"), ("/livez", "LIVE"), ("/healthz", "HEALTHY")],
)
def test_probe_content(url, path, expected):
response = requests.get(f"{url}{path}", timeout=TIMEOUT)
assert response.status_code == 200, response.text
assert (
response.text == expected
), f"Expected {expected!r} from {path}, got {response.text!r}"
def test_root_lists_endpoints(url):
response = requests.get(f"{url}/", timeout=TIMEOUT)
assert response.status_code == 200, response.text
for path in (
"/readyz",
"/livez",
"/healthz",
"/{data_parser}/conf",
"/{data_parser}/diag",
):
assert path in response.text, f"Missing {path} from endpoint index"
@pytest.mark.usefixtures("versioned_endpoint")
@pytest.mark.parametrize("endpoint", ENDPOINTS)
@pytest.mark.parametrize("bad_token", [False, True], ids=["anonymous", "invalid-token"])
def test_versioned_endpoint_rejects_credentials(url, endpoint, bad_token):
credentials = {}
if bad_token:
credentials = {"Authorization": "Bearer invalid-token"}
response = requests.get(
f"{url}/{VERSION}/{endpoint}", headers=credentials, timeout=TIMEOUT
)
assert response.status_code == 401, response.text
@pytest.mark.usefixtures("versioned_endpoint")
@pytest.mark.parametrize("endpoint", ENDPOINTS)
def test_versioned_endpoint_reuses_connection(target, headers, endpoint):
"""Like slurmctld metrics tests, require two replies on the same socket."""
credentials = headers[atf.properties["slurm-user"]]
with socket.create_connection(target, timeout=TIMEOUT) as sock:
for close in (False, True):
fields = [f"GET /{VERSION}/{endpoint} HTTP/1.1", "Host: localhost"]
fields.extend(f"{key}: {value}" for key, value in credentials.items())
if close:
fields.append("Connection: close")
sock.sendall(("\r\n".join(fields) + "\r\n\r\n").encode())
with HTTPResponse(sock) as response:
response.begin()
assert response.status == 200, f"{endpoint}: HTTP {response.status}"
assert (
response.will_close == close
), f"{endpoint}: unexpected close={response.will_close}"
data = json.loads(response.read())
assert not data.get("errors"), data
if endpoint == "conf":
assert (
data["slurmdbd_conf"]["DbdPort"] == target[1]
), f"Wrong listening port in configuration: {data}"
assert data["slurmdbd_conf"][
"HttpAuthParameters"
], "Configured HTTP auth parameters missing from /conf"
elif endpoint == "diag":
assert (
data["statistics"]["time_start"] > 0
), "Statistics collection start time missing"
assert data["statistics"]["RPCs"], "No RPC counters returned"
assert data["statistics"]["users"], "No user counters returned"
assert sock.recv(1) == b"", "slurmdbd ignored Connection: close"
@pytest.mark.usefixtures("versioned_endpoint")
def test_diag_averages(url, headers):
response = requests.get(
f"{url}/{VERSION}/diag",
headers=headers[atf.properties["slurm-user"]],
timeout=TIMEOUT,
)
assert response.status_code == 200, response.text
stats = response.json()["statistics"]
records = stats["RPCs"] + stats["users"]
assert records, "No RPC or user statistics to validate"
expected = []
for record in records:
count = record["count"]
average = record["time"]["total"] // count if count else 0
assert record["time"]["average"] == average, record
expected.append(average)
assert any(expected), "No nonzero RPC average was exercised"
@pytest.mark.usefixtures("versioned_endpoint")
@pytest.mark.parametrize("endpoint", ENDPOINTS)
@pytest.mark.skipif(
PRIVILEGED_TEST_USER,
reason="Ticket 25218: requires a non-root, non-SlurmUser test user",
)
def test_authenticated_user_access(url, headers, endpoint):
user = atf.properties["test-user"]
expected_status = 200
if endpoint == "diag":
level = (
atf.run_command_output(
f"sacctmgr -nP show user {user} format=AdminLevel",
user=atf.properties["slurm-user"],
fatal=True,
)
.strip()
.lower()
)
expected_status = 200 if "admin" in level else 403
response = requests.get(
f"{url}/{VERSION}/{endpoint}", headers=headers[user], timeout=TIMEOUT
)
assert response.status_code == expected_status, response.text
def test_verbose_readyz_requires_authentication(url):
response = requests.get(f"{url}/readyz?verbose", timeout=TIMEOUT)
assert response.status_code == 401, response.text
def test_verbose_readyz_for_slurm_user(url, headers):
response = requests.get(
f"{url}/readyz?verbose",
headers=headers[atf.properties["slurm-user"]],
timeout=TIMEOUT,
)
assert response.status_code == 200, response.text
assert response.text, "Verbose readiness returned an empty body"
assert response.text != "READY", "Verbose readiness returned the normal body"
@pytest.mark.skipif(
PRIVILEGED_TEST_USER,
reason="Ticket 25218: requires a non-root, non-SlurmUser test user",
)
def test_verbose_readyz_rejects_regular_user(url, headers):
response = requests.get(
f"{url}/readyz?verbose",
headers=headers[atf.properties["test-user"]],
timeout=TIMEOUT,
)
assert response.status_code == 403, response.text