diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53043863e0d..8cf48934338 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1067,6 +1067,62 @@ repos: - requirements/static/ci/invoke.in # <---- Invoke ----------------------------------------------------------------------------------------------------- + # <---- PKG ci requirements----------------------------------------------------------------------------------------- + - id: pip-tools-compile + alias: compile-ci-pkg-3.7-requirements + name: PKG tests CI Py3.7 Requirements + files: ^requirements/((base|zeromq|pytest)\.txt|static/(pkg/linux\.in|ci/((pkg|common)\.in|py3\.7/pkg\.txt)))$ + pass_filenames: false + args: + - -v + - --py-version=3.7 + - --include=requirements/base.txt + - --include=requirements/zeromq.txt + - --include=requirements/pytest.txt + - requirements/static/ci/pkgtests.in + + - id: pip-tools-compile + alias: compile-ci-pkg-3.8-requirements + name: PKG tests CI Py3.8 Requirements + files: ^requirements/((base|zeromq|pytest)\.txt|static/(pkg/linux\.in|ci/((pkg|common)\.in|py3\.7/pkg\.txt)))$ + pass_filenames: false + args: + - -v + - --py-version=3.8 + - --include=requirements/base.txt + - --include=requirements/zeromq.txt + - --include=requirements/pytest.txt + - requirements/static/ci/pkgtests.in + + - id: pip-tools-compile + alias: compile-ci-pkg-3.9-requirements + name: PKG tests CI Py3.9 Requirements + files: ^requirements/((base|zeromq|pytest)\.txt|static/(pkg/linux\.in|ci/((pkg|common)\.in|py3\.7/pkg\.txt)))$ + pass_filenames: false + args: + - -v + - --py-version=3.9 + - --include=requirements/base.txt + - --include=requirements/zeromq.txt + - --include=requirements/pytest.txt + - requirements/static/ci/pkgtests.in + + + - id: pip-tools-compile + alias: compile-ci-pkg-3.10-requirements + name: PKG tests CI Py3.10 Requirements + files: ^requirements/((base|zeromq|pytest)\.txt|static/(pkg/linux\.in|ci/((pkg|common)\.in|py3\.7/pkg\.txt)))$ + pass_filenames: false + args: + - -v + - --py-version=3.10 + - --include=requirements/base.txt + - --include=requirements/zeromq.txt + - --include=requirements/pytest.txt + - requirements/static/ci/pkgtests.in + + + # ----- Tools ----------------------------------------------------------------------------------------------------> - id: pip-tools-compile alias: compile-ci-tools-3.9-requirements diff --git a/noxfile.py b/noxfile.py index 629139a3cbd..e25834ef763 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1760,3 +1760,25 @@ def build(session): ] session.run("sha256sum", *packages, external=True) session.run("python", "-m", "twine", "check", "dist/*") + + +@nox.session(python=_PYTHON_VERSIONS, name="test-pkgs") +@nox.parametrize("coverage", [False, True]) +def test_pkgs(session, coverage): + """ + pytest pkg tests session + """ + pydir = _get_pydir(session) + # Install requirements + if _upgrade_pip_setuptools_and_wheel(session): + requirements_file = os.path.join( + "requirements", "static", "ci", _get_pydir(session), "pkgtests.txt" + ) + + install_command = ["--progress-bar=off", "-r", requirements_file] + session.install(*install_command, silent=PIP_INSTALL_SILENT) + + cmd_args = [ + "pkg/tests/", + ] + session.posargs + _pytest(session, coverage, cmd_args) diff --git a/pkg/tests/__init__.py b/pkg/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/tests/conftest.py b/pkg/tests/conftest.py new file mode 100644 index 00000000000..47d6ebd1acb --- /dev/null +++ b/pkg/tests/conftest.py @@ -0,0 +1,341 @@ +import logging +import pathlib +import re +import shutil + +import pytest +from pytestskipmarkers.utils import platform +from saltfactories.utils import random_string +from saltfactories.utils.tempfiles import SaltPillarTree, SaltStateTree + +from tests.support.helpers import ( + ARTIFACTS_DIR, + CODE_DIR, + TESTS_DIR, + ApiRequest, + SaltMaster, + SaltPkgInstall, + TestUser, +) + +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="session") +def version(): + """ + get version number from artifact + """ + _version = "" + for artifact in ARTIFACTS_DIR.glob("**/*.*"): + _version = re.search( + r"([0-9].*)(\-[0-9].fc|\-[0-9].el|\+ds|\-[0-9].am|\-[0-9]-[a-z]*-[a-z]*[0-9_]*.(tar.gz|zip|exe|pkg|rpm))", + artifact.name, + ) + if _version: + _version = _version.groups()[0].replace("_", "-").replace("~", "") + break + return _version + + +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( + # "--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( + "--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", + ) + + +@pytest.fixture(scope="session") +def salt_factories_root_dir(request, tmp_path_factory): + root_dir = SaltPkgInstall.salt_factories_root_dir( + request.config.getoption("--system-service") + ) + if root_dir is not None: + yield root_dir + 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_install": True, + } + + +@pytest.fixture(scope="session") +def install_salt(request, salt_factories_root_dir): + with SaltPkgInstall( + conf_dir=salt_factories_root_dir / "etc" / "salt", + system_service=request.config.getoption("--system-service"), + upgrade=request.config.getoption("--upgrade"), + no_uninstall=request.config.getoption("--no-uninstall"), + no_install=request.config.getoption("--no-install"), + ) 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()), + ], + } + config_overrides = { + "timeout": 30, + "file_roots": state_tree.as_dict(), + "pillar_roots": pillar_tree.as_dict(), + "rest_cherrypy": {"port": 8000, "disable_ssl": True}, + "external_auth": {"auto": {"saltdev": [".*"]}}, + } + if (platform.is_windows() or platform.is_darwin()) and install_salt.singlebin: + start_timeout = 240 + # For every minion started we have to accept it's key. + # On windows, using single binary, it has to decompress it and run the command. Too slow. + # So, just in this scenario, use open mode + config_overrides["open_mode"] = True + 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) + with factory.started(start_timeout=start_timeout): + yield factory + + +@pytest.fixture(scope="session") +def salt_minion(salt_master, install_salt): + """ + Start up a minion + """ + start_timeout = None + if (platform.is_windows() or platform.is_darwin()) and install_salt.singlebin: + start_timeout = 240 + 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(), + } + config_overrides = { + "id": minion_id, + "file_roots": salt_master.config["file_roots"].copy(), + "pillar_roots": salt_master.config["pillar_roots"].copy(), + } + factory = salt_master.salt_minion_daemon( + minion_id, + overrides=config_overrides, + defaults=config_defaults, + ) + 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 salt_api(salt_master, install_salt): + """ + start up and configure salt_api + """ + start_timeout = None + if platform.is_windows() and install_salt.singlebin: + start_timeout = 240 + 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 diff --git a/pkg/tests/files/check_imports.sls b/pkg/tests/files/check_imports.sls new file mode 100644 index 00000000000..0dde9d6ad33 --- /dev/null +++ b/pkg/tests/files/check_imports.sls @@ -0,0 +1,53 @@ +#!py +import importlib + +def run(): + config = {} + for test_import in [ + 'templates', 'platform', 'cli', 'executors', 'config', 'wheel', 'netapi', + 'cache', 'proxy', 'transport', 'metaproxy', 'modules', 'tokens', 'matchers', + 'acl', 'auth', 'log', 'engines', 'client', 'returners', 'runners', 'tops', + 'output', 'daemons', 'thorium', 'renderers', 'states', 'cloud', 'roster', + 'beacons', 'pillar', 'spm', 'utils', 'sdb', 'fileserver', 'defaults', + 'ext', 'queues', 'grains', 'serializers' + ]: + try: + import_name = "salt.{}".format(test_import) + importlib.import_module(import_name) + config['test_imports_succeeded'] = { + 'test.succeed_without_changes': [ + { + 'name': import_name + }, + ], + } + except ModuleNotFoundError as err: + config['test_imports_failed'] = { + 'test.fail_without_changes': [ + { + 'name': import_name, + 'comment': "The imports test failed. The error was: {}".format(err) + }, + ], + } + + for stdlib_import in ["telnetlib"]: + try: + importlib.import_module(stdlib_import) + config['stdlib_imports_succeeded'] = { + 'test.succeed_without_changes': [ + { + 'name': stdlib_import + }, + ], + } + except ModuleNotFoundError as err: + config['stdlib_imports_failed'] = { + 'test.fail_without_changes': [ + { + 'name': stdlib_import, + 'comment': "The stdlib imports test failed. The error was: {}".format(err) + }, + ], + } + return config diff --git a/pkg/tests/files/check_python.py b/pkg/tests/files/check_python.py new file mode 100644 index 00000000000..f1d46b76df7 --- /dev/null +++ b/pkg/tests/files/check_python.py @@ -0,0 +1,13 @@ +import sys + +import salt.utils.data + +user_arg = sys.argv + +if user_arg[1] == "raise": + raise Exception("test") + +if salt.utils.data.is_true(user_arg[1]): + sys.exit(0) +else: + sys.exit(1) diff --git a/pkg/tests/files/debianbased.sls b/pkg/tests/files/debianbased.sls new file mode 100644 index 00000000000..2d1fb4cb35f --- /dev/null +++ b/pkg/tests/files/debianbased.sls @@ -0,0 +1,24 @@ +{% set services_enabled = ['salt-master', 'salt-minion', 'salt-syndic', 'salt-api'] %} +{% set services_disabled = [] %} + +{% for service in services_enabled %} +check_services_enabled_{{ service }}: + service.enabled: + - name: {{ service }} +run_if_changes_{{ service }}: + cmd.run: + - name: failtest service is enabled + - onchanges: + - service: check_services_enabled_{{ service }} +{% endfor %} + +{% for service in services_disabled %} +check_services_disabled_{{ service }}: + service.disabled: + - name: {{ service }} +run_if_changes_{{ service }}: + cmd.run: + - name: failtest service is disabled + - onchanges: + - service: check_services_disabled_{{ service }} +{% endfor %} diff --git a/pkg/tests/files/redhatbased.sls b/pkg/tests/files/redhatbased.sls new file mode 100644 index 00000000000..1ea16e95c9f --- /dev/null +++ b/pkg/tests/files/redhatbased.sls @@ -0,0 +1,24 @@ +{% set services_enabled = [] %} +{% set services_disabled = ['salt-master', 'salt-minion', 'salt-syndic', 'salt-api'] %} + +{% for service in services_enabled %} +check_services_enabled_{{ service }}: + service.enabled: + - name: {{ service }} +run_if_changes_{{ service }}: + cmd.run: + - name: failtest service is enabled + - onchanges: + - service: check_services_enabled_{{ service }} +{% endfor %} + +{% for service in services_disabled %} +check_services_disabled_{{ service }}: + service.disabled: + - name: {{ service }} +run_if_changes_{{ service }}: + cmd.run: + - name: failtest service is disabled + - onchanges: + - service: check_services_disabled_{{ service }} +{% endfor %} diff --git a/pkg/tests/integration/__init__.py b/pkg/tests/integration/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/tests/integration/test_check_imports.py b/pkg/tests/integration/test_check_imports.py new file mode 100644 index 00000000000..742b08c3917 --- /dev/null +++ b/pkg/tests/integration/test_check_imports.py @@ -0,0 +1,17 @@ +import logging + +from saltfactories.utils.functional import MultiStateResult + +log = logging.getLogger(__name__) + + +def test_check_imports(salt_cli, salt_minion): + """ + Test imports + """ + ret = salt_cli.run("state.sls", "check_imports", minion_tgt=salt_minion.id) + assert ret.returncode == 0 + assert ret.data + result = MultiStateResult(raw=ret.data) + for state_ret in result: + assert state_ret.result is True diff --git a/pkg/tests/integration/test_enabled_disabled.py b/pkg/tests/integration/test_enabled_disabled.py new file mode 100644 index 00000000000..887da53e1c0 --- /dev/null +++ b/pkg/tests/integration/test_enabled_disabled.py @@ -0,0 +1,43 @@ +import pytest +from saltfactories.utils.functional import MultiStateResult + + +@pytest.mark.skip_on_windows(reason="Linux test only") +def test_services(install_salt, salt_cli, salt_minion): + """ + Check if Services are enabled/disabled + """ + if install_salt.compressed: + pytest.skip("Skip test on single binary and onedir package") + + ret = salt_cli.run("grains.get", "os_family", minion_tgt=salt_minion.id) + assert ret.returncode == 0 + assert ret.data + + state_name = desired_state = None + os_family = ret.data + + if os_family == "Debian": + state_name = "debianbased" + desired_state = "enabled" + elif os_family == "RedHat": + state_name = "redhatbased" + desired_state = "disabled" + else: + pytest.fail(f"Don't know how to handle os_family={os_family}") + + ret = salt_cli.run("state.apply", state_name, minion_tgt=salt_minion.id) + assert ret.returncode == 0 + assert ret.data + + expected_in_comment = f"is already {desired_state}, and is in the desired state" + + result = MultiStateResult(raw=ret.data) + for state_ret in result: + assert state_ret.result is True + if "__id__" not in state_ret.full_return: + # This is a state requirement + # For example: + # State was not run because none of the onchanges reqs changed + continue + assert expected_in_comment in state_ret.comment diff --git a/pkg/tests/integration/test_hash.py b/pkg/tests/integration/test_hash.py new file mode 100644 index 00000000000..026246e6808 --- /dev/null +++ b/pkg/tests/integration/test_hash.py @@ -0,0 +1,42 @@ +import hashlib +import logging +import sys + +import pytest + +log = logging.getLogger(__name__) + + +@pytest.mark.usefixtures("version") +def test_hashes(install_salt, salt_cli, salt_minion): + """ + Test the hashes generated for both single binary + and the onedir packages. + """ + if not install_salt.compressed: + pytest.skip("This test requires the single binary or onedir package") + + hashes = install_salt.salt_hashes + pkg = install_salt.pkgs[0] + + with open(pkg, "rb") as fh: + file_bytes = fh.read() + + delimiter = "/" + if sys.platform.startswith("win"): + delimiter = "\\" + + for _hash in hashes.keys(): + hash_file = hashes[_hash]["file"] + found_hash = False + with open(hash_file) as fp: + for line in fp: + if pkg.rsplit(delimiter, 1)[-1] in line: + found_hash = True + assert ( + getattr(hashlib, _hash.lower())(file_bytes).hexdigest() + == line.split()[0] + ) + + if not found_hash: + assert False, f"A {_hash} hash was not found in {hash_file} for pkg {pkg}" diff --git a/pkg/tests/integration/test_help.py b/pkg/tests/integration/test_help.py new file mode 100644 index 00000000000..7379f2e915d --- /dev/null +++ b/pkg/tests/integration/test_help.py @@ -0,0 +1,13 @@ +def test_help(install_salt): + """ + Test --help works for all salt cmds + """ + for cmd in install_salt.binary_paths.values(): + if "salt-cloud" in cmd: + assert True + elif "salt-ssh" in cmd: + assert True + else: + ret = install_salt.proc.run(*cmd, "--help") + assert "Usage" in ret.stdout + assert ret.returncode == 0 diff --git a/pkg/tests/integration/test_pip.py b/pkg/tests/integration/test_pip.py new file mode 100644 index 00000000000..e118fcf4a82 --- /dev/null +++ b/pkg/tests/integration/test_pip.py @@ -0,0 +1,125 @@ +import os +import pathlib +import shutil +import subprocess + +import pytest +from pytestskipmarkers.utils import platform + + +@pytest.fixture +def pypath(): + if platform.is_windows(): + return pathlib.Path(os.getenv("LocalAppData"), "salt", "pypath") + return pathlib.Path(f"{os.sep}opt", "saltstack", "salt", "pypath") + + +@pytest.fixture(autouse=True) +def wipe_pypath(pypath): + try: + yield + finally: + # Let's make sure pypath is clean after each test, since it's contents + # are not actually part of the test suite, and they break other test + # suite assumptions + for path in pypath.glob("*"): + if path.is_dir(): + shutil.rmtree(path, ignore_errors=True) + else: + path.unlink() + + +def test_pip_install(salt_call_cli): + """ + Test pip.install and ensure + module can use installed library + """ + dep = "PyGithub" + repo = "https://github.com/saltstack/salt.git" + + try: + install = salt_call_cli.run("--local", "pip.install", dep) + assert install.returncode == 0 + + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "Authentication information could" in use_lib.stderr + finally: + ret = salt_call_cli.run("--local", "pip.uninstall", dep) + assert ret.returncode == 0 + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "The github execution module cannot be loaded" in use_lib.stderr + + +def demote(user_uid, user_gid): + def result(): + os.setgid(user_gid) + os.setuid(user_uid) + + return result + + +@pytest.mark.skip_on_windows(reason="We can't easily demote users on Windows") +def test_pip_non_root(install_salt, test_account, pypath): + # Let's make sure pypath does not exist + shutil.rmtree(pypath) + + assert not pypath.exists() + # We should be able to issue a --help without being root + ret = subprocess.run( + install_salt.binary_paths["salt"] + ["--help"], + preexec_fn=demote(test_account.uid, test_account.gid), + env=test_account.env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + assert ret.returncode == 0, ret.stderr + assert "Usage" in ret.stdout + assert not pypath.exists() + + # Try to pip install something, should fail + ret = subprocess.run( + install_salt.binary_paths["pip"] + ["install", "pep8"], + preexec_fn=demote(test_account.uid, test_account.gid), + env=test_account.env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + assert ret.returncode == 1, ret.stderr + assert f"The path '{pypath}' does not exist or could not be created." in ret.stderr + assert not pypath.exists() + + # Let tiamat-pip create the pypath directory for us + ret = subprocess.run( + install_salt.binary_paths["pip"] + ["install", "-h"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + assert ret.returncode == 0, ret.stderr + + # Now, we should still not be able to install as non-root + ret = subprocess.run( + install_salt.binary_paths["pip"] + ["install", "pep8"], + preexec_fn=demote(test_account.uid, test_account.gid), + env=test_account.env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + assert ret.returncode != 0, ret.stderr + + # But we should be able to install as root + ret = subprocess.run( + install_salt.binary_paths["pip"] + ["install", "pep8"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + assert ret.returncode == 0, ret.stderr diff --git a/pkg/tests/integration/test_pip_upgrade.py b/pkg/tests/integration/test_pip_upgrade.py new file mode 100644 index 00000000000..20f6cd08218 --- /dev/null +++ b/pkg/tests/integration/test_pip_upgrade.py @@ -0,0 +1,92 @@ +import logging +import subprocess + +import pytest + +log = logging.getLogger(__name__) + + +def test_pip_install(install_salt, salt_call_cli): + """ + Test pip.install and ensure that a package included in the tiamat build can be upgraded + """ + ret = subprocess.run( + install_salt.binary_paths["salt"] + ["--versions-report"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + check=True, + shell=False, + ) + assert ret.returncode == 0 + + possible_upgrades = [ + "docker-py", + "msgpack", + "pycparser", + "python-gnupg", + "pyyaml", + "pyzmq", + "jinja2", + ] + found_new = False + for dep in possible_upgrades: + get_latest = salt_call_cli.run("--local", "pip.list_all_versions", dep) + if not get_latest.data: + # No information available + continue + dep_version = get_latest.data[-1] + installed_version = None + for line in ret.stdout.splitlines(): + if dep in line.lower(): + installed_version = line.lower().strip().split(":")[-1].strip() + break + else: + pytest.fail(f"Failed to find {dep} in the versions report output") + + if dep_version == installed_version: + log.warning(f"The {dep} dependency is already latest") + else: + found_new = True + break + + if found_new: + try: + install = salt_call_cli.run( + "--local", "pip.install", f"{dep}=={dep_version}" + ) + assert install + log.warning(install) + # The assert is commented out because pip will actually trigger a failure since + # we're breaking the dependency tree, but, for the purpose of this test, we can + # ignore it. + # + # assert install.returncode == 0 + + ret = subprocess.run( + install_salt.binary_paths["salt"] + ["--versions-report"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + check=True, + shell=False, + ) + assert ret.returncode == 0 + for line in ret.stdout.splitlines(): + if dep in line.lower(): + new_version = line.lower().strip().split(":")[-1].strip() + if new_version == installed_version: + pytest.fail( + f"The newly installed version of {dep} does not show in the versions report" + ) + assert new_version == dep_version + break + else: + pytest.fail(f"Failed to find {dep} in the versions report output") + finally: + log.info(f"Uninstalling {dep_version}") + assert salt_call_cli.run( + "--local", "pip.uninstall", f"{dep}=={dep_version}" + ) + else: + pytest.skip("Did not find an upgrade version for any of the dependencies") diff --git a/pkg/tests/integration/test_pkg.py b/pkg/tests/integration/test_pkg.py new file mode 100644 index 00000000000..2913ba6fc78 --- /dev/null +++ b/pkg/tests/integration/test_pkg.py @@ -0,0 +1,32 @@ +import sys + +import pytest + +pytestmark = [ + pytest.mark.skip_unless_on_linux, +] + + +@pytest.fixture(scope="module") +def grains(salt_call_cli): + ret = salt_call_cli.run("--local", "grains.items") + assert ret.data, ret + return ret.data + + +@pytest.fixture(scope="module") +def pkgname(grains): + if sys.platform.startswith("win"): + return "putty" + elif grains["os_family"] == "RedHat": + if grains["os"] == "VMware Photon OS": + return "snoopy" + return "units" + elif grains["os_family"] == "Debian": + return "ifenslave" + return "figlet" + + +def test_pkg_install(salt_call_cli, pkgname): + ret = salt_call_cli.run("--local", "state.single", "pkg.installed", pkgname) + assert ret.returncode == 0 diff --git a/pkg/tests/integration/test_python.py b/pkg/tests/integration/test_python.py new file mode 100644 index 00000000000..878905e5484 --- /dev/null +++ b/pkg/tests/integration/test_python.py @@ -0,0 +1,31 @@ +import subprocess + +import pytest + +from tests.support.helpers import TESTS_DIR + + +@pytest.mark.parametrize("exp_ret,user_arg", [(1, "false"), (0, "true")]) +def test_python_script(install_salt, exp_ret, user_arg): + ret = subprocess.run( + install_salt.binary_paths["salt"] + + ["python", str(TESTS_DIR / "files" / "check_python.py"), user_arg], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + + assert ret.returncode == exp_ret, ret.stderr + + +def test_python_script_exception(install_salt): + ret = subprocess.run( + install_salt.binary_paths["salt"] + + ["python", str(TESTS_DIR / "files" / "check_python.py"), "raise"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + universal_newlines=True, + ) + assert "Exception: test" in ret.stderr diff --git a/pkg/tests/integration/test_salt_api.py b/pkg/tests/integration/test_salt_api.py new file mode 100644 index 00000000000..0c9485038c1 --- /dev/null +++ b/pkg/tests/integration/test_salt_api.py @@ -0,0 +1,14 @@ +def test_salt_api(api_request): + """ + Test running a command against the salt api + """ + ret = api_request.post( + "/run", + data={ + "client": "local", + "tgt": "*", + "fun": "test.arg", + "arg": ["foo", "bar"], + }, + ) + assert ret["args"] == ["foo", "bar"] diff --git a/pkg/tests/integration/test_salt_call.py b/pkg/tests/integration/test_salt_call.py new file mode 100644 index 00000000000..13af02bb394 --- /dev/null +++ b/pkg/tests/integration/test_salt_call.py @@ -0,0 +1,59 @@ +import pytest + + +def test_salt_call_local(salt_call_cli): + """ + Test salt-call --local test.ping + """ + ret = salt_call_cli.run("--local", "test.ping") + assert ret.data is True + assert ret.returncode == 0 + + +def test_salt_call(salt_call_cli): + """ + Test salt-call test.ping + """ + ret = salt_call_cli.run("test.ping") + assert ret.data is True + assert ret.returncode == 0 + + +def test_sls(salt_call_cli): + """ + Test calling a sls file + """ + ret = salt_call_cli.run("state.apply", "test") + assert ret.data, ret + sls_ret = ret.data[next(iter(ret.data))] + assert sls_ret["changes"]["testing"]["new"] == "Something pretended to change" + assert ret.returncode == 0 + + +def test_salt_call_local_sys_doc_none(salt_call_cli): + """ + Test salt-call --local sys.doc none + """ + ret = salt_call_cli.run("--local", "sys.doc", "none") + assert not ret.data + assert ret.returncode == 0 + + +def test_salt_call_local_sys_doc_aliasses(salt_call_cli): + """ + Test salt-call --local sys.doc aliasses + """ + ret = salt_call_cli.run("--local", "sys.doc", "aliases.list_aliases") + assert "aliases.list_aliases" in ret.data + assert ret.returncode == 0 + + +@pytest.mark.skip_on_windows() +def test_salt_call_cmd_run_id_runas(salt_call_cli, test_account, caplog): + """ + Test salt-call --local cmd_run id with runas + """ + ret = salt_call_cli.run("--local", "cmd.run", "id", runas=test_account.username) + assert "Environment could not be retrieved for user" not in caplog.text + assert str(test_account.uid) in ret.stdout + assert str(test_account.gid) in ret.stdout diff --git a/pkg/tests/integration/test_salt_exec.py b/pkg/tests/integration/test_salt_exec.py new file mode 100644 index 00000000000..9b7d7fc7f68 --- /dev/null +++ b/pkg/tests/integration/test_salt_exec.py @@ -0,0 +1,25 @@ +from sys import platform + + +def test_salt_cmd_run(salt_cli, salt_minion): + """ + Test salt cmd.run 'ipconfig' or 'ls -lah /' + """ + ret = None + if platform.startswith("win"): + ret = salt_cli.run("cmd.run", "ipconfig", minion_tgt=salt_minion.id) + else: + ret = salt_cli.run("cmd.run", "ls -lah /", minion_tgt=salt_minion.id) + assert ret + assert ret.stdout + + +def test_salt_list_users(salt_cli, salt_minion): + """ + Test salt user.list_users + """ + ret = salt_cli.run("user.list_users", minion_tgt=salt_minion.id) + if platform.startswith("win"): + assert "Administrator" in ret.stdout + else: + assert "root" in ret.stdout diff --git a/pkg/tests/integration/test_salt_grains.py b/pkg/tests/integration/test_salt_grains.py new file mode 100644 index 00000000000..e42dbb1c1c8 --- /dev/null +++ b/pkg/tests/integration/test_salt_grains.py @@ -0,0 +1,34 @@ +def test_grains_items(salt_cli, salt_minion): + """ + Test grains.items + """ + ret = salt_cli.run("grains.items", minion_tgt=salt_minion.id) + assert ret.data, ret + assert "osrelease" in ret.data + + +def test_grains_item_os(salt_cli, salt_minion): + """ + Test grains.item os + """ + ret = salt_cli.run("grains.item", "os", minion_tgt=salt_minion.id) + assert ret.data, ret + assert "os" in ret.data + + +def test_grains_item_pythonversion(salt_cli, salt_minion): + """ + Test grains.item pythonversion + """ + ret = salt_cli.run("grains.item", "pythonversion", minion_tgt=salt_minion.id) + assert ret.data, ret + assert "pythonversion" in ret.data + + +def test_grains_setval_key_val(salt_cli, salt_minion): + """ + Test grains.setval key val + """ + ret = salt_cli.run("grains.setval", "key", "val", minion_tgt=salt_minion.id) + assert ret.data, ret + assert "key" in ret.data diff --git a/pkg/tests/integration/test_salt_key.py b/pkg/tests/integration/test_salt_key.py new file mode 100644 index 00000000000..5a2db4cddea --- /dev/null +++ b/pkg/tests/integration/test_salt_key.py @@ -0,0 +1,7 @@ +def test_salt_key(salt_key_cli, salt_minion): + """ + Test running salt-key -L + """ + ret = salt_key_cli.run("-L") + assert ret.data + assert salt_minion.id in ret.data["minions"] diff --git a/pkg/tests/integration/test_salt_minion.py b/pkg/tests/integration/test_salt_minion.py new file mode 100644 index 00000000000..1c9e743dad5 --- /dev/null +++ b/pkg/tests/integration/test_salt_minion.py @@ -0,0 +1,19 @@ +def test_salt_minion_ping(salt_cli, salt_minion): + """ + Test running a command against a targeted minion + """ + ret = salt_cli.run("test.ping", minion_tgt=salt_minion.id) + assert ret.returncode == 0 + assert ret.data is True + + +def test_salt_minion_setproctitle(salt_cli, salt_minion): + """ + Test that setproctitle is working + for the running Salt minion + """ + ret = salt_cli.run( + "ps.pgrep", "MinionProcessManager", full=True, minion_tgt=salt_minion.id + ) + assert ret.returncode == 0 + assert ret.data != "" diff --git a/pkg/tests/integration/test_salt_output.py b/pkg/tests/integration/test_salt_output.py new file mode 100644 index 00000000000..953618b2dfb --- /dev/null +++ b/pkg/tests/integration/test_salt_output.py @@ -0,0 +1,15 @@ +import pytest + + +@pytest.mark.parametrize("output_fmt", ["yaml", "json"]) +def test_salt_output(salt_cli, salt_minion, output_fmt): + """ + Test --output + """ + ret = salt_cli.run( + f"--output={output_fmt}", "test.fib", "7", minion_tgt=salt_minion.id + ) + if output_fmt == "json": + assert 13 in ret.data + else: + ret.stdout.matcher.fnmatch_lines(["*- 13*"]) diff --git a/pkg/tests/integration/test_salt_pillar.py b/pkg/tests/integration/test_salt_pillar.py new file mode 100644 index 00000000000..43656fce4e5 --- /dev/null +++ b/pkg/tests/integration/test_salt_pillar.py @@ -0,0 +1,6 @@ +def test_salt_pillar(salt_cli, salt_minion): + """ + Test pillar.items + """ + ret = salt_cli.run("pillar.items", minion_tgt=salt_minion.id) + assert "info" in ret.data diff --git a/pkg/tests/integration/test_salt_state_file.py b/pkg/tests/integration/test_salt_state_file.py new file mode 100644 index 00000000000..585167a7e55 --- /dev/null +++ b/pkg/tests/integration/test_salt_state_file.py @@ -0,0 +1,16 @@ +import sys + + +def test_salt_state_file(salt_cli, salt_minion): + """ + Test state file + """ + if sys.platform.startswith("win"): + ret = salt_cli.run("state.apply", "win_states", minion_tgt=salt_minion.id) + else: + ret = salt_cli.run("state.apply", "states", minion_tgt=salt_minion.id) + + assert ret.data, ret + sls_ret = ret.data[next(iter(ret.data))] + assert "changes" in sls_ret + assert "name" in sls_ret diff --git a/pkg/tests/integration/test_systemd_config.py b/pkg/tests/integration/test_systemd_config.py new file mode 100644 index 00000000000..c8f1312526d --- /dev/null +++ b/pkg/tests/integration/test_systemd_config.py @@ -0,0 +1,43 @@ +import subprocess + +import pytest + + +@pytest.mark.skip_on_windows(reason="Linux test only") +def test_system_config(salt_cli, salt_minion): + """ + Test system config + """ + get_family = salt_cli.run("grains.get", "os_family", minion_tgt=salt_minion.id) + assert get_family.returncode == 0 + get_finger = salt_cli.run("grains.get", "osfinger", minion_tgt=salt_minion.id) + assert get_finger.returncode == 0 + + if get_family.data == "RedHat": + if get_finger.data in ( + "CentOS Stream-8", + "CentOS Linux-8", + "CentOS Stream-9", + "Fedora Linux-36", + ): + ret = subprocess.call( + "systemctl show -p ${config} salt-minion.service", shell=True + ) + assert ret == 0 + else: + ret = subprocess.call( + "systemctl show -p ${config} salt-minion.service", shell=True + ) + assert ret == 1 + + elif "Debian" in get_family.stdout: + if "Debian-9" in get_finger.stdout: + ret = subprocess.call( + "systemctl show -p ${config} salt-minion.service", shell=True + ) + assert ret == 1 + else: + ret = subprocess.call( + "systemctl show -p ${config} salt-minion.service", shell=True + ) + assert ret == 0 diff --git a/pkg/tests/integration/test_version.py b/pkg/tests/integration/test_version.py new file mode 100644 index 00000000000..5f9b6239eae --- /dev/null +++ b/pkg/tests/integration/test_version.py @@ -0,0 +1,110 @@ +import sys + +import pytest +from pytestskipmarkers.utils import platform + + +def test_salt_version(version, install_salt): + """ + Test version outputed from salt --version + """ + ret = install_salt.proc.run(*install_salt.binary_paths["salt"], "--version") + assert ret.stdout.strip() == f"salt {version}" + + +def test_salt_versions_report_master(install_salt): + """ + Test running --versions-report on master + """ + ret = install_salt.proc.run( + *install_salt.binary_paths["master"], "--versions-report" + ) + ret.stdout.matcher.fnmatch_lines(["*Salt Version:*"]) + if sys.platform == "win32": + ret.stdout.matcher.fnmatch_lines(["*Python: 3.8.16*"]) + else: + ret.stdout.matcher.fnmatch_lines(["*Python: 3.9.16*"]) + + +def test_salt_versions_report_minion(salt_cli, salt_minion): + """ + Test running test.versions_report on minion + """ + ret = salt_cli.run("test.versions_report", minion_tgt=salt_minion.id) + ret.stdout.matcher.fnmatch_lines(["*Salt Version:*"]) + + +@pytest.mark.parametrize( + "binary", ["master", "cloud", "syndic", "minion", "call", "api"] +) +def test_compare_versions(version, binary, install_salt): + """ + Test compare versions + """ + if platform.is_windows() and install_salt.singlebin: + pytest.skip( + "Already tested in `test_salt_version`. No need to repeat " + "for windows single binary installs." + ) + if binary in ["master", "cloud", "syndic"]: + if sys.platform.startswith("win"): + pytest.skip(f"{binary} not installed on windows") + + ret = install_salt.proc.run(*install_salt.binary_paths[binary], "--version") + ret.stdout.matcher.fnmatch_lines([f"*{version}*"]) + + +@pytest.mark.skip_unless_on_darwin() +@pytest.mark.parametrize( + "symlink", + [ + # We can't create a salt symlink because there is a salt directory + # "salt", + "salt-api", + "salt-call", + "salt-cloud", + "salt-cp", + "salt-key", + "salt-master", + "salt-minion", + "salt-proxy", + "salt-run", + "salt-spm", + "salt-ssh", + "salt-syndic", + ], +) +def test_symlinks_created(version, symlink, install_salt): + """ + Test symlinks created + """ + if not install_salt.installer_pkg: + pytest.skip( + "This test is for the installer package only (pkg). It does not " + "apply to the tarball" + ) + ret = install_salt.proc.run(install_salt.bin_dir / symlink, "--version") + ret.stdout.matcher.fnmatch_lines([f"*{version}*"]) + + +def test_compare_pkg_versions_redhat_rc(version, install_salt): + """ + Test compare pkg versions for redhat RC packages. + A tilde should be included in RC Packages and it + should test to be a lower version than a non RC package + of the same version. For example, v3004~rc1 should be + less than v3004. + """ + if install_salt.distro_id not in ("centos", "redhat", "amzn", "fedora"): + pytest.skip("Only tests rpm packages") + + pkg = [x for x in install_salt.pkgs if "rpm" in x] + if not pkg: + pytest.skip("Not testing rpm packages") + pkg = pkg[0].split("/")[-1] + if "rc" not in pkg: + pytest.skip("Not testing an RC package") + assert "~" in pkg + comp_pkg = pkg.split("~")[0] + ret = install_salt.proc.run("rpmdev-vercmp", pkg, comp_pkg) + ret.stdout.matcher.fnmatch_lines([f"{pkg} < {comp_pkg}"]) diff --git a/pkg/tests/support/__init__.py b/pkg/tests/support/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/tests/support/coverage/sitecustomize.py b/pkg/tests/support/coverage/sitecustomize.py new file mode 100644 index 00000000000..bee2ff80f2f --- /dev/null +++ b/pkg/tests/support/coverage/sitecustomize.py @@ -0,0 +1,11 @@ +""" +Python will always try to import sitecustomize. +We use that fact to try and support code coverage for sub-processes +""" + +try: + import coverage + + coverage.process_startup() +except ImportError: + pass diff --git a/pkg/tests/support/helpers.py b/pkg/tests/support/helpers.py new file mode 100644 index 00000000000..cd346d44ce2 --- /dev/null +++ b/pkg/tests/support/helpers.py @@ -0,0 +1,1404 @@ +import atexit +import contextlib +import logging +import os +import pathlib +import pprint +import re +import shutil +import tarfile +import textwrap +import time +from typing import TYPE_CHECKING, Any, Dict, List +from zipfile import ZipFile + +import attr +import distro +import psutil +import pytest +import requests +from pytestshellutils.shell import DaemonImpl, Subprocess +from pytestshellutils.utils.processes import ( + ProcessResult, + _get_cmdline, + terminate_process, +) +from pytestskipmarkers.utils import platform +from saltfactories.bases import SystemdSaltDaemonImpl +from saltfactories.cli import call, key, salt +from saltfactories.daemons import api, master, minion + +try: + import crypt + + HAS_CRYPT = True +except ImportError: + HAS_CRYPT = False +try: + import pwd + + HAS_PWD = True +except ImportError: + HAS_PWD = False + + +TESTS_DIR = pathlib.Path(__file__).resolve().parent.parent +CODE_DIR = TESTS_DIR.parent +ARTIFACTS_DIR = CODE_DIR / "artifacts" + +log = logging.getLogger(__name__) + + +@attr.s(kw_only=True, slots=True) +class SaltPkgInstall: + conf_dir: pathlib.Path = attr.ib() + system_service: bool = attr.ib(default=False) + proc: Subprocess = attr.ib(init=False) + pkgs: List[str] = attr.ib(factory=list) + onedir: bool = attr.ib(default=False) + singlebin: bool = attr.ib(default=False) + compressed: bool = attr.ib(default=False) + hashes: Dict[str, Dict[str, Any]] = attr.ib() + root: pathlib.Path = attr.ib(default=None) + run_root: pathlib.Path = attr.ib(default=None) + ssm_bin: pathlib.Path = attr.ib(default=None) + bin_dir: pathlib.Path = attr.ib(default=None) + # The artifact is an installer (exe, pkg, rpm, deb) + installer_pkg: bool = attr.ib(default=False) + upgrade: bool = attr.ib(default=False) + # install salt or not. This allows someone + # to test a currently installed version of salt + no_install: bool = attr.ib(default=False) + no_uninstall: bool = attr.ib(default=False) + + distro_id: str = attr.ib(init=False) + pkg_mngr: str = attr.ib(init=False) + rm_pkg: str = attr.ib(init=False) + salt_pkgs: List[str] = attr.ib(init=False) + binary_paths: List[pathlib.Path] = attr.ib(init=False) + + @proc.default + def _default_proc(self): + return Subprocess() + + @hashes.default + def _default_hashes(self): + return { + "BLAKE2B": {"file": None, "tool": "-blake2b512"}, + "SHA3_512": {"file": None, "tool": "-sha3-512"}, + "SHA512": {"file": None, "tool": "-sha512"}, + } + + @distro_id.default + def _default_distro_id(self): + return distro.id().lower() + + @pkg_mngr.default + def _default_pkg_mngr(self): + if self.distro_id in ("centos", "redhat", "amzn", "fedora"): + return "yum" + elif self.distro_id in ("ubuntu", "debian"): + ret = self.proc.run("apt-get", "update") + self._check_retcode(ret) + return "apt-get" + + @rm_pkg.default + def _default_rm_pkg(self): + if self.distro_id in ("centos", "redhat", "amzn", "fedora"): + return "remove" + elif self.distro_id in ("ubuntu", "debian"): + return "purge" + + @salt_pkgs.default + def _default_salt_pkgs(self): + salt_pkgs = [ + "salt-api", + "salt-syndic", + "salt-ssh", + "salt-master", + "salt-cloud", + "salt-minion", + ] + if self.distro_id in ("centos", "redhat", "amzn", "fedora"): + salt_pkgs.append("salt") + elif self.distro_id in ("ubuntu", "debian"): + salt_pkgs.append("salt-common") + return salt_pkgs + + def __attrs_post_init__(self): + file_ext_re = r"tar\.gz" + if platform.is_darwin(): + file_ext_re = r"tar\.gz|pkg" + if platform.is_windows(): + file_ext_re = "zip|exe" + for f_path in ARTIFACTS_DIR.glob("**/*.*"): + f_path = str(f_path) + if re.search(f"salt-(.*).({file_ext_re})$", f_path): + # Compressed can be zip, tar.gz, exe, or pkg. All others are + # deb and rpm + self.compressed = True + file_ext = os.path.splitext(f_path)[1].strip(".") + if file_ext == "gz": + if f_path.endswith("tar.gz"): + file_ext = "tar.gz" + self.pkgs.append(f_path) + if platform.is_windows(): + self.root = pathlib.Path(os.getenv("LocalAppData")).resolve() + if file_ext == "zip": + with ZipFile(f_path, "r") as zip: + first = zip.infolist()[0] + if first.filename == "salt/ssm.exe": + self.onedir = True + self.bin_dir = self.root / "salt" / "salt" + self.run_root = self.bin_dir / "salt.exe" + self.ssm_bin = self.root / "salt" / "ssm.exe" + elif first.filename == "salt.exe": + self.singlebin = True + self.run_root = self.root / "salt.exe" + self.ssm_bin = self.root / "ssm.exe" + else: + log.error( + "Unexpected archive layout. First: %s", + first.filename, + ) + elif file_ext == "exe": + self.onedir = True + self.installer_pkg = True + install_dir = pathlib.Path( + os.getenv("ProgramFiles"), "Salt Project", "Salt" + ).resolve() + self.bin_dir = install_dir / "bin" + self.run_root = self.bin_dir / "salt.exe" + self.ssm_bin = self.bin_dir / "ssm.exe" + else: + log.error("Unexpected file extension: %s", file_ext) + else: + if platform.is_darwin(): + self.root = pathlib.Path(os.sep, "opt") + else: + self.root = pathlib.Path(os.sep, "usr", "local", "bin") + + if file_ext == "pkg": + self.onedir = True + self.installer_pkg = True + self.bin_dir = self.root / "salt" / "bin" + self.run_root = self.bin_dir / "run" + elif file_ext == "tar.gz": + with tarfile.open(f_path) as tar: + # The first item will be called salt + first = next(iter(tar.getmembers())) + if first.name == "salt" and first.isdir(): + self.onedir = True + self.bin_dir = self.root / "salt" / "run" + self.run_root = self.bin_dir / "run" + elif first.name == "salt" and first.isfile(): + self.singlebin = True + self.run_root = self.root / "salt" + else: + log.error( + "Unexpected archive layout. First: %s (isdir: %s, isfile: %s)", + first.name, + first.isdir(), + first.isfile(), + ) + else: + log.error("Unexpected file extension: %s", file_ext) + + if re.search(r"salt(.*)(x86_64|all|amd64|aarch64)\.(rpm|deb)$", f_path): + self.installer_pkg = True + self.pkgs.append(f_path) + + if not self.pkgs: + pytest.fail("Could not find Salt Artifacts") + + if not self.compressed: + self.binary_paths = { + "salt": ["salt"], + "api": ["salt-api"], + "call": ["salt-call"], + "cloud": ["salt-cloud"], + "cp": ["salt-cp"], + "key": ["salt-key"], + "master": ["salt-master"], + "minion": ["salt-minion"], + "proxy": ["salt-proxy"], + "run": ["salt-run"], + "ssh": ["salt-ssh"], + "syndic": ["salt-syndic"], + "spm": ["spm"], + "pip": ["salt-pip"], + } + else: + self.binary_paths = { + "salt": [str(self.run_root)], + "api": [str(self.run_root), "api"], + "call": [str(self.run_root), "call"], + "cloud": [str(self.run_root), "cloud"], + "cp": [str(self.run_root), "cp"], + "key": [str(self.run_root), "key"], + "master": [str(self.run_root), "master"], + "minion": [str(self.run_root), "minion"], + "proxy": [str(self.run_root), "proxy"], + "run": [str(self.run_root), "run"], + "ssh": [str(self.run_root), "ssh"], + "syndic": [str(self.run_root), "syndic"], + "spm": [str(self.run_root), "spm"], + "pip": [str(self.run_root), "pip"], + } + + @staticmethod + def salt_factories_root_dir(system_service: bool = False) -> pathlib.Path: + if system_service is False: + return None + if platform.is_windows(): + return pathlib.Path("C:/salt") + if platform.is_darwin(): + return pathlib.Path("/opt/salt") + return pathlib.Path("/") + + def _check_retcode(self, ret): + """ + helper function ot check subprocess.run + returncode equals 0, if not raise assertionerror + """ + if ret.returncode != 0: + log.error(ret) + assert ret.returncode == 0 + return True + + @property + def salt_hashes(self): + for _hash in self.hashes.keys(): + for fpath in ARTIFACTS_DIR.glob(f"**/*{_hash}*"): + fpath = str(fpath) + if re.search(f"{_hash}", fpath): + self.hashes[_hash]["file"] = fpath + + return self.hashes + + def _install_ssm_service(self): + # Register the services + # run_root and ssm_bin are configured in helper.py to point to the + # correct binary location + log.debug("Installing master service") + ret = self.proc.run( + str(self.ssm_bin), + "install", + "salt-master", + str(self.run_root), + "master", + "-c", + str(self.conf_dir), + ) + self._check_retcode(ret) + log.debug("Installing minion service") + ret = self.proc.run( + str(self.ssm_bin), + "install", + "salt-minion", + str(self.run_root), + "minion", + "-c", + str(self.conf_dir), + ) + self._check_retcode(ret) + log.debug("Installing api service") + ret = self.proc.run( + str(self.ssm_bin), + "install", + "salt-api", + str(self.run_root), + "api", + "-c", + str(self.conf_dir), + ) + self._check_retcode(ret) + + def _install_compressed(self, upgrade=False): + pkg = self.pkgs[0] + log.info("Installing %s", pkg) + if platform.is_windows(): + if pkg.endswith("zip"): + # Extract the files + log.debug("Extracting zip file") + with ZipFile(pkg, "r") as zip: + zip.extractall(path=self.root) + elif pkg.endswith("exe"): + # Install the package + log.debug("Installing: %s", str(pkg)) + if upgrade: + ret = self.proc.run(str(pkg), "/S") + else: + ret = self.proc.run(str(pkg), "/start-minion=0", "/S") + self._check_retcode(ret) + # Remove the service installed by the installer + log.debug("Removing installed salt-minion service") + self.proc.run( + str(self.ssm_bin), + "remove", + "salt-minion", + "confirm", + ) + else: + log.error("Unknown package type: %s", pkg) + if self.system_service: + self._install_ssm_service() + elif platform.is_darwin(): + if pkg.endswith("pkg"): + daemons_dir = pathlib.Path(os.sep, "Library", "LaunchDaemons") + service_name = "com.saltstack.salt.minion" + plist_file = daemons_dir / f"{service_name}.plist" + log.debug("Installing: %s", str(pkg)) + ret = self.proc.run("installer", "-pkg", str(pkg), "-target", "/") + self._check_retcode(ret) + # Stop the service installed by the installer + self.proc.run( + "launchctl", + "disable", + f"system/{service_name}", + ) + self.proc.run("launchctl", "bootout", "system", str(plist_file)) + else: + log.debug("Extracting tarball into %s", self.root) + with tarfile.open(pkg) as tar: # , "r:gz") + tar.extractall(path=str(self.root)) + else: + log.debug("Extracting tarball into %s", self.root) + with tarfile.open(pkg) as tar: # , "r:gz") + tar.extractall(path=str(self.root)) + + def _install_pkgs(self, upgrade=False): + if upgrade: + log.info("Installing packages:\n%s", pprint.pformat(self.pkgs)) + if self.distro_id in ("ubuntu", "debian"): + # --allow-downgrades and yum's downgrade is a workaround since + # dpkg/yum is seeing 3005 version as a greater version than our nightly builds. + # Also this helps work around the situation when the Salt + # branch has not been updated with code so the versions might + # be the same and you can still install and test the new + # package. + ret = self.proc.run( + self.pkg_mngr, "upgrade", "-y", "--allow-downgrades", *self.pkgs + ) + else: + ret = self.proc.run(self.pkg_mngr, "upgrade", "-y", *self.pkgs) + if ( + ret.returncode != 0 + or "does not update installed package" in ret.stdout + or "cannot update it" in ret.stderr + ): + log.info( + "The new packages version is not returning as new. Attempting to downgrade" + ) + ret = self.proc.run(self.pkg_mngr, "downgrade", "-y", *self.pkgs) + if ret.returncode != 0: + log.error("Could not install the packages") + return False + else: + log.info("Installing packages:\n%s", pprint.pformat(self.pkgs)) + ret = self.proc.run(self.pkg_mngr, "install", "-y", *self.pkgs) + log.info(ret) + self._check_retcode(ret) + + def install(self, upgrade=False): + if self.compressed: + self._install_compressed(upgrade=upgrade) + else: + self._install_pkgs(upgrade=upgrade) + + def install_previous(self): + """ + Install previous version. This is used for + upgrade tests. + """ + major_ver = "3005" + min_ver = f"{major_ver}" + os_name, version, code_name = distro.linux_distribution() + if os_name: + os_name = os_name.split()[0].lower() + if os_name == "centos" or os_name == "fedora": + os_name = "redhat" + # TODO: When tiamat is considered production we need to update these + # TODO: paths to the tiamat paths instead of the old package paths. + if os_name.lower() in ["redhat", "centos", "amazon", "fedora"]: + for fp in pathlib.Path("/etc", "yum.repos.d").glob("epel*"): + fp.unlink() + ret = self.proc.run( + "rpm", + "--import", + f"https://repo.saltproject.io/salt/py3/{os_name}/{version}/x86_64/{major_ver}/SALTSTACK-GPG-KEY.pub", + ) + self._check_retcode(ret) + ret = self.proc.run( + "curl", + "-fsSL", + f"https://repo.saltproject.io/salt/py3/{os_name}/{version}/x86_64/{major_ver}.repo", + "-o", + f"/etc/yum.repos.d/salt-{os_name}.repo", + ) + self._check_retcode(ret) + ret = self.proc.run(self.pkg_mngr, "clean", "expire-cache") + self._check_retcode(ret) + ret = self.proc.run( + self.pkg_mngr, + "install", + *self.salt_pkgs, + "-y", + ) + self._check_retcode(ret) + + elif os_name.lower() in ["debian", "ubuntu"]: + ret = self.proc.run(self.pkg_mngr, "install", "curl", "-y") + self._check_retcode(ret) + ret = self.proc.run(self.pkg_mngr, "install", "apt-transport-https", "-y") + self._check_retcode(ret) + ret = self.proc.run( + "curl", + "-fsSL", + "-o", + "/usr/share/keyrings/salt-archive-keyring.gpg", + f"https://repo.saltproject.io/salt/py3/{os_name}/{version}/amd64/{major_ver}/salt-archive-keyring.gpg", + ) + self._check_retcode(ret) + with open( + pathlib.Path("/etc", "apt", "sources.list.d", "salt.list"), "w" + ) as fp: + fp.write( + "deb [signed-by=/usr/share/keyrings/salt-archive-keyring.gpg arch=amd64] " + f"https://repo.saltproject.io/salt/py3/{os_name}/{version}/amd64/{major_ver} {code_name} main" + ) + ret = self.proc.run(self.pkg_mngr, "update") + self._check_retcode(ret) + ret = self.proc.run( + self.pkg_mngr, + "install", + *self.salt_pkgs, + "-y", + ) + self._check_retcode(ret) + + elif platform.is_windows(): + win_pkg = f"salt-{min_ver}-1-windows-amd64.exe" + win_pkg_url = ( + f"https://repo.saltproject.io/salt/py3/windows/{major_ver}/{win_pkg}" + ) + pkg_path = pathlib.Path(r"C:\TEMP", win_pkg) + pkg_path.parent.mkdir(exist_ok=True) + ret = requests.get(win_pkg_url) + with open(pkg_path, "wb") as fp: + fp.write(ret.content) + ret = self.proc.run(pkg_path, "/start-minion=0", "/S") + self._check_retcode(ret) + log.debug("Removing installed salt-minion service") + self.proc.run( + "cmd", "/c", str(self.ssm_bin), "remove", "salt-minion", "confirm" + ) + + if self.system_service: + self._install_system_service() + + self.onedir = True + self.installer_pkg = True + install_dir = pathlib.Path( + os.getenv("ProgramFiles"), "Salt Project", "Salt" + ).resolve() + self.bin_dir = install_dir / "bin" + self.run_root = self.bin_dir / "salt.exe" + self.ssm_bin = self.bin_dir / "ssm.exe" + + def _uninstall_compressed(self): + if platform.is_windows(): + if self.system_service: + # Uninstall the services + log.debug("Uninstalling master service") + self.proc.run( + str(self.ssm_bin), + "stop", + "salt-master", + ) + self.proc.run( + str(self.ssm_bin), + "remove", + "salt-master", + "confirm", + ) + log.debug("Uninstalling minion service") + self.proc.run( + str(self.ssm_bin), + "stop", + "salt-minion", + ) + self.proc.run( + str(self.ssm_bin), + "remove", + "salt-minion", + "confirm", + ) + log.debug("Uninstalling api service") + self.proc.run( + str(self.ssm_bin), + "stop", + "salt-api", + ) + self.proc.run( + str(self.ssm_bin), + "remove", + "salt-api", + "confirm", + ) + log.debug("Removing the Salt Service Manager") + if self.ssm_bin: + try: + self.ssm_bin.unlink() + except PermissionError: + atexit.register(self.ssm_bin.unlink) + if platform.is_darwin(): + # From here: https://stackoverflow.com/a/46118276/4581998 + daemons_dir = pathlib.Path(os.sep, "Library", "LaunchDaemons") + for service in ("minion", "master", "api", "syndic"): + service_name = f"com.saltstack.salt.{service}" + plist_file = daemons_dir / f"{service_name}.plist" + # Stop the services + self.proc.run("launchctl", "disable", f"system/{service_name}") + self.proc.run("launchctl", "bootout", "system", str(plist_file)) + + # Remove Symlink to salt-config + if os.path.exists("/usr/local/sbin/salt-config"): + os.unlink("/usr/local/sbin/salt-config") + + # Remove supporting files + self.proc.run( + "pkgutil", + "--only-files", + "--files", + "com.saltstack.salt", + "|", + "grep", + "-v", + "opt", + "|", + "tr", + "'\n'", + "' '", + "|", + "xargs", + "-0", + "rm", + "-f", + ) + + # Remove directories + if os.path.exists("/etc/salt"): + shutil.rmtree("/etc/salt") + + # Remove path + if os.path.exists("/etc/paths.d/salt"): + os.remove("/etc/paths.d/salt") + + # Remove receipt + self.proc.run("pkgutil", "--forget", "com.saltstack.salt") + + if self.singlebin: + log.debug("Deleting the salt binary: %s", self.run_root) + if self.run_root: + try: + self.run_root.unlink() + except PermissionError: + atexit.register(self.run_root.unlink) + else: + log.debug("Deleting the onedir directory: %s", self.root / "salt") + shutil.rmtree(str(self.root / "salt")) + + def _uninstall_pkgs(self): + log.debug("Un-Installing packages:\n%s", pprint.pformat(self.salt_pkgs)) + ret = self.proc.run(self.pkg_mngr, self.rm_pkg, "-y", *self.salt_pkgs) + self._check_retcode(ret) + + def uninstall(self): + if self.compressed: + self._uninstall_compressed() + else: + self._uninstall_pkgs() + + def assert_uninstalled(self): + """ + Assert that the paths in /opt/saltstack/ were correctly + removed or not removed + """ + return + if platform.is_windows(): + # I'm not sure where the /opt/saltstack path is coming from + # This is the path we're using to test windows + opt_path = pathlib.Path(os.getenv("LocalAppData"), "salt", "pypath") + else: + opt_path = pathlib.Path(os.sep, "opt", "saltstack", "salt", "pypath") + if not opt_path.exists(): + if platform.is_windows(): + assert not opt_path.parent.exists() + else: + assert not opt_path.parent.parent.exists() + else: + opt_path_contents = list(opt_path.rglob("*")) + if not opt_path_contents: + pytest.fail( + f"The path '{opt_path}' exists but there are no files in it." + ) + else: + for path in list(opt_path_contents): + if path.name in (".installs.json", "__pycache__"): + opt_path_contents.remove(path) + if opt_path_contents: + pytest.fail( + "The test left some files behind: {}".format( + ", ".join([str(p) for p in opt_path_contents]) + ) + ) + + def write_launchd_conf(self, service): + service_name = f"com.saltstack.salt.{service}" + ret = self.proc.run("launchctl", "list", service_name) + # 113 means it couldn't find a service with that name + if ret.returncode == 113: + daemons_dir = pathlib.Path(os.sep, "Library", "LaunchDaemons") + plist_file = daemons_dir / f"{service_name}.plist" + # Make sure we're using this plist file + if plist_file.exists(): + log.warning("Removing existing plist file for service: %s", service) + plist_file.unlink() + + log.debug("Creating plist file for service: %s", service) + contents = textwrap.dedent( + f"""\ + + + + + Label + {service_name} + RunAtLoad + + KeepAlive + + ProgramArguments + + {self.run_root} + {service} + -c + {self.conf_dir} + + SoftResourceLimits + + NumberOfFiles + 100000 + + HardResourceLimits + + NumberOfFiles + 100000 + + + + """ + ) + plist_file.write_text(contents, encoding="utf-8") + contents = plist_file.read_text() + log.debug("Created '%s'. Contents:\n%s", plist_file, contents) + + # Delete the plist file upon completion + atexit.register(plist_file.unlink) + + def write_systemd_conf(self, service, binary): + ret = self.proc.run("systemctl", "daemon-reload") + self._check_retcode(ret) + ret = self.proc.run("systemctl", "status", service) + if ret.returncode in (3, 4): + log.warning( + "No systemd unit file was found for service %s. Creating one.", service + ) + contents = textwrap.dedent( + """\ + [Unit] + Description={service} + + [Service] + KillMode=process + Type=notify + NotifyAccess=all + LimitNOFILE=8192 + ExecStart={tgt} -c {conf_dir} + + [Install] + WantedBy=multi-user.target + """ + ) + if isinstance(binary, list) and len(binary) == 1: + binary = shutil.which(binary[0]) or binary[0] + elif isinstance(binary, list): + binary = " ".join(binary) + unit_path = pathlib.Path( + os.sep, "etc", "systemd", "system", f"{service}.service" + ) + contents = contents.format( + service=service, tgt=binary, conf_dir=self.conf_dir + ) + log.info("Created '%s'. Contents:\n%s", unit_path, contents) + unit_path.write_text(contents, encoding="utf-8") + ret = self.proc.run("systemctl", "daemon-reload") + atexit.register(unit_path.unlink) + self._check_retcode(ret) + + def __enter__(self): + if not self.no_install: + if self.upgrade: + self.install_previous() + else: + self.install() + return self + + def __exit__(self, *_): + if not self.no_uninstall: + self.uninstall() + self.assert_uninstalled() + + +class PkgSystemdSaltDaemonImpl(SystemdSaltDaemonImpl): + def get_service_name(self): + if self._service_name is None: + self._service_name = self.factory.script_name + return self._service_name + + +@attr.s(kw_only=True) +class PkgLaunchdSaltDaemonImpl(PkgSystemdSaltDaemonImpl): + + plist_file = attr.ib() + + @plist_file.default + def _default_plist_file(self): + daemons_dir = pathlib.Path(os.sep, "Library", "LaunchDaemons") + return daemons_dir / f"{self.get_service_name()}.plist" + + def get_service_name(self): + if self._service_name is None: + service_name = super().get_service_name() + if "-" in service_name: + service_name = service_name.split("-")[-1] + self._service_name = f"com.saltstack.salt.{service_name}" + return self._service_name + + def cmdline(self, *args): # pylint: disable=arguments-differ + """ + Construct a list of arguments to use when starting the subprocess. + + :param str args: + Additional arguments to use when starting the subprocess + + """ + if args: # pragma: no cover + log.debug( + "%s.run() is ignoring the passed in arguments: %r", + self.__class__.__name__, + args, + ) + self._internal_run( + "launchctl", + "enable", + f"system/{self.get_service_name()}", + ) + return ( + "launchctl", + "bootstrap", + "system", + str(self.plist_file), + ) + + def is_running(self): + """ + Returns true if the sub-process is alive. + """ + if self._process is None: + ret = self._internal_run("launchctl", "list", self.get_service_name()) + if ret.stdout == "": + return False + + if "PID" not in ret.stdout: + return False + + pid = None + # PID in a line that looks like this + # "PID" = 445; + for line in ret.stdout.splitlines(): + if "PID" in line: + pid = line.rstrip(";").split(" = ")[1] + + if pid is None: + return False + + self._process = psutil.Process(int(pid)) + + return self._process.is_running() + + def _terminate(self): + """ + This method actually terminates the started daemon. + """ + # We completely override the parent class method because we're not using + # the self._terminal property, it's a launchd service + if self._process is None: # pragma: no cover + if TYPE_CHECKING: + # Make mypy happy + assert self._terminal_result + return ( + self._terminal_result + ) # pylint: disable=access-member-before-definition + + atexit.unregister(self.terminate) + log.info("Stopping %s", self.factory) + pid = self.pid + # Collect any child processes information before terminating the process + with contextlib.suppress(psutil.NoSuchProcess): + for child in psutil.Process(pid).children(recursive=True): + if ( + child not in self._children + ): # pylint: disable=access-member-before-definition + self._children.append( + child + ) # pylint: disable=access-member-before-definition + + if self._process.is_running(): # pragma: no cover + cmdline = _get_cmdline(self._process) + else: + cmdline = [] + + # Disable the service + self._internal_run( + "launchctl", + "disable", + f"system/{self.get_service_name()}", + ) + # Unload the service + self._internal_run("launchctl", "bootout", "system", str(self.plist_file)) + + if self._process.is_running(): # pragma: no cover + try: + self._process.wait() + except psutil.TimeoutExpired: + self._process.terminate() + try: + self._process.wait() + except psutil.TimeoutExpired: + pass + + exitcode = self._process.wait() or 0 + + # Dereference the internal _process attribute + self._process = None + # Lets log and kill any child processes left behind, including the main subprocess + # if it failed to properly stop + terminate_process( + pid=pid, + kill_children=True, + children=self._children, # pylint: disable=access-member-before-definition + slow_stop=self.factory.slow_stop, + ) + + if self._terminal_stdout is not None: + self._terminal_stdout.close() # pylint: disable=access-member-before-definition + if self._terminal_stderr is not None: + self._terminal_stderr.close() # pylint: disable=access-member-before-definition + stdout = stderr = "" + try: + self._terminal_result = ProcessResult( + returncode=exitcode, stdout=stdout, stderr=stderr, cmdline=cmdline + ) + log.info("%s %s", self.factory.__class__.__name__, self._terminal_result) + return self._terminal_result + finally: + self._terminal = None + self._terminal_stdout = None + self._terminal_stderr = None + self._terminal_timeout = None + self._children = [] + + +@attr.s(kw_only=True) +class PkgSsmSaltDaemonImpl(PkgSystemdSaltDaemonImpl): + def cmdline(self, *args): # pylint: disable=arguments-differ + """ + Construct a list of arguments to use when starting the subprocess. + + :param str args: + Additional arguments to use when starting the subprocess + + """ + if args: # pragma: no cover + log.debug( + "%s.run() is ignoring the passed in arguments: %r", + self.__class__.__name__, + args, + ) + return ( + str(self.factory.salt_pkg_install.ssm_bin), + "start", + self.get_service_name(), + ) + + def is_running(self): + """ + Returns true if the sub-process is alive. + """ + if self._process is None: + n = 1 + while True: + if self._process is not None: + break + time.sleep(1) + ret = self._internal_run( + str(self.factory.salt_pkg_install.ssm_bin), + "processes", + self.get_service_name(), + ) + log.warning(ret) + if not ret.stdout or (ret.stdout and not ret.stdout.strip()): + if n >= 120: + return False + n += 1 + continue + for line in ret.stdout.splitlines(): + log.warning("Line: %s", line) + if not line.strip(): + continue + mainpid = line.strip().split()[0] + self._process = psutil.Process(int(mainpid)) + break + return self._process.is_running() + + def _terminate(self): + """ + This method actually terminates the started daemon. + """ + # We completely override the parent class method because we're not using the + # self._terminal property, it's a systemd service + if self._process is None: # pragma: no cover + if TYPE_CHECKING: + # Make mypy happy + assert self._terminal_result + return ( + self._terminal_result + ) # pylint: disable=access-member-before-definition + + atexit.unregister(self.terminate) + log.info("Stopping %s", self.factory) + pid = self.pid + # Collect any child processes information before terminating the process + with contextlib.suppress(psutil.NoSuchProcess): + for child in psutil.Process(pid).children(recursive=True): + if ( + child not in self._children + ): # pylint: disable=access-member-before-definition + self._children.append( + child + ) # pylint: disable=access-member-before-definition + + if self._process.is_running(): # pragma: no cover + cmdline = _get_cmdline(self._process) + else: + cmdline = [] + + # Tell ssm to stop the service + try: + self._internal_run( + str(self.factory.salt_pkg_install.ssm_bin), + "stop", + self.get_service_name(), + ) + except FileNotFoundError: + pass + + if self._process.is_running(): # pragma: no cover + try: + self._process.wait() + except psutil.TimeoutExpired: + self._process.terminate() + try: + self._process.wait() + except psutil.TimeoutExpired: + pass + + exitcode = self._process.wait() or 0 + + # Dereference the internal _process attribute + self._process = None + # Lets log and kill any child processes left behind, including the main subprocess + # if it failed to properly stop + terminate_process( + pid=pid, + kill_children=True, + children=self._children, # pylint: disable=access-member-before-definition + slow_stop=self.factory.slow_stop, + ) + + if self._terminal_stdout is not None: + self._terminal_stdout.close() # pylint: disable=access-member-before-definition + if self._terminal_stderr is not None: + self._terminal_stderr.close() # pylint: disable=access-member-before-definition + stdout = stderr = "" + try: + self._terminal_result = ProcessResult( + returncode=exitcode, stdout=stdout, stderr=stderr, cmdline=cmdline + ) + log.info("%s %s", self.factory.__class__.__name__, self._terminal_result) + return self._terminal_result + finally: + self._terminal = None + self._terminal_stdout = None + self._terminal_stderr = None + self._terminal_timeout = None + self._children = [] + + +@attr.s(kw_only=True) +class PkgMixin: + salt_pkg_install: SaltPkgInstall = attr.ib() + + def get_script_path(self): + if self.salt_pkg_install.compressed: + return str(self.salt_pkg_install.run_root) + return super().get_script_path() + + def get_base_script_args(self): + base_script_args = [] + if self.salt_pkg_install.compressed: + if self.script_name == "spm": + base_script_args.append(self.script_name) + elif self.script_name != "salt": + base_script_args.append(self.script_name.split("salt-")[-1]) + base_script_args.extend(super().get_base_script_args()) + return base_script_args + + def cmdline(self, *args, **kwargs): + _cmdline = super().cmdline(*args, **kwargs) + if self.salt_pkg_install.compressed is False: + return _cmdline + if _cmdline[0] == self.python_executable: + _cmdline.pop(0) + return _cmdline + + +@attr.s(kw_only=True) +class DaemonPkgMixin(PkgMixin): + def __attrs_post_init__(self): + if not platform.is_windows() and self.salt_pkg_install.system_service: + if platform.is_darwin(): + self.write_launchd_conf() + else: + self.write_systemd_conf() + + def get_service_name(self): + return self.script_name + + def write_launchd_conf(self): + raise NotImplementedError + + def write_systemd_conf(self): + raise NotImplementedError + + +@attr.s(kw_only=True) +class SaltMaster(DaemonPkgMixin, master.SaltMaster): + """ + Subclassed just to tweak the binary paths if needed and factory classes. + """ + + def __attrs_post_init__(self): + self.script_name = "salt-master" + master.SaltMaster.__attrs_post_init__(self) + DaemonPkgMixin.__attrs_post_init__(self) + + def _get_impl_class(self): + if self.system_install and self.salt_pkg_install.system_service: + if platform.is_windows(): + return PkgSsmSaltDaemonImpl + if platform.is_darwin(): + return PkgLaunchdSaltDaemonImpl + return PkgSystemdSaltDaemonImpl + return DaemonImpl + + def write_launchd_conf(self): + self.salt_pkg_install.write_launchd_conf("master") + + def write_systemd_conf(self): + self.salt_pkg_install.write_systemd_conf( + "salt-master", self.salt_pkg_install.binary_paths["master"] + ) + + def salt_minion_daemon(self, minion_id, **kwargs): + return super().salt_minion_daemon( + minion_id, + factory_class=SaltMinion, + salt_pkg_install=self.salt_pkg_install, + **kwargs, + ) + + def salt_api_daemon(self, **kwargs): + return super().salt_api_daemon( + factory_class=SaltApi, salt_pkg_install=self.salt_pkg_install, **kwargs + ) + + def salt_key_cli(self, **factory_class_kwargs): + return super().salt_key_cli( + factory_class=SaltKey, + salt_pkg_install=self.salt_pkg_install, + **factory_class_kwargs, + ) + + def salt_cli(self, **factory_class_kwargs): + return super().salt_cli( + factory_class=SaltCli, + salt_pkg_install=self.salt_pkg_install, + **factory_class_kwargs, + ) + + +@attr.s(kw_only=True, slots=True) +class SaltMinion(DaemonPkgMixin, minion.SaltMinion): + """ + Subclassed just to tweak the binary paths if needed and factory classes. + """ + + def __attrs_post_init__(self): + self.script_name = "salt-minion" + minion.SaltMinion.__attrs_post_init__(self) + DaemonPkgMixin.__attrs_post_init__(self) + + def _get_impl_class(self): + if self.system_install and self.salt_pkg_install.system_service: + if platform.is_windows(): + return PkgSsmSaltDaemonImpl + if platform.is_darwin(): + return PkgLaunchdSaltDaemonImpl + return PkgSystemdSaltDaemonImpl + return DaemonImpl + + def write_launchd_conf(self): + self.salt_pkg_install.write_launchd_conf("minion") + + def write_systemd_conf(self): + self.salt_pkg_install.write_systemd_conf( + "salt-minion", self.salt_pkg_install.binary_paths["minion"] + ) + + def salt_call_cli(self, **factory_class_kwargs): + return super().salt_call_cli( + factory_class=SaltCall, + salt_pkg_install=self.salt_pkg_install, + **factory_class_kwargs, + ) + + +@attr.s(kw_only=True, slots=True) +class SaltApi(DaemonPkgMixin, api.SaltApi): + """ + Subclassed just to tweak the binary paths if needed. + """ + + def __attrs_post_init__(self): + self.script_name = "salt-api" + api.SaltApi.__attrs_post_init__(self) + DaemonPkgMixin.__attrs_post_init__(self) + + def _get_impl_class(self): + if self.system_install and self.salt_pkg_install.system_service: + if platform.is_windows(): + return PkgSsmSaltDaemonImpl + if platform.is_darwin(): + return PkgLaunchdSaltDaemonImpl + return PkgSystemdSaltDaemonImpl + return DaemonImpl + + def write_launchd_conf(self): + self.salt_pkg_install.write_launchd_conf("api") + + def write_systemd_conf(self): + self.salt_pkg_install.write_systemd_conf( + "salt-api", + self.salt_pkg_install.binary_paths["api"], + ) + + +@attr.s(kw_only=True, slots=True) +class SaltCall(PkgMixin, call.SaltCall): + """ + Subclassed just to tweak the binary paths if needed. + """ + + def __attrs_post_init__(self): + call.SaltCall.__attrs_post_init__(self) + self.script_name = "salt-call" + + +@attr.s(kw_only=True, slots=True) +class SaltCli(PkgMixin, salt.SaltCli): + """ + Subclassed just to tweak the binary paths if needed. + """ + + def __attrs_post_init__(self): + self.script_name = "salt" + salt.SaltCli.__attrs_post_init__(self) + + +@attr.s(kw_only=True, slots=True) +class SaltKey(PkgMixin, key.SaltKey): + """ + Subclassed just to tweak the binary paths if needed. + """ + + def __attrs_post_init__(self): + self.script_name = "salt-key" + key.SaltKey.__attrs_post_init__(self) + + +@attr.s(kw_only=True, slots=True) +class TestUser: + """ + Add a test user + """ + + salt_call_cli = attr.ib() + + username = attr.ib(default="saltdev") + # Must follow Windows Password Complexity requirements + password = attr.ib(default="P@ssW0rd") + _pw_record = attr.ib(init=False, repr=False, default=None) + + def salt_call_local(self, *args): + ret = self.salt_call_cli.run("--local", *args) + if ret.returncode != 0: + log.error(ret) + assert ret.returncode == 0 + return ret.data + + def add_user(self): + log.debug("Adding system account %r", self.username) + if platform.is_windows(): + self.salt_call_local("user.add", self.username, self.password) + else: + self.salt_call_local("user.add", self.username) + hash_passwd = crypt.crypt(self.password, crypt.mksalt(crypt.METHOD_SHA512)) + self.salt_call_local("shadow.set_password", self.username, hash_passwd) + assert self.username in self.salt_call_local("user.list_users") + + def remove_user(self): + log.debug("Removing system account %r", self.username) + if platform.is_windows(): + self.salt_call_local( + "user.delete", self.username, "purge=True", "force=True" + ) + else: + self.salt_call_local("user.delete", self.username, "remove=True") + + @property + def pw_record(self): + if self._pw_record is None and HAS_PWD: + self._pw_record = pwd.getpwnam(self.username) + return self._pw_record + + @property + def uid(self): + if HAS_PWD: + return self.pw_record.pw_uid + return None + + @property + def gid(self): + if HAS_PWD: + return self.pw_record.pw_gid + return None + + @property + def env(self): + environ = os.environ.copy() + environ["LOGNAME"] = environ["USER"] = self.username + environ["HOME"] = self.pw_record.pw_dir + return environ + + def __enter__(self): + self.add_user() + return self + + def __exit__(self, *_): + self.remove_user() + + +@attr.s(kw_only=True, slots=True) +class ApiRequest: + salt_api: SaltApi = attr.ib(repr=False) + test_account: TestUser = attr.ib(repr=False) + session: requests.Session = attr.ib(init=False, repr=False) + api_uri: str = attr.ib(init=False) + auth_data: Dict[str, str] = attr.ib(init=False) + + @session.default + def _default_session(self): + return requests.Session() + + @api_uri.default + def _default_api_uri(self): + return f"http://localhost:{self.salt_api.config['rest_cherrypy']['port']}" + + @auth_data.default + def _default_auth_data(self): + return { + "username": self.test_account.username, + "password": self.test_account.password, + "eauth": "auto", + "out": "json", + } + + def post(self, url, data): + post_data = dict(**self.auth_data, **data) + resp = self.session.post(f"{self.api_uri}/run", data=post_data).json() + minion = next(iter(resp["return"][0])) + return resp["return"][0][minion] + + def __enter__(self): + self.session.__enter__() + return self + + def __exit__(self, *args): + self.session.__exit__(*args) + + +@pytest.helpers.register +def remove_stale_minion_key(master, minion_id): + key_path = os.path.join(master.config["pki_dir"], "minions", minion_id) + if os.path.exists(key_path): + os.unlink(key_path) + else: + log.debug("The minion(id=%r) key was not found at %s", minion_id, key_path) + + +@pytest.helpers.register +def remove_stale_master_key(master): + keys_path = os.path.join(master.config["pki_dir"], "master") + for key_name in ("master.pem", "master.pub"): + key_path = os.path.join(keys_path, key_name) + if os.path.exists(key_path): + os.unlink(key_path) + else: + log.debug( + "The master(id=%r) %s key was not found at %s", + master.id, + key_name, + key_path, + ) + key_path = os.path.join(master.config["pki_dir"], "minion", "minion_master.pub") + if os.path.exists(key_path): + os.unlink(key_path) + else: + log.debug( + "The master(id=%r) minion_master.pub key was not found at %s", + master.id, + key_path, + ) diff --git a/pkg/tests/upgrade/test_salt_upgrade.py b/pkg/tests/upgrade/test_salt_upgrade.py new file mode 100644 index 00000000000..eb802e73427 --- /dev/null +++ b/pkg/tests/upgrade/test_salt_upgrade.py @@ -0,0 +1,70 @@ +import pytest + + +@pytest.mark.skip_on_windows( + reason="Salt Master scripts not included in old windows packages" +) +def test_salt_upgrade(salt_call_cli, salt_minion, install_salt): + """ + Test upgrade of Salt + """ + if not install_salt.upgrade: + pytest.skip("Not testing an upgrade, do not run") + # verify previous install version is setup correctly and works + ret = salt_call_cli.run("test.ping") + assert ret.returncode == 0 + assert ret.data + + # test pip install before an upgrade + dep = "PyGithub" + repo = "https://github.com/saltstack/salt.git" + install = salt_call_cli.run("--local", "pip.install", dep) + assert install.returncode == 0 + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "Authentication information could" in use_lib.stderr + # upgrade Salt from previous version and test + install_salt.install(upgrade=True) + ret = salt_call_cli.run("test.ping") + assert ret.returncode == 0 + assert ret.data + + # test pip install after an upgrade + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "Authentication information could" in use_lib.stderr + + +@pytest.mark.skip_unless_on_windows() +def test_salt_upgrade_windows_1(install_salt, salt_call_cli): + """ + Test upgrade of Salt on windows + """ + if not install_salt.upgrade: + pytest.skip("Not testing an upgrade, do not run") + # verify previous install version is setup correctly and works + ret = salt_call_cli.run("--local", "test.ping") + assert ret.data is True + assert ret.returncode == 0 + # test pip install before an upgrade + dep = "PyGithub" + repo = "https://github.com/saltstack/salt.git" + install = salt_call_cli.run("--local", "pip.install", dep) + assert install.returncode == 0 + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "Authentication information could" in use_lib.stderr + + +@pytest.mark.skip_unless_on_windows() +def test_salt_upgrade_windows_2(salt_call_cli, salt_minion, install_salt): + """ + Test upgrade of Salt on windows + """ + if install_salt.no_uninstall: + pytest.skip("Not testing an upgrade, do not run") + # upgrade Salt from previous version and test + install_salt.install(upgrade=True) + ret = salt_call_cli.run("test.ping") + assert ret.returncode == 0 + assert ret.data + repo = "https://github.com/saltstack/salt.git" + use_lib = salt_call_cli.run("--local", "github.get_repo_info", repo) + assert "Authentication information could" in use_lib.stderr diff --git a/requirements/static/ci/pkgtests.in b/requirements/static/ci/pkgtests.in new file mode 100644 index 00000000000..96daaa07175 --- /dev/null +++ b/requirements/static/ci/pkgtests.in @@ -0,0 +1,2 @@ +pytest-pudb +cherrypy diff --git a/requirements/static/ci/py3.10/pkgtests.txt b/requirements/static/ci/py3.10/pkgtests.txt new file mode 100644 index 00000000000..eefe2630ccb --- /dev/null +++ b/requirements/static/ci/py3.10/pkgtests.txt @@ -0,0 +1,204 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements/static/ci/py3.10/pkgtests.txt requirements/base.txt requirements/pytest.txt requirements/static/ci/pkgtests.in requirements/zeromq.txt +# +attrs==22.2.0 + # via + # pytest + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics +autocommand==2.2.2 + # via jaraco.text +certifi==2022.12.7 + # via requests +charset-normalizer==3.0.1 + # via requests +cheroot==9.0.0 + # via cherrypy +cherrypy==18.8.0 + # via + # -r requirements/base.txt + # -r requirements/static/ci/pkgtests.in +contextvars==2.4 + # via -r requirements/base.txt +distlib==0.3.6 + # via virtualenv +distro==1.8.0 + # via + # -r requirements/base.txt + # pytest-skip-markers +docker==6.0.1 + # via pytest-salt-factories +exceptiongroup==1.1.0 + # via pytest +filelock==3.9.0 + # via virtualenv +flaky==3.7.0 + # via -r requirements/pytest.txt +idna==3.4 + # via requests +immutables==0.19 + # via contextvars +inflect==6.0.2 + # via jaraco.text +iniconfig==2.0.0 + # via pytest +jaraco.classes==3.2.3 + # via jaraco.collections +jaraco.collections==3.8.0 + # via cherrypy +jaraco.context==4.2.0 + # via jaraco.text +jaraco.functools==3.5.2 + # via + # cheroot + # jaraco.text +jaraco.text==3.11.0 + # via jaraco.collections +jedi==0.18.2 + # via pudb +jinja2==3.1.2 + # via -r requirements/base.txt +jmespath==1.0.1 + # via -r requirements/base.txt +looseversion==1.0.3 + # via -r requirements/base.txt +markupsafe==2.1.1 + # via + # -r requirements/base.txt + # jinja2 + # werkzeug +mock==5.0.1 + # via -r requirements/pytest.txt +more-itertools==9.0.0 + # via + # cheroot + # cherrypy + # jaraco.classes + # jaraco.functools + # jaraco.text +msgpack==1.0.4 + # via + # -r requirements/base.txt + # pytest-salt-factories +packaging==23.0 + # via + # -r requirements/base.txt + # docker + # pudb + # pytest +parso==0.8.3 + # via jedi +platformdirs==2.6.2 + # via virtualenv +pluggy==1.0.0 + # via pytest +portend==3.1.0 + # via cherrypy +psutil==5.9.4 + # via + # -r requirements/base.txt + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pudb==2022.1.3 + # via pytest-pudb +pycryptodomex==3.16.0 + # via -r requirements/crypto.txt +pydantic==1.10.4 + # via inflect +pygments==2.14.0 + # via pudb +pytest-custom-exit-code==0.3.0 + # via -r requirements/pytest.txt +pytest-helpers-namespace==2021.12.29 + # via + # -r requirements/pytest.txt + # pytest-salt-factories + # pytest-shell-utilities +pytest-httpserver==1.0.6 + # via -r requirements/pytest.txt +pytest-pudb==0.7.0 + # via -r requirements/static/ci/pkgtests.in +pytest-salt-factories[docker]==1.0.0rc23 ; sys_platform != "win32" + # via -r requirements/pytest.txt +pytest-shell-utilities==1.7.0 + # via pytest-salt-factories +pytest-skip-markers==1.4.0 + # via + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pytest-subtests==0.9.0 + # via -r requirements/pytest.txt +pytest-system-statistics==1.0.2 + # via pytest-salt-factories +pytest-tempdir==2019.10.12 + # via + # -r requirements/pytest.txt + # pytest-salt-factories +pytest-timeout==2.1.0 + # via -r requirements/pytest.txt +pytest==7.2.1 ; python_version > "3.6" + # via + # -r requirements/pytest.txt + # pytest-custom-exit-code + # pytest-helpers-namespace + # pytest-pudb + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-subtests + # pytest-system-statistics + # pytest-tempdir + # pytest-timeout +pytz==2022.7.1 + # via tempora +pyyaml==6.0 + # via + # -r requirements/base.txt + # pytest-salt-factories +pyzmq==25.0.0 ; python_version >= "3.9" + # via + # -r requirements/zeromq.txt + # pytest-salt-factories +requests==2.28.2 + # via + # -r requirements/base.txt + # docker +six==1.16.0 + # via cheroot +tempora==5.2.0 + # via portend +tomli==2.0.1 + # via pytest +typing-extensions==4.4.0 + # via + # pydantic + # pytest-shell-utilities + # pytest-system-statistics +urllib3==1.26.14 + # via + # docker + # requests +urwid-readline==0.13 + # via pudb +urwid==2.1.2 + # via + # pudb + # urwid-readline +virtualenv==20.17.1 + # via pytest-salt-factories +websocket-client==1.4.2 + # via docker +werkzeug==2.2.2 + # via pytest-httpserver +zc.lockfile==2.0 + # via cherrypy + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/static/ci/py3.7/pkgtests.txt b/requirements/static/ci/py3.7/pkgtests.txt new file mode 100644 index 00000000000..814089fe42a --- /dev/null +++ b/requirements/static/ci/py3.7/pkgtests.txt @@ -0,0 +1,219 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements/static/ci/py3.7/pkgtests.txt requirements/base.txt requirements/pytest.txt requirements/static/ci/pkgtests.in requirements/zeromq.txt +# +attrs==22.2.0 + # via + # pytest + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics +autocommand==2.2.2 + # via jaraco.text +certifi==2022.12.7 + # via requests +charset-normalizer==3.0.1 + # via requests +cheroot==9.0.0 + # via cherrypy +cherrypy==18.8.0 + # via + # -r requirements/base.txt + # -r requirements/static/ci/pkgtests.in +contextvars==2.4 + # via -r requirements/base.txt +distlib==0.3.6 + # via virtualenv +distro==1.8.0 + # via + # -r requirements/base.txt + # pytest-skip-markers +docker==6.0.1 + # via pytest-salt-factories +exceptiongroup==1.1.0 + # via pytest +filelock==3.9.0 + # via virtualenv +flaky==3.7.0 + # via -r requirements/pytest.txt +idna==3.4 + # via requests +immutables==0.19 + # via contextvars +importlib-metadata==6.0.0 + # via + # cheroot + # pluggy + # pytest + # virtualenv +importlib-resources==5.10.2 + # via jaraco.text +inflect==6.0.2 + # via jaraco.text +iniconfig==2.0.0 + # via pytest +jaraco.classes==3.2.3 + # via jaraco.collections +jaraco.collections==3.8.0 + # via cherrypy +jaraco.context==4.2.0 + # via jaraco.text +jaraco.functools==3.5.2 + # via + # cheroot + # jaraco.text +jaraco.text==3.11.0 + # via jaraco.collections +jedi==0.18.2 + # via pudb +jinja2==3.1.2 + # via -r requirements/base.txt +jmespath==1.0.1 + # via -r requirements/base.txt +looseversion==1.0.3 + # via -r requirements/base.txt +markupsafe==2.1.1 + # via + # -r requirements/base.txt + # jinja2 + # werkzeug +mock==5.0.1 + # via -r requirements/pytest.txt +more-itertools==9.0.0 + # via + # cheroot + # cherrypy + # jaraco.classes + # jaraco.functools + # jaraco.text +msgpack==1.0.4 + # via + # -r requirements/base.txt + # pytest-salt-factories +packaging==23.0 + # via + # -r requirements/base.txt + # docker + # pudb + # pytest +parso==0.8.3 + # via jedi +platformdirs==2.6.2 + # via virtualenv +pluggy==1.0.0 + # via pytest +portend==3.1.0 + # via cherrypy +psutil==5.9.4 + # via + # -r requirements/base.txt + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pudb==2022.1.3 + # via pytest-pudb +pycryptodomex==3.16.0 + # via -r requirements/crypto.txt +pydantic==1.10.4 + # via inflect +pygments==2.14.0 + # via pudb +pytest-custom-exit-code==0.3.0 + # via -r requirements/pytest.txt +pytest-helpers-namespace==2021.12.29 + # via + # -r requirements/pytest.txt + # pytest-salt-factories + # pytest-shell-utilities +pytest-httpserver==1.0.6 + # via -r requirements/pytest.txt +pytest-pudb==0.7.0 + # via -r requirements/static/ci/pkgtests.in +pytest-salt-factories[docker]==1.0.0rc23 ; sys_platform != "win32" + # via -r requirements/pytest.txt +pytest-shell-utilities==1.7.0 + # via pytest-salt-factories +pytest-skip-markers==1.4.0 + # via + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pytest-subtests==0.9.0 + # via -r requirements/pytest.txt +pytest-system-statistics==1.0.2 + # via pytest-salt-factories +pytest-tempdir==2019.10.12 + # via + # -r requirements/pytest.txt + # pytest-salt-factories +pytest-timeout==2.1.0 + # via -r requirements/pytest.txt +pytest==7.2.1 ; python_version > "3.6" + # via + # -r requirements/pytest.txt + # pytest-custom-exit-code + # pytest-helpers-namespace + # pytest-pudb + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-subtests + # pytest-system-statistics + # pytest-tempdir + # pytest-timeout +pytz==2022.7.1 + # via tempora +pyyaml==6.0 + # via + # -r requirements/base.txt + # pytest-salt-factories +pyzmq==25.0.0 ; python_version < "3.9" + # via + # -r requirements/zeromq.txt + # pytest-salt-factories +requests==2.28.2 + # via + # -r requirements/base.txt + # docker +six==1.16.0 + # via cheroot +tempora==5.2.0 + # via portend +tomli==2.0.1 + # via pytest +typing-extensions==4.4.0 + # via + # immutables + # importlib-metadata + # platformdirs + # pydantic + # pytest-shell-utilities + # pytest-system-statistics +urllib3==1.26.14 + # via + # docker + # requests +urwid-readline==0.13 + # via pudb +urwid==2.1.2 + # via + # pudb + # urwid-readline +virtualenv==20.17.1 + # via pytest-salt-factories +websocket-client==1.4.2 + # via docker +werkzeug==2.2.2 + # via pytest-httpserver +zc.lockfile==2.0 + # via cherrypy +zipp==3.11.0 + # via + # importlib-metadata + # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/static/ci/py3.8/pkgtests.txt b/requirements/static/ci/py3.8/pkgtests.txt new file mode 100644 index 00000000000..8f2a1d87ec2 --- /dev/null +++ b/requirements/static/ci/py3.8/pkgtests.txt @@ -0,0 +1,208 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements/static/ci/py3.8/pkgtests.txt requirements/base.txt requirements/pytest.txt requirements/static/ci/pkgtests.in requirements/zeromq.txt +# +attrs==22.2.0 + # via + # pytest + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics +autocommand==2.2.2 + # via jaraco.text +certifi==2022.12.7 + # via requests +charset-normalizer==3.0.1 + # via requests +cheroot==9.0.0 + # via cherrypy +cherrypy==18.8.0 + # via + # -r requirements/base.txt + # -r requirements/static/ci/pkgtests.in +contextvars==2.4 + # via -r requirements/base.txt +distlib==0.3.6 + # via virtualenv +distro==1.8.0 + # via + # -r requirements/base.txt + # pytest-skip-markers +docker==6.0.1 + # via pytest-salt-factories +exceptiongroup==1.1.0 + # via pytest +filelock==3.9.0 + # via virtualenv +flaky==3.7.0 + # via -r requirements/pytest.txt +idna==3.4 + # via requests +immutables==0.19 + # via contextvars +importlib-resources==5.10.2 + # via jaraco.text +inflect==6.0.2 + # via jaraco.text +iniconfig==2.0.0 + # via pytest +jaraco.classes==3.2.3 + # via jaraco.collections +jaraco.collections==3.8.0 + # via cherrypy +jaraco.context==4.2.0 + # via jaraco.text +jaraco.functools==3.5.2 + # via + # cheroot + # jaraco.text +jaraco.text==3.11.0 + # via jaraco.collections +jedi==0.18.2 + # via pudb +jinja2==3.1.2 + # via -r requirements/base.txt +jmespath==1.0.1 + # via -r requirements/base.txt +looseversion==1.0.3 + # via -r requirements/base.txt +markupsafe==2.1.1 + # via + # -r requirements/base.txt + # jinja2 + # werkzeug +mock==5.0.1 + # via -r requirements/pytest.txt +more-itertools==9.0.0 + # via + # cheroot + # cherrypy + # jaraco.classes + # jaraco.functools + # jaraco.text +msgpack==1.0.4 + # via + # -r requirements/base.txt + # pytest-salt-factories +packaging==23.0 + # via + # -r requirements/base.txt + # docker + # pudb + # pytest +parso==0.8.3 + # via jedi +platformdirs==2.6.2 + # via virtualenv +pluggy==1.0.0 + # via pytest +portend==3.1.0 + # via cherrypy +psutil==5.9.4 + # via + # -r requirements/base.txt + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pudb==2022.1.3 + # via pytest-pudb +pycryptodomex==3.16.0 + # via -r requirements/crypto.txt +pydantic==1.10.4 + # via inflect +pygments==2.14.0 + # via pudb +pytest-custom-exit-code==0.3.0 + # via -r requirements/pytest.txt +pytest-helpers-namespace==2021.12.29 + # via + # -r requirements/pytest.txt + # pytest-salt-factories + # pytest-shell-utilities +pytest-httpserver==1.0.6 + # via -r requirements/pytest.txt +pytest-pudb==0.7.0 + # via -r requirements/static/ci/pkgtests.in +pytest-salt-factories[docker]==1.0.0rc23 ; sys_platform != "win32" + # via -r requirements/pytest.txt +pytest-shell-utilities==1.7.0 + # via pytest-salt-factories +pytest-skip-markers==1.4.0 + # via + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pytest-subtests==0.9.0 + # via -r requirements/pytest.txt +pytest-system-statistics==1.0.2 + # via pytest-salt-factories +pytest-tempdir==2019.10.12 + # via + # -r requirements/pytest.txt + # pytest-salt-factories +pytest-timeout==2.1.0 + # via -r requirements/pytest.txt +pytest==7.2.1 ; python_version > "3.6" + # via + # -r requirements/pytest.txt + # pytest-custom-exit-code + # pytest-helpers-namespace + # pytest-pudb + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-subtests + # pytest-system-statistics + # pytest-tempdir + # pytest-timeout +pytz==2022.7.1 + # via tempora +pyyaml==6.0 + # via + # -r requirements/base.txt + # pytest-salt-factories +pyzmq==25.0.0 ; python_version < "3.9" + # via + # -r requirements/zeromq.txt + # pytest-salt-factories +requests==2.28.2 + # via + # -r requirements/base.txt + # docker +six==1.16.0 + # via cheroot +tempora==5.2.0 + # via portend +tomli==2.0.1 + # via pytest +typing-extensions==4.4.0 + # via + # pydantic + # pytest-shell-utilities + # pytest-system-statistics +urllib3==1.26.14 + # via + # docker + # requests +urwid-readline==0.13 + # via pudb +urwid==2.1.2 + # via + # pudb + # urwid-readline +virtualenv==20.17.1 + # via pytest-salt-factories +websocket-client==1.4.2 + # via docker +werkzeug==2.2.2 + # via pytest-httpserver +zc.lockfile==2.0 + # via cherrypy +zipp==3.11.0 + # via importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/static/ci/py3.9/pkgtests.txt b/requirements/static/ci/py3.9/pkgtests.txt new file mode 100644 index 00000000000..f8094150671 --- /dev/null +++ b/requirements/static/ci/py3.9/pkgtests.txt @@ -0,0 +1,204 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements/static/ci/py3.9/pkgtests.txt requirements/base.txt requirements/pytest.txt requirements/static/ci/pkgtests.in requirements/zeromq.txt +# +attrs==22.2.0 + # via + # pytest + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-system-statistics +autocommand==2.2.2 + # via jaraco.text +certifi==2022.12.7 + # via requests +charset-normalizer==3.0.1 + # via requests +cheroot==9.0.0 + # via cherrypy +cherrypy==18.8.0 + # via + # -r requirements/base.txt + # -r requirements/static/ci/pkgtests.in +contextvars==2.4 + # via -r requirements/base.txt +distlib==0.3.6 + # via virtualenv +distro==1.8.0 + # via + # -r requirements/base.txt + # pytest-skip-markers +docker==6.0.1 + # via pytest-salt-factories +exceptiongroup==1.1.0 + # via pytest +filelock==3.9.0 + # via virtualenv +flaky==3.7.0 + # via -r requirements/pytest.txt +idna==3.4 + # via requests +immutables==0.19 + # via contextvars +inflect==6.0.2 + # via jaraco.text +iniconfig==2.0.0 + # via pytest +jaraco.classes==3.2.3 + # via jaraco.collections +jaraco.collections==3.8.0 + # via cherrypy +jaraco.context==4.2.0 + # via jaraco.text +jaraco.functools==3.5.2 + # via + # cheroot + # jaraco.text +jaraco.text==3.11.0 + # via jaraco.collections +jedi==0.18.2 + # via pudb +jinja2==3.1.2 + # via -r requirements/base.txt +jmespath==1.0.1 + # via -r requirements/base.txt +looseversion==1.0.3 + # via -r requirements/base.txt +markupsafe==2.1.1 + # via + # -r requirements/base.txt + # jinja2 + # werkzeug +mock==5.0.1 + # via -r requirements/pytest.txt +more-itertools==9.0.0 + # via + # cheroot + # cherrypy + # jaraco.classes + # jaraco.functools + # jaraco.text +msgpack==1.0.4 + # via + # -r requirements/base.txt + # pytest-salt-factories +packaging==23.0 + # via + # -r requirements/base.txt + # docker + # pudb + # pytest +parso==0.8.3 + # via jedi +platformdirs==2.6.2 + # via virtualenv +pluggy==1.0.0 + # via pytest +portend==3.1.0 + # via cherrypy +psutil==5.9.4 + # via + # -r requirements/base.txt + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pudb==2022.1.3 + # via pytest-pudb +pycryptodomex==3.16.0 + # via -r requirements/crypto.txt +pydantic==1.10.4 + # via inflect +pygments==2.14.0 + # via pudb +pytest-custom-exit-code==0.3.0 + # via -r requirements/pytest.txt +pytest-helpers-namespace==2021.12.29 + # via + # -r requirements/pytest.txt + # pytest-salt-factories + # pytest-shell-utilities +pytest-httpserver==1.0.6 + # via -r requirements/pytest.txt +pytest-pudb==0.7.0 + # via -r requirements/static/ci/pkgtests.in +pytest-salt-factories[docker]==1.0.0rc23 ; sys_platform != "win32" + # via -r requirements/pytest.txt +pytest-shell-utilities==1.7.0 + # via pytest-salt-factories +pytest-skip-markers==1.4.0 + # via + # pytest-salt-factories + # pytest-shell-utilities + # pytest-system-statistics +pytest-subtests==0.9.0 + # via -r requirements/pytest.txt +pytest-system-statistics==1.0.2 + # via pytest-salt-factories +pytest-tempdir==2019.10.12 + # via + # -r requirements/pytest.txt + # pytest-salt-factories +pytest-timeout==2.1.0 + # via -r requirements/pytest.txt +pytest==7.2.1 ; python_version > "3.6" + # via + # -r requirements/pytest.txt + # pytest-custom-exit-code + # pytest-helpers-namespace + # pytest-pudb + # pytest-salt-factories + # pytest-shell-utilities + # pytest-skip-markers + # pytest-subtests + # pytest-system-statistics + # pytest-tempdir + # pytest-timeout +pytz==2022.7.1 + # via tempora +pyyaml==6.0 + # via + # -r requirements/base.txt + # pytest-salt-factories +pyzmq==25.0.0 ; python_version >= "3.9" + # via + # -r requirements/zeromq.txt + # pytest-salt-factories +requests==2.28.2 + # via + # -r requirements/base.txt + # docker +six==1.16.0 + # via cheroot +tempora==5.2.0 + # via portend +tomli==2.0.1 + # via pytest +typing-extensions==4.4.0 + # via + # pydantic + # pytest-shell-utilities + # pytest-system-statistics +urllib3==1.26.14 + # via + # docker + # requests +urwid-readline==0.13 + # via pudb +urwid==2.1.2 + # via + # pudb + # urwid-readline +virtualenv==20.17.1 + # via pytest-salt-factories +websocket-client==1.4.2 + # via docker +werkzeug==2.2.2 + # via pytest-httpserver +zc.lockfile==2.0 + # via cherrypy + +# The following packages are considered to be unsafe in a requirements file: +# setuptools