| ############################################################################ |
| # 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" |