diff --git a/_lastrevision b/_lastrevision index e724caf..3ccde1b 100644 --- a/_lastrevision +++ b/_lastrevision @@ -1 +1 @@ -d0c2f35ff4a0b21786b20c884cbb191ad2e63904 \ No newline at end of file +32a086961fa3b3b60287a78169b2ad66a000073a \ No newline at end of file diff --git a/_multibuild b/_multibuild new file mode 100644 index 0000000..a0cd1a3 --- /dev/null +++ b/_multibuild @@ -0,0 +1,3 @@ + + testsuite + diff --git a/discover-both-.yml-and-.yaml-playbooks-bsc-1211888.patch b/discover-both-.yml-and-.yaml-playbooks-bsc-1211888.patch new file mode 100644 index 0000000..5aefe29 --- /dev/null +++ b/discover-both-.yml-and-.yaml-playbooks-bsc-1211888.patch @@ -0,0 +1,188 @@ +From 05fbd376090c5d7f997c510db0abb62be54d6d40 Mon Sep 17 00:00:00 2001 +From: Johannes Hahn +Date: Tue, 20 Feb 2024 15:38:08 +0100 +Subject: [PATCH] Discover both *.yml and *.yaml playbooks (bsc#1211888) + +Allow for 'playbook_extension' to be either a string or a tuple and +change the default behavior to discover both. +--- + changelog/66048.changed.md | 1 + + salt/modules/ansiblegate.py | 46 +++++++++---------- + .../pytests/unit/modules/test_ansiblegate.py | 3 ++ + .../example_playbooks/playbook1.yaml | 5 ++ + 4 files changed, 30 insertions(+), 25 deletions(-) + create mode 100644 changelog/66048.changed.md + create mode 100644 tests/unit/files/playbooks/example_playbooks/playbook1.yaml + +diff --git a/changelog/66048.changed.md b/changelog/66048.changed.md +new file mode 100644 +index 0000000000..b042e0d313 +--- /dev/null ++++ b/changelog/66048.changed.md +@@ -0,0 +1 @@ ++Ansiblegate discover_playbooks was changed to find playbooks as either *.yml or *.yaml files +diff --git a/salt/modules/ansiblegate.py b/salt/modules/ansiblegate.py +index 2f60a7444f..920c374e5a 100644 +--- a/salt/modules/ansiblegate.py ++++ b/salt/modules/ansiblegate.py +@@ -111,7 +111,7 @@ def __virtual__(): + if proc.returncode != 0: + return ( + False, +- "Failed to get the listing of ansible modules:\n{}".format(proc.stderr), ++ f"Failed to get the listing of ansible modules:\n{proc.stderr}", + ) + + module_funcs = dir(sys.modules[__name__]) +@@ -240,7 +240,7 @@ def call(module, *args, **kwargs): + _kwargs = {k: v for (k, v) in kwargs.items() if not k.startswith("__pub")} + + for key, value in _kwargs.items(): +- module_args.append("{}={}".format(key, salt.utils.json.dumps(value))) ++ module_args.append(f"{key}={salt.utils.json.dumps(value)}") + + with NamedTemporaryFile(mode="w") as inventory: + +@@ -367,15 +367,15 @@ def playbooks( + if diff: + command.append("--diff") + if isinstance(extra_vars, dict): +- command.append("--extra-vars='{}'".format(json.dumps(extra_vars))) ++ command.append(f"--extra-vars='{json.dumps(extra_vars)}'") + elif isinstance(extra_vars, str) and extra_vars.startswith("@"): +- command.append("--extra-vars={}".format(extra_vars)) ++ command.append(f"--extra-vars={extra_vars}") + if flush_cache: + command.append("--flush-cache") + if inventory: +- command.append("--inventory={}".format(inventory)) ++ command.append(f"--inventory={inventory}") + if limit: +- command.append("--limit={}".format(limit)) ++ command.append(f"--limit={limit}") + if list_hosts: + command.append("--list-hosts") + if list_tags: +@@ -383,25 +383,25 @@ def playbooks( + if list_tasks: + command.append("--list-tasks") + if module_path: +- command.append("--module-path={}".format(module_path)) ++ command.append(f"--module-path={module_path}") + if skip_tags: +- command.append("--skip-tags={}".format(skip_tags)) ++ command.append(f"--skip-tags={skip_tags}") + if start_at_task: +- command.append("--start-at-task={}".format(start_at_task)) ++ command.append(f"--start-at-task={start_at_task}") + if syntax_check: + command.append("--syntax-check") + if tags: +- command.append("--tags={}".format(tags)) ++ command.append(f"--tags={tags}") + if playbook_kwargs: + for key, value in playbook_kwargs.items(): + key = key.replace("_", "-") + if value is True: +- command.append("--{}".format(key)) ++ command.append(f"--{key}") + elif isinstance(value, str): +- command.append("--{}={}".format(key, value)) ++ command.append(f"--{key}={value}") + elif isinstance(value, dict): +- command.append("--{}={}".format(key, json.dumps(value))) +- command.append("--forks={}".format(forks)) ++ command.append(f"--{key}={json.dumps(value)}") ++ command.append(f"--forks={forks}") + cmd_kwargs = { + "env": { + "ANSIBLE_STDOUT_CALLBACK": "json", +@@ -502,7 +502,7 @@ def discover_playbooks( + List of paths to discover playbooks from. + + :param playbook_extension: +- File extension of playbooks file to search for. Default: "yml" ++ File extension(s) of playbook files to search for, can be a string or tuple of strings. Default: (".yml", ".yaml") + + :param hosts_filename: + Filename of custom playbook inventory to search for. Default: "hosts" +@@ -533,19 +533,17 @@ def discover_playbooks( + ) + + if not playbook_extension: +- playbook_extension = "yml" ++ playbook_extension = (".yml", ".yaml") + if not hosts_filename: + hosts_filename = "hosts" + + if path: + if not os.path.isabs(path): + raise CommandExecutionError( +- "The given path is not an absolute path: {}".format(path) ++ f"The given path is not an absolute path: {path}" + ) + if not os.path.isdir(path): +- raise CommandExecutionError( +- "The given path is not a directory: {}".format(path) +- ) ++ raise CommandExecutionError(f"The given path is not a directory: {path}") + return { + path: _explore_path(path, playbook_extension, hosts_filename, syntax_check) + } +@@ -573,7 +571,7 @@ def _explore_path(path, playbook_extension, hosts_filename, syntax_check): + # Check files in the given path + for _f in os.listdir(path): + _path = os.path.join(path, _f) +- if os.path.isfile(_path) and _path.endswith("." + playbook_extension): ++ if os.path.isfile(_path) and _path.endswith(playbook_extension): + ret[_f] = {"fullpath": _path} + # Check for custom inventory file + if os.path.isfile(os.path.join(path, hosts_filename)): +@@ -584,9 +582,7 @@ def _explore_path(path, playbook_extension, hosts_filename, syntax_check): + # Check files in the 1st level of subdirectories + for _f2 in os.listdir(_path): + _path2 = os.path.join(_path, _f2) +- if os.path.isfile(_path2) and _path2.endswith( +- "." + playbook_extension +- ): ++ if os.path.isfile(_path2) and _path2.endswith(playbook_extension): + ret[os.path.join(_f, _f2)] = {"fullpath": _path2} + # Check for custom inventory file + if os.path.isfile(os.path.join(_path, hosts_filename)): +@@ -599,7 +595,7 @@ def _explore_path(path, playbook_extension, hosts_filename, syntax_check): + ) + except Exception as exc: + raise CommandExecutionError( +- "There was an exception while discovering playbooks: {}".format(exc) ++ f"There was an exception while discovering playbooks: {exc}" + ) + + # Run syntax check validation +diff --git a/tests/pytests/unit/modules/test_ansiblegate.py b/tests/pytests/unit/modules/test_ansiblegate.py +index 6201809c22..272da721bf 100644 +--- a/tests/pytests/unit/modules/test_ansiblegate.py ++++ b/tests/pytests/unit/modules/test_ansiblegate.py +@@ -198,6 +198,9 @@ def test_ansible_discover_playbooks_single_path(): + assert ret[playbooks_dir]["playbook1.yml"] == { + "fullpath": os.path.join(playbooks_dir, "playbook1.yml") + } ++ assert ret[playbooks_dir]["playbook1.yaml"] == { ++ "fullpath": os.path.join(playbooks_dir, "playbook1.yaml") ++ } + assert ret[playbooks_dir]["example-playbook2/site.yml"] == { + "fullpath": os.path.join(playbooks_dir, "example-playbook2/site.yml"), + "custom_inventory": os.path.join(playbooks_dir, "example-playbook2/hosts"), +diff --git a/tests/unit/files/playbooks/example_playbooks/playbook1.yaml b/tests/unit/files/playbooks/example_playbooks/playbook1.yaml +new file mode 100644 +index 0000000000..e258a101e1 +--- /dev/null ++++ b/tests/unit/files/playbooks/example_playbooks/playbook1.yaml +@@ -0,0 +1,5 @@ ++--- ++- hosts: all ++ gather_facts: false ++ tasks: ++ - ping: +-- +2.43.1 + diff --git a/fix-problematic-tests-and-allow-smooth-tests-executi.patch b/fix-problematic-tests-and-allow-smooth-tests-executi.patch new file mode 100644 index 0000000..ecc3de8 --- /dev/null +++ b/fix-problematic-tests-and-allow-smooth-tests-executi.patch @@ -0,0 +1,2695 @@ +From 1b1bbc3e46ab2eed98f07a23368877fc068dbc06 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= + +Date: Mon, 26 Feb 2024 11:25:22 +0000 +Subject: [PATCH] Fix problematic tests and allow smooth tests + executions on containers + +* Align boto imports in tests with Salt modules + +Some modules `import boto` to set log levels. The related tests don't +import `boto`. This can cause a problem when `boto` is not available. + +Tests are skipped when HAS_BOTO in the test_boto_*.py is False. Not +trying to `import boto` can leave HAS_BOTO=True in the test file even +though HAS_BOTO is False on the application side. In this case, tests +are not skipped and fail. + +* Fix mock order in test_dig (test_network.py) + +`salt.utils.path.which` should not be mocked before `network.__utils__`. The +Salt loader calls `salt.utils.network.linux_interfaces`, which needs the real +`salt.utils.path.which`. + +* Fix mock calls + +Signed-off-by: Pedro Algarvio +(cherry picked from commit 3506e7fd0e84320b2873370f1fe527025c244dca) + +* Skip venafiapi test if vcert module not available + +The same HAS_VCERT check is done in the runner module. + +* Moving tests/integration/modules/test_cmdmod.py to pytest, Gareth J Greenaway original author + +(cherry picked from commit 2c1040b4c2885efaa86576fd88eb36bb550b5996) + +* The repo.saltproject.io `index.html` file changed it's contents. Fix tests. + +Signed-off-by: Pedro Algarvio +(cherry picked from commit 672f6586d7c3cdb0e8c5ee42524895035aafcc23) + +* Skip hwclock test when executed inside a container + +* Skip git pillar tests when executed inside a container + +These tests require a Git repository container, which is hard to correctly set +up when executing the tests in the container in GH Actions. + +Using --network host can help, but there was still an error (the git repos were +empty) when I tried to set this up. + +* Skip test requiring systemd inside container + +* Exclude tests for hgfs if missing hglib + +* Skip and fix tests when running on containers + +* Fix some failing test causing problem in SUSE environments + +* Skip more tests when running on containers + +* Use skipif instead of skip_if as it seems not behaving equally + +* Skip more tests that cannot run in a container + +* Remove SSH test which doesn't make sense after lock mechanism + +* Fix failing boto tests + +* Skip unmaintained tests upstream around zcbuildout + +* Skip some tests that does not run well in GH Actions + +--------- + +Co-authored-by: Pedro Algarvio +Co-authored-by: Gareth J. Greenaway +Co-authored-by: Alexander Graul +--- + .../integration/externalapi/test_venafiapi.py | 10 +- + tests/integration/modules/test_cmdmod.py | 634 ------------------ + tests/integration/modules/test_cp.py | 24 +- + tests/integration/modules/test_timezone.py | 3 + + tests/integration/pillar/test_git_pillar.py | 3 + + tests/integration/ssh/test_state.py | 47 -- + tests/pytests/functional/cache/test_consul.py | 4 + + tests/pytests/functional/cache/test_mysql.py | 4 + + .../functional/fileserver/hgfs/test_hgfs.py | 2 + + .../pytests/functional/modules/test_cmdmod.py | 561 ++++++++++++++++ + .../functional/modules/test_dockermod.py | 4 + + .../pytests/functional/modules/test_swarm.py | 5 + + .../pytests/functional/modules/test_system.py | 3 + + .../pillar/hg_pillar/test_hg_pillar.py | 1 + + .../states/rabbitmq/test_cluster.py | 4 + + .../functional/states/rabbitmq/test_plugin.py | 4 + + .../functional/states/rabbitmq/test_policy.py | 4 + + .../states/rabbitmq/test_upstream.py | 4 + + .../functional/states/rabbitmq/test_user.py | 4 + + .../functional/states/rabbitmq/test_vhost.py | 4 + + .../functional/states/test_docker_network.py | 5 + + tests/pytests/functional/states/test_pkg.py | 6 +- + .../integration/cli/test_syndic_eauth.py | 3 + + .../integration/daemons/test_memory_leak.py | 4 + + .../integration/modules/test_cmdmod.py | 93 +++ + .../pytests/integration/modules/test_virt.py | 4 + + tests/pytests/integration/ssh/test_log.py | 3 + + tests/pytests/integration/ssh/test_master.py | 5 + + .../integration/ssh/test_py_versions.py | 3 + + .../pytests/integration/ssh/test_ssh_setup.py | 2 + + .../scenarios/compat/test_with_versions.py | 4 + + .../multimaster/test_failover_master.py | 3 + + tests/pytests/scenarios/setup/test_install.py | 6 + + tests/pytests/unit/modules/test_aptpkg.py | 12 +- + .../pytests/unit/modules/test_linux_sysctl.py | 8 +- + tests/pytests/unit/modules/test_win_ip.py | 4 +- + tests/pytests/unit/test_master.py | 2 +- + tests/pytests/unit/test_minion.py | 4 +- + tests/pytests/unit/utils/event/test_event.py | 24 +- + tests/unit/modules/test_boto_apigateway.py | 1 + + .../unit/modules/test_boto_cognitoidentity.py | 1 + + .../modules/test_boto_elasticsearch_domain.py | 1 + + tests/unit/modules/test_boto_lambda.py | 1 + + tests/unit/modules/test_network.py | 6 +- + tests/unit/modules/test_nilrt_ip.py | 4 +- + tests/unit/modules/test_zcbuildout.py | 2 + + .../unit/netapi/rest_tornado/test_saltnado.py | 22 +- + tests/unit/states/test_boto_apigateway.py | 1 + + .../unit/states/test_boto_cognitoidentity.py | 1 + + tests/unit/states/test_zcbuildout.py | 1 + + 50 files changed, 824 insertions(+), 741 deletions(-) + delete mode 100644 tests/integration/modules/test_cmdmod.py + create mode 100644 tests/pytests/functional/modules/test_cmdmod.py + +diff --git a/tests/integration/externalapi/test_venafiapi.py b/tests/integration/externalapi/test_venafiapi.py +index ad08605430f..3ae1e3392d8 100644 +--- a/tests/integration/externalapi/test_venafiapi.py ++++ b/tests/integration/externalapi/test_venafiapi.py +@@ -13,6 +13,14 @@ from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + from cryptography.x509.oid import NameOID + ++try: ++ import vcert ++ from vcert.common import CertificateRequest ++ ++ HAS_VCERT = True ++except ImportError: ++ HAS_VCERT = False ++ + from tests.support.case import ShellCase + + +@@ -36,6 +44,7 @@ def with_random_name(func): + return wrapper + + ++@pytest.mark.skipif(HAS_VCERT is False, reason="The vcert module must be installed.") + class VenafiTest(ShellCase): + """ + Test the venafi runner +@@ -86,7 +95,6 @@ class VenafiTest(ShellCase): + @with_random_name + @pytest.mark.slow_test + def test_sign(self, name): +- + csr_pem = """-----BEGIN CERTIFICATE REQUEST----- + MIIFbDCCA1QCAQAwgbQxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARVdGFoMRIwEAYD + VQQHDAlTYWx0IExha2UxFDASBgNVBAoMC1ZlbmFmaSBJbmMuMRQwEgYDVQQLDAtJ +diff --git a/tests/integration/modules/test_cmdmod.py b/tests/integration/modules/test_cmdmod.py +deleted file mode 100644 +index 800111174f0..00000000000 +--- a/tests/integration/modules/test_cmdmod.py ++++ /dev/null +@@ -1,634 +0,0 @@ +-import os +-import random +-import sys +-import tempfile +-from contextlib import contextmanager +- +-import pytest +- +-import salt.utils.path +-import salt.utils.platform +-import salt.utils.user +-from tests.support.case import ModuleCase +-from tests.support.helpers import SKIP_INITIAL_PHOTONOS_FAILURES, dedent +-from tests.support.runtests import RUNTIME_VARS +- +-AVAILABLE_PYTHON_EXECUTABLE = salt.utils.path.which_bin( +- ["python", "python2", "python2.6", "python2.7"] +-) +- +- +-@pytest.mark.windows_whitelisted +-class CMDModuleTest(ModuleCase): +- """ +- Validate the cmd module +- """ +- +- def setUp(self): +- self.runas_usr = "nobody" +- if salt.utils.platform.is_darwin(): +- self.runas_usr = "macsalttest" +- +- @contextmanager +- def _ensure_user_exists(self, name): +- if name in self.run_function("user.info", [name]).values(): +- # User already exists; don't touch +- yield +- else: +- # Need to create user for test +- self.run_function("user.add", [name]) +- try: +- yield +- finally: +- self.run_function("user.delete", [name], remove=True) +- +- @pytest.mark.slow_test +- @pytest.mark.skip_on_windows +- def test_run(self): +- """ +- cmd.run +- """ +- shell = os.environ.get("SHELL") +- if shell is None: +- # Failed to get the SHELL var, don't run +- self.skipTest("Unable to get the SHELL environment variable") +- +- self.assertTrue(self.run_function("cmd.run", ["echo $SHELL"])) +- self.assertEqual( +- self.run_function( +- "cmd.run", ["echo $SHELL", "shell={}".format(shell)], python_shell=True +- ).rstrip(), +- shell, +- ) +- self.assertEqual( +- self.run_function("cmd.run", ["ls / | grep etc"], python_shell=True), "etc" +- ) +- self.assertEqual( +- self.run_function( +- "cmd.run", +- ['echo {{grains.id}} | awk "{print $1}"'], +- template="jinja", +- python_shell=True, +- ), +- "minion", +- ) +- self.assertEqual( +- self.run_function( +- "cmd.run", ["grep f"], stdin="one\ntwo\nthree\nfour\nfive\n" +- ), +- "four\nfive", +- ) +- self.assertEqual( +- self.run_function( +- "cmd.run", ['echo "a=b" | sed -e s/=/:/g'], python_shell=True +- ), +- "a:b", +- ) +- +- @pytest.mark.slow_test +- def test_stdout(self): +- """ +- cmd.run_stdout +- """ +- self.assertEqual( +- self.run_function("cmd.run_stdout", ['echo "cheese"']).rstrip(), +- "cheese" if not salt.utils.platform.is_windows() else '"cheese"', +- ) +- +- @pytest.mark.slow_test +- def test_stderr(self): +- """ +- cmd.run_stderr +- """ +- if sys.platform.startswith(("freebsd", "openbsd")): +- shell = "/bin/sh" +- else: +- shell = "/bin/bash" +- +- self.assertEqual( +- self.run_function( +- "cmd.run_stderr", +- ['echo "cheese" 1>&2', "shell={}".format(shell)], +- python_shell=True, +- ).rstrip(), +- "cheese" if not salt.utils.platform.is_windows() else '"cheese"', +- ) +- +- @pytest.mark.slow_test +- def test_run_all(self): +- """ +- cmd.run_all +- """ +- if sys.platform.startswith(("freebsd", "openbsd")): +- shell = "/bin/sh" +- else: +- shell = "/bin/bash" +- +- ret = self.run_function( +- "cmd.run_all", +- ['echo "cheese" 1>&2', "shell={}".format(shell)], +- python_shell=True, +- ) +- self.assertTrue("pid" in ret) +- self.assertTrue("retcode" in ret) +- self.assertTrue("stdout" in ret) +- self.assertTrue("stderr" in ret) +- self.assertTrue(isinstance(ret.get("pid"), int)) +- self.assertTrue(isinstance(ret.get("retcode"), int)) +- self.assertTrue(isinstance(ret.get("stdout"), str)) +- self.assertTrue(isinstance(ret.get("stderr"), str)) +- self.assertEqual( +- ret.get("stderr").rstrip(), +- "cheese" if not salt.utils.platform.is_windows() else '"cheese"', +- ) +- +- @pytest.mark.slow_test +- def test_retcode(self): +- """ +- cmd.retcode +- """ +- self.assertEqual( +- self.run_function("cmd.retcode", ["exit 0"], python_shell=True), 0 +- ) +- self.assertEqual( +- self.run_function("cmd.retcode", ["exit 1"], python_shell=True), 1 +- ) +- +- @pytest.mark.slow_test +- def test_run_all_with_success_retcodes(self): +- """ +- cmd.run with success_retcodes +- """ +- ret = self.run_function( +- "cmd.run_all", ["exit 42"], success_retcodes=[42], python_shell=True +- ) +- +- self.assertTrue("retcode" in ret) +- self.assertEqual(ret.get("retcode"), 0) +- +- @pytest.mark.slow_test +- def test_retcode_with_success_retcodes(self): +- """ +- cmd.run with success_retcodes +- """ +- ret = self.run_function( +- "cmd.retcode", ["exit 42"], success_retcodes=[42], python_shell=True +- ) +- +- self.assertEqual(ret, 0) +- +- @pytest.mark.slow_test +- def test_run_all_with_success_stderr(self): +- """ +- cmd.run with success_retcodes +- """ +- random_file = "{}{}{}".format( +- RUNTIME_VARS.TMP_ROOT_DIR, os.path.sep, random.random() +- ) +- +- if salt.utils.platform.is_windows(): +- func = "type" +- expected_stderr = "cannot find the file specified" +- else: +- func = "cat" +- expected_stderr = "No such file or directory" +- ret = self.run_function( +- "cmd.run_all", +- ["{} {}".format(func, random_file)], +- success_stderr=[expected_stderr], +- python_shell=True, +- ) +- +- self.assertTrue("retcode" in ret) +- self.assertEqual(ret.get("retcode"), 0) +- +- @pytest.mark.slow_test +- def test_blacklist_glob(self): +- """ +- cmd_blacklist_glob +- """ +- self.assertEqual( +- self.run_function("cmd.run", ["bad_command --foo"]).rstrip(), +- 'ERROR: The shell command "bad_command --foo" is not permitted', +- ) +- +- @pytest.mark.slow_test +- def test_script(self): +- """ +- cmd.script +- """ +- args = "saltines crackers biscuits=yes" +- script = "salt://script.py" +- ret = self.run_function("cmd.script", [script, args], saltenv="base") +- self.assertEqual(ret["stdout"], args) +- +- @pytest.mark.slow_test +- def test_script_query_string(self): +- """ +- cmd.script +- """ +- args = "saltines crackers biscuits=yes" +- script = "salt://script.py?saltenv=base" +- ret = self.run_function("cmd.script", [script, args], saltenv="base") +- self.assertEqual(ret["stdout"], args) +- +- @pytest.mark.slow_test +- def test_script_retcode(self): +- """ +- cmd.script_retcode +- """ +- script = "salt://script.py" +- ret = self.run_function("cmd.script_retcode", [script], saltenv="base") +- self.assertEqual(ret, 0) +- +- @pytest.mark.slow_test +- def test_script_cwd(self): +- """ +- cmd.script with cwd +- """ +- tmp_cwd = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP) +- args = "saltines crackers biscuits=yes" +- script = "salt://script.py" +- ret = self.run_function( +- "cmd.script", [script, args], cwd=tmp_cwd, saltenv="base" +- ) +- self.assertEqual(ret["stdout"], args) +- +- @pytest.mark.slow_test +- def test_script_cwd_with_space(self): +- """ +- cmd.script with cwd +- """ +- tmp_cwd = "{}{}test 2".format( +- tempfile.mkdtemp(dir=RUNTIME_VARS.TMP), os.path.sep +- ) +- os.mkdir(tmp_cwd) +- +- args = "saltines crackers biscuits=yes" +- script = "salt://script.py" +- ret = self.run_function( +- "cmd.script", [script, args], cwd=tmp_cwd, saltenv="base" +- ) +- self.assertEqual(ret["stdout"], args) +- +- @pytest.mark.destructive_test +- def test_tty(self): +- """ +- cmd.tty +- """ +- for tty in ("tty0", "pts3"): +- if os.path.exists(os.path.join("/dev", tty)): +- ret = self.run_function("cmd.tty", [tty, "apply salt liberally"]) +- self.assertTrue("Success" in ret) +- +- @pytest.mark.skip_on_windows +- @pytest.mark.skip_if_binaries_missing("which") +- def test_which(self): +- """ +- cmd.which +- """ +- cmd_which = self.run_function("cmd.which", ["cat"]) +- self.assertIsInstance(cmd_which, str) +- cmd_run = self.run_function("cmd.run", ["which cat"]) +- self.assertIsInstance(cmd_run, str) +- self.assertEqual(cmd_which.rstrip(), cmd_run.rstrip()) +- +- @pytest.mark.skip_on_windows +- @pytest.mark.skip_if_binaries_missing("which") +- def test_which_bin(self): +- """ +- cmd.which_bin +- """ +- cmds = ["pip3", "pip2", "pip", "pip-python"] +- ret = self.run_function("cmd.which_bin", [cmds]) +- self.assertTrue(os.path.split(ret)[1] in cmds) +- +- @pytest.mark.slow_test +- def test_has_exec(self): +- """ +- cmd.has_exec +- """ +- self.assertTrue( +- self.run_function("cmd.has_exec", [AVAILABLE_PYTHON_EXECUTABLE]) +- ) +- self.assertFalse( +- self.run_function("cmd.has_exec", ["alllfsdfnwieulrrh9123857ygf"]) +- ) +- +- @pytest.mark.slow_test +- def test_exec_code(self): +- """ +- cmd.exec_code +- """ +- code = dedent( +- """ +- import sys +- sys.stdout.write('cheese') +- """ +- ) +- self.assertEqual( +- self.run_function( +- "cmd.exec_code", [AVAILABLE_PYTHON_EXECUTABLE, code] +- ).rstrip(), +- "cheese", +- ) +- +- @pytest.mark.slow_test +- def test_exec_code_with_single_arg(self): +- """ +- cmd.exec_code +- """ +- code = dedent( +- """ +- import sys +- sys.stdout.write(sys.argv[1]) +- """ +- ) +- arg = "cheese" +- self.assertEqual( +- self.run_function( +- "cmd.exec_code", [AVAILABLE_PYTHON_EXECUTABLE, code], args=arg +- ).rstrip(), +- arg, +- ) +- +- @pytest.mark.slow_test +- def test_exec_code_with_multiple_args(self): +- """ +- cmd.exec_code +- """ +- code = dedent( +- """ +- import sys +- sys.stdout.write(sys.argv[1]) +- """ +- ) +- arg = "cheese" +- self.assertEqual( +- self.run_function( +- "cmd.exec_code", [AVAILABLE_PYTHON_EXECUTABLE, code], args=[arg, "test"] +- ).rstrip(), +- arg, +- ) +- +- @pytest.mark.slow_test +- def test_quotes(self): +- """ +- cmd.run with quoted command +- """ +- cmd = """echo 'SELECT * FROM foo WHERE bar="baz"' """ +- expected_result = 'SELECT * FROM foo WHERE bar="baz"' +- if salt.utils.platform.is_windows(): +- expected_result = "'SELECT * FROM foo WHERE bar=\"baz\"'" +- result = self.run_function("cmd.run_stdout", [cmd]).strip() +- self.assertEqual(result, expected_result) +- +- @pytest.mark.skip_if_not_root +- @pytest.mark.skip_on_windows(reason="Skip on Windows, requires password") +- def test_quotes_runas(self): +- """ +- cmd.run with quoted command +- """ +- cmd = """echo 'SELECT * FROM foo WHERE bar="baz"' """ +- expected_result = 'SELECT * FROM foo WHERE bar="baz"' +- result = self.run_function( +- "cmd.run_all", [cmd], runas=RUNTIME_VARS.RUNNING_TESTS_USER +- ) +- errmsg = "The command returned: {}".format(result) +- self.assertEqual(result["retcode"], 0, errmsg) +- self.assertEqual(result["stdout"], expected_result, errmsg) +- +- @pytest.mark.destructive_test +- @pytest.mark.skip_if_not_root +- @pytest.mark.skip_on_windows(reason="Skip on Windows, uses unix commands") +- @pytest.mark.slow_test +- def test_avoid_injecting_shell_code_as_root(self): +- """ +- cmd.run should execute the whole command as the "runas" user, not +- running substitutions as root. +- """ +- cmd = "echo $(id -u)" +- +- root_id = self.run_function("cmd.run_stdout", [cmd]) +- runas_root_id = self.run_function( +- "cmd.run_stdout", [cmd], runas=RUNTIME_VARS.RUNNING_TESTS_USER +- ) +- with self._ensure_user_exists(self.runas_usr): +- user_id = self.run_function("cmd.run_stdout", [cmd], runas=self.runas_usr) +- +- self.assertNotEqual(user_id, root_id) +- self.assertNotEqual(user_id, runas_root_id) +- self.assertEqual(root_id, runas_root_id) +- +- @pytest.mark.destructive_test +- @pytest.mark.skip_if_not_root +- @pytest.mark.skip_on_windows(reason="Skip on Windows, uses unix commands") +- @pytest.mark.slow_test +- def test_cwd_runas(self): +- """ +- cmd.run should be able to change working directory correctly, whether +- or not runas is in use. +- """ +- cmd = "pwd" +- tmp_cwd = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP) +- os.chmod(tmp_cwd, 0o711) +- +- cwd_normal = self.run_function("cmd.run_stdout", [cmd], cwd=tmp_cwd).rstrip( +- "\n" +- ) +- self.assertEqual(tmp_cwd, cwd_normal) +- +- with self._ensure_user_exists(self.runas_usr): +- cwd_runas = self.run_function( +- "cmd.run_stdout", [cmd], cwd=tmp_cwd, runas=self.runas_usr +- ).rstrip("\n") +- self.assertEqual(tmp_cwd, cwd_runas) +- +- @pytest.mark.destructive_test +- @pytest.mark.skip_if_not_root +- @pytest.mark.skip_unless_on_darwin(reason="Applicable to MacOS only") +- @pytest.mark.slow_test +- def test_runas_env(self): +- """ +- cmd.run should be able to change working directory correctly, whether +- or not runas is in use. +- """ +- with self._ensure_user_exists(self.runas_usr): +- user_path = self.run_function( +- "cmd.run_stdout", ['printf %s "$PATH"'], runas=self.runas_usr +- ) +- # XXX: Not sure of a better way. Environment starts out with +- # /bin:/usr/bin and should be populated by path helper and the bash +- # profile. +- self.assertNotEqual("/bin:/usr/bin", user_path) +- +- @pytest.mark.destructive_test +- @pytest.mark.skip_if_not_root +- @pytest.mark.skip_unless_on_darwin(reason="Applicable to MacOS only") +- @pytest.mark.slow_test +- def test_runas_complex_command_bad_cwd(self): +- """ +- cmd.run should not accidentally run parts of a complex command when +- given a cwd which cannot be used by the user the command is run as. +- +- Due to the need to use `su -l` to login to another user on MacOS, we +- cannot cd into directories that the target user themselves does not +- have execute permission for. To an extent, this test is testing that +- buggy behaviour, but its purpose is to ensure that the greater bug of +- running commands after failing to cd does not occur. +- """ +- tmp_cwd = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP) +- os.chmod(tmp_cwd, 0o700) +- +- with self._ensure_user_exists(self.runas_usr): +- cmd_result = self.run_function( +- "cmd.run_all", +- ['pwd; pwd; : $(echo "You have failed the test" >&2)'], +- cwd=tmp_cwd, +- runas=self.runas_usr, +- ) +- +- self.assertEqual("", cmd_result["stdout"]) +- self.assertNotIn("You have failed the test", cmd_result["stderr"]) +- self.assertNotEqual(0, cmd_result["retcode"]) +- +- @SKIP_INITIAL_PHOTONOS_FAILURES +- @pytest.mark.skip_on_windows +- @pytest.mark.skip_if_not_root +- @pytest.mark.destructive_test +- @pytest.mark.slow_test +- def test_runas(self): +- """ +- Ensure that the env is the runas user's +- """ +- with self._ensure_user_exists(self.runas_usr): +- out = self.run_function( +- "cmd.run", ["env"], runas=self.runas_usr +- ).splitlines() +- self.assertIn("USER={}".format(self.runas_usr), out) +- +- @pytest.mark.skip_if_binaries_missing("sleep", reason="sleep cmd not installed") +- def test_timeout(self): +- """ +- cmd.run trigger timeout +- """ +- out = self.run_function( +- "cmd.run", ["sleep 2 && echo hello"], f_timeout=1, python_shell=True +- ) +- self.assertTrue("Timed out" in out) +- +- @pytest.mark.skip_if_binaries_missing("sleep", reason="sleep cmd not installed") +- def test_timeout_success(self): +- """ +- cmd.run sufficient timeout to succeed +- """ +- out = self.run_function( +- "cmd.run", ["sleep 1 && echo hello"], f_timeout=2, python_shell=True +- ) +- self.assertEqual(out, "hello") +- +- @pytest.mark.slow_test +- def test_hide_output(self): +- """ +- Test the hide_output argument +- """ +- ls_command = ( +- ["ls", "/"] if not salt.utils.platform.is_windows() else ["dir", "c:\\"] +- ) +- +- error_command = ["thiscommanddoesnotexist"] +- +- # cmd.run +- out = self.run_function("cmd.run", ls_command, hide_output=True) +- self.assertEqual(out, "") +- +- # cmd.shell +- out = self.run_function("cmd.shell", ls_command, hide_output=True) +- self.assertEqual(out, "") +- +- # cmd.run_stdout +- out = self.run_function("cmd.run_stdout", ls_command, hide_output=True) +- self.assertEqual(out, "") +- +- # cmd.run_stderr +- out = self.run_function("cmd.shell", error_command, hide_output=True) +- self.assertEqual(out, "") +- +- # cmd.run_all (command should have produced stdout) +- out = self.run_function("cmd.run_all", ls_command, hide_output=True) +- self.assertEqual(out["stdout"], "") +- self.assertEqual(out["stderr"], "") +- +- # cmd.run_all (command should have produced stderr) +- out = self.run_function("cmd.run_all", error_command, hide_output=True) +- self.assertEqual(out["stdout"], "") +- self.assertEqual(out["stderr"], "") +- +- @pytest.mark.slow_test +- def test_cmd_run_whoami(self): +- """ +- test return of whoami +- """ +- if not salt.utils.platform.is_windows(): +- user = RUNTIME_VARS.RUNTIME_CONFIGS["master"]["user"] +- else: +- user = salt.utils.user.get_specific_user() +- if user.startswith("sudo_"): +- user = user.replace("sudo_", "") +- cmd = self.run_function("cmd.run", ["whoami"]) +- try: +- self.assertEqual(user.lower(), cmd.lower()) +- except AssertionError as exc: +- if not salt.utils.platform.is_windows(): +- raise exc from None +- if "\\" in user: +- user = user.split("\\")[-1] +- self.assertEqual(user.lower(), cmd.lower()) +- +- @pytest.mark.skip_unless_on_windows(reason="Minion is not Windows") +- @pytest.mark.slow_test +- def test_windows_env_handling(self): +- """ +- Ensure that nt.environ is used properly with cmd.run* +- """ +- out = self.run_function( +- "cmd.run", ["set"], env={"abc": "123", "ABC": "456"} +- ).splitlines() +- self.assertIn("abc=123", out) +- self.assertIn("ABC=456", out) +- +- @pytest.mark.slow_test +- @pytest.mark.skip_unless_on_windows(reason="Minion is not Windows") +- def test_windows_powershell_script_args(self): +- """ +- Ensure that powershell processes inline script in args +- """ +- val = "i like cheese" +- args = ( +- '-SecureString (ConvertTo-SecureString -String "{}" -AsPlainText -Force)' +- " -ErrorAction Stop".format(val) +- ) +- script = "salt://issue-56195/test.ps1" +- ret = self.run_function( +- "cmd.script", [script], args=args, shell="powershell", saltenv="base" +- ) +- self.assertEqual(ret["stdout"], val) +- +- @pytest.mark.slow_test +- @pytest.mark.skip_unless_on_windows(reason="Minion is not Windows") +- @pytest.mark.skip_if_binaries_missing("pwsh") +- def test_windows_powershell_script_args_pwsh(self): +- """ +- Ensure that powershell processes inline script in args with powershell +- core +- """ +- val = "i like cheese" +- args = ( +- '-SecureString (ConvertTo-SecureString -String "{}" -AsPlainText -Force)' +- " -ErrorAction Stop".format(val) +- ) +- script = "salt://issue-56195/test.ps1" +- ret = self.run_function( +- "cmd.script", [script], args=args, shell="pwsh", saltenv="base" +- ) +- self.assertEqual(ret["stdout"], val) +diff --git a/tests/integration/modules/test_cp.py b/tests/integration/modules/test_cp.py +index ad7538b4ba8..cd3e4c2f5ad 100644 +--- a/tests/integration/modules/test_cp.py ++++ b/tests/integration/modules/test_cp.py +@@ -234,9 +234,9 @@ class CPModuleTest(ModuleCase): + self.run_function("cp.get_url", ["https://repo.saltproject.io/index.html", tgt]) + with salt.utils.files.fopen(tgt, "r") as instructions: + data = salt.utils.stringutils.to_unicode(instructions.read()) +- self.assertIn("Bootstrap", data) +- self.assertIn("Debian", data) +- self.assertIn("Windows", data) ++ self.assertIn("Salt Project", data) ++ self.assertIn("Package", data) ++ self.assertIn("Repo", data) + self.assertNotIn("AYBABTU", data) + + @pytest.mark.slow_test +@@ -250,9 +250,9 @@ class CPModuleTest(ModuleCase): + + with salt.utils.files.fopen(ret, "r") as instructions: + data = salt.utils.stringutils.to_unicode(instructions.read()) +- self.assertIn("Bootstrap", data) +- self.assertIn("Debian", data) +- self.assertIn("Windows", data) ++ self.assertIn("Salt Project", data) ++ self.assertIn("Package", data) ++ self.assertIn("Repo", data) + self.assertNotIn("AYBABTU", data) + + @pytest.mark.slow_test +@@ -273,9 +273,9 @@ class CPModuleTest(ModuleCase): + time.sleep(sleep) + if ret.find("HTTP 599") != -1: + raise Exception("https://repo.saltproject.io/index.html returned 599 error") +- self.assertIn("Bootstrap", ret) +- self.assertIn("Debian", ret) +- self.assertIn("Windows", ret) ++ self.assertIn("Salt Project", ret) ++ self.assertIn("Package", ret) ++ self.assertIn("Repo", ret) + self.assertNotIn("AYBABTU", ret) + + @pytest.mark.slow_test +@@ -346,9 +346,9 @@ class CPModuleTest(ModuleCase): + """ + src = "https://repo.saltproject.io/index.html" + ret = self.run_function("cp.get_file_str", [src]) +- self.assertIn("Bootstrap", ret) +- self.assertIn("Debian", ret) +- self.assertIn("Windows", ret) ++ self.assertIn("Salt Project", ret) ++ self.assertIn("Package", ret) ++ self.assertIn("Repo", ret) + self.assertNotIn("AYBABTU", ret) + + @pytest.mark.slow_test +diff --git a/tests/integration/modules/test_timezone.py b/tests/integration/modules/test_timezone.py +index 8d7180cbd13..c1dc8a7b73d 100644 +--- a/tests/integration/modules/test_timezone.py ++++ b/tests/integration/modules/test_timezone.py +@@ -4,6 +4,7 @@ Integration tests for timezone module + Linux and Solaris are supported + """ + import pytest ++import os + + from tests.support.case import ModuleCase + +@@ -15,6 +16,8 @@ except ImportError: + HAS_TZLOCAL = False + + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++@pytest.mark.skipif(INSIDE_CONTAINER, reason="No hwclock in a container") + class TimezoneLinuxModuleTest(ModuleCase): + def setUp(self): + """ +diff --git a/tests/integration/pillar/test_git_pillar.py b/tests/integration/pillar/test_git_pillar.py +index 68c14daaa15..5b4cbda95c9 100644 +--- a/tests/integration/pillar/test_git_pillar.py ++++ b/tests/integration/pillar/test_git_pillar.py +@@ -63,6 +63,7 @@ https://github.com/git/git/commit/6bc0cb5 + https://github.com/unbit/uwsgi/commit/ac1e354 + """ + ++import os + import random + import string + import sys +@@ -100,9 +101,11 @@ try: + except Exception: # pylint: disable=broad-except + HAS_PYGIT2 = False + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" + pytestmark = [ + SKIP_INITIAL_PHOTONOS_FAILURES, + pytest.mark.skip_on_platforms(windows=True, darwin=True), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Communication problems between containers."), + ] + + +diff --git a/tests/integration/ssh/test_state.py b/tests/integration/ssh/test_state.py +index a9fd3e7f2d3..69245454e85 100644 +--- a/tests/integration/ssh/test_state.py ++++ b/tests/integration/ssh/test_state.py +@@ -283,53 +283,6 @@ class SSHStateTest(SSHCase): + check_file = self.run_function("file.file_exists", [SSH_SLS_FILE], wipe=False) + self.assertTrue(check_file) + +- @pytest.mark.slow_test +- def test_state_running(self): +- """ +- test state.running with salt-ssh +- """ +- +- retval = [] +- +- def _run_in_background(): +- retval.append(self.run_function("state.sls", ["running"], wipe=False)) +- +- bg_thread = threading.Thread(target=_run_in_background) +- bg_thread.start() +- +- expected = 'The function "state.pkg" is running as' +- state_ret = [] +- for _ in range(30): +- if not bg_thread.is_alive(): +- continue +- get_sls = self.run_function("state.running", wipe=False) +- state_ret.append(get_sls) +- if expected in " ".join(get_sls): +- # We found the expected return +- break +- time.sleep(1) +- else: +- if not bg_thread.is_alive(): +- bg_failed_msg = "Failed to return clean data" +- if retval and bg_failed_msg in retval.pop().get("_error", ""): +- pytest.skip("Background state run failed, skipping") +- self.fail( +- "Did not find '{}' in state.running return: {}".format( +- expected, state_ret +- ) +- ) +- +- # make sure we wait until the earlier state is complete +- future = time.time() + 120 +- while True: +- if expected not in " ".join(self.run_function("state.running", wipe=False)): +- break +- if time.time() > future: +- self.fail( +- "state.pkg is still running overtime. Test did not clean up" +- " correctly." +- ) +- + def tearDown(self): + """ + make sure to clean up any old ssh directories +diff --git a/tests/pytests/functional/cache/test_consul.py b/tests/pytests/functional/cache/test_consul.py +index 3a38e495a93..c6e16d2588e 100644 +--- a/tests/pytests/functional/cache/test_consul.py ++++ b/tests/pytests/functional/cache/test_consul.py +@@ -1,4 +1,5 @@ + import logging ++import os + import socket + import time + +@@ -13,9 +14,12 @@ docker = pytest.importorskip("docker") + + log = logging.getLogger(__name__) + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing("dockerd"), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/functional/cache/test_mysql.py b/tests/pytests/functional/cache/test_mysql.py +index c283872c08c..e15fc732a4a 100644 +--- a/tests/pytests/functional/cache/test_mysql.py ++++ b/tests/pytests/functional/cache/test_mysql.py +@@ -1,4 +1,5 @@ + import logging ++import os + + import pytest + +@@ -11,9 +12,12 @@ docker = pytest.importorskip("docker") + + log = logging.getLogger(__name__) + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing("dockerd"), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/functional/fileserver/hgfs/test_hgfs.py b/tests/pytests/functional/fileserver/hgfs/test_hgfs.py +index 571fe75e403..bfd927fd0fe 100644 +--- a/tests/pytests/functional/fileserver/hgfs/test_hgfs.py ++++ b/tests/pytests/functional/fileserver/hgfs/test_hgfs.py +@@ -16,6 +16,8 @@ try: + except ImportError: + HAS_HG = False + ++pytestmark = [pytest.mark.skipif(not HAS_HG, reason="missing hglib library")] ++ + + @pytest.fixture(scope="module") + def configure_loader_modules(master_opts): +diff --git a/tests/pytests/functional/modules/test_cmdmod.py b/tests/pytests/functional/modules/test_cmdmod.py +new file mode 100644 +index 00000000000..d30b474c6d2 +--- /dev/null ++++ b/tests/pytests/functional/modules/test_cmdmod.py +@@ -0,0 +1,561 @@ ++import os ++import random ++import sys ++from contextlib import contextmanager ++ ++import pytest ++ ++import salt.config ++import salt.utils.path ++import salt.utils.platform ++import salt.utils.user ++from tests.support.helpers import SKIP_INITIAL_PHOTONOS_FAILURES, dedent ++ ++pytestmark = [pytest.mark.windows_whitelisted] ++ ++ ++@pytest.fixture(scope="module") ++def cmdmod(modules): ++ return modules.cmd ++ ++ ++@pytest.fixture(scope="module") ++def usermod(modules): ++ return modules.user ++ ++ ++@pytest.fixture(scope="module") ++def available_python_executable(): ++ yield salt.utils.path.which_bin(["python", "python3"]) ++ ++ ++@pytest.fixture ++def runas_usr(): ++ runas_usr = "nobody" ++ if salt.utils.platform.is_darwin(): ++ runas_usr = "macsalttest" ++ yield runas_usr ++ ++ ++@pytest.fixture ++def running_username(): ++ """ ++ Return the username that is running the code. ++ """ ++ return salt.utils.user.get_user() ++ ++ ++@pytest.fixture ++def script_contents(state_tree): ++ _contents = """ ++ #!/usr/bin/env python3 ++ import sys ++ print(" ".join(sys.argv[1:])) ++ """ ++ ++ with pytest.helpers.temp_file("script.py", _contents, state_tree): ++ yield ++ ++ ++@pytest.fixture ++def issue_56195_test_ps1(state_tree): ++ _contents = """ ++ [CmdLetBinding()] ++ Param( ++ [SecureString] $SecureString ++ ) ++ $Credential = New-Object System.Net.NetworkCredential("DummyId", $SecureString) ++ $Credential.Password ++ """ ++ ++ with pytest.helpers.temp_file("issue_56195_test.ps1", _contents, state_tree): ++ yield ++ ++ ++@contextmanager ++def _ensure_user_exists(name, usermod): ++ if name in usermod.info(name).values(): ++ # User already exists; don't touch ++ yield ++ else: ++ # Need to create user for test ++ usermod.add(name) ++ try: ++ yield ++ finally: ++ usermod.delete(name, remove=True) ++ ++ ++@pytest.mark.slow_test ++def test_run(cmdmod): ++ """ ++ cmd.run ++ """ ++ shell = os.environ.get("SHELL") ++ if shell is None: ++ # Failed to get the SHELL var, don't run ++ pytest.skip("Unable to get the SHELL environment variable") ++ ++ assert cmdmod.run("echo $SHELL") ++ assert cmdmod.run("echo $SHELL", shell=shell, python_shell=True).rstrip() == shell ++ assert cmdmod.run("ls / | grep etc", python_shell=True) == "etc" ++ assert ( ++ cmdmod.run( ++ 'echo {{grains.id}} | awk "{print $1}"', ++ template="jinja", ++ python_shell=True, ++ ) ++ == "func-tests-minion" ++ ) ++ assert cmdmod.run("grep f", stdin="one\ntwo\nthree\nfour\nfive\n") == "four\nfive" ++ assert cmdmod.run('echo "a=b" | sed -e s/=/:/g', python_shell=True) == "a:b" ++ ++ ++@pytest.mark.slow_test ++def test_stdout(cmdmod): ++ """ ++ cmd.run_stdout ++ """ ++ assert ( ++ cmdmod.run_stdout('echo "cheese"').rstrip() == "cheese" ++ if not salt.utils.platform.is_windows() ++ else '"cheese"' ++ ) ++ ++ ++@pytest.mark.slow_test ++def test_stderr(cmdmod): ++ """ ++ cmd.run_stderr ++ """ ++ if sys.platform.startswith(("freebsd", "openbsd")): ++ shell = "/bin/sh" ++ else: ++ shell = "/bin/bash" ++ ++ assert ( ++ cmdmod.run_stderr( ++ 'echo "cheese" 1>&2', ++ shell=shell, ++ python_shell=True, ++ ).rstrip() ++ == "cheese" ++ if not salt.utils.platform.is_windows() ++ else '"cheese"' ++ ) ++ ++ ++@pytest.mark.slow_test ++def test_run_all(cmdmod): ++ """ ++ cmd.run_all ++ """ ++ if sys.platform.startswith(("freebsd", "openbsd")): ++ shell = "/bin/sh" ++ else: ++ shell = "/bin/bash" ++ ++ ret = cmdmod.run_all( ++ 'echo "cheese" 1>&2', ++ shell=shell, ++ python_shell=True, ++ ) ++ assert "pid" in ret ++ assert "retcode" in ret ++ assert "stdout" in ret ++ assert "stderr" in ret ++ assert isinstance(ret.get("pid"), int) ++ assert isinstance(ret.get("retcode"), int) ++ assert isinstance(ret.get("stdout"), str) ++ assert isinstance(ret.get("stderr"), str) ++ assert ( ++ ret.get("stderr").rstrip() == "cheese" ++ if not salt.utils.platform.is_windows() ++ else '"cheese"' ++ ) ++ ++ ++@pytest.mark.slow_test ++def test_retcode(cmdmod): ++ """ ++ cmd.retcode ++ """ ++ assert cmdmod.retcode("exit 0", python_shell=True) == 0 ++ assert cmdmod.retcode("exit 1", python_shell=True) == 1 ++ ++ ++@pytest.mark.slow_test ++def test_run_all_with_success_retcodes(cmdmod): ++ """ ++ cmd.run with success_retcodes ++ """ ++ ret = cmdmod.run_all("exit 42", success_retcodes=[42], python_shell=True) ++ ++ assert "retcode" in ret ++ assert ret.get("retcode") == 0 ++ ++ ++@pytest.mark.slow_test ++def test_retcode_with_success_retcodes(cmdmod): ++ """ ++ cmd.run with success_retcodes ++ """ ++ ret = cmdmod.retcode("exit 42", success_retcodes=[42], python_shell=True) ++ ++ assert ret == 0 ++ ++ ++@pytest.mark.slow_test ++def test_run_all_with_success_stderr(cmdmod, tmp_path): ++ """ ++ cmd.run with success_retcodes ++ """ ++ random_file = str(tmp_path / f"{random.random()}") ++ ++ if salt.utils.platform.is_windows(): ++ func = "type" ++ expected_stderr = "cannot find the file specified" ++ else: ++ func = "cat" ++ expected_stderr = "No such file or directory" ++ ret = cmdmod.run_all( ++ f"{func} {random_file}", ++ success_stderr=[expected_stderr], ++ python_shell=True, ++ ) ++ ++ assert "retcode" in ret ++ assert ret.get("retcode") == 0 ++ ++ ++@pytest.mark.slow_test ++def test_script(cmdmod, script_contents): ++ """ ++ cmd.script ++ """ ++ args = "saltines crackers biscuits=yes" ++ script = "salt://script.py" ++ ret = cmdmod.script(script, args, saltenv="base") ++ assert ret["stdout"] == args ++ ++ ++@pytest.mark.slow_test ++def test_script_query_string(cmdmod, script_contents): ++ """ ++ cmd.script ++ """ ++ args = "saltines crackers biscuits=yes" ++ script = "salt://script.py?saltenv=base" ++ ret = cmdmod.script(script, args, saltenv="base") ++ assert ret["stdout"] == args ++ ++ ++@pytest.mark.slow_test ++def test_script_retcode(cmdmod, script_contents): ++ """ ++ cmd.script_retcode ++ """ ++ script = "salt://script.py" ++ ret = cmdmod.script_retcode(script, saltenv="base") ++ assert ret == 0 ++ ++ ++@pytest.mark.slow_test ++def test_script_cwd(cmdmod, script_contents, tmp_path): ++ """ ++ cmd.script with cwd ++ """ ++ tmp_cwd = str(tmp_path) ++ args = "saltines crackers biscuits=yes" ++ script = "salt://script.py" ++ ret = cmdmod.script(script, args, cwd=tmp_cwd, saltenv="base") ++ assert ret["stdout"] == args ++ ++ ++@pytest.mark.slow_test ++def test_script_cwd_with_space(cmdmod, script_contents, tmp_path): ++ """ ++ cmd.script with cwd ++ """ ++ tmp_cwd = str(tmp_path / "test 2") ++ os.mkdir(tmp_cwd) ++ ++ args = "saltines crackers biscuits=yes" ++ script = "salt://script.py" ++ ret = cmdmod.script(script, args, cwd=tmp_cwd, saltenv="base") ++ assert ret["stdout"] == args ++ ++ ++@pytest.mark.destructive_test ++def test_tty(cmdmod): ++ """ ++ cmd.tty ++ """ ++ for tty in ("tty0", "pts3"): ++ if os.path.exists(os.path.join("/dev", tty)): ++ ret = cmdmod.tty(tty, "apply salt liberally") ++ assert "Success" in ret ++ ++ ++@pytest.mark.skip_on_windows ++@pytest.mark.skip_if_binaries_missing("which") ++def test_which(cmdmod): ++ """ ++ cmd.which ++ """ ++ cmd_which = cmdmod.which("cat") ++ assert isinstance(cmd_which, str) ++ cmd_run = cmdmod.run("which cat") ++ assert isinstance(cmd_run, str) ++ assert cmd_which.rstrip() == cmd_run.rstrip() ++ ++ ++@pytest.mark.skip_on_windows ++@pytest.mark.skip_if_binaries_missing("which") ++def test_which_bin(cmdmod): ++ """ ++ cmd.which_bin ++ """ ++ cmds = ["pip3", "pip2", "pip", "pip-python"] ++ ret = cmdmod.which_bin(cmds) ++ assert os.path.split(ret)[1] in cmds ++ ++ ++@pytest.mark.slow_test ++def test_has_exec(cmdmod, available_python_executable): ++ """ ++ cmd.has_exec ++ """ ++ assert cmdmod.has_exec(available_python_executable) ++ assert not cmdmod.has_exec("alllfsdfnwieulrrh9123857ygf") ++ ++ ++@pytest.mark.slow_test ++def test_exec_code(cmdmod, available_python_executable): ++ """ ++ cmd.exec_code ++ """ ++ code = dedent( ++ """ ++ import sys ++ sys.stdout.write('cheese') ++ """ ++ ) ++ assert cmdmod.exec_code(available_python_executable, code).rstrip() == "cheese" ++ ++ ++@pytest.mark.slow_test ++def test_exec_code_with_single_arg(cmdmod, available_python_executable): ++ """ ++ cmd.exec_code ++ """ ++ code = dedent( ++ """ ++ import sys ++ sys.stdout.write(sys.argv[1]) ++ """ ++ ) ++ arg = "cheese" ++ assert cmdmod.exec_code(available_python_executable, code, args=arg).rstrip() == arg ++ ++ ++@pytest.mark.slow_test ++def test_exec_code_with_multiple_args(cmdmod, available_python_executable): ++ """ ++ cmd.exec_code ++ """ ++ code = dedent( ++ """ ++ import sys ++ sys.stdout.write(sys.argv[1]) ++ """ ++ ) ++ arg = "cheese" ++ assert ( ++ cmdmod.exec_code(available_python_executable, code, args=[arg, "test"]).rstrip() ++ == arg ++ ) ++ ++ ++@pytest.mark.slow_test ++def test_quotes(cmdmod): ++ """ ++ cmd.run with quoted command ++ """ ++ cmd = """echo 'SELECT * FROM foo WHERE bar="baz"' """ ++ expected_result = 'SELECT * FROM foo WHERE bar="baz"' ++ result = cmdmod.run_stdout(cmd).strip() ++ assert result == expected_result ++ ++ ++@pytest.mark.skip_if_not_root ++@pytest.mark.skip_on_windows(reason="Skip on Windows, requires password") ++def test_quotes_runas(cmdmod, running_username): ++ """ ++ cmd.run with quoted command ++ """ ++ cmd = """echo 'SELECT * FROM foo WHERE bar="baz"' """ ++ expected_result = 'SELECT * FROM foo WHERE bar="baz"' ++ result = cmdmod.run_all(cmd, runas=running_username) ++ errmsg = f"The command returned: {result}" ++ assert result["retcode"] == 0, errmsg ++ assert result["stdout"] == expected_result, errmsg ++ ++ ++@pytest.mark.destructive_test ++@pytest.mark.skip_if_not_root ++@pytest.mark.skip_on_windows(reason="Skip on Windows, uses unix commands") ++@pytest.mark.slow_test ++def test_cwd_runas(cmdmod, usermod, runas_usr, tmp_path): ++ """ ++ cmd.run should be able to change working directory correctly, whether ++ or not runas is in use. ++ """ ++ cmd = "pwd" ++ tmp_cwd = str(tmp_path) ++ os.chmod(tmp_cwd, 0o711) ++ ++ cwd_normal = cmdmod.run_stdout(cmd, cwd=tmp_cwd).rstrip("\n") ++ assert tmp_cwd == cwd_normal ++ ++ with _ensure_user_exists(runas_usr, usermod): ++ cwd_runas = cmdmod.run_stdout(cmd, cwd=tmp_cwd, runas=runas_usr).rstrip("\n") ++ assert tmp_cwd == cwd_runas ++ ++ ++@pytest.mark.destructive_test ++@pytest.mark.skip_if_not_root ++@pytest.mark.skip_unless_on_darwin(reason="Applicable to MacOS only") ++@pytest.mark.slow_test ++def test_runas_env(cmdmod, usermod, runas_usr): ++ """ ++ cmd.run should be able to change working directory correctly, whether ++ or not runas is in use. ++ """ ++ with _ensure_user_exists(runas_usr, usermod): ++ user_path = cmdmod.run_stdout('printf %s "$PATH"', runas=runas_usr) ++ # XXX: Not sure of a better way. Environment starts out with ++ # /bin:/usr/bin and should be populated by path helper and the bash ++ # profile. ++ assert "/bin:/usr/bin" != user_path ++ ++ ++@pytest.mark.destructive_test ++@pytest.mark.skip_if_not_root ++@pytest.mark.skip_unless_on_darwin(reason="Applicable to MacOS only") ++@pytest.mark.slow_test ++def test_runas_complex_command_bad_cwd(cmdmod, usermod, runas_usr, tmp_path): ++ """ ++ cmd.run should not accidentally run parts of a complex command when ++ given a cwd which cannot be used by the user the command is run as. ++ Due to the need to use `su -l` to login to another user on MacOS, we ++ cannot cd into directories that the target user themselves does not ++ have execute permission for. To an extent, this test is testing that ++ buggy behaviour, but its purpose is to ensure that the greater bug of ++ running commands after failing to cd does not occur. ++ """ ++ tmp_cwd = str(tmp_path) ++ os.chmod(tmp_cwd, 0o700) ++ ++ with _ensure_user_exists(runas_usr, usermod): ++ cmd_result = cmdmod.run_all( ++ 'pwd; pwd; : $(echo "You have failed the test" >&2)', ++ cwd=tmp_cwd, ++ runas=runas_usr, ++ ) ++ ++ assert "" == cmd_result["stdout"] ++ assert "You have failed the test" not in cmd_result["stderr"] ++ assert 0 != cmd_result["retcode"] ++ ++ ++@SKIP_INITIAL_PHOTONOS_FAILURES ++@pytest.mark.skip_on_windows ++@pytest.mark.skip_if_not_root ++@pytest.mark.destructive_test ++@pytest.mark.slow_test ++def test_runas(cmdmod, usermod, runas_usr): ++ """ ++ Ensure that the env is the runas user's ++ """ ++ with _ensure_user_exists(runas_usr, usermod): ++ out = cmdmod.run("env", runas=runas_usr).splitlines() ++ assert f"USER={runas_usr}" in out ++ ++ ++@pytest.mark.skip_if_binaries_missing("sleep", reason="sleep cmd not installed") ++def test_timeout(cmdmod): ++ """ ++ cmd.run trigger timeout ++ """ ++ out = cmdmod.run("sleep 2 && echo hello", timeout=1, python_shell=True) ++ assert "Timed out" in out ++ ++ ++@pytest.mark.skip_if_binaries_missing("sleep", reason="sleep cmd not installed") ++def test_timeout_success(cmdmod): ++ """ ++ cmd.run sufficient timeout to succeed ++ """ ++ out = cmdmod.run("sleep 1 && echo hello", timeout=2, python_shell=True) ++ assert out == "hello" ++ ++ ++@pytest.mark.slow_test ++def test_cmd_run_whoami(cmdmod, running_username): ++ """ ++ test return of whoami ++ """ ++ if not salt.utils.platform.is_windows(): ++ user = running_username ++ else: ++ user = salt.utils.user.get_specific_user() ++ if user.startswith("sudo_"): ++ user = user.replace("sudo_", "") ++ cmd = cmdmod.run("whoami") ++ assert user.lower() == cmd.lower() ++ ++ ++@pytest.mark.skip_unless_on_windows(reason="Minion is not Windows") ++@pytest.mark.slow_test ++def test_windows_env_handling(cmdmod): ++ """ ++ Ensure that nt.environ is used properly with cmd.run* ++ """ ++ out = cmdmod.run("set", env={"abc": "123", "ABC": "456"}).splitlines() ++ assert "abc=123" in out ++ assert "ABC=456" in out ++ ++ ++@pytest.mark.slow_test ++@pytest.mark.skip_unless_on_windows(reason="Minion is not Windows") ++def test_windows_powershell_script_args(cmdmod, issue_56195_test_ps1): ++ """ ++ Ensure that powershell processes inline script in args ++ """ ++ val = "i like cheese" ++ args = ( ++ '-SecureString (ConvertTo-SecureString -String "{}" -AsPlainText -Force)' ++ " -ErrorAction Stop".format(val) ++ ) ++ script = "salt://issue_56195_test.ps1" ++ ret = cmdmod.script(script, args=args, shell="powershell", saltenv="base") ++ assert ret["stdout"] == val ++ ++ ++@pytest.mark.slow_test ++@pytest.mark.skip_unless_on_windows(reason="Minion is not Windows") ++@pytest.mark.skip_if_binaries_missing("pwsh") ++def test_windows_powershell_script_args_pwsh(cmdmod, issue_56195_test_ps1): ++ """ ++ Ensure that powershell processes inline script in args with powershell ++ core ++ """ ++ val = "i like cheese" ++ args = ( ++ '-SecureString (ConvertTo-SecureString -String "{}" -AsPlainText -Force)' ++ " -ErrorAction Stop".format(val) ++ ) ++ script = "salt://issue_56195_test.ps1" ++ ret = cmdmod.script(script, args=args, shell="pwsh", saltenv="base") ++ assert ret["stdout"] == val +diff --git a/tests/pytests/functional/modules/test_dockermod.py b/tests/pytests/functional/modules/test_dockermod.py +index 3c7bb25e461..a5b40869352 100644 +--- a/tests/pytests/functional/modules/test_dockermod.py ++++ b/tests/pytests/functional/modules/test_dockermod.py +@@ -2,6 +2,7 @@ + Integration tests for the docker_container states + """ + import logging ++import os + + import pytest + from saltfactories.utils import random_string +@@ -11,9 +12,12 @@ pytest.importorskip("docker") + + log = logging.getLogger(__name__) + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing("docker", "dockerd", check_all=False), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run inside a container"), + ] + + +diff --git a/tests/pytests/functional/modules/test_swarm.py b/tests/pytests/functional/modules/test_swarm.py +index 8c0ce8cbd93..9dc70f5b3dc 100644 +--- a/tests/pytests/functional/modules/test_swarm.py ++++ b/tests/pytests/functional/modules/test_swarm.py +@@ -1,10 +1,15 @@ ++import os ++ + import pytest + + import salt.utils.versions + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing("dockerd"), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="No hwclock in a container"), + ] + + # The swarm module need the docker-py library installed +diff --git a/tests/pytests/functional/modules/test_system.py b/tests/pytests/functional/modules/test_system.py +index 2dabaaebfad..3b669c46afd 100644 +--- a/tests/pytests/functional/modules/test_system.py ++++ b/tests/pytests/functional/modules/test_system.py +@@ -9,9 +9,12 @@ import pytest + + import salt.utils.files + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.skip_unless_on_linux, + pytest.mark.slow_test, ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="No systemd in container."), + ] + + log = logging.getLogger(__name__) +diff --git a/tests/pytests/functional/pillar/hg_pillar/test_hg_pillar.py b/tests/pytests/functional/pillar/hg_pillar/test_hg_pillar.py +index 183b002d8b2..44603d96f1d 100644 +--- a/tests/pytests/functional/pillar/hg_pillar/test_hg_pillar.py ++++ b/tests/pytests/functional/pillar/hg_pillar/test_hg_pillar.py +@@ -60,6 +60,7 @@ def hg_setup_and_teardown(): + @pytest.mark.skip_on_windows( + reason="just testing if this or hgfs causes the issue with total crash" + ) ++@pytest.mark.skipif(not HAS_HG, reason="missing hglib library") + def test_ext_pillar(hg_setup_and_teardown): + data = hg_pillar.ext_pillar("*", None, hg_setup_and_teardown) + assert data == {"testinfo": "info", "testinfo2": "info"} +diff --git a/tests/pytests/functional/states/rabbitmq/test_cluster.py b/tests/pytests/functional/states/rabbitmq/test_cluster.py +index f8b4bdc225e..210b22a2360 100644 +--- a/tests/pytests/functional/states/rabbitmq/test_cluster.py ++++ b/tests/pytests/functional/states/rabbitmq/test_cluster.py +@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_cluster states + """ + + import logging ++import os + + import pytest + +@@ -13,11 +14,14 @@ pytest.importorskip("docker") + + log = logging.getLogger(__name__) + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing( + "docker", "dockerd", reason="Docker not installed" + ), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/functional/states/rabbitmq/test_plugin.py b/tests/pytests/functional/states/rabbitmq/test_plugin.py +index e1b686e3365..f1191490536 100644 +--- a/tests/pytests/functional/states/rabbitmq/test_plugin.py ++++ b/tests/pytests/functional/states/rabbitmq/test_plugin.py +@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_plugin states + """ + + import logging ++import os + + import pytest + +@@ -14,11 +15,14 @@ log = logging.getLogger(__name__) + + pytest.importorskip("docker") + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing( + "docker", "dockerd", reason="Docker not installed" + ), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/functional/states/rabbitmq/test_policy.py b/tests/pytests/functional/states/rabbitmq/test_policy.py +index e5cee97cbc8..7ccf6a522e0 100644 +--- a/tests/pytests/functional/states/rabbitmq/test_policy.py ++++ b/tests/pytests/functional/states/rabbitmq/test_policy.py +@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_policy states + """ + + import logging ++import os + + import pytest + +@@ -14,11 +15,14 @@ log = logging.getLogger(__name__) + + pytest.importorskip("docker") + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing( + "docker", "dockerd", reason="Docker not installed" + ), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/functional/states/rabbitmq/test_upstream.py b/tests/pytests/functional/states/rabbitmq/test_upstream.py +index cfdad35aba6..c7bcf3b0d44 100644 +--- a/tests/pytests/functional/states/rabbitmq/test_upstream.py ++++ b/tests/pytests/functional/states/rabbitmq/test_upstream.py +@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_user states + """ + + import logging ++import os + + import pytest + +@@ -13,11 +14,14 @@ log = logging.getLogger(__name__) + + pytest.importorskip("docker") + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing( + "docker", "dockerd", reason="Docker not installed" + ), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/functional/states/rabbitmq/test_user.py b/tests/pytests/functional/states/rabbitmq/test_user.py +index 2f9b22d28d2..31723df7be8 100644 +--- a/tests/pytests/functional/states/rabbitmq/test_user.py ++++ b/tests/pytests/functional/states/rabbitmq/test_user.py +@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_user states + """ + + import logging ++import os + + import pytest + +@@ -13,11 +14,14 @@ log = logging.getLogger(__name__) + + pytest.importorskip("docker") + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing( + "docker", "dockerd", reason="Docker not installed" + ), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/functional/states/rabbitmq/test_vhost.py b/tests/pytests/functional/states/rabbitmq/test_vhost.py +index a648d41854f..d6ac6901a25 100644 +--- a/tests/pytests/functional/states/rabbitmq/test_vhost.py ++++ b/tests/pytests/functional/states/rabbitmq/test_vhost.py +@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_user states + """ + + import logging ++import os + + import pytest + +@@ -13,11 +14,14 @@ log = logging.getLogger(__name__) + + pytest.importorskip("docker") + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing( + "docker", "dockerd", reason="Docker not installed" + ), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/functional/states/test_docker_network.py b/tests/pytests/functional/states/test_docker_network.py +index 16a78b13a4a..0da01ed8bac 100644 +--- a/tests/pytests/functional/states/test_docker_network.py ++++ b/tests/pytests/functional/states/test_docker_network.py +@@ -1,5 +1,6 @@ + import functools + import logging ++import os + import random + + import pytest +@@ -15,9 +16,13 @@ pytest.importorskip("docker") + + log = logging.getLogger(__name__) + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing("docker", "dockerd", check_all=False), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/functional/states/test_pkg.py b/tests/pytests/functional/states/test_pkg.py +index 0e82dc608ba..12318c996d1 100644 +--- a/tests/pytests/functional/states/test_pkg.py ++++ b/tests/pytests/functional/states/test_pkg.py +@@ -64,7 +64,7 @@ def PKG_CAP_TARGETS(grains): + _PKG_CAP_TARGETS = [] + if grains["os_family"] == "Suse": + if grains["os"] == "SUSE": +- _PKG_CAP_TARGETS = [("perl(ZNC)", "znc-perl")] ++ _PKG_CAP_TARGETS = [("perl(Error)", "perl-Error")] + if not _PKG_CAP_TARGETS: + pytest.skip("Capability not provided") + return _PKG_CAP_TARGETS +@@ -856,8 +856,8 @@ def test_pkg_cap_003_installed_multipkg_with_version( + This is a destructive test as it installs and then removes two packages + """ + target, realpkg = PKG_CAP_TARGETS[0] +- version = latest_version(target) +- realver = latest_version(realpkg) ++ version = modules.pkg.version(target) ++ realver = modules.pkg.version(realpkg) + + # If this condition is False, we need to find new targets. + # This needs to be able to test successful installation of packages. +diff --git a/tests/pytests/integration/cli/test_syndic_eauth.py b/tests/pytests/integration/cli/test_syndic_eauth.py +index 57e9c0a467a..218022b9e3c 100644 +--- a/tests/pytests/integration/cli/test_syndic_eauth.py ++++ b/tests/pytests/integration/cli/test_syndic_eauth.py +@@ -1,4 +1,5 @@ + import json ++import os + import pathlib + import tempfile + import time +@@ -7,9 +8,11 @@ import pytest + + docker = pytest.importorskip("docker") + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" + + pytestmark = [ + pytest.mark.core_test, ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/integration/daemons/test_memory_leak.py b/tests/pytests/integration/daemons/test_memory_leak.py +index 1b782760418..8157091c44e 100644 +--- a/tests/pytests/integration/daemons/test_memory_leak.py ++++ b/tests/pytests/integration/daemons/test_memory_leak.py +@@ -1,3 +1,4 @@ ++import os + import time + from multiprocessing import Manager, Process + +@@ -8,6 +9,8 @@ pytestmark = [ + pytest.mark.slow_test, + ] + ++GITHUB_ACTIONS = bool(os.getenv("GITHUB_ACTIONS", False)) ++ + + @pytest.fixture + def testfile_path(tmp_path): +@@ -45,6 +48,7 @@ def file_add_delete_sls(testfile_path, base_env_state_tree_root_dir): + + + @pytest.mark.skip_on_darwin(reason="MacOS is a spawning platform, won't work") ++@pytest.mark.skipif(GITHUB_ACTIONS, reason="Test is failing in GitHub Actions") + @pytest.mark.flaky(max_runs=4) + def test_memory_leak(salt_cli, salt_minion, file_add_delete_sls): + max_usg = None +diff --git a/tests/pytests/integration/modules/test_cmdmod.py b/tests/pytests/integration/modules/test_cmdmod.py +index 4e8ce5824ee..d9c326c3f0a 100644 +--- a/tests/pytests/integration/modules/test_cmdmod.py ++++ b/tests/pytests/integration/modules/test_cmdmod.py +@@ -1,5 +1,11 @@ ++import logging ++ + import pytest + ++import salt.utils.user ++ ++log = logging.getLogger(__name__) ++ + + @pytest.fixture(scope="module") + def non_root_account(): +@@ -7,6 +13,14 @@ def non_root_account(): + yield account + + ++@pytest.fixture ++def running_username(): ++ """ ++ Return the username that is running the code. ++ """ ++ return salt.utils.user.get_user() ++ ++ + @pytest.mark.skip_if_not_root + def test_exec_code_all(salt_call_cli, non_root_account): + ret = salt_call_cli.run( +@@ -22,3 +36,82 @@ def test_long_stdout(salt_cli, salt_minion): + ) + assert ret.returncode == 0 + assert len(ret.data.strip()) == len(echo_str) ++ ++ ++@pytest.mark.skip_if_not_root ++@pytest.mark.skip_on_windows(reason="Skip on Windows, uses unix commands") ++def test_avoid_injecting_shell_code_as_root( ++ salt_call_cli, non_root_account, running_username ++): ++ """ ++ cmd.run should execute the whole command as the "runas" user, not ++ running substitutions as root. ++ """ ++ cmd = "echo $(id -u)" ++ ++ ret = salt_call_cli.run("cmd.run_stdout", cmd) ++ root_id = ret.json ++ ret = salt_call_cli.run("cmd.run_stdout", cmd, runas=running_username) ++ runas_root_id = ret.json ++ ++ ret = salt_call_cli.run("cmd.run_stdout", cmd, runas=non_root_account.username) ++ user_id = ret.json ++ ++ assert user_id != root_id ++ assert user_id != runas_root_id ++ assert root_id == runas_root_id ++ ++ ++@pytest.mark.slow_test ++def test_blacklist_glob(salt_call_cli): ++ """ ++ cmd_blacklist_glob ++ """ ++ cmd = "bad_command --foo" ++ ret = salt_call_cli.run( ++ "cmd.run", ++ cmd, ++ ) ++ ++ assert ( ++ ret.stderr.rstrip() ++ == "Error running 'cmd.run': The shell command \"bad_command --foo\" is not permitted" ++ ) ++ ++ ++@pytest.mark.slow_test ++def test_hide_output(salt_call_cli): ++ """ ++ Test the hide_output argument ++ """ ++ ls_command = ( ++ ["ls", "/"] if not salt.utils.platform.is_windows() else ["dir", "c:\\"] ++ ) ++ ++ error_command = ["thiscommanddoesnotexist"] ++ ++ # cmd.run ++ ret = salt_call_cli.run("cmd.run", ls_command, hide_output=True) ++ assert ret.data == "" ++ ++ # cmd.shell ++ ret = salt_call_cli.run("cmd.shell", ls_command, hide_output=True) ++ assert ret.data == "" ++ ++ # cmd.run_stdout ++ ret = salt_call_cli.run("cmd.run_stdout", ls_command, hide_output=True) ++ assert ret.data == "" ++ ++ # cmd.run_stderr ++ ret = salt_call_cli.run("cmd.shell", error_command, hide_output=True) ++ assert ret.data == "" ++ ++ # cmd.run_all (command should have produced stdout) ++ ret = salt_call_cli.run("cmd.run_all", ls_command, hide_output=True) ++ assert ret.data["stdout"] == "" ++ assert ret.data["stderr"] == "" ++ ++ # cmd.run_all (command should have produced stderr) ++ ret = salt_call_cli.run("cmd.run_all", error_command, hide_output=True) ++ assert ret.data["stdout"] == "" ++ assert ret.data["stderr"] == "" +diff --git a/tests/pytests/integration/modules/test_virt.py b/tests/pytests/integration/modules/test_virt.py +index 57ec239c4e9..1b7f30154a7 100644 +--- a/tests/pytests/integration/modules/test_virt.py ++++ b/tests/pytests/integration/modules/test_virt.py +@@ -2,6 +2,7 @@ + Validate the virt module + """ + import logging ++import os + from numbers import Number + from xml.etree import ElementTree + +@@ -14,9 +15,12 @@ docker = pytest.importorskip("docker") + + log = logging.getLogger(__name__) + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing("docker"), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/integration/ssh/test_log.py b/tests/pytests/integration/ssh/test_log.py +index e87c4a8581f..683feb8bd91 100644 +--- a/tests/pytests/integration/ssh/test_log.py ++++ b/tests/pytests/integration/ssh/test_log.py +@@ -2,6 +2,7 @@ + Integration tests for salt-ssh logging + """ + import logging ++import os + import time + + import pytest +@@ -11,12 +12,14 @@ from tests.support.helpers import Keys + + pytest.importorskip("docker") + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" + + log = logging.getLogger(__name__) + + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing("dockerd"), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/integration/ssh/test_master.py b/tests/pytests/integration/ssh/test_master.py +index 31e318870cb..0c2f482cf9f 100644 +--- a/tests/pytests/integration/ssh/test_master.py ++++ b/tests/pytests/integration/ssh/test_master.py +@@ -2,6 +2,8 @@ + Simple Smoke Tests for Connected SSH minions + """ + ++import os ++ + import pytest + from saltfactories.utils.functional import StateResult + +@@ -10,7 +12,10 @@ pytestmark = [ + pytest.mark.skip_on_windows(reason="salt-ssh not available on Windows"), + ] + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + ++@pytest.mark.skipif(INSIDE_CONTAINER, reason="No systemd in container.") + @pytest.mark.skip_if_not_root + def test_service(salt_ssh_cli, grains): + service = "cron" +diff --git a/tests/pytests/integration/ssh/test_py_versions.py b/tests/pytests/integration/ssh/test_py_versions.py +index 52ab819e808..71d4cfaa94e 100644 +--- a/tests/pytests/integration/ssh/test_py_versions.py ++++ b/tests/pytests/integration/ssh/test_py_versions.py +@@ -2,6 +2,7 @@ + Integration tests for salt-ssh py_versions + """ + import logging ++import os + import socket + import time + +@@ -12,12 +13,14 @@ from tests.support.helpers import Keys + + pytest.importorskip("docker") + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" + + log = logging.getLogger(__name__) + + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing("dockerd"), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/integration/ssh/test_ssh_setup.py b/tests/pytests/integration/ssh/test_ssh_setup.py +index eddf31caccd..79b55ad90a5 100644 +--- a/tests/pytests/integration/ssh/test_ssh_setup.py ++++ b/tests/pytests/integration/ssh/test_ssh_setup.py +@@ -17,12 +17,14 @@ from tests.support.helpers import Keys + + pytest.importorskip("docker") + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" + + log = logging.getLogger(__name__) + + pytestmark = [ + pytest.mark.slow_test, + pytest.mark.skip_if_binaries_missing("dockerd"), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/scenarios/compat/test_with_versions.py b/tests/pytests/scenarios/compat/test_with_versions.py +index 75a2b87f24c..498dd6a60de 100644 +--- a/tests/pytests/scenarios/compat/test_with_versions.py ++++ b/tests/pytests/scenarios/compat/test_with_versions.py +@@ -5,6 +5,7 @@ + Test current salt master with older salt minions + """ + import logging ++import os + import pathlib + + import pytest +@@ -18,6 +19,8 @@ docker = pytest.importorskip("docker") + + log = logging.getLogger(__name__) + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + + pytestmark = [ + pytest.mark.slow_test, +@@ -25,6 +28,7 @@ pytestmark = [ + pytest.mark.skipif( + salt.utils.platform.is_photonos() is True, reason="Skip on PhotonOS" + ), ++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"), + ] + + +diff --git a/tests/pytests/scenarios/failover/multimaster/test_failover_master.py b/tests/pytests/scenarios/failover/multimaster/test_failover_master.py +index 6efecfb8334..9f6251a4d6f 100644 +--- a/tests/pytests/scenarios/failover/multimaster/test_failover_master.py ++++ b/tests/pytests/scenarios/failover/multimaster/test_failover_master.py +@@ -12,7 +12,10 @@ pytestmark = [ + + log = logging.getLogger(__name__) + ++GITHUB_ACTIONS = bool(os.getenv("GITHUB_ACTIONS", False)) + ++ ++@pytest.mark.skipif(GITHUB_ACTIONS, reason="Test is failing in GitHub Actions") + def test_pki(salt_mm_failover_master_1, salt_mm_failover_master_2, caplog): + """ + Verify https://docs.saltproject.io/en/latest/topics/tutorials/multimaster_pki.html +diff --git a/tests/pytests/scenarios/setup/test_install.py b/tests/pytests/scenarios/setup/test_install.py +index 48f1d5889f6..7664fda804e 100644 +--- a/tests/pytests/scenarios/setup/test_install.py ++++ b/tests/pytests/scenarios/setup/test_install.py +@@ -3,6 +3,7 @@ Tests for building and installing salt + """ + import json + import logging ++import os + import pathlib + import re + import sys +@@ -16,11 +17,16 @@ from salt.modules.virtualenv_mod import KNOWN_BINARY_NAMES + + log = logging.getLogger(__name__) + ++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container" ++ + pytestmark = [ + pytest.mark.core_test, + pytest.mark.windows_whitelisted, + pytest.mark.skip_initial_onedir_failure, + pytest.mark.skip_if_binaries_missing(*KNOWN_BINARY_NAMES, check_all=False), ++ pytest.mark.skipif( ++ INSIDE_CONTAINER, reason="No gcc and python3-devel in container." ++ ), + ] + + +diff --git a/tests/pytests/unit/modules/test_aptpkg.py b/tests/pytests/unit/modules/test_aptpkg.py +index eb72447c3aa..6f0b905ef73 100644 +--- a/tests/pytests/unit/modules/test_aptpkg.py ++++ b/tests/pytests/unit/modules/test_aptpkg.py +@@ -1360,17 +1360,17 @@ def test_call_apt_dpkg_lock(): + ] + + cmd_mock = MagicMock(side_effect=cmd_side_effect) +- cmd_call = ( ++ cmd_call = [ + call( + ["dpkg", "-l", "python"], +- env={}, +- ignore_retcode=False, + output_loglevel="quiet", + python_shell=True, ++ env={}, ++ ignore_retcode=False, + username="Darth Vader", + ), +- ) +- expected_calls = [cmd_call * 5] ++ ] ++ expected_calls = cmd_call * 5 + + with patch.dict( + aptpkg.__salt__, +@@ -1390,7 +1390,7 @@ def test_call_apt_dpkg_lock(): + + # We should attempt to call the cmd 5 times + assert cmd_mock.call_count == 5 +- cmd_mock.has_calls(expected_calls) ++ cmd_mock.assert_has_calls(expected_calls) + + + def test_services_need_restart_checkrestart_missing(): +diff --git a/tests/pytests/unit/modules/test_linux_sysctl.py b/tests/pytests/unit/modules/test_linux_sysctl.py +index 0bdd24039d7..6b0875bc460 100644 +--- a/tests/pytests/unit/modules/test_linux_sysctl.py ++++ b/tests/pytests/unit/modules/test_linux_sysctl.py +@@ -215,7 +215,7 @@ def test_persist_no_conf_failure(): + ): + with pytest.raises(CommandExecutionError): + linux_sysctl.persist("net.ipv4.ip_forward", 42, config=None) +- fopen_mock.called_once() ++ fopen_mock.assert_called_once() + + + def test_persist_no_conf_success(): +@@ -353,7 +353,7 @@ def test_persist_value_with_spaces_already_set(tmp_path): + """ + config = str(tmp_path / "existing_sysctl_with_spaces.conf") + value = "|/usr/share/kdump-tools/dump-core %p %s %t %e" +- config_file_content = "kernel.core_pattern = {}\n".format(value) ++ config_file_content = f"kernel.core_pattern = {value}\n" + with fopen(config, "w", encoding="utf-8") as config_file: + config_file.write(config_file_content) + mock_run = MagicMock(return_value=value) +@@ -383,7 +383,7 @@ def test_persist_value_with_spaces_already_configured(tmp_path): + """ + config = str(tmp_path / "existing_sysctl_with_spaces.conf") + value = "|/usr/share/kdump-tools/dump-core %p %s %t %e" +- config_file_content = "kernel.core_pattern = {}\n".format(value) ++ config_file_content = f"kernel.core_pattern = {value}\n" + with fopen(config, "w", encoding="utf-8") as config_file: + config_file.write(config_file_content) + mock_run = MagicMock(return_value="") +@@ -451,7 +451,7 @@ def test_persist_value_with_spaces_update_config(tmp_path): + assert os.path.isfile(config) + with fopen(config, encoding="utf-8") as config_file: + written = config_file.read() +- assert written == "kernel.core_pattern = {}\n".format(value) ++ assert written == f"kernel.core_pattern = {value}\n" + + + def test_persist_value_with_spaces_new_file(tmp_path): +diff --git a/tests/pytests/unit/modules/test_win_ip.py b/tests/pytests/unit/modules/test_win_ip.py +index 38eb6b1ac5f..94a3fe7ca93 100644 +--- a/tests/pytests/unit/modules/test_win_ip.py ++++ b/tests/pytests/unit/modules/test_win_ip.py +@@ -151,7 +151,7 @@ def test_enable(): + ): + assert win_ip.enable("Ethernet") + +- mock_cmd.called_once_with( ++ mock_cmd.assert_called_once_with( + [ + "netsh", + "interface", +@@ -180,7 +180,7 @@ def test_disable(): + ): + assert win_ip.disable("Ethernet") + +- mock_cmd.called_once_with( ++ mock_cmd.assert_called_once_with( + [ + "netsh", + "interface", +diff --git a/tests/pytests/unit/test_master.py b/tests/pytests/unit/test_master.py +index d338307d1f8..679229066d4 100644 +--- a/tests/pytests/unit/test_master.py ++++ b/tests/pytests/unit/test_master.py +@@ -61,7 +61,7 @@ def test_fileserver_duration(): + end = time.time() + # Interval is equal to timeout so the _do_update method will be called + # one time. +- update.called_once() ++ update.assert_called_once() + # Timeout is 1 second + duration = end - start + if duration > 2 and salt.utils.platform.spawning_platform(): +diff --git a/tests/pytests/unit/test_minion.py b/tests/pytests/unit/test_minion.py +index 740743194e4..a9e91742a2d 100644 +--- a/tests/pytests/unit/test_minion.py ++++ b/tests/pytests/unit/test_minion.py +@@ -655,7 +655,9 @@ def test_gen_modules_executors(minion_opts): + with patch("salt.pillar.get_pillar", return_value=MockPillarCompiler()): + with patch("salt.loader.executors") as execmock: + minion.gen_modules() +- assert execmock.called_with(minion.opts, minion.functions) ++ execmock.assert_called_with( ++ minion.opts, functions=minion.functions, proxy=minion.proxy, context={} ++ ) + finally: + minion.destroy() + +diff --git a/tests/pytests/unit/utils/event/test_event.py b/tests/pytests/unit/utils/event/test_event.py +index e289e72dad0..f4b6c159996 100644 +--- a/tests/pytests/unit/utils/event/test_event.py ++++ b/tests/pytests/unit/utils/event/test_event.py +@@ -38,7 +38,7 @@ def sock_dir(tmp_path): + def _assert_got_event(evt, data, msg=None, expected_failure=False): + assert evt is not None, msg + for key in data: +- assert key in evt, "{}: Key {} missing".format(msg, key) ++ assert key in evt, f"{msg}: Key {key} missing" + assertMsg = "{0}: Key {1} value mismatch, {2} != {3}" + assertMsg = assertMsg.format(msg, key, data[key], evt[key]) + if not expected_failure: +@@ -59,8 +59,8 @@ def test_minion_event(sock_dir): + :10 + ] + with salt.utils.event.MinionEvent(opts, listen=False) as me: +- assert me.puburi == str(sock_dir / "minion_event_{}_pub.ipc".format(id_hash)) +- assert me.pulluri == str(sock_dir / "minion_event_{}_pull.ipc".format(id_hash)) ++ assert me.puburi == str(sock_dir / f"minion_event_{id_hash}_pub.ipc") ++ assert me.pulluri == str(sock_dir / f"minion_event_{id_hash}_pull.ipc") + + + def test_minion_event_tcp_ipc_mode(): +@@ -73,8 +73,8 @@ def test_minion_event_tcp_ipc_mode(): + def test_minion_event_no_id(sock_dir): + with salt.utils.event.MinionEvent(dict(sock_dir=str(sock_dir)), listen=False) as me: + id_hash = hashlib.sha256(salt.utils.stringutils.to_bytes("")).hexdigest()[:10] +- assert me.puburi == str(sock_dir / "minion_event_{}_pub.ipc".format(id_hash)) +- assert me.pulluri == str(sock_dir / "minion_event_{}_pull.ipc".format(id_hash)) ++ assert me.puburi == str(sock_dir / f"minion_event_{id_hash}_pub.ipc") ++ assert me.pulluri == str(sock_dir / f"minion_event_{id_hash}_pull.ipc") + + + @pytest.mark.slow_test +@@ -256,9 +256,9 @@ def test_event_many(sock_dir): + with eventpublisher_process(str(sock_dir)): + with salt.utils.event.MasterEvent(str(sock_dir), listen=True) as me: + for i in range(500): +- me.fire_event({"data": "{}".format(i)}, "testevents") ++ me.fire_event({"data": f"{i}"}, "testevents") + evt = me.get_event(tag="testevents") +- _assert_got_event(evt, {"data": "{}".format(i)}, "Event {}".format(i)) ++ _assert_got_event(evt, {"data": f"{i}"}, f"Event {i}") + + + @pytest.mark.slow_test +@@ -268,10 +268,10 @@ def test_event_many_backlog(sock_dir): + with salt.utils.event.MasterEvent(str(sock_dir), listen=True) as me: + # Must not exceed zmq HWM + for i in range(500): +- me.fire_event({"data": "{}".format(i)}, "testevents") ++ me.fire_event({"data": f"{i}"}, "testevents") + for i in range(500): + evt = me.get_event(tag="testevents") +- _assert_got_event(evt, {"data": "{}".format(i)}, "Event {}".format(i)) ++ _assert_got_event(evt, {"data": f"{i}"}, f"Event {i}") + + + # Test the fire_master function. As it wraps the underlying fire_event, +@@ -300,7 +300,7 @@ def test_connect_pull_should_debug_log_on_StreamClosedError(): + event = SaltEvent(node=None) + with patch.object(event, "pusher") as mock_pusher: + with patch.object( +- salt.utils.event.log, "debug", auto_spec=True ++ salt.utils.event.log, "debug", autospec=True + ) as mock_log_debug: + mock_pusher.connect.side_effect = ( + salt.ext.tornado.iostream.StreamClosedError +@@ -317,10 +317,10 @@ def test_connect_pull_should_error_log_on_other_errors(error): + event = SaltEvent(node=None) + with patch.object(event, "pusher") as mock_pusher: + with patch.object( +- salt.utils.event.log, "debug", auto_spec=True ++ salt.utils.event.log, "debug", autospec=True + ) as mock_log_debug: + with patch.object( +- salt.utils.event.log, "error", auto_spec=True ++ salt.utils.event.log, "error", autospec=True + ) as mock_log_error: + mock_pusher.connect.side_effect = error + event.connect_pull() +diff --git a/tests/unit/modules/test_boto_apigateway.py b/tests/unit/modules/test_boto_apigateway.py +index 5f3d2a49822..ebf50679bd8 100644 +--- a/tests/unit/modules/test_boto_apigateway.py ++++ b/tests/unit/modules/test_boto_apigateway.py +@@ -15,6 +15,7 @@ from tests.support.unit import TestCase + + # pylint: disable=import-error,no-name-in-module + try: ++ import boto + import boto3 + import botocore + from botocore.exceptions import ClientError +diff --git a/tests/unit/modules/test_boto_cognitoidentity.py b/tests/unit/modules/test_boto_cognitoidentity.py +index 1e213a169ac..974832f9ff9 100644 +--- a/tests/unit/modules/test_boto_cognitoidentity.py ++++ b/tests/unit/modules/test_boto_cognitoidentity.py +@@ -14,6 +14,7 @@ from tests.support.unit import TestCase + + # pylint: disable=import-error,no-name-in-module + try: ++ import boto + import boto3 + from botocore.exceptions import ClientError + +diff --git a/tests/unit/modules/test_boto_elasticsearch_domain.py b/tests/unit/modules/test_boto_elasticsearch_domain.py +index 5c5845aa25b..0578a81e8ef 100644 +--- a/tests/unit/modules/test_boto_elasticsearch_domain.py ++++ b/tests/unit/modules/test_boto_elasticsearch_domain.py +@@ -14,6 +14,7 @@ from tests.support.unit import TestCase + + # pylint: disable=import-error,no-name-in-module + try: ++ import boto + import boto3 + from botocore.exceptions import ClientError + +diff --git a/tests/unit/modules/test_boto_lambda.py b/tests/unit/modules/test_boto_lambda.py +index d32dc9345b6..ecaa532f1ff 100644 +--- a/tests/unit/modules/test_boto_lambda.py ++++ b/tests/unit/modules/test_boto_lambda.py +@@ -18,6 +18,7 @@ from tests.support.unit import TestCase + + # pylint: disable=import-error,no-name-in-module + try: ++ import boto + import boto3 + from botocore import __version__ as found_botocore_version + from botocore.exceptions import ClientError +diff --git a/tests/unit/modules/test_network.py b/tests/unit/modules/test_network.py +index 34b06250fc6..9eef9a02f58 100644 +--- a/tests/unit/modules/test_network.py ++++ b/tests/unit/modules/test_network.py +@@ -153,9 +153,11 @@ class NetworkTestCase(TestCase, LoaderModuleMockMixin): + """ + Test for Performs a DNS lookup with dig + """ +- with patch("salt.utils.path.which", MagicMock(return_value="dig")), patch.dict( ++ with patch.dict( + network.__utils__, {"network.sanitize_host": MagicMock(return_value="A")} +- ), patch.dict(network.__salt__, {"cmd.run": MagicMock(return_value="A")}): ++ ), patch("salt.utils.path.which", MagicMock(return_value="dig")), patch.dict( ++ network.__salt__, {"cmd.run": MagicMock(return_value="A")} ++ ): + self.assertEqual(network.dig("host"), "A") + + def test_arp(self): +diff --git a/tests/unit/modules/test_nilrt_ip.py b/tests/unit/modules/test_nilrt_ip.py +index 1261473edb4..50dc13b20b8 100644 +--- a/tests/unit/modules/test_nilrt_ip.py ++++ b/tests/unit/modules/test_nilrt_ip.py +@@ -28,7 +28,7 @@ class NilrtIPTestCase(TestCase, LoaderModuleMockMixin): + "salt.modules.nilrt_ip._change_dhcp_config", return_value=True + ) as change_dhcp_config_mock: + assert nilrt_ip._change_state("test_interface", "down") +- assert change_dhcp_config_mock.called_with("test_interface", False) ++ change_dhcp_config_mock.assert_called_with("test_interface", False) + + def test_change_state_up_state(self): + """ +@@ -42,7 +42,7 @@ class NilrtIPTestCase(TestCase, LoaderModuleMockMixin): + "salt.modules.nilrt_ip._change_dhcp_config", return_value=True + ) as change_dhcp_config_mock: + assert nilrt_ip._change_state("test_interface", "up") +- assert change_dhcp_config_mock.called_with("test_interface") ++ change_dhcp_config_mock.assert_called_with("test_interface") + + def test_set_static_all_with_dns(self): + """ +diff --git a/tests/unit/modules/test_zcbuildout.py b/tests/unit/modules/test_zcbuildout.py +index f793e3fc3f8..5a5996e110e 100644 +--- a/tests/unit/modules/test_zcbuildout.py ++++ b/tests/unit/modules/test_zcbuildout.py +@@ -451,6 +451,7 @@ class BuildoutOnlineTestCase(Base): + ) + + @pytest.mark.slow_test ++ @pytest.mark.skip(reason="TODO this test should probably be fixed") + def test_run_buildout(self): + if salt.modules.virtualenv_mod.virtualenv_ver(self.ppy_st) >= (20, 0, 0): + self.skipTest( +@@ -467,6 +468,7 @@ class BuildoutOnlineTestCase(Base): + self.assertTrue("Installing b" in out) + + @pytest.mark.slow_test ++ @pytest.mark.skip(reason="TODO this test should probably be fixed") + def test_buildout(self): + if salt.modules.virtualenv_mod.virtualenv_ver(self.ppy_st) >= (20, 0, 0): + self.skipTest( +diff --git a/tests/unit/netapi/rest_tornado/test_saltnado.py b/tests/unit/netapi/rest_tornado/test_saltnado.py +index 7b63a65d4f3..c4758e700ab 100644 +--- a/tests/unit/netapi/rest_tornado/test_saltnado.py ++++ b/tests/unit/netapi/rest_tornado/test_saltnado.py +@@ -647,7 +647,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase): + with patch.object( + self.handler.application.event_listener, + "get_event", +- autospec=True, + side_effect=fancy_get_event, + ), patch.dict( + self.handler.application.opts, +@@ -698,7 +697,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase): + with patch.object( + self.handler.application.event_listener, + "get_event", +- autospec=True, + side_effect=fancy_get_event, + ), patch.object( + self.handler, +@@ -729,8 +727,8 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase): + { + "tag": "fnord", + "data": { +- "return": "return from fnord {}".format(i), +- "id": "fnord {}".format(i), ++ "return": f"return from fnord {i}", ++ "id": f"fnord {i}", + }, + } + ) +@@ -760,7 +758,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase): + with patch.object( + self.handler.application.event_listener, + "get_event", +- autospec=True, + side_effect=fancy_get_event, + ), patch.object( + self.handler, +@@ -794,8 +791,8 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase): + { + "tag": "fnord", + "data": { +- "return": "return from fnord {}".format(i), +- "id": "fnord {}".format(i), ++ "return": f"return from fnord {i}", ++ "id": f"fnord {i}", + }, + } + ) +@@ -820,7 +817,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase): + with patch.object( + self.handler.application.event_listener, + "get_event", +- autospec=True, + side_effect=fancy_get_event, + ), patch.dict( + self.handler.application.opts, +@@ -843,12 +839,12 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase): + completed_events = [salt.ext.tornado.gen.Future() for _ in range(10)] + events_by_id = {} + for i, event in enumerate(completed_events): +- id_ = "fnord {}".format(i) ++ id_ = f"fnord {i}" + events_by_id[id_] = event + event.set_result( + { + "tag": "fnord", +- "data": {"return": "return from {}".format(id_), "id": id_}, ++ "data": {"return": f"return from {id_}", "id": id_}, + } + ) + expected_result = { +@@ -878,7 +874,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase): + with patch.object( + self.handler.application.event_listener, + "get_event", +- autospec=True, + side_effect=fancy_get_event, + ), patch.dict( + self.handler.application.opts, +@@ -904,12 +899,12 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase): + events_by_id = {} + # Setup some real-enough looking return data + for i, event in enumerate(completed_events): +- id_ = "fnord {}".format(i) ++ id_ = f"fnord {i}" + events_by_id[id_] = event + event.set_result( + { + "tag": "fnord", +- "data": {"return": "return from {}".format(id_), "id": id_}, ++ "data": {"return": f"return from {id_}", "id": id_}, + } + ) + # Hard coded instead of dynamic to avoid potentially writing a test +@@ -971,7 +966,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase): + with patch.object( + self.handler.application.event_listener, + "get_event", +- autospec=True, + side_effect=fancy_get_event, + ), patch.object( + self.handler, +diff --git a/tests/unit/states/test_boto_apigateway.py b/tests/unit/states/test_boto_apigateway.py +index 51c85d6058a..1edde8d303c 100644 +--- a/tests/unit/states/test_boto_apigateway.py ++++ b/tests/unit/states/test_boto_apigateway.py +@@ -20,6 +20,7 @@ from tests.support.unit import TestCase + from tests.unit.modules.test_boto_apigateway import BotoApiGatewayTestCaseMixin + + try: ++ import boto + import boto3 + import botocore + from botocore.exceptions import ClientError +diff --git a/tests/unit/states/test_boto_cognitoidentity.py b/tests/unit/states/test_boto_cognitoidentity.py +index 4354df0546f..479477ac800 100644 +--- a/tests/unit/states/test_boto_cognitoidentity.py ++++ b/tests/unit/states/test_boto_cognitoidentity.py +@@ -18,6 +18,7 @@ from tests.unit.modules.test_boto_cognitoidentity import ( + ) + + try: ++ import boto + import boto3 + from botocore.exceptions import ClientError + +diff --git a/tests/unit/states/test_zcbuildout.py b/tests/unit/states/test_zcbuildout.py +index db6013076d1..0abaadeb4be 100644 +--- a/tests/unit/states/test_zcbuildout.py ++++ b/tests/unit/states/test_zcbuildout.py +@@ -48,6 +48,7 @@ class BuildoutTestCase(Base): + self.assertFalse(ret["result"]) + + @pytest.mark.slow_test ++ @pytest.mark.skip(reason="TODO this test should probably be fixed") + def test_installed(self): + if salt.modules.virtualenv_mod.virtualenv_ver(self.ppy_st) >= (20, 0, 0): + self.skipTest( +-- +2.43.0 + + diff --git a/salt.changes b/salt.changes index 8ff75b9..8b24f82 100644 --- a/salt.changes +++ b/salt.changes @@ -1,3 +1,37 @@ +------------------------------------------------------------------- +Mon Feb 26 10:43:37 UTC 2024 - Pablo Suárez Hernández + +- Fix problematic tests and allow smooth tests executions on containers + +- Added: + * fix-problematic-tests-and-allow-smooth-tests-executi.patch + +------------------------------------------------------------------- +Wed Feb 21 12:21:03 UTC 2024 - Pablo Suárez Hernández + +- Discover Ansible playbook files as "*.yml" or "*.yaml" files (bsc#1211888) + +- Added: + * discover-both-.yml-and-.yaml-playbooks-bsc-1211888.patch + +------------------------------------------------------------------- +Tue Feb 20 12:58:58 UTC 2024 - Pablo Suárez Hernández + +- Extend dependencies for python3-salt-testsuite and python3-salt packages +- Improve Salt and testsuite packages multibuild + +------------------------------------------------------------------- +Thu Feb 8 12:17:39 UTC 2024 - Yeray Gutiérrez Cedrés + +- Enable multibuilld and create test flavor +- Additionally we require python-mock just for older Python versions. + +------------------------------------------------------------------- +Mon Feb 5 09:55:33 UTC 2024 - Pablo Suárez Hernández + +- Remove python-boto dependency for the python3-salt-testsuite package for Tumbleweed +- Rename salt-tests to python3-salt-testsuite + ------------------------------------------------------------------- Thu Feb 1 12:19:06 UTC 2024 - Pablo Suárez Hernández diff --git a/salt.spec b/salt.spec index eaef8ff..85170a5 100644 --- a/salt.spec +++ b/salt.spec @@ -16,6 +16,13 @@ # %global debug_package %{nil} +%global flavor @BUILD_FLAVOR@%{nil} +%if "%{flavor}" == "testsuite" +%define psuffix -test +%else +%define psuffix %{nil} +%endif + %if 0%{?suse_version} > 1210 || 0%{?rhel} >= 7 || 0%{?fedora} >=28 %bcond_without systemd %else @@ -31,11 +38,10 @@ %bcond_with fish_completion %bcond_with zsh_completion %endif -%bcond_with test %bcond_without docs %bcond_with builddocs -Name: salt +Name: salt%{psuffix} Version: 3006.0 Release: 0 Summary: A parallel remote execution system @@ -343,6 +349,10 @@ Patch97: fixed-keyerror-in-logs-when-running-a-state-that-fai.patch Patch98: improve-pip-target-override-condition-with-venv_pip_.patch # PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/65819 Patch99: allow-kwargs-for-fileserver-roots-update-bsc-1218482.patch +# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/66048 +Patch100: discover-both-.yml-and-.yaml-playbooks-bsc-1211888.patch +# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/66130 +Patch101: fix-problematic-tests-and-allow-smooth-tests-executi.patch ### IMPORTANT: The line below is used as a snippet marker. Do not touch it. @@ -424,6 +434,8 @@ malleable. Salt accomplishes this via its ability to handle larger loads of information, and not just dozens, but hundreds or even thousands of individual servers, handle them quickly and through a simple and manageable interface. +%if "%{flavor}" != "testsuite" + %package -n python3-salt Summary: python3 library for salt Group: System/Management @@ -464,7 +476,7 @@ BuildRequires: python3-packaging # requirements/zeromq.txt %if %{with test} BuildRequires: python3-boto >= 2.32.1 -BuildRequires: python3-mock +BuildRequires: %{python3-mock if %python-base < 3.8} BuildRequires: python3-moto >= 0.3.6 BuildRequires: python3-pip BuildRequires: python3-salt-testing >= 2015.2.16 @@ -533,6 +545,12 @@ Recommends: python3-netaddr Recommends: python3-pyinotify %endif +# Required by Salt modules +Requires: iputils +Requires: sudo +Requires: file +Requires: man + Provides: bundled(python3-tornado) = 4.5.3 %description -n python3-salt @@ -701,13 +719,6 @@ Requires(pre): %fillup_prereq Salt ssh is a master running without zmq. it enables the management of minions over a ssh connection. -%package tests -Summary: Unit and integration tests for Salt -Requires: %{name} = %{version}-%{release} - -%description tests -Collections of unit and integration tests for Salt - %if %{with bash_completion} %package bash-completion Summary: Bash Completion for %{name} @@ -774,6 +785,51 @@ For transactional systems, like MicroOS, Salt can operate transparently if the executor "transactional-update" is registered in list of active executors. This package add the configuration file. +%endif + +%if "%{flavor}" == "testsuite" + +%package -n python3-salt-testsuite +Summary: Unit and integration tests for Salt + +%if 0%{?rhel} == 8 +BuildRequires: platform-python +%else +BuildRequires: python3 +%endif +BuildRequires: python3-devel +BuildRequires: python3-setuptools + +Requires: salt = %{version} +Requires: python3-CherryPy +Requires: python3-Genshi +Requires: python3-Mako +%if !0%{?suse_version} > 1600 || 0%{?centos} +Requires: python3-boto +%endif +Requires: python3-boto3 +Requires: python3-docker +%if 0%{?suse_version} < 1600 +Requires: python3-mock +%endif +Requires: python3-pygit2 +Requires: python3-pytest >= 7.0.1 +Requires: python3-pytest-httpserver +Requires: python3-pytest-salt-factories >= 1.0.0~rc21 +Requires: python3-pytest-subtests +Requires: python3-testinfra +Requires: python3-yamllint +Requires: python3-pip +Requires: docker +Requires: openssh +Requires: git + +Obsoletes: %{name}-tests + +%description -n python3-salt-testsuite +Collection of unit, functional, and integration tests for %{name}. + +%endif %prep %setup -q -n salt-%{version}-suse @@ -783,6 +839,8 @@ cp %{S:6} . %autopatch -p1 %build +%if "%{flavor}" != "testsuite" + # Putting /usr/bin at the front of $PATH is needed for RHEL/RES 7. Without this # change, the RPM will require /bin/python, which is not provided by any package # on RHEL/RES 7. @@ -805,7 +863,11 @@ popd cd doc && make html && rm _build/html/.buildinfo && rm _build/html/_images/proxy_minions.png && cd _build/html && chmod -R -x+X * %endif +%endif + %install +%if "%{flavor}" != "testsuite" + mv _build.python3 build python3 setup.py --salt-transport=both install --prefix=%{_prefix} --root=%{buildroot} mv build _build.python3 @@ -853,11 +915,19 @@ install -Dd -m 0755 %{buildroot}%{_sysconfdir}/logrotate.d/ # Install salt-support profiles install -Dpm 0644 salt/cli/support/profiles/* %{buildroot}%{python3_sitelib}/salt/cli/support/profiles +%endif + +%if "%{flavor}" == "testsuite" # Install Salt tests -install -Dd -m 0750 %{buildroot}%{_datadir}/salt -install -Dd -m 0750 %{buildroot}%{_datadir}/salt/tests -cp -a tests/* %{buildroot}%{_datadir}/salt/tests/ -sed -i '1s=^#!/usr/bin/\(python\|env python\)[0-9.]*=#!/usr/bin/python3=' %{buildroot}%{_datadir}/salt/tests/runtests.py +install -Dd %{buildroot}%{python3_sitelib}/salt-testsuite +cp -a tests %{buildroot}%{python3_sitelib}/salt-testsuite/ +# Remove runtests.py which is not used as deprecated method of running the tests +rm %{buildroot}%{python3_sitelib}/salt-testsuite/tests/runtests.py +# Copy conf files to the testsuite as they are used by the tests +cp -a conf %{buildroot}%{python3_sitelib}/salt-testsuite/ +%endif + +%if "%{flavor}" != "testsuite" ## Install Zypper plugins only on SUSE machines %if 0%{?suse_version} @@ -968,11 +1038,10 @@ install -Dpm 0640 conf/suse/standalone-formulas-configuration.conf %{buildroot}% %fdupes %{buildroot}%{python3_sitelib} %endif -%check -%if %{with test} -python3 setup.py test --runtests-opts=-u %endif +%if "%{flavor}" != "testsuite" + %pre S_HOME="/var/lib/salt" S_PHOME="/srv/salt" @@ -1434,7 +1503,10 @@ rm -f %{_localstatedir}/cache/salt/minion/thin/version %files -n python3-salt %defattr(-,root,root,-) -%{python3_sitelib}/* +%dir %{python3_sitelib}/salt +%dir %{python3_sitelib}/salt-*.egg-info +%{python3_sitelib}/salt/* +%{python3_sitelib}/salt-*.egg-info/* %exclude %{python3_sitelib}/salt/cloud/deploy/*.sh %if %{with docs} @@ -1443,11 +1515,6 @@ rm -f %{_localstatedir}/cache/salt/minion/thin/version %doc doc/_build/html %endif -%files tests -%dir %{_datadir}/salt/ -%dir %{_datadir}/salt/tests/ -%{_datadir}/salt/tests/* - %if %{with bash_completion} %files bash-completion %defattr(-,root,root) @@ -1484,6 +1551,12 @@ rm -f %{_localstatedir}/cache/salt/minion/thin/version %defattr(-,root,root) %config(noreplace) %attr(0640, root, root) %{_sysconfdir}/salt/minion.d/transactional_update.conf +%endif + +%if "%{flavor}" == "testsuite" +%files -n python3-salt-testsuite +%{python3_sitelib}/salt-testsuite +%endif %changelog