| ############################################################################ | |
| # Copyright (C) SchedMD LLC. | |
| ############################################################################ | |
| import os | |
| import pytest | |
| import atf | |
| test_name = os.path.splitext(os.path.basename(__file__))[0] | |
| qos_list = [f"{test_name}_qos{i}" for i in range(3)] | |
| acct = f"{test_name}_acct" | |
| resv_name = f"{test_name}_resv" | |
| testuser = atf.properties["test-user"] | |
| @pytest.fixture(scope="module", autouse=True) | |
| def setup(): | |
| atf.require_version( | |
| (25, 11), | |
| "bin/scontrol", | |
| reason="Creating reservations with qos= added in scontrol 25.11", | |
| ) | |
| atf.require_accounting(True) | |
| atf.require_config_parameter_includes("AccountingStorageEnforce", "qos") | |
| atf.require_slurm_running() | |
| @pytest.fixture(scope="module", autouse=True) | |
| def setup_db(): | |
| # Create test QOS'es and a User that can use them | |
| for qos in qos_list: | |
| atf.run_command( | |
| f"sacctmgr -i add qos {qos}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| atf.run_command( | |
| f"sacctmgr -i add account {acct}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| qos_str = ",".join(qos_list) | |
| atf.run_command( | |
| f"sacctmgr -i add user {testuser} account={acct} qos={qos_str}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| yield | |
| atf.run_command( | |
| f"sacctmgr -i remove user {testuser} account={acct}", | |
| user=atf.properties["slurm-user"], | |
| quiet=True, | |
| ) | |
| atf.run_command( | |
| f"sacctmgr -i remove account {acct}", | |
| user=atf.properties["slurm-user"], | |
| quiet=True, | |
| ) | |
| for qos in qos_list: | |
| atf.run_command( | |
| f"sacctmgr -i remove qos {qos}", | |
| user=atf.properties["slurm-user"], | |
| quiet=True, | |
| ) | |
| @pytest.fixture(scope="function") | |
| def resv(request): | |
| qos_access = f"qos={qos_list[0]}" | |
| if hasattr(request, "param"): | |
| qos_access = request.param | |
| # Create the reservation for QOS test | |
| result = atf.run_command( | |
| f"scontrol create reservationname={resv_name} {qos_access} start=now duration=1 nodecnt=1", | |
| user=atf.properties["slurm-user"], | |
| ) | |
| assert ( | |
| result["exit_code"] == 0 | |
| ), f"Couldn't create the reservation with {qos_access}" | |
| yield resv_name | |
| atf.run_command( | |
| f"scontrol delete reservation {resv_name}", | |
| user=atf.properties["slurm-user"], | |
| quiet=True, | |
| ) | |
| def test_invalid_args(resv): | |
| """Test that invalid configurations are rejected""" | |
| # Mix allow and deny syntax | |
| qos_access = f"qos={qos_list[0]},-{qos_list[1]}" | |
| result = atf.run_command( | |
| f"scontrol create reservationname=invalid_resv {qos_access} start=now duration=1 nodecnt=1", | |
| user=atf.properties["slurm-user"], | |
| ) | |
| assert ( | |
| result["exit_code"] != 0 | |
| ), "Creating reservation with allowed and denied qos should fail" | |
| # Default reservation only allows qos 0 | |
| # Clear all qos access control | |
| result = atf.run_command( | |
| f"scontrol update reservation ReservationName={resv} qos-={qos_list[0]}", | |
| user=atf.properties["slurm-user"], | |
| ) | |
| assert result["exit_code"] != 0, "Updating reservation to clear all qos should fail" | |
| # Change from allow list to deny list | |
| result = atf.run_command( | |
| f"scontrol update reservation ReservationName={resv} qos=-{qos_list[0]}", | |
| user=atf.properties["slurm-user"], | |
| ) | |
| assert ( | |
| result["exit_code"] != 0 | |
| ), "Updating reservation from allow list to deny list should fail" | |
| tests = [ | |
| { | |
| "qos_access": f"qos={qos_list[0]}", | |
| "expected": [0], | |
| "name": "=0", | |
| }, | |
| { | |
| "qos_access": f"qos={qos_list[0]},{qos_list[1]}", | |
| "expected": [0, 1], | |
| "name": "=0,1", | |
| }, | |
| { | |
| "qos_access": f"qos=-{qos_list[0]}", | |
| "expected": [1, 2], | |
| "name": "=-0", | |
| }, | |
| { | |
| "qos_access": f"qos=-{qos_list[0]},-{qos_list[1]}", | |
| "expected": [2], | |
| "name": "=-0,-1", | |
| }, | |
| { | |
| "qos_access": f"qos-={qos_list[0]},{qos_list[1]}", | |
| "expected": [2], | |
| "name": "-=0,1", | |
| }, | |
| ] | |
| args = [(x["qos_access"], x["expected"]) for x in tests] | |
| ids = [x["name"] for x in tests] | |
| def assert_resv_access(resv, qos, is_allowed): | |
| result = atf.run_command( | |
| f"srun -N1 --reservation={resv} --account={acct} --qos={qos} true", | |
| ) | |
| if is_allowed: | |
| assert ( | |
| result["exit_code"] == 0 | |
| ), "ExitCode wasn't 0. The job should not have been denied!" | |
| assert ( | |
| "Problem using reservation" not in result["stderr"] | |
| ), "The job should not have been denied for user!" | |
| else: | |
| assert ( | |
| result["exit_code"] != 0 | |
| ), "ExitCode was 0. The job should have been denied!" | |
| def assert_resv_access_each(resv, expect): | |
| # Try to run a job in each QOS | |
| for i in range(len(qos_list)): | |
| qos = qos_list[i] | |
| assert_resv_access(resv, qos, i in expect) | |
| @pytest.mark.parametrize("resv, expect", args, indirect=["resv"], ids=ids) | |
| def test_reservation_qos(resv, expect): | |
| """Test that a reservation created with QOS= restricts access correctly""" | |
| assert_resv_access_each(resv, expect) | |
| def test_reservation_qos_any(resv): | |
| """Test that a job may use reservations when any QOS is allowed""" | |
| # Default reservation only allows qos 0 | |
| assert_resv_access_each(resv, [0]) | |
| assert_resv_access(resv, f"{qos_list[0]},{qos_list[1]}", True) | |
| assert_resv_access(resv, f"{qos_list[0]},{qos_list[2]}", True) | |
| assert_resv_access(resv, f"{qos_list[1]},{qos_list[2]}", False) | |
| def test_reservation_qos_update(resv): | |
| """Test that updating a reservation's QOS= restricts access correctly""" | |
| # Add qos 1 | |
| atf.run_command( | |
| f"scontrol update ReservationName={resv} qos+={qos_list[1]}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| assert_resv_access_each(resv, [0, 1]) | |
| # Remove qos 0 | |
| atf.run_command( | |
| f"scontrol update ReservationName={resv} qos-={qos_list[0]}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| assert_resv_access_each(resv, [1]) | |
| # Set to qos 2 | |
| atf.run_command( | |
| f"scontrol update ReservationName={resv} qos={qos_list[2]}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| assert_resv_access_each(resv, [2]) |