blob: 92602d8a76db4d7fd001e67ee9fe5827ad8df8a4 [file]
############################################################################
# Copyright (C) SchedMD LLC.
############################################################################
import re
import pytest
import atf
pytestmark = pytest.mark.slow
@pytest.fixture(scope="module", autouse=True)
def setup():
# Dev 50538: MetricsType added in 25.11
atf.require_version((25, 11), component="bin/scontrol")
# Ensure exactly 3 nodes exist
atf.require_nodes(3)
# Ensure MetricsType is openmetrics
atf.require_config_parameter("MetricsType", "metrics/openmetrics")
# JWT is needed for HTTP auth to access metrics endpoints
atf.require_config_parameter_includes("AuthAltTypes", "auth/jwt")
atf.require_config_parameter_includes("AuthAltTypes", "auth/jwt", source="slurmdbd")
# Define two partitions: debug and power
atf.require_config_parameter(
"PartitionName",
{
"debug": {"Nodes": "ALL"},
"power": {"Nodes": "ALL"},
},
)
# Start/reconfigure Slurm
atf.require_slurm_running()
def _get_metric_value(lines: str, name: str) -> str | None:
for line in lines.splitlines():
if line.startswith(name + " "):
return line.split(" ", 1)[1]
return None
def _get_labeled_metric_value(
lines: str, name: str, label_key: str, label_val: str
) -> str | None:
pattern = rf"^{re.escape(name)}\{{[^}}]*{re.escape(label_key)}=\"{re.escape(label_val)}\"[^}}]*\}}\s+(\d+)\s*$"
for line in lines.splitlines():
m = re.match(pattern, line)
if m:
return m.group(1)
return None
@pytest.fixture(scope="function")
def setup_metrics_test_case(request):
httpauth, mauth, mparam, pdata, use_slurm_user, xfail = request.param
if httpauth or mauth or mparam:
atf.require_version(
(26, 5),
component="bin/scontrol",
reason="Issue 50792 and 50898: Auth for MetricsType and HttpAuthParameters added in 26.05",
)
username = atf.properties["test-user"]
if use_slurm_user:
username = atf.properties["slurm-user"]
# Restart Slurm to apply proper mode
atf.stop_slurm()
atf.set_config_parameter("MetricsAuthUsers", mauth)
atf.set_config_parameter("MetricsParameters", mparam)
atf.set_config_parameter("PrivateData", pdata)
if httpauth:
atf.require_config_parameter(
"SlurmctldHttpAuthParameters",
f"jwt_key={atf.properties['slurm-config-dir']}/jwt_hs256.key",
)
atf.require_slurm_running()
# Submit a job as SlurmUser, and another as atf (normal) user
job_id1 = atf.submit_job_sbatch(
"-N1 -n1 -p debug --wrap='srun sleep infinity'",
user=atf.properties["slurm-user"],
)
job_id2 = atf.submit_job_sbatch(
"-N2 -n2 -p debug --wrap='srun sleep infinity'",
user=atf.properties["test-user"],
)
atf.wait_for_job_state(job_id1, "RUNNING", user=atf.properties["slurm-user"])
atf.wait_for_job_state(job_id2, "RUNNING", user=atf.properties["test-user"])
# Allow slurmctld to update metrics after jobs submission
# Note: Always execute this as admin user to ensure access to both jobs
for t in atf.timer():
resp = atf.request_slurmctld(
"metrics/jobs", user=atf.properties["slurm-user"]
).text
metric = _get_metric_value(resp, "slurm_jobs")
if metric and int(metric) == 2:
break
else:
pytest.fail("Unable to get metrics/jobs properly")
yield username, xfail
@pytest.mark.parametrize(
# Parameters are:
# - SlurmctldHttpAuthParameters
# - MetricsAuthUsers
# - MetricsParameters
# - PrivateData
# - Submit job as SlurmUser? True or False
# - Expect a permission error? True or False
"setup_metrics_test_case",
[
# setup_metrics_test_case0
(False, None, None, None, True, False),
# setup_metrics_test_case1
(True, None, None, "jobs", True, False),
# setup_metrics_test_case2
(True, None, "ignore_private_data", None, True, False),
# setup_metrics_test_case3
(True, atf.properties["slurm-user"], None, None, True, False),
# setup_metrics_test_case4
(True, "nonexisting_user", None, None, True, False),
# setup_metrics_test_case5
(True, None, "ignore_private_data", "jobs", True, False),
# setup_metrics_test_case6
(True, atf.properties["slurm-user"], None, "jobs", True, False),
# setup_metrics_test_case7
(True, "nonexisting_user", None, "jobs", True, False),
# setup_metrics_test_case8
(
True,
atf.properties["slurm-user"],
"ignore_private_data",
"jobs",
True,
False,
),
# setup_metrics_test_case9
(True, "nonexisting_user", "ignore_private_data", "jobs", True, False),
# setup_metrics_test_case10
(True, atf.properties["slurm-user"], "ignore_private_data", None, True, False),
# setup_metrics_test_case11
(True, "nonexisting_user", "ignore_private_data", None, True, False),
# setup_metrics_test_case12
(False, None, None, None, False, False),
# setup_metrics_test_case13
(True, None, None, "jobs", False, True),
# setup_metrics_test_case14
(True, None, "ignore_private_data", None, False, False),
# setup_metrics_test_case15
(True, atf.properties["test-user"], None, None, False, False),
# setup_metrics_test_case16
(True, "nonexisting_user", None, None, False, True),
# setup_metrics_test_case17
(True, None, "ignore_private_data", "jobs", False, False),
# setup_metrics_test_case18
(True, atf.properties["test-user"], None, "jobs", False, False),
# setup_metrics_test_case19
(True, "nonexisting_user", None, "jobs", False, True),
# setup_metrics_test_case20
(
True,
atf.properties["test-user"],
"ignore_private_data",
"jobs",
False,
False,
),
# setup_metrics_test_case21
(True, "nonexisting_user", "ignore_private_data", "jobs", False, True),
# setup_metrics_test_case22
(True, atf.properties["test-user"], "ignore_private_data", None, False, False),
# setup_metrics_test_case23
(False, "nonexisting_user", "ignore_private_data", None, False, True),
],
indirect=["setup_metrics_test_case"],
)
def test_http_metrics_openmetrics_endpoints(setup_metrics_test_case):
username, xfail = setup_metrics_test_case
if not xfail:
# Root endpoint: print available metric endpoints
r = atf.request_slurmctld("metrics", user=username)
assert r.status_code == 200, "Expected HTTP access to metrics endpoints"
assert r.text is not None, "Root endpoint is empty"
# Partitions endpoint: expect slurm_partitions 2
parts_output = atf.request_slurmctld("metrics/partitions", user=username).text
parts_val = _get_metric_value(parts_output, "slurm_partitions")
assert (
parts_val is not None
), f"Missing slurm_partitions in output:\n{parts_output}"
assert int(parts_val) == 2, f"Expected slurm_partitions 2, got {parts_val}"
# Nodes endpoint: expect slurm_nodes 3
nodes_output = atf.request_slurmctld("metrics/nodes", user=username).text
nodes_val = _get_metric_value(nodes_output, "slurm_nodes")
assert nodes_val is not None, f"Missing slurm_nodes in output:\n{nodes_output}"
assert int(nodes_val) == 3, f"Expected slurm_nodes 3, got {nodes_val}"
# Scheduler endpoint: expect a timestamp value
sched_output = atf.request_slurmctld("metrics/scheduler", user=username).text
sched_val = _get_metric_value(sched_output, "slurm_sched_stats_timestamp")
assert (
sched_val is not None
), f"Missing slurm_sched_stats_timestamp in output:\n{sched_output}"
assert int(sched_val) > 0, f"Expected positive timestamp, got {sched_val}"
# Jobs endpoint: expect total jobs == 2
jobs_output = atf.request_slurmctld("metrics/jobs", user=username).text
jobs_val = _get_metric_value(jobs_output, "slurm_jobs")
assert jobs_val is not None, f"Missing slurm_jobs in output:\n{jobs_output}"
assert int(jobs_val) == 2, f"Expected slurm_jobs == 2, got {jobs_val}"
# Jobs-users-accts endpoint: expect current slurm-user shows 1 job
jobs_ua_output = atf.request_slurmctld(
"metrics/jobs-users-accts", user=username
).text
jobs_ua_val = _get_labeled_metric_value(
jobs_ua_output, "slurm_user_jobs", "username", username
)
assert (
jobs_ua_val is not None
), f'Missing slurm_user_jobs for username="{username}" in output:\n{jobs_ua_output}'
assert (
int(jobs_ua_val) == 1
), f'Expected slurm_user_jobs{{username="{username}"}} == 1, got {jobs_ua_val}'
else:
# Root endpoint: expect no access (FORBIDDEN)
r = atf.request_slurmctld("metrics", user=username)
assert (
r.status_code == 403
), "Expected HTTP FORBIDDEN code for metrics root endpoint"
# Partitions endpoint: expect no access (FORBIDDEN)
r = atf.request_slurmctld("metrics/partitions", user=username)
assert r.status_code == 403, "Expected HTTP FORBIDDEN code for slurm_partitions"
# Nodes endpoint: expect no access (FORBIDDEN)
r = atf.request_slurmctld("metrics/nodes", user=username)
assert r.status_code == 403, "Expected HTTP FORBIDDEN code for slurm_nodes"
# Scheduler endpoint: expect no access (FORBIDDEN)
r = atf.request_slurmctld("metrics/scheduler", user=username)
assert (
r.status_code == 403
), "Expected HTTP FORBIDDEN code for slurm_sched_stats"
# Jobs endpoint: expect no access (FORBIDDEN)
r = atf.request_slurmctld("metrics/jobs", user=username)
assert r.status_code == 403, "Expected HTTP FORBIDDEN code for slurm_jobs"
# Jobs-users-accts endpoint: expect no access (FORBIDDEN)
r = atf.request_slurmctld("metrics/jobs-users-accts", user=username)
assert r.status_code == 403, "Expected HTTP FORBIDDEN code for slurm_user_jobs"