| ############################################################################ | |
| # Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| ############################################################################ | |
| """Test user name case handling with the PreserveCaseUser option.""" | |
| import re | |
| import pytest | |
| import atf | |
| pytestmark = pytest.mark.slow | |
| # Global variables | |
| cluster = "test_cluster" | |
| user_lcase = "test_user" | |
| user_ucase = user_lcase.upper() | |
| acct = "test_acct" | |
| acct2 = "test_acct2" | |
| wckey = "test_wckey" | |
| assoc_table = f"{cluster}_assoc_table" | |
| user_table = "user_table" | |
| @pytest.fixture(scope="module", autouse=True) | |
| def setup(sql_statement_repeat): | |
| """Test setup with required configurations.""" | |
| atf.require_version( | |
| (26, 11), | |
| component="sbin/slurmdbd", | |
| reason="Ticket 23675: user name case not preserved when toggling PreserveCaseUser", | |
| ) | |
| atf.require_accounting(modify=True) | |
| atf.require_config_parameter_includes("AccountingStorageEnforce", "associations") | |
| atf.require_config_parameter("AllowNoDefAcct", "No", source="slurmdbd") | |
| atf.require_config_parameter("TrackWCKey", "yes", source="slurmdbd") | |
| atf.require_config_parameter_excludes( | |
| "Parameters", "PreserveCaseUser", source="slurmdbd" | |
| ) | |
| atf.require_slurm_running() | |
| atf.run_command( | |
| f"sacctmgr -i add cluster {cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| atf.run_command( | |
| f"sacctmgr -i add account {acct} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| atf.run_command( | |
| f"sacctmgr -i add account {acct2} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| atf.run_command( | |
| f"sacctmgr -i add user {user_lcase} defaultaccount={acct} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| # | |
| # Age user creation time by a week so user will be marked as deleted and | |
| # not actually removed from the user table later. | |
| # Alternatively, we could submit a job as the user to accomplish this but | |
| # there would need to be a user created on the system with the matching | |
| # case. Aging is easier. | |
| # | |
| atf.stop_slurmdbd(quiet=True) | |
| mysql_command = sql_statement_repeat + ' -e "' | |
| mysql_command += f"update {user_table} set creation_time=creation_time-7*24*3600 where name='{user_lcase}'; " | |
| mysql_command += f"update {assoc_table} set creation_time=creation_time-7*24*3600 where user='{user_lcase}'" | |
| mysql_command += '"' | |
| atf.run_command( | |
| mysql_command, | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| atf.start_slurmdbd(quiet=True) | |
| # Leave the aged user soft-deleted with PCU unset: the same baseline | |
| # each test's teardown restores, so every test starts identically. | |
| _reset_db_baseline() | |
| def _reset_db_baseline(): | |
| """Return the db to baseline: PreserveCaseUser unset and the aged test | |
| user (and its associations) soft-deleted.""" | |
| atf.remove_config_parameter_value( | |
| "Parameters", "PreserveCaseUser", source="slurmdbd" | |
| ) | |
| atf.run_command( | |
| f"sacctmgr -i remove user {user_lcase} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| ) | |
| atf.run_command( | |
| f"sacctmgr -i remove assoc where user={user_lcase} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| ) | |
| @pytest.fixture(scope="function") | |
| def setup_db(request): | |
| """Set up the db for a test: PreserveCaseUser set with the requested | |
| user present, starting from the baseline established by setup(). Tears | |
| down to that baseline afterwards, so each test is independent and can | |
| be run in isolation.""" | |
| user = request.param | |
| # Set PreserveCaseUser and add the requested user | |
| atf.add_config_parameter_value("Parameters", "PreserveCaseUser", source="slurmdbd") | |
| atf.run_command( | |
| f"sacctmgr -i add user {user} defaultaccount={acct} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| # Ensure the case of the user and assoc rows match | |
| atf.run_command( | |
| f"sacctmgr -i modify user {user} cluster={cluster} set newname={user}", | |
| user=atf.properties["slurm-user"], | |
| ) | |
| # Start test with PCU set and user created | |
| yield | |
| # Restore baseline so the next test starts clean | |
| _reset_db_baseline() | |
| @pytest.mark.parametrize("setup_db", [user_lcase], indirect=True) | |
| def test_remove_and_add_user_no_pcu_to_pcu(setup_db): | |
| """Test removing and adding user with different case when | |
| transitioning from PCU not being set to it being set | |
| """ | |
| # Lower case user was created with PCU not set | |
| # Now removing and adding upper case user with PCU set | |
| # Remove the lower case user | |
| atf.run_command( | |
| f"sacctmgr -i remove user {user_lcase} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| # The removed row must remain soft-deleted (deleted=1), not purged, | |
| # otherwise the differing-case collision this test exercises never occurs. | |
| deleted_user = atf.run_command_output( | |
| f"sacctmgr -nP show user {user_lcase} withdeleted format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert ( | |
| deleted_user | |
| ), f"Removed user '{user_lcase}' should remain soft-deleted to exercise the case collision" | |
| # Now add the upper case user | |
| atf.run_command( | |
| f"sacctmgr -i add user {user_ucase} acct={acct} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| user_ut = atf.run_command_output( | |
| f"sacctmgr -nP show user {user_ucase} format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert user_ut, f"User '{user_ucase}' does not exist" | |
| user_at = atf.run_command_output( | |
| f"sacctmgr -nP show assoc where user={user_ucase} format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert ( | |
| user_ut == user_ucase | |
| ), f"User table user '{user_ut}' should preserve original case '{user_ucase}'" | |
| assert ( | |
| user_at == user_ucase | |
| ), f"Assoc table user '{user_at}' should preserve original case '{user_ucase}'" | |
| @pytest.mark.parametrize("setup_db", [user_ucase], indirect=True) | |
| def test_remove_and_add_user_pcu_to_no_pcu(setup_db): | |
| """Test removing and adding user with different case when | |
| transitioning from PCU being set to it not being set | |
| """ | |
| # Upper case user was created with PCU set | |
| # Now removing and adding lower case user with PCU not set | |
| # Turn off PCU, remove existing user and add new one | |
| atf.remove_config_parameter_value( | |
| "Parameters", "PreserveCaseUser", source="slurmdbd" | |
| ) | |
| atf.run_command( | |
| f"sacctmgr -i remove user {user_lcase} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| # The removed row must remain soft-deleted (deleted=1), not purged, | |
| # otherwise the differing-case collision this test exercises never occurs. | |
| deleted_user = atf.run_command_output( | |
| f"sacctmgr -nP show user {user_lcase} withdeleted format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert ( | |
| deleted_user | |
| ), f"Removed user '{user_lcase}' should remain soft-deleted to exercise the case collision" | |
| atf.run_command( | |
| f"sacctmgr -i add user {user_lcase} acct={acct} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| # Check that the user case in the user and assoc tables match | |
| user_ut = atf.run_command_output( | |
| f"sacctmgr -nP show user {user_lcase} format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert user_ut, f"User '{user_lcase}' does not exist" | |
| user_at = atf.run_command_output( | |
| f"sacctmgr -nP show assoc where user={user_lcase} format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert ( | |
| user_ut == user_lcase | |
| ), f"User table user '{user_ut}' should be lowercased to '{user_lcase}'" | |
| assert ( | |
| user_at == user_lcase | |
| ), f"Assoc table user '{user_at}' should be lowercased to '{user_lcase}'" | |
| @pytest.mark.parametrize("setup_db", [user_ucase], indirect=True) | |
| def test_modify_user_reports_original_case(setup_db): | |
| """Test modifying a user with different case reports the original case.""" | |
| output = atf.run_command_output( | |
| f"sacctmgr -i modify user {user_lcase} cluster={cluster} set adminlevel=operator", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| assert ( | |
| " Modified users...\n" in output | |
| ), f"sacctmgr modify did not report 'Modified users':\n{output}" | |
| assert re.search( | |
| rf"^\s+{user_ucase}$", output, re.MULTILINE | |
| ), f"Modify user should list '{user_ucase}' instead of normalizing to '{user_lcase}'" | |
| # The stored user must still hold the original case after the modify | |
| user_ut = atf.run_command_output( | |
| f"sacctmgr -nP show user {user_lcase} format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert ( | |
| user_ut == user_ucase | |
| ), f"Stored user '{user_ut}' should preserve original case '{user_ucase}'" | |
| @pytest.mark.parametrize("setup_db", [user_ucase], indirect=True) | |
| def test_remove_user_reports_original_case(setup_db): | |
| """Test removing a user with different case reports the original case.""" | |
| output = atf.run_command_output( | |
| f"sacctmgr -i remove user {user_lcase}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| assert ( | |
| " Deleting users" in output | |
| ), f"sacctmgr remove did not report 'Deleting users':\n{output}" | |
| assert re.search( | |
| rf"^\s+{user_ucase}$", output, re.MULTILINE | |
| ), f"Remove user should list '{user_ucase}' instead of normalizing to '{user_lcase}'" | |
| @pytest.mark.parametrize("setup_db", [user_ucase], indirect=True) | |
| def test_add_assoc_different_case_preserves_user(setup_db): | |
| """Test adding an assoc for an existing user with a different case | |
| reuses the existing user (original case) instead of creating a new | |
| assoc with the differing case.""" | |
| # user_ucase already exists (acct) under PCU. Add an assoc for the | |
| # same user but with a differing case (user_lcase) and a different | |
| # account (acct2). The new assoc must use the original case. | |
| atf.run_command( | |
| f"sacctmgr -i add user {user_lcase} acct={acct2} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| # The user table should still hold only the original-case user | |
| user_ut = atf.run_command_output( | |
| f"sacctmgr -nP show user {user_lcase} format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert ( | |
| user_ut == user_ucase | |
| ), f"User table user '{user_ut}' should preserve original case '{user_ucase}'" | |
| # The new assoc on acct2 should be created under the original case | |
| user_at = atf.run_command_output( | |
| f"sacctmgr -nP show assoc where user={user_lcase} account={acct2} cluster={cluster} format=user", | |
| fatal=True, | |
| ).rstrip() | |
| assert user_at == user_ucase, ( | |
| f"New assoc user '{user_at}' should preserve original case '{user_ucase}' " | |
| f"instead of using the differing case '{user_lcase}'" | |
| ) | |
| @pytest.mark.parametrize("setup_db", [user_ucase], indirect=True) | |
| def test_add_assoc_different_case_with_default_preserves_user(setup_db): | |
| """Test adding an assoc for an existing user with a different case and an | |
| explicit default account still reuses the existing user (original case) | |
| instead of creating an assoc with the differing case.""" | |
| # user_ucase already exists (acct) under PCU. Add an assoc for the same | |
| # user with a differing case (user_lcase), a different account (acct2), | |
| # and an explicit default account so case preservation runs even when the | |
| # default-account branch is taken. | |
| atf.run_command( | |
| f"sacctmgr -i add user {user_lcase} acct={acct2} defaultaccount={acct2} " | |
| f"cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| # The user table should still hold only the original-case user | |
| user_ut = atf.run_command_output( | |
| f"sacctmgr -nP show user {user_lcase} format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert ( | |
| user_ut == user_ucase | |
| ), f"User table user '{user_ut}' should preserve original case '{user_ucase}'" | |
| # The new assoc on acct2 should be created under the original case | |
| user_at = atf.run_command_output( | |
| f"sacctmgr -nP show assoc where user={user_lcase} account={acct2} cluster={cluster} format=user", | |
| fatal=True, | |
| ).rstrip() | |
| assert user_at == user_ucase, ( | |
| f"New assoc user '{user_at}' should preserve original case '{user_ucase}' " | |
| f"instead of using the differing case '{user_lcase}'" | |
| ) | |
| @pytest.mark.parametrize("setup_db", [user_ucase], indirect=True) | |
| def test_modify_user_by_default_account_preserves_case(setup_db): | |
| """Test modifying users selected by default account preserves the | |
| original user case.""" | |
| # user_ucase already exists with default account acct under PCU. | |
| # Select it by default account rather than by name; the modify must | |
| # still resolve to the user and preserve its stored case. | |
| output = atf.run_command_output( | |
| f"sacctmgr -i modify user where defaultaccount={acct} cluster={cluster} set adminlevel=operator", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| assert ( | |
| " Modified users...\n" in output | |
| ), f"sacctmgr modify did not report 'Modified users':\n{output}" | |
| assert re.search( | |
| rf"^\s+{user_ucase}$", output, re.MULTILINE | |
| ), f"Modify by default account should list '{user_ucase}' instead of normalizing to '{user_lcase}'" | |
| # The stored user must still hold the original case after the modify | |
| user_ut = atf.run_command_output( | |
| f"sacctmgr -nP show user {user_lcase} format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert ( | |
| user_ut == user_ucase | |
| ), f"Stored user '{user_ut}' should preserve original case '{user_ucase}'" | |
| @pytest.mark.parametrize("setup_db", [user_ucase], indirect=True) | |
| def test_modify_user_by_default_wckey_preserves_case(setup_db): | |
| """Test modifying users selected by default wckey preserves the | |
| original user case.""" | |
| # Give user_ucase a default wckey (the first wckey added becomes the | |
| # default) so it can be selected by default wckey below. | |
| atf.run_command( | |
| f"sacctmgr -i add user {user_ucase} wckey={wckey} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| # Select the user by default wckey rather than by name; the modify | |
| # must still resolve to the user and preserve its stored case. | |
| output = atf.run_command_output( | |
| f"sacctmgr -i modify user where defaultwckey={wckey} cluster={cluster} set adminlevel=operator", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| assert ( | |
| " Modified users...\n" in output | |
| ), f"sacctmgr modify did not report 'Modified users':\n{output}" | |
| assert re.search( | |
| rf"^\s+{user_ucase}$", output, re.MULTILINE | |
| ), f"Modify by default wckey should list '{user_ucase}' instead of normalizing to '{user_lcase}'" | |
| # The stored user must still hold the original case after the modify | |
| user_ut = atf.run_command_output( | |
| f"sacctmgr -nP show user {user_lcase} format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert ( | |
| user_ut == user_ucase | |
| ), f"Stored user '{user_ut}' should preserve original case '{user_ucase}'" | |
| @pytest.mark.parametrize("setup_db", [user_ucase], indirect=True) | |
| def test_add_user_no_pcu_forces_lowercase(setup_db): | |
| """Test that without PreserveCaseUser a mixed-case user name is forced | |
| to lowercase in both the user and assoc tables.""" | |
| # Turn off PCU, then add a mixed-case user | |
| atf.remove_config_parameter_value( | |
| "Parameters", "PreserveCaseUser", source="slurmdbd" | |
| ) | |
| atf.run_command( | |
| f"sacctmgr -i remove user {user_lcase} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| atf.run_command( | |
| f"sacctmgr -i add user {user_ucase} acct={acct} cluster={cluster}", | |
| user=atf.properties["slurm-user"], | |
| fatal=True, | |
| ) | |
| # The stored name must be lowercased in both tables | |
| user_ut = atf.run_command_output( | |
| f"sacctmgr -nP show user {user_lcase} format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert ( | |
| user_ut == user_lcase | |
| ), f"Without PreserveCaseUser, user '{user_ut}' should be lowercased to '{user_lcase}'" | |
| user_at = atf.run_command_output( | |
| f"sacctmgr -nP show assoc where user={user_lcase} format=user cluster={cluster}", | |
| fatal=True, | |
| ).rstrip() | |
| assert ( | |
| user_at == user_lcase | |
| ), f"Without PreserveCaseUser, assoc user '{user_at}' should be lowercased to '{user_lcase}'" |