import logging import os import pathlib import shutil import subprocess import sys import pytest import yaml from pytestskipmarkers.utils import platform from saltfactories.utils import random_string from saltfactories.utils.tempfiles import SaltPillarTree, SaltStateTree import salt.config from tests.support.helpers import ( CODE_DIR, TESTS_DIR, ApiRequest, SaltMaster, SaltMasterWindows, SaltPkgInstall, TestUser, ) from tests.support.sminion import create_sminion log = logging.getLogger(__name__) @pytest.fixture(scope="session") def version(install_salt): """ get version number from artifact """ return install_salt.version @pytest.fixture(scope="session") def sminion(): return create_sminion() @pytest.fixture(scope="session") def grains(sminion): return sminion.opts["grains"].copy() @pytest.fixture(scope="session", autouse=True) def _system_up_to_date( grains, shell, ): if grains["os_family"] == "Debian": ret = shell.run("apt", "update") assert ret.returncode == 0 env = os.environ.copy() env["DEBIAN_FRONTEND"] = "noninteractive" ret = shell.run( "apt", "upgrade", "-y", "-o", "DPkg::Options::=--force-confdef", "-o", "DPkg::Options::=--force-confold", env=env, ) assert ret.returncode == 0 elif grains["os_family"] == "Redhat": ret = shell.run("yum", "update", "-y") assert ret.returncode == 0 def pytest_addoption(parser): """ register argparse-style options and ini-style config values. """ test_selection_group = parser.getgroup("Tests Runtime Selection") test_selection_group.addoption( "--pkg-system-service", default=False, action="store_true", help="Run the daemons as system services", ) test_selection_group.addoption( "--upgrade", default=False, action="store_true", help="Install previous version and then upgrade then run tests", ) test_selection_group.addoption( "--downgrade", default=False, action="store_true", help="Install current version and then downgrade to the previous version and run tests", ) test_selection_group.addoption( "--no-install", default=False, action="store_true", help="Do not install salt and use a previous install Salt package", ) test_selection_group.addoption( "--no-uninstall", default=False, action="store_true", help="Do not uninstall salt packages after test run is complete", ) test_selection_group.addoption( "--classic", default=False, action="store_true", help="Test an upgrade from the classic packages.", ) test_selection_group.addoption( "--prev-version", action="store", help="Test an upgrade from the version specified.", ) test_selection_group.addoption( "--use-prev-version", action="store_true", help="Tells the test suite to validate the version using the previous version (for downgrades)", ) test_selection_group.addoption( "--download-pkgs", default=False, action="store_true", help="Test package download tests", ) @pytest.hookimpl(tryfirst=True) def pytest_runtest_setup(item): """ Fixtures injection based on markers or test skips based on CLI arguments """ if ( str(item.fspath).startswith(str(pathlib.Path(__file__).parent / "download")) and item.config.getoption("--download-pkgs") is False ): raise pytest.skip.Exception( "The package download tests are disabled. Pass '--download-pkgs' to pytest " "to enable them.", _use_item_location=True, ) @pytest.fixture(scope="session") def salt_factories_root_dir(request, tmp_path_factory): root_dir = SaltPkgInstall.salt_factories_root_dir( request.config.getoption("--pkg-system-service") ) if root_dir is not None: yield root_dir else: if platform.is_darwin(): root_dir = pathlib.Path("/tmp/salt-tests-tmpdir") root_dir.mkdir(mode=0o777, parents=True, exist_ok=True) else: root_dir = tmp_path_factory.mktemp("salt-tests") try: yield root_dir finally: shutil.rmtree(str(root_dir), ignore_errors=True) @pytest.fixture(scope="session") def salt_factories_config(salt_factories_root_dir): return { "code_dir": CODE_DIR, "root_dir": salt_factories_root_dir, "system_service": True, } @pytest.fixture(scope="session") def install_salt(request, salt_factories_root_dir): with SaltPkgInstall( conf_dir=salt_factories_root_dir / "etc" / "salt", pkg_system_service=request.config.getoption("--pkg-system-service"), upgrade=request.config.getoption("--upgrade"), downgrade=request.config.getoption("--downgrade"), no_uninstall=request.config.getoption("--no-uninstall"), no_install=request.config.getoption("--no-install"), classic=request.config.getoption("--classic"), prev_version=request.config.getoption("--prev-version"), use_prev_version=request.config.getoption("--use-prev-version"), ) as fixture: yield fixture @pytest.fixture(scope="session") def salt_factories(salt_factories, salt_factories_root_dir): salt_factories.root_dir = salt_factories_root_dir return salt_factories @pytest.fixture(scope="session") def state_tree(): if platform.is_windows(): file_root = pathlib.Path("C:/salt/srv/salt") elif platform.is_darwin(): file_root = pathlib.Path("/opt/srv/salt") else: file_root = pathlib.Path("/srv/salt") envs = { "base": [ str(file_root), str(TESTS_DIR / "files"), ], } tree = SaltStateTree(envs=envs) test_sls_contents = """ test_foo: test.succeed_with_changes: - name: foo """ states_sls_contents = """ update: pkg.installed: - name: bash salt_dude: user.present: - name: dude - fullname: Salt Dude """ win_states_sls_contents = """ create_empty_file: file.managed: - name: C://salt/test/txt salt_dude: user.present: - name: dude - fullname: Salt Dude """ with tree.base.temp_file("test.sls", test_sls_contents), tree.base.temp_file( "states.sls", states_sls_contents ), tree.base.temp_file("win_states.sls", win_states_sls_contents): yield tree @pytest.fixture(scope="session") def pillar_tree(): """ Add pillar files """ if platform.is_windows(): pillar_root = pathlib.Path("C:/salt/srv/pillar") elif platform.is_darwin(): pillar_root = pathlib.Path("/opt/srv/pillar") else: pillar_root = pathlib.Path("/srv/pillar") pillar_root.mkdir(mode=0o777, parents=True, exist_ok=True) tree = SaltPillarTree( envs={ "base": [ str(pillar_root), ] }, ) top_file_contents = """ base: '*': - test """ test_file_contents = """ info: test """ with tree.base.temp_file("top.sls", top_file_contents), tree.base.temp_file( "test.sls", test_file_contents ): yield tree @pytest.fixture(scope="module") def sls(state_tree): """ Add an sls file """ test_sls_contents = """ test_foo: test.succeed_with_changes: - name: foo """ states_sls_contents = """ update: pkg.installed: - name: bash salt_dude: user.present: - name: dude - fullname: Salt Dude """ win_states_sls_contents = """ create_empty_file: file.managed: - name: C://salt/test/txt salt_dude: user.present: - name: dude - fullname: Salt Dude """ with state_tree.base.temp_file( "tests.sls", test_sls_contents ), state_tree.base.temp_file( "states.sls", states_sls_contents ), state_tree.base.temp_file( "win_states.sls", win_states_sls_contents ): yield @pytest.fixture(scope="session") def salt_master(salt_factories, install_salt, state_tree, pillar_tree): """ Start up a master """ start_timeout = None # Since the daemons are "packaged" with tiamat, the salt plugins provided # by salt-factories won't be discovered. Provide the required `*_dirs` on # the configuration so that they can still be used. config_defaults = { "engines_dirs": [ str(salt_factories.get_salt_engines_path()), ], "log_handlers_dirs": [ str(salt_factories.get_salt_log_handlers_path()), ], } if platform.is_darwin(): config_defaults["enable_fqdns_grains"] = False config_overrides = { "timeout": 30, "file_roots": state_tree.as_dict(), "pillar_roots": pillar_tree.as_dict(), "rest_cherrypy": {"port": 8000, "disable_ssl": True}, "netapi_enable_clients": ["local"], "external_auth": {"auto": {"saltdev": [".*"]}}, } test_user = False master_config = install_salt.config_path / "master" if master_config.exists(): with open(master_config) as fp: data = yaml.safe_load(fp) if data and "user" in data: test_user = True # We are testing a different user, so we need to test the system # configs, or else permissions will not be correct. config_overrides["user"] = data["user"] config_overrides["log_file"] = salt.config.DEFAULT_MASTER_OPTS.get( "log_file" ) config_overrides["root_dir"] = salt.config.DEFAULT_MASTER_OPTS.get( "root_dir" ) config_overrides["key_logfile"] = salt.config.DEFAULT_MASTER_OPTS.get( "key_logfile" ) config_overrides["pki_dir"] = salt.config.DEFAULT_MASTER_OPTS.get( "pki_dir" ) config_overrides["api_logfile"] = salt.config.DEFAULT_API_OPTS.get( "api_logfile" ) config_overrides["api_pidfile"] = salt.config.DEFAULT_API_OPTS.get( "api_pidfile" ) # verify files were set with correct owner/group verify_files = [ pathlib.Path("/etc", "salt", "pki", "master"), pathlib.Path("/etc", "salt", "master.d"), pathlib.Path("/var", "cache", "salt", "master"), ] for _file in verify_files: assert _file.owner() == "salt" assert _file.group() == "salt" master_script = False if platform.is_windows(): if install_salt.classic: master_script = True if install_salt.relenv: master_script = True elif not install_salt.upgrade: master_script = True if ( not install_salt.relenv and install_salt.use_prev_version and not install_salt.classic ): master_script = False if master_script: salt_factories.system_service = False scripts_dir = salt_factories.root_dir / "Scripts" scripts_dir.mkdir(exist_ok=True) salt_factories.scripts_dir = scripts_dir config_overrides["open_mode"] = True python_executable = install_salt.bin_dir / "Scripts" / "python.exe" if install_salt.classic: python_executable = install_salt.bin_dir / "python.exe" if install_salt.relenv: python_executable = install_salt.install_dir / "Scripts" / "python.exe" factory = salt_factories.salt_master_daemon( random_string("master-"), defaults=config_defaults, overrides=config_overrides, factory_class=SaltMasterWindows, salt_pkg_install=install_salt, python_executable=python_executable, ) salt_factories.system_service = True else: factory = salt_factories.salt_master_daemon( random_string("master-"), defaults=config_defaults, overrides=config_overrides, factory_class=SaltMaster, salt_pkg_install=install_salt, ) factory.after_terminate(pytest.helpers.remove_stale_master_key, factory) if test_user: # Salt factories calls salt.utils.verify.verify_env # which sets root perms on /etc/salt/pki/master since we are running # the test suite as root, but we want to run Salt master as salt # We ensure those permissions where set by the package earlier subprocess.run( [ "chown", "-R", "salt:salt", str(pathlib.Path("/etc", "salt", "pki", "master")), ] ) # The engines_dirs is created in .nox path. We need to set correct perms # for the user running the Salt Master subprocess.run(["chown", "-R", "salt:salt", str(CODE_DIR.parent / ".nox")]) file_roots = pathlib.Path("/srv/", "salt") pillar_roots = pathlib.Path("/srv/", "pillar") for _dir in [file_roots, pillar_roots]: subprocess.run(["chown", "-R", "salt:salt", str(_dir)]) with factory.started(start_timeout=start_timeout): yield factory @pytest.fixture(scope="session") def salt_minion(salt_factories, salt_master, install_salt): """ Start up a minion """ start_timeout = None minion_id = random_string("minion-") # Since the daemons are "packaged" with tiamat, the salt plugins provided # by salt-factories won't be discovered. Provide the required `*_dirs` on # the configuration so that they can still be used. config_defaults = { "engines_dirs": salt_master.config["engines_dirs"].copy(), "log_handlers_dirs": salt_master.config["log_handlers_dirs"].copy(), } if platform.is_darwin(): config_defaults["enable_fqdns_grains"] = False config_overrides = { "id": minion_id, "file_roots": salt_master.config["file_roots"].copy(), "pillar_roots": salt_master.config["pillar_roots"].copy(), } if platform.is_windows(): config_overrides[ "winrepo_dir" ] = rf"{salt_factories.root_dir}\srv\salt\win\repo" config_overrides[ "winrepo_dir_ng" ] = rf"{salt_factories.root_dir}\srv\salt\win\repo_ng" config_overrides["winrepo_source_dir"] = r"salt://win/repo_ng" factory = salt_master.salt_minion_daemon( minion_id, overrides=config_overrides, defaults=config_defaults, ) # Salt factories calls salt.utils.verify.verify_env # which sets root perms on /srv/salt and /srv/pillar since we are running # the test suite as root, but we want to run Salt master as salt if not platform.is_windows() and not platform.is_darwin(): file_roots = pathlib.Path("/srv/", "salt") pillar_roots = pathlib.Path("/srv/", "pillar") for _dir in [file_roots, pillar_roots]: subprocess.run(["chown", "-R", "salt:salt", str(_dir)]) factory.after_terminate( pytest.helpers.remove_stale_minion_key, salt_master, factory.id ) with factory.started(start_timeout=start_timeout): yield factory @pytest.fixture(scope="module") def salt_cli(salt_master): return salt_master.salt_cli() @pytest.fixture(scope="module") def salt_key_cli(salt_master): return salt_master.salt_key_cli() @pytest.fixture(scope="module") def salt_call_cli(salt_minion): return salt_minion.salt_call_cli() @pytest.fixture(scope="module") def test_account(salt_call_cli): with TestUser(salt_call_cli=salt_call_cli) as account: yield account @pytest.fixture(scope="module") def extras_pypath(): extras_dir = "extras-{}.{}".format(*sys.version_info) if platform.is_windows(): return pathlib.Path( os.getenv("ProgramFiles"), "Salt Project", "Salt", extras_dir ) elif platform.is_darwin(): return pathlib.Path("/opt", "salt", extras_dir) else: return pathlib.Path("/opt", "saltstack", "salt", extras_dir) @pytest.fixture(scope="module") def extras_pypath_bin(extras_pypath): return extras_pypath / "bin" @pytest.fixture(scope="module") def salt_api(salt_master, install_salt, extras_pypath): """ start up and configure salt_api """ shutil.rmtree(str(extras_pypath), ignore_errors=True) start_timeout = None factory = salt_master.salt_api_daemon() with factory.started(start_timeout=start_timeout): yield factory @pytest.fixture(scope="module") def api_request(test_account, salt_api): with ApiRequest(salt_api=salt_api, test_account=test_account) as session: yield session