salt/tests/pytests/pkg/conftest.py
2024-05-09 16:39:56 +01:00

558 lines
18 KiB
Python

import logging
import os
import pathlib
import shutil
import subprocess
import sys
import pytest
import yaml
from pytestskipmarkers.utils import platform
from saltfactories.utils import random_string
import salt.config
import salt.utils.files
from tests.conftest import CODE_DIR
from tests.support.pkg import ApiRequest, SaltMaster, SaltMasterWindows, SaltPkgInstall
log = logging.getLogger(__name__)
# Variable defining a FIPS test run or not
FIPS_TESTRUN = os.environ.get("FIPS_TESTRUN", "0") == "1"
@pytest.fixture(scope="session")
def version(install_salt):
"""
get version number from artifact
"""
return install_salt.version
@pytest.fixture(scope="session", autouse=True)
def _system_up_to_date(
grains,
shell,
):
if grains["os_family"] == "Debian":
ret = shell.run("apt", "update")
assert ret.returncode == 0
env = os.environ.copy()
env["DEBIAN_FRONTEND"] = "noninteractive"
ret = shell.run(
"apt",
"upgrade",
"-y",
"-o",
"DPkg::Options::=--force-confdef",
"-o",
"DPkg::Options::=--force-confold",
env=env,
)
assert ret.returncode == 0
elif grains["os_family"] == "Redhat":
ret = shell.run("yum", "update", "-y")
assert ret.returncode == 0
def pytest_addoption(parser):
"""
register argparse-style options and ini-style config values.
"""
test_selection_group = parser.getgroup("Tests Runtime Selection")
test_selection_group.addoption(
"--pkg-system-service",
default=False,
action="store_true",
help="Run the daemons as system services",
)
test_selection_group.addoption(
"--upgrade",
default=False,
action="store_true",
help="Install previous version and then upgrade then run tests",
)
test_selection_group.addoption(
"--downgrade",
default=False,
action="store_true",
help="Install current version and then downgrade to the previous version and run tests",
)
test_selection_group.addoption(
"--no-install",
default=False,
action="store_true",
help="Do not install salt and use a previous install Salt package",
)
test_selection_group.addoption(
"--no-uninstall",
default=False,
action="store_true",
help="Do not uninstall salt packages after test run is complete",
)
test_selection_group.addoption(
"--classic",
default=False,
action="store_true",
help="Test an upgrade from the classic packages.",
)
test_selection_group.addoption(
"--prev-version",
action="store",
help="Test an upgrade from the version specified.",
)
test_selection_group.addoption(
"--use-prev-version",
action="store_true",
help="Tells the test suite to validate the version using the previous version (for downgrades)",
)
test_selection_group.addoption(
"--download-pkgs",
default=False,
action="store_true",
help="Test package download tests",
)
@pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_collection_modifyitems(config, items):
"""
called after collection has been performed, may filter or re-order
the items in-place.
:param _pytest.main.Session session: the pytest session object
:param _pytest.config.Config config: pytest config object
:param List[_pytest.nodes.Item] items: list of item objects
"""
# Let PyTest or other plugins handle the initial collection
yield
selected = []
deselected = []
pkg_tests_path = pathlib.Path(__file__).parent
if config.getoption("--upgrade"):
for item in items:
if str(item.fspath).startswith(str(pkg_tests_path / "upgrade")):
selected.append(item)
else:
deselected.append(item)
elif config.getoption("--downgrade"):
for item in items:
if str(item.fspath).startswith(str(pkg_tests_path / "downgrade")):
selected.append(item)
else:
deselected.append(item)
elif config.getoption("--download-pkgs"):
for item in items:
if str(item.fspath).startswith(str(pkg_tests_path / "download")):
selected.append(item)
else:
deselected.append(item)
else:
exclude_paths = (
str(pkg_tests_path / "upgrade"),
str(pkg_tests_path / "downgrade"),
str(pkg_tests_path / "download"),
)
for item in items:
if str(item.fspath).startswith(exclude_paths):
deselected.append(item)
else:
selected.append(item)
if deselected:
# Selection changed
items[:] = selected
config.hook.pytest_deselected(items=deselected)
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_setup(item):
"""
Fixtures injection based on markers or test skips based on CLI arguments
"""
pkg_tests_path = pathlib.Path(__file__).parent
if (
str(item.fspath).startswith(str(pkg_tests_path / "download"))
and item.config.getoption("--download-pkgs") is False
):
raise pytest.skip.Exception(
"The package download tests are disabled. Pass '--download-pkgs' to pytest "
"to enable them.",
_use_item_location=True,
)
for key in ("upgrade", "downgrade"):
if (
str(item.fspath).startswith(str(pkg_tests_path / key))
and item.config.getoption(f"--{key}") is False
):
raise pytest.skip.Exception(
f"The package {key} tests are disabled. Pass '--{key}' to pytest "
"to enable them.",
_use_item_location=True,
)
@pytest.fixture(scope="session")
def salt_factories_root_dir(request, tmp_path_factory):
root_dir = SaltPkgInstall.salt_factories_root_dir(
request.config.getoption("--pkg-system-service")
)
if root_dir is not None:
yield root_dir
else:
if platform.is_darwin():
root_dir = pathlib.Path("/tmp/salt-tests-tmpdir")
root_dir.mkdir(mode=0o777, parents=True, exist_ok=True)
else:
root_dir = tmp_path_factory.mktemp("salt-tests")
try:
yield root_dir
finally:
shutil.rmtree(str(root_dir), ignore_errors=True)
@pytest.fixture(scope="session")
def salt_factories_config(salt_factories_root_dir):
return {
"code_dir": CODE_DIR,
"root_dir": salt_factories_root_dir,
"system_service": True,
}
@pytest.fixture(scope="session")
def install_salt(request, salt_factories_root_dir):
with SaltPkgInstall(
conf_dir=salt_factories_root_dir / "etc" / "salt",
pkg_system_service=request.config.getoption("--pkg-system-service"),
upgrade=request.config.getoption("--upgrade"),
downgrade=request.config.getoption("--downgrade"),
no_uninstall=request.config.getoption("--no-uninstall"),
no_install=request.config.getoption("--no-install"),
classic=request.config.getoption("--classic"),
prev_version=request.config.getoption("--prev-version"),
use_prev_version=request.config.getoption("--use-prev-version"),
) as fixture:
yield fixture
@pytest.fixture(scope="session")
def salt_factories(salt_factories, salt_factories_root_dir):
salt_factories.root_dir = salt_factories_root_dir
return salt_factories
@pytest.fixture(scope="session")
def salt_master(salt_factories, install_salt, pkg_tests_account):
"""
Start up a master
"""
if platform.is_windows():
state_tree = "C:/salt/srv/salt"
pillar_tree = "C:/salt/srv/pillar"
elif platform.is_darwin():
state_tree = "/opt/srv/salt"
pillar_tree = "/opt/srv/pillar"
else:
state_tree = "/srv/salt"
pillar_tree = "/srv/pillar"
start_timeout = None
# Since the daemons are "packaged" with tiamat, the salt plugins provided
# by salt-factories won't be discovered. Provide the required `*_dirs` on
# the configuration so that they can still be used.
config_defaults = {
"engines_dirs": [
str(salt_factories.get_salt_engines_path()),
],
"log_handlers_dirs": [
str(salt_factories.get_salt_log_handlers_path()),
],
}
if platform.is_darwin():
config_defaults["enable_fqdns_grains"] = False
config_overrides = {
"timeout": 30,
"file_roots": {
"base": [
state_tree,
]
},
"pillar_roots": {
"base": [
pillar_tree,
]
},
"rest_cherrypy": {
"port": 8000,
"disable_ssl": True,
},
"netapi_enable_clients": ["local"],
"external_auth": {
"auto": {
pkg_tests_account.username: [
".*",
],
},
},
"fips_mode": FIPS_TESTRUN,
"open_mode": True,
}
salt_user_in_config_file = False
master_config = install_salt.config_path / "master"
if master_config.exists() and master_config.stat().st_size:
with salt.utils.files.fopen(master_config) as fp:
data = yaml.safe_load(fp)
if data is None:
# File exists but is mostly commented out
data = {}
user_in_config_file = data.get("user")
if user_in_config_file and user_in_config_file == "salt":
salt_user_in_config_file = True
# We are testing a different user, so we need to test the system
# configs, or else permissions will not be correct.
config_overrides["user"] = user_in_config_file
config_overrides["log_file"] = salt.config.DEFAULT_MASTER_OPTS.get(
"log_file"
)
config_overrides["root_dir"] = salt.config.DEFAULT_MASTER_OPTS.get(
"root_dir"
)
config_overrides["key_logfile"] = salt.config.DEFAULT_MASTER_OPTS.get(
"key_logfile"
)
config_overrides["pki_dir"] = salt.config.DEFAULT_MASTER_OPTS.get(
"pki_dir"
)
config_overrides["api_logfile"] = salt.config.DEFAULT_API_OPTS.get(
"api_logfile"
)
config_overrides["api_pidfile"] = salt.config.DEFAULT_API_OPTS.get(
"api_pidfile"
)
# verify files were set with correct owner/group
verify_files = [
pathlib.Path("/etc", "salt", "pki", "master"),
pathlib.Path("/etc", "salt", "master.d"),
pathlib.Path("/var", "cache", "salt", "master"),
]
for _file in verify_files:
if _file.owner() != "salt":
log.warning(
"The owner of '%s' is '%s' when it should be 'salt'",
_file,
_file.owner(),
)
if _file.group() != "salt":
log.warning(
"The group of '%s' is '%s' when it should be 'salt'",
_file,
_file.group(),
)
master_script = False
if platform.is_windows():
if install_salt.classic:
master_script = True
if install_salt.relenv:
master_script = True
elif not install_salt.upgrade:
master_script = True
if (
not install_salt.relenv
and install_salt.use_prev_version
and not install_salt.classic
):
master_script = False
if master_script:
salt_factories.system_service = False
salt_factories.generate_scripts = True
scripts_dir = salt_factories.root_dir / "Scripts"
scripts_dir.mkdir(exist_ok=True)
salt_factories.scripts_dir = scripts_dir
python_executable = install_salt.bin_dir / "Scripts" / "python.exe"
if install_salt.classic:
python_executable = install_salt.bin_dir / "python.exe"
if install_salt.relenv:
python_executable = install_salt.install_dir / "Scripts" / "python.exe"
salt_factories.python_executable = python_executable
factory = salt_factories.salt_master_daemon(
random_string("master-"),
defaults=config_defaults,
overrides=config_overrides,
factory_class=SaltMasterWindows,
salt_pkg_install=install_salt,
)
salt_factories.system_service = True
else:
if install_salt.classic and platform.is_darwin():
os.environ["PATH"] += ":/opt/salt/bin"
factory = salt_factories.salt_master_daemon(
random_string("master-"),
defaults=config_defaults,
overrides=config_overrides,
factory_class=SaltMaster,
salt_pkg_install=install_salt,
)
factory.after_terminate(pytest.helpers.remove_stale_master_key, factory)
if salt_user_in_config_file:
# Salt factories calls salt.utils.verify.verify_env
# which sets root perms on /etc/salt/pki/master since we are running
# the test suite as root, but we want to run Salt master as salt
# We ensure those permissions where set by the package earlier
subprocess.run(
[
"chown",
"-R",
"salt:salt",
str(pathlib.Path("/etc", "salt", "pki", "master")),
],
check=True,
)
if not platform.is_windows() and not platform.is_darwin():
# The engines_dirs is created in .nox path. We need to set correct perms
# for the user running the Salt Master
check_paths = [state_tree, pillar_tree, CODE_DIR / ".nox"]
for path in check_paths:
if os.path.exists(path) is False:
continue
subprocess.run(["chown", "-R", "salt:salt", str(path)], check=False)
with factory.started(start_timeout=start_timeout):
yield factory
@pytest.fixture(scope="session")
def salt_minion(salt_factories, salt_master, install_salt):
"""
Start up a minion
"""
start_timeout = None
minion_id = random_string("minion-")
# Since the daemons are "packaged" with tiamat, the salt plugins provided
# by salt-factories won't be discovered. Provide the required `*_dirs` on
# the configuration so that they can still be used.
config_defaults = {
"engines_dirs": salt_master.config["engines_dirs"].copy(),
"log_handlers_dirs": salt_master.config["log_handlers_dirs"].copy(),
}
if platform.is_darwin():
config_defaults["enable_fqdns_grains"] = False
config_overrides = {
"id": minion_id,
"file_roots": salt_master.config["file_roots"].copy(),
"pillar_roots": salt_master.config["pillar_roots"].copy(),
"fips_mode": FIPS_TESTRUN,
"open_mode": True,
}
if platform.is_windows():
config_overrides["winrepo_dir"] = (
rf"{salt_factories.root_dir}\srv\salt\win\repo"
)
config_overrides["winrepo_dir_ng"] = (
rf"{salt_factories.root_dir}\srv\salt\win\repo_ng"
)
config_overrides["winrepo_source_dir"] = r"salt://win/repo_ng"
if install_salt.classic and platform.is_windows():
salt_factories.python_executable = None
if install_salt.classic and platform.is_darwin():
os.environ["PATH"] += ":/opt/salt/bin"
factory = salt_master.salt_minion_daemon(
minion_id,
overrides=config_overrides,
defaults=config_defaults,
)
# Salt factories calls salt.utils.verify.verify_env
# which sets root perms on /srv/salt and /srv/pillar since we are running
# the test suite as root, but we want to run Salt master as salt
if not platform.is_windows() and not platform.is_darwin():
import pwd
try:
pwd.getpwnam("salt")
except KeyError:
# The salt user does not exist
pass
else:
state_tree = "/srv/salt"
pillar_tree = "/srv/pillar"
check_paths = [state_tree, pillar_tree, CODE_DIR / ".nox"]
for path in check_paths:
if os.path.exists(path) is False:
continue
subprocess.run(["chown", "-R", "salt:salt", str(path)], check=False)
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="session")
def pkg_tests_account():
with pytest.helpers.create_account() as account:
yield account
@pytest.fixture(scope="module")
def extras_pypath():
extras_dir = "extras-{}.{}".format(*sys.version_info)
if platform.is_windows():
return pathlib.Path(
os.getenv("ProgramFiles"), "Salt Project", "Salt", extras_dir
)
elif platform.is_darwin():
return pathlib.Path("/opt", "salt", extras_dir)
else:
return pathlib.Path("/opt", "saltstack", "salt", extras_dir)
@pytest.fixture(scope="module")
def extras_pypath_bin(extras_pypath):
return extras_pypath / "bin"
@pytest.fixture(scope="module")
def salt_api(salt_master, install_salt, extras_pypath):
"""
start up and configure salt_api
"""
shutil.rmtree(str(extras_pypath), ignore_errors=True)
start_timeout = None
factory = salt_master.salt_api_daemon()
with factory.started(start_timeout=start_timeout):
yield factory
@pytest.fixture(scope="module")
def api_request(pkg_tests_account, salt_api):
with ApiRequest(
port=salt_api.config["rest_cherrypy"]["port"], account=pkg_tests_account
) as session:
yield session