From 63ff8ce775eec43b2f768b72fba4154c7832b1f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yeray=20Guti=C3=A9rrez=20Cedr=C3=A9s?= Date: Wed, 7 Aug 2024 08:54:24 +0100 Subject: [PATCH 01/11] Skip more tests related to old OpenSSL algorithms * Skip more tests related to old OpenSSL algorithms * Check the comment from state apply ret instead of exception --------- Co-authored-by: vzhestkov --- tests/pytests/functional/modules/test_x509_v2.py | 10 ++++++++-- tests/pytests/functional/states/test_x509_v2.py | 9 +++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/pytests/functional/modules/test_x509_v2.py b/tests/pytests/functional/modules/test_x509_v2.py index c060ad2971..2e8152d04a 100644 --- a/tests/pytests/functional/modules/test_x509_v2.py +++ b/tests/pytests/functional/modules/test_x509_v2.py @@ -1400,7 +1400,10 @@ def test_create_csr_raw(x509, rsa_privkey): @pytest.mark.slow_test @pytest.mark.parametrize("algo", ["rsa", "ec", "ed25519", "ed448"]) def test_create_private_key(x509, algo): - res = x509.create_private_key(algo=algo) + try: + res = x509.create_private_key(algo=algo) + except UnsupportedAlgorithm: + pytest.skip(f"Algorithm '{algo}' is not supported on this OpenSSL version") assert res.startswith("-----BEGIN PRIVATE KEY-----") @@ -1408,7 +1411,10 @@ def test_create_private_key(x509, algo): @pytest.mark.parametrize("algo", ["rsa", "ec", "ed25519", "ed448"]) def test_create_private_key_with_passphrase(x509, algo): passphrase = "hunter2" - res = x509.create_private_key(algo=algo, passphrase=passphrase) + try: + res = x509.create_private_key(algo=algo, passphrase=passphrase) + except UnsupportedAlgorithm: + pytest.skip(f"Algorithm '{algo}' is not supported on this OpenSSL version") assert res.startswith("-----BEGIN ENCRYPTED PRIVATE KEY-----") # ensure it can be loaded x509.get_private_key_size(res, passphrase=passphrase) diff --git a/tests/pytests/functional/states/test_x509_v2.py b/tests/pytests/functional/states/test_x509_v2.py index e74bdd73f3..929be014cd 100644 --- a/tests/pytests/functional/states/test_x509_v2.py +++ b/tests/pytests/functional/states/test_x509_v2.py @@ -6,6 +6,7 @@ try: import cryptography import cryptography.x509 as cx509 + from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, rsa from cryptography.hazmat.primitives.serialization import ( @@ -691,6 +692,8 @@ def existing_csr_exts(x509, csr_args, csr_args_exts, ca_key, rsa_privkey, reques def existing_pk(x509, pk_args, request): pk_args.update(request.param) ret = x509.private_key_managed(**pk_args) + if ret.result == False and "UnsupportedAlgorithm" in ret.comment: + pytest.skip(f"Algorithm '{pk_args['algo']}' is not supported on this OpenSSL version") _assert_pk_basic( ret, pk_args.get("algo", "rsa"), @@ -2140,6 +2143,8 @@ def test_private_key_managed(x509, pk_args, algo, encoding, passphrase): pk_args["encoding"] = encoding pk_args["passphrase"] = passphrase ret = x509.private_key_managed(**pk_args) + if ret.result == False and "UnsupportedAlgorithm" in ret.comment: + pytest.skip(f"Algorithm '{algo}' is not supported on this OpenSSL version") _assert_pk_basic(ret, algo, encoding, passphrase) @@ -2167,6 +2172,8 @@ def test_private_key_managed_keysize(x509, pk_args, algo, keysize): ) def test_private_key_managed_existing(x509, pk_args): ret = x509.private_key_managed(**pk_args) + if ret.result == False and "UnsupportedAlgorithm" in ret.comment: + pytest.skip(f"Algorithm '{pk_args['algo']}' is not supported on this OpenSSL version") _assert_not_changed(ret) @@ -2194,6 +2201,8 @@ def test_private_key_managed_existing_new_with_passphrase_change(x509, pk_args): def test_private_key_managed_algo_change(x509, pk_args): pk_args["algo"] = "ed25519" ret = x509.private_key_managed(**pk_args) + if ret.result == False and "UnsupportedAlgorithm" in ret.comment: + pytest.skip("Algorithm 'ed25519' is not supported on this OpenSSL version") _assert_pk_basic(ret, "ed25519") From 25c3df7713bd2a19a0980358fa72c1c48a08a1f4 Mon Sep 17 00:00:00 2001 From: Marek Czernek Date: Wed, 7 Aug 2024 10:28:07 +0200 Subject: [PATCH 02/11] Make tests compatible with venv bundle Co-authored-by: cmcmarrow --- tests/pytests/functional/modules/test_sdb.py | 1 + tests/pytests/functional/modules/test_yaml.py | 2 +- .../rthooks/test_salt_utils_vt_terminal.py | 22 +++++-- .../pyinstaller/rthooks/test_subprocess.py | 22 +++++-- .../utils/yamllint/test_yamllint.py | 2 +- tests/pytests/unit/modules/test_pip.py | 63 +++++++++++++------ .../unit/modules/test_transactional_update.py | 13 ++-- tests/pytests/unit/states/test_pkgrepo.py | 3 +- tests/pytests/unit/test_fileserver.py | 8 +-- tests/pytests/unit/utils/test_gitfs.py | 18 ++++++ tests/pytests/unit/utils/test_msgpack.py | 2 +- tests/pytests/unit/utils/test_pycrypto.py | 25 ++++---- tests/unit/test_config.py | 20 +++++- tests/unit/utils/test_sdb.py | 2 +- tests/unit/utils/test_templates.py | 34 ++++++++++ 15 files changed, 177 insertions(+), 60 deletions(-) diff --git a/tests/pytests/functional/modules/test_sdb.py b/tests/pytests/functional/modules/test_sdb.py index 5519bf8ab5..837e7515d3 100644 --- a/tests/pytests/functional/modules/test_sdb.py +++ b/tests/pytests/functional/modules/test_sdb.py @@ -16,6 +16,7 @@ def minion_config_overrides(): } +@pytest.mark.skip("Great module migration") @pytest.mark.parametrize( "expected_value", ( diff --git a/tests/pytests/functional/modules/test_yaml.py b/tests/pytests/functional/modules/test_yaml.py index 2a8fbc113f..9aad0dfdc8 100644 --- a/tests/pytests/functional/modules/test_yaml.py +++ b/tests/pytests/functional/modules/test_yaml.py @@ -13,7 +13,7 @@ import salt.modules.yaml import salt.utils.yamllint - YAMLLINT_AVAILABLE = True + YAMLLINT_AVAILABLE = salt.utils.yamllint.has_yamllint() except ImportError: YAMLLINT_AVAILABLE = False diff --git a/tests/pytests/functional/utils/pyinstaller/rthooks/test_salt_utils_vt_terminal.py b/tests/pytests/functional/utils/pyinstaller/rthooks/test_salt_utils_vt_terminal.py index c45b5730a8..ea687c0776 100644 --- a/tests/pytests/functional/utils/pyinstaller/rthooks/test_salt_utils_vt_terminal.py +++ b/tests/pytests/functional/utils/pyinstaller/rthooks/test_salt_utils_vt_terminal.py @@ -8,6 +8,9 @@ from tests.support import mock from tests.support.helpers import PatchedEnviron +LD_LIBRARY_PATH = "" +if os.environ.get('VIRTUAL_ENV'): + LD_LIBRARY_PATH = f"{os.environ.get('VIRTUAL_ENV')}/lib" @pytest.fixture(params=("LD_LIBRARY_PATH", "LIBPATH")) def envvar(request): @@ -17,9 +20,14 @@ def envvar(request): @pytest.fixture def meipass(envvar): with mock.patch("salt.utils.pyinstaller.rthooks._overrides.sys") as patched_sys: - patched_sys._MEIPASS = "{}_VALUE".format(envvar) - assert overrides.sys._MEIPASS == "{}_VALUE".format(envvar) - yield "{}_VALUE".format(envvar) + ld_path_mock_val = f"{envvar}_VALUE" + if envvar == "LD_LIBRARY_PATH" and LD_LIBRARY_PATH: + # venv-minion python wrapper hardcodes LD_LIB_PATH that + # we cannot overwrite from the testsuite + ld_path_mock_val = LD_LIBRARY_PATH + patched_sys._MEIPASS = ld_path_mock_val + assert overrides.sys._MEIPASS == ld_path_mock_val + yield ld_path_mock_val assert not hasattr(sys, "_MEIPASS") assert not hasattr(overrides.sys, "_MEIPASS") @@ -111,7 +119,8 @@ def test_vt_terminal_environ_cleanup(envvar, meipass): returned_env = json.loads(buffer_o) assert returned_env != original_env assert envvar in returned_env - assert returned_env[envvar] == "" + envvar_value = LD_LIBRARY_PATH if envvar == "LD_LIBRARY_PATH" else "" + assert returned_env[envvar] == envvar_value def test_vt_terminal_environ_cleanup_passed_directly_not_removed(envvar, meipass): @@ -139,4 +148,7 @@ def test_vt_terminal_environ_cleanup_passed_directly_not_removed(envvar, meipass returned_env = json.loads(buffer_o) assert returned_env != original_env assert envvar in returned_env - assert returned_env[envvar] == envvar + envvar_val = envvar + if LD_LIBRARY_PATH and envvar == "LD_LIBRARY_PATH": + envvar_val = LD_LIBRARY_PATH + assert returned_env[envvar] == envvar_val diff --git a/tests/pytests/functional/utils/pyinstaller/rthooks/test_subprocess.py b/tests/pytests/functional/utils/pyinstaller/rthooks/test_subprocess.py index 836e392d01..e4b5420d5e 100644 --- a/tests/pytests/functional/utils/pyinstaller/rthooks/test_subprocess.py +++ b/tests/pytests/functional/utils/pyinstaller/rthooks/test_subprocess.py @@ -9,6 +9,9 @@ from tests.support import mock from tests.support.helpers import PatchedEnviron +LD_LIBRARY_PATH = "" +if os.environ.get('VIRTUAL_ENV'): + LD_LIBRARY_PATH = f"{os.environ.get('VIRTUAL_ENV')}/lib" @pytest.fixture(params=("LD_LIBRARY_PATH", "LIBPATH")) def envvar(request): @@ -18,9 +21,14 @@ def envvar(request): @pytest.fixture def meipass(envvar): with mock.patch("salt.utils.pyinstaller.rthooks._overrides.sys") as patched_sys: - patched_sys._MEIPASS = "{}_VALUE".format(envvar) - assert overrides.sys._MEIPASS == "{}_VALUE".format(envvar) - yield "{}_VALUE".format(envvar) + ld_path_mock_val = f"{envvar}_VALUE" + if envvar == "LD_LIBRARY_PATH" and LD_LIBRARY_PATH: + # venv-minion python wrapper hardcodes LD_LIB_PATH that + # we cannot overwrite from the testsuite + ld_path_mock_val = LD_LIBRARY_PATH + patched_sys._MEIPASS = ld_path_mock_val + assert overrides.sys._MEIPASS == ld_path_mock_val + yield ld_path_mock_val assert not hasattr(sys, "_MEIPASS") assert not hasattr(overrides.sys, "_MEIPASS") @@ -88,7 +96,8 @@ def test_subprocess_popen_environ_cleanup(envvar, meipass): returned_env = json.loads(stdout) assert returned_env != original_env assert envvar in returned_env - assert returned_env[envvar] == "" + envvar_value = LD_LIBRARY_PATH if envvar == "LD_LIBRARY_PATH" else "" + assert returned_env[envvar] == envvar_value def test_subprocess_popen_environ_cleanup_passed_directly_not_removed(envvar, meipass): @@ -108,4 +117,7 @@ def test_subprocess_popen_environ_cleanup_passed_directly_not_removed(envvar, me returned_env = json.loads(stdout) assert returned_env != original_env assert envvar in returned_env - assert returned_env[envvar] == envvar + envvar_val = envvar + if LD_LIBRARY_PATH and envvar == "LD_LIBRARY_PATH": + envvar_val = LD_LIBRARY_PATH + assert returned_env[envvar] == envvar_val diff --git a/tests/pytests/functional/utils/yamllint/test_yamllint.py b/tests/pytests/functional/utils/yamllint/test_yamllint.py index 403c6fc610..3c730523c4 100644 --- a/tests/pytests/functional/utils/yamllint/test_yamllint.py +++ b/tests/pytests/functional/utils/yamllint/test_yamllint.py @@ -7,7 +7,7 @@ try: import salt.utils.yamllint as yamllint - YAMLLINT_AVAILABLE = True + YAMLLINT_AVAILABLE = yamllint.has_yamllint() except ImportError: YAMLLINT_AVAILABLE = False diff --git a/tests/pytests/unit/modules/test_pip.py b/tests/pytests/unit/modules/test_pip.py index 4b2da77786..fbe0dc5f1c 100644 --- a/tests/pytests/unit/modules/test_pip.py +++ b/tests/pytests/unit/modules/test_pip.py @@ -15,6 +15,10 @@ os.path.join(RUNTIME_VARS.CODE_DIR, "setup.py") ) +TARGET = [] +if os.environ.get('VENV_PIP_TARGET'): + TARGET = ["--target", os.environ.get('VENV_PIP_TARGET')] + class FakeFopen: def __init__(self, filename): @@ -102,6 +106,7 @@ def test_install_frozen_app(python_binary): expected = [ *python_binary, "install", + *TARGET, pkg, ] mock.assert_called_with( @@ -123,6 +128,7 @@ def test_install_source_app(python_binary): expected = [ *python_binary, "install", + *TARGET, pkg, ] mock.assert_called_with( @@ -143,6 +149,7 @@ def test_fix4361(python_binary): "install", "--requirement", "requirements.txt", + *TARGET, ] mock.assert_called_with( expected_cmd, @@ -169,7 +176,7 @@ def test_install_multiple_editable(python_binary): "git+https://github.com/saltstack/salt-testing.git#egg=SaltTesting", ] - expected = [*python_binary, "install"] + expected = [*python_binary, "install", *TARGET] for item in editables: expected.extend(["--editable", item]) @@ -205,7 +212,7 @@ def test_install_multiple_pkgs_and_editables(python_binary): "git+https://github.com/saltstack/salt-testing.git#egg=SaltTesting", ] - expected = [*python_binary, "install"] + expected = [*python_binary, "install", *TARGET] expected.extend(pkgs) for item in editables: expected.extend(["--editable", item]) @@ -241,6 +248,7 @@ def test_install_multiple_pkgs_and_editables(python_binary): expected = [ *python_binary, "install", + *TARGET, pkgs[0], "--editable", editables[0], @@ -268,7 +276,7 @@ def test_issue5940_install_multiple_pip_mirrors(python_binary): expected = [*python_binary, "install", "--use-mirrors"] for item in mirrors: expected.extend(["--mirrors", item]) - expected.append("pep8") + expected = [*expected, *TARGET, "pep8"] # Passing mirrors as a list mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) @@ -300,6 +308,7 @@ def test_issue5940_install_multiple_pip_mirrors(python_binary): "--use-mirrors", "--mirrors", mirrors[0], + *TARGET, "pep8", ] @@ -327,7 +336,7 @@ def test_install_with_multiple_find_links(python_binary): expected = [*python_binary, "install"] for item in find_links: expected.extend(["--find-links", item]) - expected.append(pkg) + expected = [*expected, *TARGET, pkg] # Passing mirrors as a list mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) @@ -370,6 +379,7 @@ def test_install_with_multiple_find_links(python_binary): "install", "--find-links", find_links[0], + *TARGET, pkg, ] @@ -435,6 +445,7 @@ def test_install_cached_requirements_used(python_binary): "install", "--requirement", "my_cached_reqs", + *TARGET, ] mock.assert_called_with( expected, @@ -491,6 +502,7 @@ def test_install_log_argument_in_resulting_command(python_binary): "install", "--log", log_path, + *TARGET, pkg, ] mock.assert_called_with( @@ -521,7 +533,7 @@ def test_install_timeout_argument_in_resulting_command(python_binary): with patch.dict(pip.__salt__, {"cmd.run_all": mock}): pip.install(pkg, timeout=10) mock.assert_called_with( - expected + [10, pkg], + expected + [10, *TARGET, pkg], saltenv="base", runas=None, use_vt=False, @@ -533,7 +545,7 @@ def test_install_timeout_argument_in_resulting_command(python_binary): with patch.dict(pip.__salt__, {"cmd.run_all": mock}): pip.install(pkg, timeout="10") mock.assert_called_with( - expected + ["10", pkg], + expected + ["10", *TARGET, pkg], saltenv="base", runas=None, use_vt=False, @@ -557,6 +569,7 @@ def test_install_index_url_argument_in_resulting_command(python_binary): "install", "--index-url", index_url, + *TARGET, pkg, ] mock.assert_called_with( @@ -579,6 +592,7 @@ def test_install_extra_index_url_argument_in_resulting_command(python_binary): "install", "--extra-index-url", extra_index_url, + *TARGET, pkg, ] mock.assert_called_with( @@ -595,7 +609,7 @@ def test_install_no_index_argument_in_resulting_command(python_binary): mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) with patch.dict(pip.__salt__, {"cmd.run_all": mock}): pip.install(pkg, no_index=True) - expected = [*python_binary, "install", "--no-index", pkg] + expected = [*python_binary, "install", "--no-index", *TARGET, pkg] mock.assert_called_with( expected, saltenv="base", @@ -611,7 +625,7 @@ def test_install_build_argument_in_resulting_command(python_binary): mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) with patch.dict(pip.__salt__, {"cmd.run_all": mock}): pip.install(pkg, build=build) - expected = [*python_binary, "install", "--build", build, pkg] + expected = [*python_binary, "install", "--build", build, *TARGET, pkg] mock.assert_called_with( expected, saltenv="base", @@ -646,6 +660,7 @@ def test_install_download_argument_in_resulting_command(python_binary): expected = [ *python_binary, "install", + *TARGET, "--download", download, pkg, @@ -664,7 +679,7 @@ def test_install_no_download_argument_in_resulting_command(python_binary): mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) with patch.dict(pip.__salt__, {"cmd.run_all": mock}): pip.install(pkg, no_download=True) - expected = [*python_binary, "install", "--no-download", pkg] + expected = [*python_binary, "install", *TARGET, "--no-download", pkg] mock.assert_called_with( expected, saltenv="base", @@ -691,6 +706,7 @@ def test_install_download_cache_dir_arguments_in_resulting_command(python_binary expected = [ *python_binary, "install", + *TARGET, cmd_arg, download_cache, pkg, @@ -720,7 +736,7 @@ def test_install_source_argument_in_resulting_command(python_binary): mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) with patch.dict(pip.__salt__, {"cmd.run_all": mock}): pip.install(pkg, source=source) - expected = [*python_binary, "install", "--source", source, pkg] + expected = [*python_binary, "install", *TARGET, "--source", source, pkg] mock.assert_called_with( expected, saltenv="base", @@ -739,6 +755,7 @@ def test_install_exists_action_argument_in_resulting_command(python_binary): expected = [ *python_binary, "install", + *TARGET, "--exists-action", action, pkg, @@ -761,7 +778,7 @@ def test_install_install_options_argument_in_resulting_command(python_binary): install_options = ["--exec-prefix=/foo/bar", "--install-scripts=/foo/bar/bin"] pkg = "pep8" - expected = [*python_binary, "install"] + expected = [*python_binary, "install", *TARGET] for item in install_options: expected.extend(["--install-option", item]) expected.append(pkg) @@ -797,6 +814,7 @@ def test_install_install_options_argument_in_resulting_command(python_binary): expected = [ *python_binary, "install", + *TARGET, "--install-option", install_options[0], pkg, @@ -814,7 +832,7 @@ def test_install_global_options_argument_in_resulting_command(python_binary): global_options = ["--quiet", "--no-user-cfg"] pkg = "pep8" - expected = [*python_binary, "install"] + expected = [*python_binary, "install", *TARGET] for item in global_options: expected.extend(["--global-option", item]) expected.append(pkg) @@ -850,6 +868,7 @@ def test_install_global_options_argument_in_resulting_command(python_binary): expected = [ *python_binary, "install", + *TARGET, "--global-option", global_options[0], pkg, @@ -868,7 +887,7 @@ def test_install_upgrade_argument_in_resulting_command(python_binary): mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) with patch.dict(pip.__salt__, {"cmd.run_all": mock}): pip.install(pkg, upgrade=True) - expected = [*python_binary, "install", "--upgrade", pkg] + expected = [*python_binary, "install", *TARGET, "--upgrade", pkg] mock.assert_called_with( expected, saltenv="base", @@ -886,6 +905,7 @@ def test_install_force_reinstall_argument_in_resulting_command(python_binary): expected = [ *python_binary, "install", + *TARGET, "--force-reinstall", pkg, ] @@ -906,6 +926,7 @@ def test_install_ignore_installed_argument_in_resulting_command(python_binary): expected = [ *python_binary, "install", + *TARGET, "--ignore-installed", pkg, ] @@ -923,7 +944,7 @@ def test_install_no_deps_argument_in_resulting_command(python_binary): mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) with patch.dict(pip.__salt__, {"cmd.run_all": mock}): pip.install(pkg, no_deps=True) - expected = [*python_binary, "install", "--no-deps", pkg] + expected = [*python_binary, "install", *TARGET, "--no-deps", pkg] mock.assert_called_with( expected, saltenv="base", @@ -938,7 +959,7 @@ def test_install_no_install_argument_in_resulting_command(python_binary): mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) with patch.dict(pip.__salt__, {"cmd.run_all": mock}): pip.install(pkg, no_install=True) - expected = [*python_binary, "install", "--no-install", pkg] + expected = [*python_binary, "install", *TARGET, "--no-install", pkg] mock.assert_called_with( expected, saltenv="base", @@ -954,7 +975,7 @@ def test_install_proxy_argument_in_resulting_command(python_binary): mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) with patch.dict(pip.__salt__, {"cmd.run_all": mock}): pip.install(pkg, proxy=proxy) - expected = [*python_binary, "install", "--proxy", proxy, pkg] + expected = [*python_binary, "install", "--proxy", proxy, *TARGET, pkg] mock.assert_called_with( expected, saltenv="base", @@ -981,7 +1002,7 @@ def test_install_proxy_false_argument_in_resulting_command(python_binary): with patch.dict(pip.__salt__, {"cmd.run_all": mock}): with patch.dict(pip.__opts__, config_mock): pip.install(pkg, proxy=proxy) - expected = [*python_binary, "install", pkg] + expected = [*python_binary, "install", *TARGET, pkg] mock.assert_called_with( expected, saltenv="base", @@ -1012,6 +1033,7 @@ def test_install_global_proxy_in_resulting_command(python_binary): "install", "--proxy", proxy, + *TARGET, pkg, ] mock.assert_called_with( @@ -1032,6 +1054,7 @@ def test_install_multiple_requirements_arguments_in_resulting_command(python_bin expected = [*python_binary, "install"] for item in cached_reqs: expected.extend(["--requirement", item]) + expected.extend(TARGET) # Passing option as a list mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) @@ -1068,6 +1091,7 @@ def test_install_multiple_requirements_arguments_in_resulting_command(python_bin "install", "--requirement", cached_reqs[0], + *TARGET, ] mock.assert_called_with( expected, @@ -1088,6 +1112,7 @@ def test_install_extra_args_arguments_in_resulting_command(python_binary): expected = [ *python_binary, "install", + *TARGET, pkg, "--latest-pip-kwarg", "param", @@ -1604,7 +1629,7 @@ def test_install_pre_argument_in_resulting_command(python_binary): with patch.dict(pip.__salt__, {"cmd.run_all": mock}): with patch("salt.modules.pip.version", MagicMock(return_value="1.3")): pip.install(pkg, pre_releases=True) - expected = [*python_binary, "install", pkg] + expected = [*python_binary, "install", *TARGET, pkg] mock.assert_called_with( expected, saltenv="base", @@ -1620,7 +1645,7 @@ def test_install_pre_argument_in_resulting_command(python_binary): ): with patch("salt.modules.pip._get_pip_bin", MagicMock(return_value=["pip"])): pip.install(pkg, pre_releases=True) - expected = ["pip", "install", "--pre", pkg] + expected = ["pip", "install", *TARGET, "--pre", pkg] mock_run_all.assert_called_with( expected, saltenv="base", diff --git a/tests/pytests/unit/modules/test_transactional_update.py b/tests/pytests/unit/modules/test_transactional_update.py index dbd72fd74b..e0ef2abd0f 100644 --- a/tests/pytests/unit/modules/test_transactional_update.py +++ b/tests/pytests/unit/modules/test_transactional_update.py @@ -1,3 +1,4 @@ +import os import pytest import salt.loader.context @@ -10,6 +11,10 @@ pytest.mark.skip_on_windows(reason="Not supported on Windows"), ] +SALT_CALL_BINARY = "salt-call" +if os.environ.get('VIRTUAL_ENV'): + SALT_CALL_BINARY = f"{os.environ.get('VIRTUAL_ENV')}/bin/salt-call" + @pytest.fixture def configure_loader_modules(): @@ -379,7 +384,7 @@ def test_call_fails_function(): "--continue", "--quiet", "run", - "salt-call", + SALT_CALL_BINARY, "--out", "json", "-l", @@ -411,7 +416,7 @@ def test_call_success_no_reboot(): "--continue", "--quiet", "run", - "salt-call", + SALT_CALL_BINARY, "--out", "json", "-l", @@ -454,7 +459,7 @@ def test_call_success_reboot(): "--continue", "--quiet", "run", - "salt-call", + SALT_CALL_BINARY, "--out", "json", "-l", @@ -488,7 +493,7 @@ def test_call_success_parameters(): "--continue", "--quiet", "run", - "salt-call", + SALT_CALL_BINARY, "--out", "json", "-l", diff --git a/tests/pytests/unit/states/test_pkgrepo.py b/tests/pytests/unit/states/test_pkgrepo.py index 5f540bd245..14d17ad3f9 100644 --- a/tests/pytests/unit/states/test_pkgrepo.py +++ b/tests/pytests/unit/states/test_pkgrepo.py @@ -1,7 +1,6 @@ """ :codeauthor: Tyler Johnson """ - import pytest import salt.states.pkgrepo as pkgrepo @@ -390,7 +389,7 @@ def test_migrated_wrong_method(): with patch.dict(pkgrepo.__grains__, grains), patch.dict( pkgrepo.__salt__, salt_mock ): - assert pkgrepo.migrated("/mnt", method_="magic") == { + assert pkgrepo.migrated("/mnt", method="magic") == { "name": "/mnt", "result": False, "changes": {}, diff --git a/tests/pytests/unit/test_fileserver.py b/tests/pytests/unit/test_fileserver.py index 8dd3ea0a27..49be3967dc 100644 --- a/tests/pytests/unit/test_fileserver.py +++ b/tests/pytests/unit/test_fileserver.py @@ -75,9 +75,7 @@ def test_file_server_url_escape(tmp_path): opts = { "fileserver_backend": ["roots"], "extension_modules": "", - "optimization_order": [ - 0, - ], + "optimization_order": [0, 1], "file_roots": { "base": [fileroot], }, @@ -102,9 +100,7 @@ def test_file_server_serve_url_escape(tmp_path): opts = { "fileserver_backend": ["roots"], "extension_modules": "", - "optimization_order": [ - 0, - ], + "optimization_order": [0, 1], "file_roots": { "base": [fileroot], }, diff --git a/tests/pytests/unit/utils/test_gitfs.py b/tests/pytests/unit/utils/test_gitfs.py index 2bf627049f..bd7d74cb2b 100644 --- a/tests/pytests/unit/utils/test_gitfs.py +++ b/tests/pytests/unit/utils/test_gitfs.py @@ -3,6 +3,7 @@ import pytest +import salt.config import salt.fileserver.gitfs import salt.utils.gitfs from salt.exceptions import FileserverConfigError @@ -24,6 +25,23 @@ import pygit2 +@pytest.fixture +def minion_opts(tmp_path): + """ + Default minion configuration with relative temporary paths to not require root permissions. + """ + root_dir = tmp_path / "minion" + opts = salt.config.DEFAULT_MINION_OPTS.copy() + opts["__role"] = "minion" + opts["root_dir"] = str(root_dir) + for name in ("cachedir", "pki_dir", "sock_dir", "conf_dir"): + dirpath = root_dir / name + dirpath.mkdir(parents=True) + opts[name] = str(dirpath) + opts["log_file"] = "logs/minion.log" + return opts + + @pytest.mark.parametrize( "role_name,role_class", ( diff --git a/tests/pytests/unit/utils/test_msgpack.py b/tests/pytests/unit/utils/test_msgpack.py index a09b6e5b8b..3d0b9d7fc8 100644 --- a/tests/pytests/unit/utils/test_msgpack.py +++ b/tests/pytests/unit/utils/test_msgpack.py @@ -3,7 +3,7 @@ import salt.utils.msgpack from tests.support.mock import MagicMock, patch - +@pytest.mark.skipif(salt.utils.msgpack.version < (1, 0, 0), reason="Test requires msgpack version >= 1.0.0") def test_load_encoding(tmp_path): """ test when using msgpack version >= 1.0.0 we diff --git a/tests/pytests/unit/utils/test_pycrypto.py b/tests/pytests/unit/utils/test_pycrypto.py index 693ad10e24..9e0b58d1b3 100644 --- a/tests/pytests/unit/utils/test_pycrypto.py +++ b/tests/pytests/unit/utils/test_pycrypto.py @@ -57,21 +57,20 @@ def test_gen_hash_crypt(algorithm, expected): """ Test gen_hash with crypt library """ - with patch("salt.utils.pycrypto.methods", {}): - ret = salt.utils.pycrypto.gen_hash( - crypt_salt=expected["salt"], password=passwd, algorithm=algorithm - ) - assert ret == expected["hashed"] + ret = salt.utils.pycrypto.gen_hash( + crypt_salt=expected["salt"], password=passwd, algorithm=algorithm + ) + assert ret == expected["hashed"] - ret = salt.utils.pycrypto.gen_hash( - crypt_salt=expected["badsalt"], password=passwd, algorithm=algorithm - ) - assert ret != expected["hashed"] + ret = salt.utils.pycrypto.gen_hash( + crypt_salt=expected["badsalt"], password=passwd, algorithm=algorithm + ) + assert ret != expected["hashed"] - ret = salt.utils.pycrypto.gen_hash( - crypt_salt=None, password=passwd, algorithm=algorithm - ) - assert ret != expected["hashed"] + ret = salt.utils.pycrypto.gen_hash( + crypt_salt=None, password=passwd, algorithm=algorithm + ) + assert ret != expected["hashed"] @pytest.mark.skipif(not salt.utils.pycrypto.HAS_CRYPT, reason="crypt not available") diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 5cc58c273d..6995b01c89 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -83,9 +83,12 @@ def test_conf_master_sample_is_commented(self): """ master_config = SAMPLE_CONF_DIR + "master" ret = salt.config._read_conf_file(master_config) + # openSUSE modified the default config in + # https://github.com/opensuse/salt/commit/6ffbf7fcc178f32c670b177b25ed64658c59f1bf + expected_config = {"user": "salt", "syndic_user": "salt"} self.assertEqual( ret, - {}, + expected_config, "Sample config file '{}' must be commented out.".format(master_config), ) @@ -347,7 +350,10 @@ def test_load_minion_config_from_environ_var(self, tempdir): with patched_environ(SALT_MINION_CONFIG=env_fpath): # Should load from env variable, not the default configuration file - config = salt.config.minion_config("{}/minion".format(CONFIG_DIR)) + # Override defaults from venv-minion conf + defaults = salt.config.DEFAULT_MINION_OPTS.copy() + defaults["default_include"] = "" + config = salt.config.minion_config("{}/minion".format(CONFIG_DIR), defaults=defaults) self.assertEqual(config["log_file"], env_fpath) root_dir = os.path.join(tempdir, "foo", "bar") @@ -1946,6 +1952,11 @@ def test_api_config_log_file_values(self): if salt.utils.platform.is_windows(): expected = "{}\\var\\log\\salt\\api".format(RUNTIME_VARS.TMP_ROOT_DIR) + if os.environ.get("VIRTUAL_ENV"): + # venv bundle configures --salt-logs-dir=%{_localstatedir}/log + # in the RPM spec file + expected = expected.replace("/salt/api", "/api") + ret = salt.config.api_config("/some/fake/path") self.assertEqual(ret["log_file"], expected) @@ -2017,6 +2028,11 @@ def test_api_config_prepend_root_dirs_return(self): mock_pid = "c:\\mock\\root\\var\\run\\salt-api.pid" mock_master_config["root_dir"] = "c:\\mock\\root" + if os.environ.get("VIRTUAL_ENV"): + # venv bundle configures --salt-logs-dir=%{_localstatedir}/log + # in the RPM spec file + mock_log = mock_log.replace("/salt", "") + with patch( "salt.config.client_config", MagicMock(return_value=mock_master_config) ): diff --git a/tests/unit/utils/test_sdb.py b/tests/unit/utils/test_sdb.py index 87886cbc52..69cbda07be 100644 --- a/tests/unit/utils/test_sdb.py +++ b/tests/unit/utils/test_sdb.py @@ -49,7 +49,7 @@ def test_sqlite_get_not_found(self): # test with SQLite database write and read def test_sqlite_get_found(self): - expected = {b"name": b"testone", b"number": 46} + expected = {"name": "testone", "number": 46} sdb.sdb_set("sdb://test_sdb_data/test1", expected, self.sdb_opts) resp = sdb.sdb_get("sdb://test_sdb_data/test1", self.sdb_opts) self.assertEqual(resp, expected) diff --git a/tests/unit/utils/test_templates.py b/tests/unit/utils/test_templates.py index 264b4ae801..604395f5e0 100644 --- a/tests/unit/utils/test_templates.py +++ b/tests/unit/utils/test_templates.py @@ -1,6 +1,7 @@ """ Unit tests for salt.utils.templates.py """ + import logging import os import sys @@ -22,6 +23,20 @@ except ImportError: HAS_CHEETAH = False +try: + import genshi as _ + + HAS_GENSHI = True +except ImportError: + HAS_GENSHI = False + +try: + import mako as _ + + HAS_MAKO = True +except ImportError: + HAS_MAKO = False + log = logging.getLogger(__name__) @@ -83,16 +98,19 @@ def test_render_jinja_tojson_unsorted(self): assert res == expected ### Tests for mako template + @pytest.mark.skipif(not HAS_MAKO, reason="Mako module not available for testing") def test_render_mako_sanity(self): tmpl = """OK""" res = salt.utils.templates.render_mako_tmpl(tmpl, dict(self.context)) self.assertEqual(res, "OK") + @pytest.mark.skipif(not HAS_MAKO, reason="Mako module not available for testing") def test_render_mako_evaluate(self): tmpl = """${ "OK" }""" res = salt.utils.templates.render_mako_tmpl(tmpl, dict(self.context)) self.assertEqual(res, "OK") + @pytest.mark.skipif(not HAS_MAKO, reason="Mako module not available for testing") def test_render_mako_evaluate_multi(self): tmpl = """ % if 1: @@ -103,6 +121,7 @@ def test_render_mako_evaluate_multi(self): stripped = res.strip() self.assertEqual(stripped, "OK") + @pytest.mark.skipif(not HAS_MAKO, reason="Mako module not available for testing") def test_render_mako_variable(self): tmpl = """${ var }""" @@ -152,21 +171,33 @@ def test_render_wempy_variable(self): self.assertEqual(res, "OK") ### Tests for genshi template (xml-based) + @pytest.mark.skipif( + not HAS_GENSHI, reason="Genshi module not available for testing" + ) def test_render_genshi_sanity(self): tmpl = """OK""" res = salt.utils.templates.render_genshi_tmpl(tmpl, dict(self.context)) self.assertEqual(res, "OK") + @pytest.mark.skipif( + not HAS_GENSHI, reason="Genshi module not available for testing" + ) def test_render_genshi_evaluate(self): tmpl = """${ "OK" }""" res = salt.utils.templates.render_genshi_tmpl(tmpl, dict(self.context)) self.assertEqual(res, "OK") + @pytest.mark.skipif( + not HAS_GENSHI, reason="Genshi module not available for testing" + ) def test_render_genshi_evaluate_condition(self): tmpl = """OK""" res = salt.utils.templates.render_genshi_tmpl(tmpl, dict(self.context)) self.assertEqual(res, "OK") + @pytest.mark.skipif( + not HAS_GENSHI, reason="Genshi module not available for testing" + ) def test_render_genshi_variable(self): tmpl = """$var""" @@ -175,6 +206,9 @@ def test_render_genshi_variable(self): res = salt.utils.templates.render_genshi_tmpl(tmpl, ctx) self.assertEqual(res, "OK") + @pytest.mark.skipif( + not HAS_GENSHI, reason="Genshi module not available for testing" + ) def test_render_genshi_variable_replace(self): tmpl = """not ok""" From 3f3c8d80427c9d90bea5fbca785b210260d33a0f Mon Sep 17 00:00:00 2001 From: Marek Czernek Date: Wed, 21 Aug 2024 16:15:02 +0200 Subject: [PATCH 03/11] Replace use of pygit2 deprecated and removed (1.15.0) oid with id (#673) Co-authored-by: David Murphy --- salt/utils/gitfs.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/salt/utils/gitfs.py b/salt/utils/gitfs.py index 061647edac..f3902c1f19 100644 --- a/salt/utils/gitfs.py +++ b/salt/utils/gitfs.py @@ -1683,7 +1683,7 @@ def _perform_checkout(checkout_ref, branch=True): # remote ref. self.repo.checkout(checkout_ref) if branch: - self.repo.reset(oid, pygit2.GIT_RESET_HARD) + self.repo.reset(pygit2_id, pygit2.GIT_RESET_HARD) return True except GitLockError as exc: if exc.errno == errno.EEXIST: @@ -1714,11 +1714,11 @@ def _perform_checkout(checkout_ref, branch=True): tag_ref = "refs/tags/" + tgt_ref if remote_ref in refs: # Get commit id for the remote ref - oid = self.peel(self.repo.lookup_reference(remote_ref)).id + pygit2_id = self.peel(self.repo.lookup_reference(remote_ref)).id if local_ref not in refs: # No local branch for this remote, so create one and point # it at the commit id of the remote ref - self.repo.create_reference(local_ref, oid) + self.repo.create_reference(local_ref, pygit2_id) try: target_sha = self.peel(self.repo.lookup_reference(remote_ref)).hex @@ -1749,7 +1749,8 @@ def _perform_checkout(checkout_ref, branch=True): # cachedir). head_ref = local_head.target # If head_ref is not a string, it will point to a - # pygit2.Oid object and we are in detached HEAD mode. + # pygit2.id object (oid is deprecated and removed) and + # we are in detached HEAD mode. # Therefore, there is no need to add a local reference. If # head_ref == local_ref, then the local reference for HEAD # in refs/heads/ already exists and again, no need to add. @@ -1918,10 +1919,10 @@ def _traverse(tree, blobs, prefix): the empty directories within it in the "blobs" list """ for entry in iter(tree): - if entry.oid not in self.repo: + if entry.id not in self.repo: # Entry is a submodule, skip it continue - blob = self.repo[entry.oid] + blob = self.repo[entry.id] if not isinstance(blob, pygit2.Tree): continue blobs.append( @@ -1940,8 +1941,8 @@ def _traverse(tree, blobs, prefix): return ret if self.root(tgt_env): try: - oid = tree[self.root(tgt_env)].oid - tree = self.repo[oid] + pygit2_id = tree[self.root(tgt_env)].id + tree = self.repo[pygit2_id] except KeyError: return ret if not isinstance(tree, pygit2.Tree): @@ -2056,17 +2057,17 @@ def _traverse(tree, blobs, prefix): the file paths and symlink info in the "blobs" dict """ for entry in iter(tree): - if entry.oid not in self.repo: + if entry.id not in self.repo: # Entry is a submodule, skip it continue - obj = self.repo[entry.oid] + obj = self.repo[entry.id] if isinstance(obj, pygit2.Blob): repo_path = salt.utils.path.join( prefix, entry.name, use_posixpath=True ) blobs.setdefault("files", []).append(repo_path) if stat.S_ISLNK(tree[entry.name].filemode): - link_tgt = self.repo[tree[entry.name].oid].data + link_tgt = self.repo[tree[entry.name].id].data blobs.setdefault("symlinks", {})[repo_path] = link_tgt elif isinstance(obj, pygit2.Tree): _traverse( @@ -2085,8 +2086,8 @@ def _traverse(tree, blobs, prefix): try: # This might need to be changed to account for a root that # spans more than one directory - oid = tree[self.root(tgt_env)].oid - tree = self.repo[oid] + pygit2_id = tree[self.root(tgt_env)].id + tree = self.repo[pygit2_id] except KeyError: return files, symlinks if not isinstance(tree, pygit2.Tree): @@ -2130,12 +2131,12 @@ def find_file(self, path, tgt_env): # path's object ID will be the target of the symlink. Follow # the symlink and set path to the location indicated # in the blob data. - link_tgt = self.repo[entry.oid].data + link_tgt = self.repo[entry.id].data path = salt.utils.path.join( os.path.dirname(path), link_tgt, use_posixpath=True ) else: - blob = self.repo[entry.oid] + blob = self.repo[entry.id] if isinstance(blob, pygit2.Tree): # Path is a directory, not a file. blob = None From 94973ee85d766d7e98d02d89f4c81e59b36cb716 Mon Sep 17 00:00:00 2001 From: Marek Czernek Date: Thu, 29 Aug 2024 10:01:12 +0200 Subject: [PATCH 04/11] Join masters if it is a list (#671) Co-authored-by: Twangboy --- changelog/64170.fixed.md | 2 + salt/utils/cloud.py | 10 +++++ tests/pytests/unit/utils/test_cloud.py | 52 ++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 changelog/64170.fixed.md diff --git a/changelog/64170.fixed.md b/changelog/64170.fixed.md new file mode 100644 index 0000000000..1d20355bf1 --- /dev/null +++ b/changelog/64170.fixed.md @@ -0,0 +1,2 @@ +Fixed issue in salt-cloud so that multiple masters specified in the cloud +are written to the minion config properly diff --git a/salt/utils/cloud.py b/salt/utils/cloud.py index b7208dc4a6..a084313059 100644 --- a/salt/utils/cloud.py +++ b/salt/utils/cloud.py @@ -1202,6 +1202,16 @@ def wait_for_passwd( time.sleep(trysleep) +def _format_master_param(master): + """ + If the master is a list, we need to convert it to a comma delimited string + Otherwise, we just return master + """ + if isinstance(master, list): + return ",".join(master) + return master + + def deploy_windows( host, port=445, diff --git a/tests/pytests/unit/utils/test_cloud.py b/tests/pytests/unit/utils/test_cloud.py index 550b63c974..db9d258d39 100644 --- a/tests/pytests/unit/utils/test_cloud.py +++ b/tests/pytests/unit/utils/test_cloud.py @@ -605,3 +605,55 @@ def test_deploy_script_ssh_timeout(): ssh_kwargs = root_cmd.call_args.kwargs assert "ssh_timeout" in ssh_kwargs assert ssh_kwargs["ssh_timeout"] == 34 + + +@pytest.mark.parametrize( + "master,expected", + [ + (None, None), + ("single_master", "single_master"), + (["master1", "master2", "master3"], "master1,master2,master3"), + ], +) +def test__format_master_param(master, expected): + result = cloud._format_master_param(master) + assert result == expected + + +@pytest.mark.skip_unless_on_windows(reason="Only applicable for Windows.") +@pytest.mark.parametrize( + "master,expected", + [ + (None, None), + ("single_master", "single_master"), + (["master1", "master2", "master3"], "master1,master2,master3"), + ], +) +def test_deploy_windows_master(master, expected): + """ + Test deploy_windows with master parameter + """ + mock_true = MagicMock(return_value=True) + mock_tuple = MagicMock(return_value=(0, 0, 0)) + with patch("salt.utils.smb.get_conn", MagicMock()), patch( + "salt.utils.smb.mkdirs", MagicMock() + ), patch("salt.utils.smb.put_file", MagicMock()), patch( + "salt.utils.smb.delete_file", MagicMock() + ), patch( + "salt.utils.smb.delete_directory", MagicMock() + ), patch( + "time.sleep", MagicMock() + ), patch.object( + cloud, "wait_for_port", mock_true + ), patch.object( + cloud, "fire_event", MagicMock() + ), patch.object( + cloud, "wait_for_psexecsvc", mock_true + ), patch.object( + cloud, "run_psexec_command", mock_tuple + ) as mock: + cloud.deploy_windows(host="test", win_installer="install.exe", master=master) + expected_cmd = "c:\\salttemp\\install.exe" + expected_args = "/S /master={} /minion-name=None".format(expected) + assert mock.call_args_list[0].args[0] == expected_cmd + assert mock.call_args_list[0].args[1] == expected_args From ff789d88541954e4fc1678dff728bc6a3ea7472e Mon Sep 17 00:00:00 2001 From: Victor Zhestkov Date: Fri, 30 Aug 2024 14:30:27 +0200 Subject: [PATCH 05/11] Remove redundant run_func from salt.master.MWorker._handle_aes * New request context * Fix docs * Remove redundant run_func from salt.master.MWorker._handle_aes * Get rid of run_func in salt.Minion._target --------- Co-authored-by: Daniel A. Wozniak --- doc/topics/releases/3007.0.rst | 0 salt/_logging/impl.py | 15 ++++++--- salt/master.py | 12 ++----- salt/minion.py | 10 ++---- salt/utils/ctx.py | 60 +++++++++++----------------------- 5 files changed, 34 insertions(+), 63 deletions(-) create mode 100644 doc/topics/releases/3007.0.rst diff --git a/doc/topics/releases/3007.0.rst b/doc/topics/releases/3007.0.rst new file mode 100644 index 0000000000..e69de29bb2 diff --git a/salt/_logging/impl.py b/salt/_logging/impl.py index 1d71cb8be8..4d1ebd2495 100644 --- a/salt/_logging/impl.py +++ b/salt/_logging/impl.py @@ -26,6 +26,8 @@ QUIET = logging.QUIET = 1000 import salt.defaults.exitcodes # isort:skip pylint: disable=unused-import +import salt.utils.ctx + from salt._logging.handlers import DeferredStreamHandler # isort:skip from salt._logging.handlers import RotatingFileHandler # isort:skip from salt._logging.handlers import StreamHandler # isort:skip @@ -33,7 +35,6 @@ from salt._logging.handlers import WatchedFileHandler # isort:skip from salt._logging.mixins import LoggingMixinMeta # isort:skip from salt.exceptions import LoggingRuntimeError # isort:skip -from salt.utils.ctx import RequestContext # isort:skip from salt.utils.immutabletypes import freeze, ImmutableDict # isort:skip from salt.utils.textformat import TextFormat # isort:skip @@ -242,10 +243,14 @@ def _log( if extra is None: extra = {} - # pylint: disable=no-member - current_jid = RequestContext.current.get("data", {}).get("jid", None) - log_fmt_jid = RequestContext.current.get("opts", {}).get("log_fmt_jid", None) - # pylint: enable=no-member + current_jid = ( + salt.utils.ctx.get_request_context().get("data", {}).get("jid", None) + ) + log_fmt_jid = ( + salt.utils.ctx.get_request_context() + .get("opts", {}) + .get("log_fmt_jid", None) + ) if current_jid is not None: extra["jid"] = current_jid diff --git a/salt/master.py b/salt/master.py index d7182d10b5..49cfb68860 100644 --- a/salt/master.py +++ b/salt/master.py @@ -38,6 +38,7 @@ import salt.utils.args import salt.utils.atomicfile import salt.utils.crypt +import salt.utils.ctx import salt.utils.event import salt.utils.files import salt.utils.gitfs @@ -58,10 +59,8 @@ from salt.cli.batch_async import BatchAsync, batch_async_required from salt.config import DEFAULT_INTERVAL from salt.defaults import DEFAULT_TARGET_DELIM -from salt.ext.tornado.stack_context import StackContext from salt.transport import TRANSPORTS from salt.utils.channel import iter_transport_opts -from salt.utils.ctx import RequestContext from salt.utils.debug import ( enable_sigusr1_handler, enable_sigusr2_handler, @@ -1108,13 +1107,8 @@ def _handle_aes(self, data): start = time.time() self.stats[cmd]["runs"] += 1 - def run_func(data): - return self.aes_funcs.run_func(data["cmd"], data) - - with StackContext( - functools.partial(RequestContext, {"data": data, "opts": self.opts}) - ): - ret = run_func(data) + with salt.utils.ctx.request_context({"data": data, "opts": self.opts}): + ret = self.aes_funcs.run_func(data["cmd"], data) if self.opts["master_stats"]: self._post_stats(start, cmd) diff --git a/salt/minion.py b/salt/minion.py index 2ccd0cd5a9..e21a017cfd 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -39,6 +39,7 @@ import salt.utils.args import salt.utils.context import salt.utils.crypt +import salt.utils.ctx import salt.utils.data import salt.utils.dictdiffer import salt.utils.dictupdate @@ -70,7 +71,6 @@ SaltSystemExit, ) from salt.template import SLS_ENCODING -from salt.utils.ctx import RequestContext from salt.utils.debug import enable_sigusr1_handler from salt.utils.event import tagify from salt.utils.network import parse_host_port @@ -1805,18 +1805,12 @@ def _target(cls, minion_instance, opts, data, connected): uid = salt.utils.user.get_uid(user=opts.get("user", None)) minion_instance.proc_dir = get_proc_dir(opts["cachedir"], uid=uid) - def run_func(minion_instance, opts, data): + with salt.utils.ctx.request_context({"data": data, "opts": opts}): if isinstance(data["fun"], tuple) or isinstance(data["fun"], list): return Minion._thread_multi_return(minion_instance, opts, data) else: return Minion._thread_return(minion_instance, opts, data) - with salt.ext.tornado.stack_context.StackContext( - functools.partial(RequestContext, {"data": data, "opts": opts}) - ): - with salt.ext.tornado.stack_context.StackContext(minion_instance.ctx): - run_func(minion_instance, opts, data) - def _execute_job_function( self, function_name, function_args, executors, opts, data ): diff --git a/salt/utils/ctx.py b/salt/utils/ctx.py index a9c0931bd8..2f4b5b4c9b 100644 --- a/salt/utils/ctx.py +++ b/salt/utils/ctx.py @@ -1,49 +1,27 @@ -import threading +import contextlib +try: + # Try the stdlib C extension first + import _contextvars as contextvars +except ImportError: + # Py<3.7 + import contextvars -class ClassProperty(property): - """ - Use a classmethod as a property - http://stackoverflow.com/a/1383402/1258307 - """ +DEFAULT_CTX_VAR = "request_ctxvar" +request_ctxvar = contextvars.ContextVar(DEFAULT_CTX_VAR) - def __get__(self, cls, owner): - return self.fget.__get__(None, owner)() # pylint: disable=no-member - -class RequestContext: +@contextlib.contextmanager +def request_context(data): """ - A context manager that saves some per-thread state globally. - Intended for use with Tornado's StackContext. - https://gist.github.com/simon-weber/7755289 - Simply import this class into any module and access the current request handler by this - class's class method property 'current'. If it returns None, there's no active request. - .. code:: python - from raas.utils.ctx import RequestContext - current_request_handler = RequestContext.current + A context manager that sets and un-sets the loader context """ + tok = request_ctxvar.set(data) + try: + yield + finally: + request_ctxvar.reset(tok) - _state = threading.local() - _state.current_request = {} - - def __init__(self, current_request): - self._current_request = current_request - - @ClassProperty - @classmethod - def current(cls): - if not hasattr(cls._state, "current_request"): - return {} - return cls._state.current_request - - def __enter__(self): - self._prev_request = self.__class__.current - self.__class__._state.current_request = self._current_request - - def __exit__(self, *exc): - self.__class__._state.current_request = self._prev_request - del self._prev_request - return False - def __call__(self): - return self +def get_request_context(): + return request_ctxvar.get({}) From 4e226426d0897f2d9dc64891ced78487b181d40e Mon Sep 17 00:00:00 2001 From: Victor Zhestkov Date: Fri, 30 Aug 2024 14:33:51 +0200 Subject: [PATCH 06/11] Improve error handling with different OpenSSL versions * Make error checking of x509 more flexible for most recent cryptography and openSSL versions * Add test for different exception value on loading private key * Add fix for test_privkey_new_with_prereq on old OpenSSL --- salt/utils/x509.py | 3 +- .../pytests/functional/states/test_x509_v2.py | 29 +++++++++++++++++++ .../integration/states/test_x509_v2.py | 7 +++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/salt/utils/x509.py b/salt/utils/x509.py index 5b2ae15882..f9fdca64d9 100644 --- a/salt/utils/x509.py +++ b/salt/utils/x509.py @@ -695,7 +695,8 @@ def load_privkey(pk, passphrase=None, get_encoding=False): return pk, "pem", None return pk except ValueError as err: - if "Bad decrypt" in str(err): + str_err = str(err) + if "Bad decrypt" in str_err or "Could not deserialize key data" in str_err: raise SaltInvocationError( "Bad decrypt - is the password correct?" ) from err diff --git a/tests/pytests/functional/states/test_x509_v2.py b/tests/pytests/functional/states/test_x509_v2.py index 929be014cd..47a1c555f8 100644 --- a/tests/pytests/functional/states/test_x509_v2.py +++ b/tests/pytests/functional/states/test_x509_v2.py @@ -3,6 +3,8 @@ import pytest +from tests.support.mock import patch + try: import cryptography import cryptography.x509 as cx509 @@ -2826,3 +2828,30 @@ def _get_privkey(pk, encoding="pem", passphrase=None): pk = base64.b64decode(pk) return pkcs12.load_pkcs12(pk, passphrase).key raise ValueError("Need correct encoding") + + +@pytest.mark.usefixtures("existing_pk") +@pytest.mark.parametrize("existing_pk", [{"passphrase": "password"}], indirect=True) +def test_exceptions_on_calling_load_pem_private_key(x509, pk_args): + pk_args["passphrase"] = "hunter1" + pk_args["overwrite"] = True + + with patch( + "cryptography.hazmat.primitives.serialization.load_pem_private_key", + side_effect=ValueError("Bad decrypt. Incorrect password?"), + ): + ret = x509.private_key_managed(**pk_args) + _assert_pk_basic(ret, "rsa", passphrase="hunter1") + + with patch( + "cryptography.hazmat.primitives.serialization.load_pem_private_key", + side_effect=ValueError( + "Could not deserialize key data. The data may be in an incorrect format, " + "the provided password may be incorrect, " + "it may be encrypted with an unsupported algorithm, " + "or it may be an unsupported key type " + "(e.g. EC curves with explicit parameters)." + ), + ): + ret = x509.private_key_managed(**pk_args) + _assert_pk_basic(ret, "rsa", passphrase="hunter1") diff --git a/tests/pytests/integration/states/test_x509_v2.py b/tests/pytests/integration/states/test_x509_v2.py index 4f94341295..ad8d904c92 100644 --- a/tests/pytests/integration/states/test_x509_v2.py +++ b/tests/pytests/integration/states/test_x509_v2.py @@ -195,6 +195,13 @@ def privkey_new(x509_salt_master, tmp_path, ca_minion_id, x509_salt_call_cli): """ with x509_salt_master.state_tree.base.temp_file("manage_cert.sls", state): ret = x509_salt_call_cli.run("state.apply", "manage_cert") + if ( + ret.returncode == 1 + and "NotImplementedError: ECDSA keys with unnamed curves" in ret.stdout + ): + pytest.skip( + "The version of OpenSSL doesn't support ECDSA keys with unnamed curves" + ) assert ret.returncode == 0 assert ret.data[next(iter(ret.data))]["changes"] assert (tmp_path / "priv.key").exists() From b2faa019f0f5aa03b03e6c54c9aa60b7f6aa4f91 Mon Sep 17 00:00:00 2001 From: Victor Zhestkov Date: Fri, 30 Aug 2024 14:35:33 +0200 Subject: [PATCH 07/11] Avoid crash on wrong output of systemctl version (bsc#1229539) * Better handling output of systemctl --version * Add more cases to test grains.core._systemd --- salt/grains/core.py | 27 +++++++- tests/pytests/unit/grains/test_core.py | 89 ++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/salt/grains/core.py b/salt/grains/core.py index 4454c303fe..98bbd3868e 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -2432,10 +2432,31 @@ def _systemd(): """ Return the systemd grain """ - systemd_info = __salt__["cmd.run"]("systemctl --version").splitlines() + systemd_version = "UNDEFINED" + systemd_features = "" + try: + systemd_output = __salt__["cmd.run_all"]("systemctl --version") + except Exception: # pylint: disable=broad-except + log.error("Exception while executing `systemctl --version`", exc_info=True) + return { + "version": systemd_version, + "features": systemd_features, + } + if systemd_output.get("retcode") == 0: + systemd_info = systemd_output.get("stdout", "").splitlines() + try: + if systemd_info[0].startswith("systemd "): + systemd_version = systemd_info[0].split()[1] + systemd_features = systemd_info[1] + except IndexError: + pass + if systemd_version == "UNDEFINED" or systemd_features == "": + log.error( + "Unexpected output returned by `systemctl --version`: %s", systemd_output + ) return { - "version": systemd_info[0].split()[1], - "features": systemd_info[1], + "version": systemd_version, + "features": systemd_features, } diff --git a/tests/pytests/unit/grains/test_core.py b/tests/pytests/unit/grains/test_core.py index 36545287b9..b64b8c4bf8 100644 --- a/tests/pytests/unit/grains/test_core.py +++ b/tests/pytests/unit/grains/test_core.py @@ -3593,3 +3593,92 @@ def _mock_is_file(filename): assert virtual_grains["virtual"] == "Nitro" assert virtual_grains["virtual_subtype"] == "Amazon EC2" + + +@pytest.mark.parametrize( + "systemd_data,expected", + ( + ( + { + "pid": 1234, + "retcode": 0, + "stdout": "systemd 254 (254.3-1)\n+PAM +AUDIT -SELINUX -APPARMOR -IMA +SMACK " + "+SECCOMP +GCRYPT +GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS " + "+FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 " + "-PWQUALITY +P11KIT -QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD " + "+BPF_FRAMEWORK +XKBCOMMON +UTMP -SYSVINIT default-hierarchy=unified", + "stderr": "", + }, + { + "version": "254", + "features": "+PAM +AUDIT -SELINUX -APPARMOR -IMA +SMACK +SECCOMP +GCRYPT +GNUTLS +OPENSSL " + "+ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP " + "+LIBFDISK +PCRE2 -PWQUALITY +P11KIT -QRENCODE +TPM2 +BZIP2 +LZ4 +XZ " + "+ZLIB +ZSTD +BPF_FRAMEWORK +XKBCOMMON +UTMP -SYSVINIT default-hierarchy=unified", + }, + ), + ( + { + "pid": 2345, + "retcode": 1, + "stdout": "", + "stderr": "some garbage in the output", + }, + { + "version": "UNDEFINED", + "features": "", + }, + ), + ( + { + "pid": 3456, + "retcode": 0, + "stdout": "unexpected stdout\none more line", + "stderr": "", + }, + { + "version": "UNDEFINED", + "features": "", + }, + ), + ( + { + "pid": 4567, + "retcode": 0, + "stdout": "", + "stderr": "", + }, + { + "version": "UNDEFINED", + "features": "", + }, + ), + ( + Exception("Some exception on calling `systemctl --version`"), + { + "version": "UNDEFINED", + "features": "", + }, + ), + ), +) +def test__systemd(systemd_data, expected): + """ + test _systemd + """ + + def mock_run_all_systemd(_): + if isinstance(systemd_data, Exception): + raise systemd_data + return systemd_data + + with patch.dict( + core.__salt__, + { + "cmd.run_all": mock_run_all_systemd, + }, + ): + ret = core._systemd() + assert "version" in ret + assert "features" in ret + assert ret == expected From 5567f2bd51d66b7797c986cf64f79f71ca57eb63 Mon Sep 17 00:00:00 2001 From: Marek Czernek Date: Wed, 4 Sep 2024 13:10:44 +0200 Subject: [PATCH 08/11] Fix test_system flaky setup_teardown fn --- tests/pytests/functional/modules/test_system.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/pytests/functional/modules/test_system.py b/tests/pytests/functional/modules/test_system.py index 2cd03a3a3e..270aafbe2c 100644 --- a/tests/pytests/functional/modules/test_system.py +++ b/tests/pytests/functional/modules/test_system.py @@ -4,10 +4,12 @@ import signal import subprocess import textwrap +import time import pytest import salt.utils.files +from salt.exceptions import CommandExecutionError INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" @@ -80,7 +82,13 @@ def setup_teardown_vars(file, service, system): file.remove("/etc/machine-info") if _systemd_timesyncd_available_: - res = service.start("systemd-timesyncd") + try: + res = service.start("systemd-timesyncd") + except CommandExecutionError: + # We possibly did too many restarts in too short time + # Wait 10s (default systemd timeout) and try again + time.sleep(10) + res = service.start("systemd-timesyncd") assert res From a6d27a6f50bbbea539ec64bf96a5b9755e32bf69 Mon Sep 17 00:00:00 2001 From: Marek Czernek Date: Wed, 4 Sep 2024 13:11:05 +0200 Subject: [PATCH 09/11] Fix test_debian to work in our infrastructure (#676) --- tests/pytests/functional/states/pkgrepo/test_debian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pytests/functional/states/pkgrepo/test_debian.py b/tests/pytests/functional/states/pkgrepo/test_debian.py index 87716706d5..7bda100b63 100644 --- a/tests/pytests/functional/states/pkgrepo/test_debian.py +++ b/tests/pytests/functional/states/pkgrepo/test_debian.py @@ -205,7 +205,7 @@ def ubuntu_state_tree(system_aptsources, state_tree, grains): - dist: {{ codename }} - file: /etc/apt/sources.list.d/firefox-beta.list - keyid: CE49EC21 - - keyserver: keyserver.ubuntu.com + - keyserver: hkp://keyserver.ubuntu.com:80 {%- endif %} {%- if backports %}{%- do ubuntu_repos.append('kubuntu-ppa') %} From d5f3df07783d8aaf3a897ca2f209e662973b930c Mon Sep 17 00:00:00 2001 From: Marek Czernek Date: Wed, 4 Sep 2024 13:11:33 +0200 Subject: [PATCH 10/11] Fix deprecated code (#677) Due to SUSE's extended support policy, we won't remove code from Salt until next major release. --- salt/_logging/handlers.py | 6 +++--- salt/log/__init__.py | 2 +- salt/log/handlers/__init__.py | 2 +- salt/log/mixins.py | 2 +- salt/log/setup.py | 4 ++-- salt/modules/aptpkg.py | 2 +- salt/modules/cassandra_mod.py | 2 +- salt/returners/cassandra_return.py | 2 +- salt/returners/django_return.py | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/salt/_logging/handlers.py b/salt/_logging/handlers.py index f4b0b6fec3..5a1a161313 100644 --- a/salt/_logging/handlers.py +++ b/salt/_logging/handlers.py @@ -36,7 +36,7 @@ class TemporaryLoggingHandler(logging.NullHandler): def __init__(self, level=logging.NOTSET, max_queue_size=10000): warn_until_date( - "20240101", + "20260101", "Please stop using '{name}.TemporaryLoggingHandler'. " "'{name}.TemporaryLoggingHandler' will go away after " "{{date}}.".format(name=__name__), @@ -225,7 +225,7 @@ class QueueHandler( def __init__(self, queue): # pylint: disable=useless-super-delegation super().__init__(queue) warn_until_date( - "20240101", + "20260101", "Please stop using '{name}.QueueHandler' and instead " "use 'logging.handlers.QueueHandler'. " "'{name}.QueueHandler' will go away after " @@ -283,7 +283,7 @@ class QueueHandler( def __init__(self, queue): # pylint: disable=useless-super-delegation super().__init__(queue) warn_until_date( - "20240101", + "20260101", "Please stop using '{name}.QueueHandler' and instead " "use 'logging.handlers.QueueHandler'. " "'{name}.QueueHandler' will go away after " diff --git a/salt/log/__init__.py b/salt/log/__init__.py index 3458474f2c..69bfa8ed15 100644 --- a/salt/log/__init__.py +++ b/salt/log/__init__.py @@ -24,7 +24,7 @@ from salt.utils.versions import warn_until_date warn_until_date( - "20240101", + "20260101", "Please stop using '{name}' and instead use 'salt._logging'. " "'{name}' will go away after {{date}}.".format(name=__name__), stacklevel=3, diff --git a/salt/log/handlers/__init__.py b/salt/log/handlers/__init__.py index 8bc740e20f..55cf10cdb7 100644 --- a/salt/log/handlers/__init__.py +++ b/salt/log/handlers/__init__.py @@ -12,7 +12,7 @@ from salt.utils.versions import warn_until_date warn_until_date( - "20240101", + "20260101", "Please stop using '{name}' and instead use 'salt._logging.handlers'. " "'{name}' will go away after {{date}}.".format(name=__name__), ) diff --git a/salt/log/mixins.py b/salt/log/mixins.py index 6619b56419..65f5ed7f78 100644 --- a/salt/log/mixins.py +++ b/salt/log/mixins.py @@ -11,7 +11,7 @@ # pylint: enable=unused-import warn_until_date( - "20240101", + "20260101", "Please stop using '{name}' and instead use 'salt._logging.mixins'. " "'{name}' will go away after {{date}}.".format(name=__name__), ) diff --git a/salt/log/setup.py b/salt/log/setup.py index 74bd7bbd3e..f4c80b0f28 100644 --- a/salt/log/setup.py +++ b/salt/log/setup.py @@ -21,7 +21,7 @@ from salt.utils.versions import warn_until_date warn_until_date( - "20240101", + "20260101", "Please stop using '{name}' and instead use 'salt._logging'. " "'{name}' will go away after {{date}}. Do note however that " "'salt._logging' is now considered a non public implementation " @@ -34,7 +34,7 @@ def _deprecated_warning(func): @wraps(func) def wrapper(*args, **kwargs): warn_until_date( - "20240101", + "20260101", "Please stop using 'salt.log.setup.{name}()' as it no longer does anything and " "will go away after {{date}}.".format(name=func.__qualname__), stacklevel=4, diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py index ad5450c415..cd40aea54f 100644 --- a/salt/modules/aptpkg.py +++ b/salt/modules/aptpkg.py @@ -3128,7 +3128,7 @@ def expand_repo_def(**kwargs): NOT USABLE IN THE CLI """ warn_until_date( - "20250101", + "20260101", "The pkg.expand_repo_def function is deprecated and set for removal " "after {date}. This is only unsed internally by the apt pkg state " "module. If that's not the case, please file an new issue requesting " diff --git a/salt/modules/cassandra_mod.py b/salt/modules/cassandra_mod.py index 029fd08fb9..db9c882192 100644 --- a/salt/modules/cassandra_mod.py +++ b/salt/modules/cassandra_mod.py @@ -45,7 +45,7 @@ def __virtual__(): ) warn_until_date( - "20240101", + "20260101", "The cassandra returner is broken and deprecated, and will be removed" " after {date}. Use the cassandra_cql returner instead", ) diff --git a/salt/returners/cassandra_return.py b/salt/returners/cassandra_return.py index ac01a4e46c..5fcc00ee8c 100644 --- a/salt/returners/cassandra_return.py +++ b/salt/returners/cassandra_return.py @@ -53,7 +53,7 @@ def __virtual__(): if not HAS_PYCASSA: return False, "Could not import cassandra returner; pycassa is not installed." warn_until_date( - "20240101", + "20260101", "The cassandra returner is broken and deprecated, and will be removed" " after {date}. Use the cassandra_cql returner instead", ) diff --git a/salt/returners/django_return.py b/salt/returners/django_return.py index 3638687555..474653f383 100644 --- a/salt/returners/django_return.py +++ b/salt/returners/django_return.py @@ -57,7 +57,7 @@ def returner_callback(sender, ret): def __virtual__(): warn_until_date( - "20240101", + "20260101", "The django returner is broken and deprecated, and will be removed" " after {date}.", ) From d933c8f0795fdada84a01a2cc754586fa720993d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= Date: Tue, 10 Sep 2024 13:46:09 +0100 Subject: [PATCH 11/11] Fix the SELinux context for Salt Minion service (bsc#1219041) (#670) Currently there are no SELinux policies for Salt. By default, the Salt Minion service runs as 'unconfined_service_t' when SELinux is enabled. This works fine in most cases but generates a problem then trying to transition to an 'unconfined_t', i.a. when running "cmd.run .... runas=nobody". Then we see this denied in audit logs: type=AVC msg=audit(1722870119.142:718): avc: denied { transition } for pid=3421 comm="su" path="/usr/bin/bash" dev="vda3" ino=28565 scontext=system_u:system_r:unconfined_service_t:s0 tcontext=unconfined_u:unconfined_r:unconfined_t:s0 tclass=process permissive=0 (This happens for cmd.run at the time of trying to invoke a shell as a different user to gather the environment variables from this particular user) Fixing the SELinuxContext for the Salt Minion systemd service to a general 'unconfined_t' workarounds this situation. SELinuxContext attribute was added on systemd version 209. --- pkg/common/salt-minion.service | 1 + pkg/old/deb/salt-minion.service | 1 + pkg/old/suse/salt-minion.service | 1 + pkg/old/suse/salt-minion.service.rhel7 | 1 + 4 files changed, 4 insertions(+) diff --git a/pkg/common/salt-minion.service b/pkg/common/salt-minion.service index 69aff18c58..696d0263c3 100644 --- a/pkg/common/salt-minion.service +++ b/pkg/common/salt-minion.service @@ -9,6 +9,7 @@ Type=notify NotifyAccess=all LimitNOFILE=8192 ExecStart=/usr/bin/salt-minion +SELinuxContext=system_u:system_r:unconfined_t:s0 [Install] WantedBy=multi-user.target diff --git a/pkg/old/deb/salt-minion.service b/pkg/old/deb/salt-minion.service index 7e6cf14654..b0ad82c133 100644 --- a/pkg/old/deb/salt-minion.service +++ b/pkg/old/deb/salt-minion.service @@ -8,6 +8,7 @@ KillMode=process NotifyAccess=all LimitNOFILE=8192 ExecStart=/usr/bin/salt-minion +SELinuxContext=system_u:system_r:unconfined_t:s0 [Install] WantedBy=multi-user.target diff --git a/pkg/old/suse/salt-minion.service b/pkg/old/suse/salt-minion.service index 12f28314cb..b99ef06352 100644 --- a/pkg/old/suse/salt-minion.service +++ b/pkg/old/suse/salt-minion.service @@ -10,6 +10,7 @@ ExecStart=/usr/bin/salt-minion KillMode=process Restart=on-failure RestartSec=15 +SELinuxContext=system_u:system_r:unconfined_t:s0 [Install] WantedBy=multi-user.target diff --git a/pkg/old/suse/salt-minion.service.rhel7 b/pkg/old/suse/salt-minion.service.rhel7 index 6917267714..92cc66d32f 100644 --- a/pkg/old/suse/salt-minion.service.rhel7 +++ b/pkg/old/suse/salt-minion.service.rhel7 @@ -9,6 +9,7 @@ ExecStart=/usr/bin/salt-minion KillMode=process Restart=on-failure RestartSec=15 +SELinuxContext=system_u:system_r:unconfined_t:s0 [Install] WantedBy=multi-user.target