diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml new file mode 100644 index 0000000..b7b33ad --- /dev/null +++ b/.github/workflows/flake8.yaml @@ -0,0 +1,21 @@ +name: flake8 lint +on: + push: + pull_request: + +jobs: + flake8-lint: + runs-on: ubuntu-20.04 + name: flake8 lint + steps: + - name: Setup python for flake8 + uses: actions/setup-python@v4 + with: + python-version: "3.8" + - uses: actions/checkout@v3 + - name: Install tox + run: python -m pip install tox + - name: Setup flake8 + run: tox --notest -e flake8 + - name: Run flake8 + run: tox -e flake8 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..2da9d9e --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,42 @@ +name: test +on: + push: + pull_request: + schedule: + - cron: "0 8 * * *" + +jobs: + test: + name: test ${{ matrix.py }} - ${{ matrix.netapi }} - ${{ matrix.salt }} + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + py: + - "3.7" + - "3.8" + netapi: + - "cherrypy" + - "tornado" + salt: + - "v3004.2" + - "v3005.1" + - "master" + steps: + - name: Setup python for test ${{ matrix.py }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.py }} + - uses: actions/checkout@v3 + - name: Install setuptools_scm + run: python -m pip install setuptools_scm + - name: Install tox + run: python -m pip install tox + - name: Install dependencies + run: sudo apt update && sudo apt install -y libc6-dev libffi-dev gcc git openssh-server libzmq3-dev + env: + DEBIAN_FRONTEND: noninteractive + - name: Setup tests + run: tox --notest -e py${{ matrix.py }}-${{ matrix.netapi }}-${{ matrix.salt }} + - name: Run tests + run: tox -e py${{ matrix.py }}-${{ matrix.netapi }}-${{ matrix.salt }} diff --git a/tests/conftest.py b/tests/conftest.py index 81d2662..5cf86aa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,6 @@ from __future__ import absolute_import, unicode_literals, print_function # Import python libraries -import distutils.spawn import logging import os.path import shutil @@ -11,48 +10,86 @@ import textwrap # Import Salt Libraries -import salt.client -import salt.config import salt.utils.yaml as yaml # Import pytest libraries import pytest -from pytestsalt.utils import SaltDaemonScriptBase, start_daemon, get_unused_localhost_port +from pytestskipmarkers.utils import ports +from saltfactories.utils import random_string, running_username # Import Pepper libraries import pepper import pepper.script -DEFAULT_MASTER_ID = 'pytest-salt-master' -DEFAULT_MINION_ID = 'pytest-salt-minion' log = logging.getLogger(__name__) @pytest.fixture(scope='session') -def install_sshd_server(): - if distutils.spawn.find_executable('sshd'): - return - __opts__ = salt.config.minion_config('tests/minion.conf') - __opts__['file_client'] = 'local' - caller = salt.client.Caller(mopts=__opts__) - caller.cmd('pkg.install', 'openssh-server') +def sshd_config_dir(salt_factories): + config_dir = salt_factories.get_root_dir_for_daemon("sshd") + yield config_dir + shutil.rmtree(str(config_dir), ignore_errors=True) -class SaltApi(SaltDaemonScriptBase): - ''' - Class which runs the salt-api daemon - ''' - - def get_script_args(self): - return ['-l', 'quiet'] +@pytest.fixture(scope='session') +def session_sshd_server(salt_factories, sshd_config_dir, session_master): + sshd_config_dict = { + "Protocol": "2", + # Turn strict modes off so that we can operate in /tmp + "StrictModes": "no", + # Logging + "SyslogFacility": "AUTH", + "LogLevel": "INFO", + # Authentication: + "LoginGraceTime": "120", + "PermitRootLogin": "without-password", + "PubkeyAuthentication": "yes", + # Don't read the user's ~/.rhosts and ~/.shosts files + "IgnoreRhosts": "yes", + "HostbasedAuthentication": "no", + # To enable empty passwords, change to yes (NOT RECOMMENDED) + "PermitEmptyPasswords": "no", + # Change to yes to enable challenge-response passwords (beware issues with + # some PAM modules and threads) + "ChallengeResponseAuthentication": "no", + # Change to no to disable tunnelled clear text passwords + "PasswordAuthentication": "no", + "X11Forwarding": "no", + "X11DisplayOffset": "10", + "PrintMotd": "no", + "PrintLastLog": "yes", + "TCPKeepAlive": "yes", + "AcceptEnv": "LANG LC_*", + "UsePAM": "yes", + } + factory = salt_factories.get_sshd_daemon( + sshd_config_dict=sshd_config_dict, + config_dir=sshd_config_dir, + ) + with factory.started(): + yield factory - def get_check_ports(self): - if 'rest_cherrypy' in self.config: - return [self.config['rest_cherrypy']['port']] - if 'rest_tornado' in self.config: - return [self.config['rest_tornado']['port']] +@pytest.fixture(scope='session') +def session_ssh_roster_config(session_sshd_server, session_master): + roster_contents = """ + localhost: + host: 127.0.0.1 + port: {} + user: {} + priv: {} + mine_functions: + test.arg: ['itworked'] + """.format( + session_sshd_server.listen_port, + running_username(), + session_sshd_server.client_key + ) + with pytest.helpers.temp_file( + "roster", roster_contents, session_master.config_dir + ) as roster_file: + yield roster_file @pytest.fixture(scope='session') @@ -60,7 +97,7 @@ def salt_api_port(): ''' Returns an unused localhost port for the api port ''' - return get_unused_localhost_port() + return ports.get_unused_localhost_port() @pytest.fixture(scope='session') @@ -121,7 +158,7 @@ def output_file(): @pytest.fixture(params=['/run', '/login']) -def pepper_cli(request, session_salt_api, salt_api_port, output_file, install_sshd_server, session_sshd_server): +def pepper_cli(request, session_salt_api, salt_api_port, output_file, session_sshd_server): ''' Wrapper to invoke Pepper with common params and inside an empty env ''' @@ -154,6 +191,20 @@ def _run_pepper_cli(*args, **kwargs): return _run_pepper_cli +@pytest.fixture(scope='session') +def session_master_factory(request, salt_factories, session_master_config_overrides): + return salt_factories.salt_master_daemon( + random_string("master-"), + overrides=session_master_config_overrides + ) + + +@pytest.fixture(scope='session') +def session_master(session_master_factory): + with session_master_factory.started(): + yield session_master_factory + + @pytest.fixture(scope='session') def session_master_config_overrides(request, salt_api_port, salt_api_backend): return { @@ -175,69 +226,40 @@ def session_master_config_overrides(request, salt_api_port, salt_api_backend): 'token_expire': 94670856, 'ignore_host_keys': True, 'ssh_wipe': True, + 'netapi_enable_clients': [ + 'local', + 'local_async', + 'local_subset', + 'ssh', + 'runner', + 'runner_async', + 'wheel', + 'wheel_async', + 'run' + ] } @pytest.fixture(scope='session') -def session_api_log_prefix(master_id): - return 'salt-api/{0}'.format(master_id) +def session_minion_factory(session_master_factory): + """Return a factory for a randomly named minion connected to master.""" + minion_factory = session_master_factory.salt_minion_daemon(random_string("minion-")) + minion_factory.after_terminate( + pytest.helpers.remove_stale_minion_key, session_master_factory, minion_factory.id + ) + return minion_factory @pytest.fixture(scope='session') -def cli_api_script_name(): - ''' - Return the CLI script basename - ''' - return 'salt-api' - - -@pytest.yield_fixture(scope='session') -def session_salt_api_before_start(): - ''' - This fixture should be overridden if you need to do - some preparation and clean up work before starting - the salt-api and after ending it. - ''' - # Prep routines go here - - # Start the salt-api - yield - - # Clean routines go here - - -@pytest.yield_fixture(scope='session') -def session_salt_api_after_start(session_salt_api): - ''' - This fixture should be overridden if you need to do - some preparation and clean up work after starting - the salt-api and before ending it. - ''' - # Prep routines go here - - # Resume test execution - yield - - # Clean routines go here +def session_minion(session_master, session_minion_factory): # noqa + assert session_master.is_running() + with session_minion_factory.started(): + yield session_minion_factory @pytest.fixture(scope='session') -def _salt_fail_hard(request, salt_fail_hard): - ''' - Return the salt fail hard value - ''' - fail_hard = request.config.getoption('salt_fail_hard') - if fail_hard is not None: - # We were passed --salt-fail-hard as a CLI option - return fail_hard - - # The salt fail hard was not passed as a CLI option - fail_hard = request.config.getini('salt_fail_hard') - if fail_hard != []: - # We were passed salt_fail_hard as a INI option - return fail_hard - - return salt_fail_hard +def session_minion_id(session_minion): + return session_minion.id @pytest.fixture(scope='session') @@ -257,91 +279,15 @@ def salt_api_backend(request): @pytest.fixture(scope='session') -def master_id(salt_master_id_counter): - ''' - Returns the master id - ''' - return DEFAULT_MASTER_ID + '-{0}'.format(salt_master_id_counter()) +def session_salt_api_factory(session_master_factory): + return session_master_factory.salt_api_daemon() @pytest.fixture(scope='session') -def minion_id(salt_minion_id_counter): - ''' - Returns the minion id - ''' - return DEFAULT_MINION_ID + '-{0}'.format(salt_minion_id_counter()) - - -@pytest.fixture(scope='session') -def session_salt_api(request, - session_salt_minion, - session_master_id, - session_master_config, - session_salt_api_before_start, # pylint: disable=unused-argument - session_api_log_prefix, - cli_api_script_name, - log_server, - _cli_bin_dir, - session_conf_dir): - ''' - Returns a running salt-api - ''' - return start_daemon(request, - daemon_name='salt-api', - daemon_id=session_master_id, - daemon_log_prefix=session_api_log_prefix, - daemon_cli_script_name=cli_api_script_name, - daemon_config=session_master_config, - daemon_config_dir=session_conf_dir, - daemon_class=SaltApi, - bin_dir_path=_cli_bin_dir, - start_timeout=30) - - -@pytest.fixture(scope='session') -def session_sshd_config_lines(session_sshd_port): - ''' - Return a list of lines which will make the sshd_config file - ''' - return [ - 'Port {0}'.format(session_sshd_port), - 'ListenAddress 127.0.0.1', - 'Protocol 2', - 'UsePrivilegeSeparation yes', - '# Turn strict modes off so that we can operate in /tmp', - 'StrictModes no', - '# Logging', - 'SyslogFacility AUTH', - 'LogLevel INFO', - '# Authentication:', - 'LoginGraceTime 120', - 'PermitRootLogin without-password', - 'StrictModes yes', - 'PubkeyAuthentication yes', - '#AuthorizedKeysFile %h/.ssh/authorized_keys', - '#AuthorizedKeysFile key_test.pub', - '# Don\'t read the user\'s ~/.rhosts and ~/.shosts files', - 'IgnoreRhosts yes', - '# similar for protocol version 2', - 'HostbasedAuthentication no', - '#IgnoreUserKnownHosts yes', - '# To enable empty passwords, change to yes (NOT RECOMMENDED)', - 'PermitEmptyPasswords no', - '# Change to yes to enable challenge-response passwords (beware issues with', - '# some PAM modules and threads)', - 'ChallengeResponseAuthentication no', - '# Change to no to disable tunnelled clear text passwords', - 'PasswordAuthentication no', - 'X11Forwarding no', - 'X11DisplayOffset 10', - 'PrintMotd no', - 'PrintLastLog yes', - 'TCPKeepAlive yes', - '#UseLogin no', - 'AcceptEnv LANG LC_*', - 'Subsystem sftp /usr/lib/openssh/sftp-server', - '#UsePAM yes', - ] +def session_salt_api(session_master, session_salt_api_factory): + assert session_master.is_running() + with session_salt_api_factory.started(): + yield session_salt_api_factory def pytest_addoption(parser): diff --git a/tests/integration/test_clients.py b/tests/integration/test_clients.py index 6012c8e..2932df6 100644 --- a/tests/integration/test_clients.py +++ b/tests/integration/test_clients.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- import json -import os +import pathlib import pytest -import sys def test_local_bad_opts(pepper_cli): @@ -17,7 +16,7 @@ def test_local_bad_opts(pepper_cli): @pytest.mark.xfail( - pytest.config.getoption("--salt-api-backend") == "rest_tornado", + 'config.getoption("--salt-api-backend") == "rest_tornado"', reason="timeout kwarg isnt popped until next version of salt/tornado" ) def test_runner_client(pepper_cli): @@ -30,34 +29,35 @@ def test_runner_client(pepper_cli): @pytest.mark.xfail( - pytest.config.getoption("--salt-api-backend") == "rest_tornado", + 'config.getoption("--salt-api-backend") == "rest_tornado"', reason="wheelClient unimplemented for now on tornado", ) -def test_wheel_client_arg(pepper_cli, session_minion_id): +def test_wheel_client_arg(pepper_cli, session_minion): ret = pepper_cli('--client=wheel', 'minions.connected') - assert ret == ['pytest-session-salt-minion-0'] + assert ret == [session_minion.id] @pytest.mark.xfail( - pytest.config.getoption("--salt-api-backend") == "rest_tornado", + 'config.getoption("--salt-api-backend") == "rest_tornado"', reason="wheelClient unimplemented for now on tornado", ) -def test_wheel_client_kwargs(pepper_cli, session_master_config_file): +def test_wheel_client_kwargs(pepper_cli, session_master): ret = pepper_cli( '--client=wheel', 'config.update_config', 'file_name=pepper', 'yaml_contents={0}'.format(json.dumps({"timeout": 5})), ) assert ret == 'Wrote pepper.conf' - assert os.path.isfile('{0}.d/pepper.conf'.format(session_master_config_file)) + + default_include_dir = pathlib.Path(session_master.config['default_include']).parent + pepper_config = (pathlib.Path(session_master.config_dir) / default_include_dir / 'pepper.conf') + assert pepper_config.exists @pytest.mark.xfail( - pytest.config.getoption("--salt-api-backend") == "rest_tornado", + 'config.getoption("--salt-api-backend") == "rest_tornado"', reason="sshClient unimplemented for now on tornado", ) -@pytest.mark.xfail(sys.version_info >= (3, 0), - reason='Broken with python3 right now') -def test_ssh_client(pepper_cli, session_roster_config, session_roster_config_file): +def test_ssh_client(pepper_cli, session_ssh_roster_config): ret = pepper_cli('--client=ssh', '*', 'test.ping') assert ret['ssh']['localhost']['return'] is True diff --git a/tests/integration/test_vanilla.py b/tests/integration/test_vanilla.py index 04d0647..2864b5a 100644 --- a/tests/integration/test_vanilla.py +++ b/tests/integration/test_vanilla.py @@ -9,7 +9,7 @@ def test_local(pepper_cli, session_minion_id): @pytest.mark.xfail( - pytest.config.getoption("--salt-api-backend") == "rest_tornado", + 'config.getoption("--salt-api-backend") == "rest_tornado"', reason="this is broken in rest_tornado until future release", ) def test_long_local(pepper_cli, session_minion_id): diff --git a/tests/requirements.txt b/tests/requirements.txt index b0be5ff..c3b1d7d 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,10 +1,10 @@ mock -pytest>=3.5.0,<4.0.0 +pytest>=6.0.0 pytest-rerunfailures pytest-cov -git+https://github.com/saltstack/pytest-salt@master#egg=pytest-salt -tornado<5.0.0 +pytest-salt-factories==0.912.2 CherryPy setuptools_scm -pyzmq>=2.2.0,<17.1.0; python_version == '3.4' # pyzmq 17.1.0 stopped building wheels for python3.4 -pyzmq>=2.2.0; python_version != '3.4' +pyzmq<=20.0.0 ; python_version < "3.6" +pyzmq>=17.0.0 ; python_version < "3.9" +pyzmq>19.0.2 ; python_version >= "3.9" diff --git a/tox.ini b/tox.ini index ece2b3c..4415136 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,16 @@ [tox] -envlist = py{27,34,35,36,37,38}-{cherrypy,tornado}-{v2018.3,v2019.2,develop},coverage,flake8 +envlist = py{3.7,3.8}-{cherrypy,tornado}-{v3004.2,v3005.1,master},coverage,flake8 skip_missing_interpreters = true skipsdist = false [testenv] -passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_* +passenv = TOXENV, CI, TRAVIS, TRAVIS_*, CODECOV_* deps = -r{toxinidir}/tests/requirements.txt - v2018.3: salt<2018.4 - v2019.2: salt<2019.3 - develop: git+https://github.com/saltstack/salt.git@develop#egg=salt + v3004.2: salt<3004.2 + v3004.2: jinja2<3.1 + v3005.1: salt<3005.1 + py3.7-{cherrypy,tornado}-{v3004.2,v3005.1}: importlib-metadata<5.0.0 + master: git+https://github.com/saltstack/salt.git@master#egg=salt changedir = {toxinidir} setenv = COVERAGE_FILE = {toxworkdir}/.coverage.{envname} @@ -25,7 +27,7 @@ commands = flake8 tests/ pepper/ scripts/pepper setup.py [testenv:coverage] skip_install = True deps = - coverage >= 4.4.1, < 5 + coverage >= 7.0.5, < 8 setenv = COVERAGE_FILE={toxworkdir}/.coverage changedir = {toxinidir} commands = @@ -49,7 +51,7 @@ changedir = {toxinidir}/htmlcov commands = python -m http.server [pytest] -addopts = --showlocals --log-file /tmp/pepper-runtests.log --no-print-logs -ra +addopts = --showlocals --log-file /tmp/pepper-runtests.log --show-capture=no -ra testpaths = tests norecursedirs = .git .tox usefixtures = pepperconfig